From 070be8e618bedaaf71e68a18990a9b9def7fe032 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 Aug 2025 11:40:57 +0000 Subject: [PATCH 1/3] Initial plan From 886b2eacab37f2197a49106c94774647426b226a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 Aug 2025 11:48:23 +0000 Subject: [PATCH 2/3] Fix flags_without_count option behavior for empty symbols Co-authored-by: arl <476650+arl@users.noreply.github.com> --- .gitmux.yml | 2 + README.md | 2 +- tmux/formater.go | 55 ++++++-- tmux/formater_test.go | 310 +++++++++++++++++++++++++++++++++++++++--- 4 files changed, 336 insertions(+), 33 deletions(-) diff --git a/.gitmux.yml b/.gitmux.yml index 51e462a..e2d18f3 100644 --- a/.gitmux.yml +++ b/.gitmux.yml @@ -86,4 +86,6 @@ tmux: # Add a space between behind & ahead upstream counts. divergence_space: false # Show flags symbols without counts. + # When true, shows only symbols (empty symbols show nothing). + # When false (default), shows symbols with counts (empty symbols show counts only). flags_without_count: false diff --git a/README.md b/README.md index 54c24c4..4700eba 100644 --- a/README.md +++ b/README.md @@ -296,7 +296,7 @@ This is the list of additional configuration `options`: | `hide_clean` | Hides the clean flag entirely | `false` | | `swap_divergence` | Swaps order of behind & ahead upstream counts | `false` | | `divergence_space` | Add a space between behind & ahead upstream counts | `false` | -| `flags_without_count`| Show flags symbols without counts | `false` | +| `flags_without_count`| Show flags symbols without counts (empty symbols show nothing instead of counts)| `false` | ## Troubleshooting diff --git a/tmux/formater.go b/tmux/formater.go index ec246f9..53062dd 100644 --- a/tmux/formater.go +++ b/tmux/formater.go @@ -297,19 +297,34 @@ func (f *Formater) currentRef() string { // formatFlag formats a flag with or without count based on the flags_without_count option func (f *Formater) formatFlag(style, symbol string, count int) string { + if count == 0 { + return "" + } + if f.Options.FlagsWithoutCount { + // When flags_without_count is true, show symbol only (empty string if symbol is empty) + if symbol == "" { + return "" + } return fmt.Sprintf("%s%s", style, symbol) } + + // When flags_without_count is false, show symbol + count, or just count if symbol is empty return fmt.Sprintf("%s%s%d", style, symbol, count) } func (f *Formater) flags() string { var flags []string if f.st.IsClean { - if f.st.NumStashed != 0 && f.Symbols.Stashed != "" { - flags = append(flags, f.formatFlag(f.Styles.Stashed, f.Symbols.Stashed, f.st.NumStashed)) + // For stashed in clean state, handle empty symbols properly + if f.st.NumStashed != 0 { + flag := f.formatFlag(f.Styles.Stashed, f.Symbols.Stashed, f.st.NumStashed) + if flag != "" { + flags = append(flags, flag) + } } + // Clean flag only shows if symbol is not empty and hide_clean is false if !f.Options.HideClean && f.Symbols.Clean != "" { flags = append(flags, fmt.Sprintf("%s%s", f.Styles.Clean, f.Symbols.Clean)) } @@ -319,24 +334,40 @@ func (f *Formater) flags() string { } } - if f.st.NumStaged != 0 && f.Symbols.Staged != "" { - flags = append(flags, f.formatFlag(f.Styles.Staged, f.Symbols.Staged, f.st.NumStaged)) + // For all other flags, handle empty symbols properly + if f.st.NumStaged != 0 { + flag := f.formatFlag(f.Styles.Staged, f.Symbols.Staged, f.st.NumStaged) + if flag != "" { + flags = append(flags, flag) + } } - if f.st.NumConflicts != 0 && f.Symbols.Conflict != "" { - flags = append(flags, f.formatFlag(f.Styles.Conflict, f.Symbols.Conflict, f.st.NumConflicts)) + if f.st.NumConflicts != 0 { + flag := f.formatFlag(f.Styles.Conflict, f.Symbols.Conflict, f.st.NumConflicts) + if flag != "" { + flags = append(flags, flag) + } } - if f.st.NumModified != 0 && f.Symbols.Modified != "" { - flags = append(flags, f.formatFlag(f.Styles.Modified, f.Symbols.Modified, f.st.NumModified)) + if f.st.NumModified != 0 { + flag := f.formatFlag(f.Styles.Modified, f.Symbols.Modified, f.st.NumModified) + if flag != "" { + flags = append(flags, flag) + } } - if f.st.NumStashed != 0 && f.Symbols.Stashed != "" { - flags = append(flags, f.formatFlag(f.Styles.Stashed, f.Symbols.Stashed, f.st.NumStashed)) + if f.st.NumStashed != 0 { + flag := f.formatFlag(f.Styles.Stashed, f.Symbols.Stashed, f.st.NumStashed) + if flag != "" { + flags = append(flags, flag) + } } - if f.st.NumUntracked != 0 && f.Symbols.Untracked != "" { - flags = append(flags, f.formatFlag(f.Styles.Untracked, f.Symbols.Untracked, f.st.NumUntracked)) + if f.st.NumUntracked != 0 { + flag := f.formatFlag(f.Styles.Untracked, f.Symbols.Untracked, f.st.NumUntracked) + if flag != "" { + flags = append(flags, flag) + } } if len(flags) > 0 { diff --git a/tmux/formater_test.go b/tmux/formater_test.go index 6890740..a6967b0 100644 --- a/tmux/formater_test.go +++ b/tmux/formater_test.go @@ -104,6 +104,200 @@ func TestFlags(t *testing.T) { } } +func TestFlagsWithoutCountBehavior(t *testing.T) { + tests := []struct { + name string + styles styles + symbols symbols + options options + st *gitstatus.Status + want string + }{ + // Case 1: non-empty symbol, count=1, flags_without_count=false + { + name: "case 1a: non-empty symbol, count=1, flags_without_count=false", + styles: styles{ + Clear: "StyleClear", + Staged: "StyleStaged", + }, + symbols: symbols{ + Staged: "S", + }, + options: options{ + FlagsWithoutCount: false, + }, + st: &gitstatus.Status{ + Porcelain: gitstatus.Porcelain{ + NumStaged: 1, + }, + }, + want: "StyleClearStyleStagedS1", + }, + // Case 1: non-empty symbol, count=1, flags_without_count=true + { + name: "case 1b: non-empty symbol, count=1, flags_without_count=true", + styles: styles{ + Clear: "StyleClear", + Staged: "StyleStaged", + }, + symbols: symbols{ + Staged: "S", + }, + options: options{ + FlagsWithoutCount: true, + }, + st: &gitstatus.Status{ + Porcelain: gitstatus.Porcelain{ + NumStaged: 1, + }, + }, + want: "StyleClearStyleStagedS", + }, + // Case 2: empty symbol, count=1, flags_without_count=false + { + name: "case 2a: empty symbol, count=1, flags_without_count=false", + styles: styles{ + Clear: "StyleClear", + Staged: "StyleStaged", + }, + symbols: symbols{ + Staged: "", + }, + options: options{ + FlagsWithoutCount: false, + }, + st: &gitstatus.Status{ + Porcelain: gitstatus.Porcelain{ + NumStaged: 1, + }, + }, + want: "StyleClearStyleStaged1", + }, + // Case 2: empty symbol, count=1, flags_without_count=true + { + name: "case 2b: empty symbol, count=1, flags_without_count=true", + styles: styles{ + Clear: "StyleClear", + Staged: "StyleStaged", + }, + symbols: symbols{ + Staged: "", + }, + options: options{ + FlagsWithoutCount: true, + }, + st: &gitstatus.Status{ + Porcelain: gitstatus.Porcelain{ + NumStaged: 1, + }, + }, + want: "", + }, + // Case 3: count=0, flags_without_count=false + { + name: "case 3a: count=0, flags_without_count=false", + styles: styles{ + Clear: "StyleClear", + Staged: "StyleStaged", + }, + symbols: symbols{ + Staged: "S", + }, + options: options{ + FlagsWithoutCount: false, + }, + st: &gitstatus.Status{ + Porcelain: gitstatus.Porcelain{ + NumStaged: 0, + }, + }, + want: "", + }, + // Case 3: count=0, flags_without_count=true + { + name: "case 3b: count=0, flags_without_count=true", + styles: styles{ + Clear: "StyleClear", + Staged: "StyleStaged", + }, + symbols: symbols{ + Staged: "S", + }, + options: options{ + FlagsWithoutCount: true, + }, + st: &gitstatus.Status{ + Porcelain: gitstatus.Porcelain{ + NumStaged: 0, + }, + }, + want: "", + }, + // Mixed case: multiple flags with different symbol states + { + name: "mixed flags: some empty symbols, some non-empty, flags_without_count=false", + styles: styles{ + Clear: "StyleClear", + Staged: "StyleStaged", + Modified: "StyleMod", + Stashed: "StyleStash", + }, + symbols: symbols{ + Staged: "", // empty symbol, should show count only + Modified: "M", // non-empty symbol, should show symbol+count + Stashed: "", // empty symbol, should show count only + }, + options: options{ + FlagsWithoutCount: false, + }, + st: &gitstatus.Status{ + NumStashed: 2, + Porcelain: gitstatus.Porcelain{ + NumStaged: 1, + NumModified: 3, + }, + }, + want: "StyleClearStyleStaged1 StyleModM3 StyleStash2", + }, + // Mixed case: multiple flags with different symbol states, flags_without_count=true + { + name: "mixed flags: some empty symbols, some non-empty, flags_without_count=true", + styles: styles{ + Clear: "StyleClear", + Staged: "StyleStaged", + Modified: "StyleMod", + Stashed: "StyleStash", + }, + symbols: symbols{ + Staged: "", // empty symbol, should show nothing + Modified: "M", // non-empty symbol, should show symbol only + Stashed: "", // empty symbol, should show nothing + }, + options: options{ + FlagsWithoutCount: true, + }, + st: &gitstatus.Status{ + NumStashed: 2, + Porcelain: gitstatus.Porcelain{ + NumStaged: 1, + NumModified: 3, + }, + }, + want: "StyleClearStyleModM", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &Formater{ + Config: Config{Styles: tt.styles, Symbols: tt.symbols, Options: tt.options}, + st: tt.st, + } + + compareStrings(t, tt.want, f.flags()) + }) + } +} + func TestFlagsWithoutCount(t *testing.T) { tests := []struct { name string @@ -988,7 +1182,7 @@ func TestFlagsWithEmptySymbols(t *testing.T) { want string }{ { - name: "empty stashed symbol hides stash count", + name: "empty stashed symbol shows stash count (flags_without_count=false default)", styles: styles{ Clear: "StyleClear", Modified: "StyleMod", @@ -996,7 +1190,7 @@ func TestFlagsWithEmptySymbols(t *testing.T) { }, symbols: symbols{ Modified: "SymbolMod", - Stashed: "", // empty symbol should hide this flag + Stashed: "", // empty symbol should show count with default flags_without_count=false }, st: &gitstatus.Status{ NumStashed: 5, @@ -1004,17 +1198,17 @@ func TestFlagsWithEmptySymbols(t *testing.T) { NumModified: 2, }, }, - want: "StyleClear" + "StyleModSymbolMod2", + want: "StyleClear" + "StyleModSymbolMod2 StyleStash5", }, { - name: "empty modified symbol hides modified count", + name: "empty modified symbol shows modified count (flags_without_count=false default)", styles: styles{ Clear: "StyleClear", Modified: "StyleMod", Stashed: "StyleStash", }, symbols: symbols{ - Modified: "", // empty symbol should hide this flag + Modified: "", // empty symbol should show count with default flags_without_count=false Stashed: "SymbolStash", }, st: &gitstatus.Status{ @@ -1023,17 +1217,17 @@ func TestFlagsWithEmptySymbols(t *testing.T) { NumModified: 2, }, }, - want: "StyleClear" + "StyleStashSymbolStash1", + want: "StyleClear" + "StyleMod2 StyleStashSymbolStash1", }, { - name: "empty staged symbol hides staged count", + name: "empty staged symbol shows staged count (flags_without_count=false default)", styles: styles{ Clear: "StyleClear", Staged: "StyleStaged", Stashed: "StyleStash", }, symbols: symbols{ - Staged: "", // empty symbol should hide this flag + Staged: "", // empty symbol should show count with default flags_without_count=false Stashed: "SymbolStash", }, st: &gitstatus.Status{ @@ -1042,17 +1236,17 @@ func TestFlagsWithEmptySymbols(t *testing.T) { NumStaged: 3, }, }, - want: "StyleClear" + "StyleStashSymbolStash1", + want: "StyleClear" + "StyleStaged3 StyleStashSymbolStash1", }, { - name: "empty untracked symbol hides untracked count", + name: "empty untracked symbol shows untracked count (flags_without_count=false default)", styles: styles{ Clear: "StyleClear", Untracked: "StyleUntracked", Stashed: "StyleStash", }, symbols: symbols{ - Untracked: "", // empty symbol should hide this flag + Untracked: "", // empty symbol should show count with default flags_without_count=false Stashed: "SymbolStash", }, st: &gitstatus.Status{ @@ -1061,17 +1255,17 @@ func TestFlagsWithEmptySymbols(t *testing.T) { NumUntracked: 7, }, }, - want: "StyleClear" + "StyleStashSymbolStash1", + want: "StyleClear" + "StyleStashSymbolStash1 StyleUntracked7", }, { - name: "empty conflict symbol hides conflict count", + name: "empty conflict symbol shows conflict count (flags_without_count=false default)", styles: styles{ Clear: "StyleClear", Conflict: "StyleConflict", Stashed: "StyleStash", }, symbols: symbols{ - Conflict: "", // empty symbol should hide this flag + Conflict: "", // empty symbol should show count with default flags_without_count=false Stashed: "SymbolStash", }, st: &gitstatus.Status{ @@ -1080,7 +1274,7 @@ func TestFlagsWithEmptySymbols(t *testing.T) { NumConflicts: 3, }, }, - want: "StyleClear" + "StyleStashSymbolStash1", + want: "StyleClear" + "StyleConflict3 StyleStashSymbolStash1", }, { name: "empty clean symbol hides clean flag", @@ -1100,23 +1294,24 @@ func TestFlagsWithEmptySymbols(t *testing.T) { want: "StyleClear" + "StyleStashSymbolStash1", }, { - name: "empty stashed symbol in clean state hides stash count", + name: "empty stashed symbol in clean state shows stash count (flags_without_count=false default)", styles: styles{ Clear: "StyleClear", Clean: "StyleClean", + Stashed: "StyleStash", }, symbols: symbols{ Clean: "SymbolClean", - Stashed: "", // empty symbol should hide this flag + Stashed: "", // empty symbol should show count with default flags_without_count=false }, st: &gitstatus.Status{ IsClean: true, NumStashed: 1, }, - want: "StyleClear" + "StyleCleanSymbolClean", + want: "StyleClear" + "StyleStash1 StyleCleanSymbolClean", }, { - name: "all symbols empty shows nothing", + name: "all symbols empty shows counts (flags_without_count=false default)", styles: styles{ Clear: "StyleClear", Clean: "StyleClean", @@ -1144,7 +1339,7 @@ func TestFlagsWithEmptySymbols(t *testing.T) { NumUntracked: 4, }, }, - want: "", + want: "StyleClear" + "StyleStaged3 StyleConflict1 StyleMod2 StyleStash1 StyleUntracked4", }, } for _, tt := range tests { @@ -1159,6 +1354,81 @@ func TestFlagsWithEmptySymbols(t *testing.T) { } } +func TestFlagsWithEmptySymbolsAndFlagsWithoutCount(t *testing.T) { + tests := []struct { + name string + styles styles + symbols symbols + options options + st *gitstatus.Status + want string + }{ + { + name: "empty symbols hide when flags_without_count=true", + styles: styles{ + Clear: "StyleClear", + Modified: "StyleMod", + Stashed: "StyleStash", + }, + symbols: symbols{ + Modified: "", // empty symbol should hide with flags_without_count=true + Stashed: "SymbolStash", + }, + options: options{ + FlagsWithoutCount: true, + }, + st: &gitstatus.Status{ + NumStashed: 1, + Porcelain: gitstatus.Porcelain{ + NumModified: 2, + }, + }, + want: "StyleClear" + "StyleStashSymbolStash", + }, + { + name: "all empty symbols hide when flags_without_count=true", + styles: styles{ + Clear: "StyleClear", + Staged: "StyleStaged", + Modified: "StyleMod", + Conflict: "StyleConflict", + Untracked: "StyleUntracked", + Stashed: "StyleStash", + }, + symbols: symbols{ + Staged: "", + Modified: "", + Conflict: "", + Untracked: "", + Stashed: "", + }, + options: options{ + FlagsWithoutCount: true, + }, + st: &gitstatus.Status{ + NumStashed: 1, + Porcelain: gitstatus.Porcelain{ + NumStaged: 3, + NumModified: 2, + NumConflicts: 1, + NumUntracked: 4, + }, + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &Formater{ + Config: Config{Styles: tt.styles, Symbols: tt.symbols, Options: tt.options}, + st: tt.st, + } + + compareStrings(t, tt.want, f.flags()) + }) + } +} + func compareStrings(t *testing.T, want, got string) { if got != want { t.Errorf(` From 0ecaeb9cd92f2f1056a83eb775fb85acf1dcfb4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 Aug 2025 13:10:45 +0000 Subject: [PATCH 3/3] Address review feedback: refactor formatFlag to appendFlag, update README and tests Co-authored-by: arl <476650+arl@users.noreply.github.com> --- README.md | 4 ++- tmux/formater.go | 58 +++++++++---------------------------------- tmux/formater_test.go | 12 ++++----- 3 files changed, 21 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 4700eba..9e3c2be 100644 --- a/README.md +++ b/README.md @@ -296,7 +296,9 @@ This is the list of additional configuration `options`: | `hide_clean` | Hides the clean flag entirely | `false` | | `swap_divergence` | Swaps order of behind & ahead upstream counts | `false` | | `divergence_space` | Add a space between behind & ahead upstream counts | `false` | -| `flags_without_count`| Show flags symbols without counts (empty symbols show nothing instead of counts)| `false` | +| `flags_without_count`| Show flags symbols without counts* | `false` | + +*When `flags_without_count` is true, shows only symbols (empty symbols show nothing). When false (default), shows symbols with counts (empty symbols show counts only). ## Troubleshooting diff --git a/tmux/formater.go b/tmux/formater.go index 53062dd..adfd40e 100644 --- a/tmux/formater.go +++ b/tmux/formater.go @@ -295,34 +295,29 @@ func (f *Formater) currentRef() string { return fmt.Sprintf("%s%s%s", f.Styles.Clear, f.Styles.Branch, branch) } -// formatFlag formats a flag with or without count based on the flags_without_count option -func (f *Formater) formatFlag(style, symbol string, count int) string { +// appendFlag appends a flag to the flags slice based on configuration options +func (f *Formater) appendFlag(flags []string, style, symbol string, count int) []string { if count == 0 { - return "" + return flags } if f.Options.FlagsWithoutCount { // When flags_without_count is true, show symbol only (empty string if symbol is empty) if symbol == "" { - return "" + return flags } - return fmt.Sprintf("%s%s", style, symbol) + return append(flags, fmt.Sprintf("%s%s", style, symbol)) } // When flags_without_count is false, show symbol + count, or just count if symbol is empty - return fmt.Sprintf("%s%s%d", style, symbol, count) + return append(flags, fmt.Sprintf("%s%s%d", style, symbol, count)) } func (f *Formater) flags() string { var flags []string if f.st.IsClean { // For stashed in clean state, handle empty symbols properly - if f.st.NumStashed != 0 { - flag := f.formatFlag(f.Styles.Stashed, f.Symbols.Stashed, f.st.NumStashed) - if flag != "" { - flags = append(flags, flag) - } - } + flags = f.appendFlag(flags, f.Styles.Stashed, f.Symbols.Stashed, f.st.NumStashed) // Clean flag only shows if symbol is not empty and hide_clean is false if !f.Options.HideClean && f.Symbols.Clean != "" { @@ -335,40 +330,11 @@ func (f *Formater) flags() string { } // For all other flags, handle empty symbols properly - if f.st.NumStaged != 0 { - flag := f.formatFlag(f.Styles.Staged, f.Symbols.Staged, f.st.NumStaged) - if flag != "" { - flags = append(flags, flag) - } - } - - if f.st.NumConflicts != 0 { - flag := f.formatFlag(f.Styles.Conflict, f.Symbols.Conflict, f.st.NumConflicts) - if flag != "" { - flags = append(flags, flag) - } - } - - if f.st.NumModified != 0 { - flag := f.formatFlag(f.Styles.Modified, f.Symbols.Modified, f.st.NumModified) - if flag != "" { - flags = append(flags, flag) - } - } - - if f.st.NumStashed != 0 { - flag := f.formatFlag(f.Styles.Stashed, f.Symbols.Stashed, f.st.NumStashed) - if flag != "" { - flags = append(flags, flag) - } - } - - if f.st.NumUntracked != 0 { - flag := f.formatFlag(f.Styles.Untracked, f.Symbols.Untracked, f.st.NumUntracked) - if flag != "" { - flags = append(flags, flag) - } - } + flags = f.appendFlag(flags, f.Styles.Staged, f.Symbols.Staged, f.st.NumStaged) + flags = f.appendFlag(flags, f.Styles.Conflict, f.Symbols.Conflict, f.st.NumConflicts) + flags = f.appendFlag(flags, f.Styles.Modified, f.Symbols.Modified, f.st.NumModified) + flags = f.appendFlag(flags, f.Styles.Stashed, f.Symbols.Stashed, f.st.NumStashed) + flags = f.appendFlag(flags, f.Styles.Untracked, f.Symbols.Untracked, f.st.NumUntracked) if len(flags) > 0 { return f.Styles.Clear + strings.Join(flags, " ") diff --git a/tmux/formater_test.go b/tmux/formater_test.go index a6967b0..0d4488a 100644 --- a/tmux/formater_test.go +++ b/tmux/formater_test.go @@ -121,7 +121,7 @@ func TestFlagsWithoutCountBehavior(t *testing.T) { Staged: "StyleStaged", }, symbols: symbols{ - Staged: "S", + Staged: "SymbolStaged", }, options: options{ FlagsWithoutCount: false, @@ -131,7 +131,7 @@ func TestFlagsWithoutCountBehavior(t *testing.T) { NumStaged: 1, }, }, - want: "StyleClearStyleStagedS1", + want: "StyleClearStyleStagedSymbolStaged1", }, // Case 1: non-empty symbol, count=1, flags_without_count=true { @@ -141,7 +141,7 @@ func TestFlagsWithoutCountBehavior(t *testing.T) { Staged: "StyleStaged", }, symbols: symbols{ - Staged: "S", + Staged: "SymbolStaged", }, options: options{ FlagsWithoutCount: true, @@ -151,7 +151,7 @@ func TestFlagsWithoutCountBehavior(t *testing.T) { NumStaged: 1, }, }, - want: "StyleClearStyleStagedS", + want: "StyleClearStyleStagedSymbolStaged", }, // Case 2: empty symbol, count=1, flags_without_count=false { @@ -201,7 +201,7 @@ func TestFlagsWithoutCountBehavior(t *testing.T) { Staged: "StyleStaged", }, symbols: symbols{ - Staged: "S", + Staged: "SymbolStaged", }, options: options{ FlagsWithoutCount: false, @@ -221,7 +221,7 @@ func TestFlagsWithoutCountBehavior(t *testing.T) { Staged: "StyleStaged", }, symbols: symbols{ - Staged: "S", + Staged: "SymbolStaged", }, options: options{ FlagsWithoutCount: true,