diff --git a/.gitmux.yml b/.gitmux.yml index 51e462a..f8d5534 100644 --- a/.gitmux.yml +++ b/.gitmux.yml @@ -87,3 +87,5 @@ tmux: divergence_space: false # Show flags symbols without counts. flags_without_count: false + # Hide flag count when symbol is empty (default false shows count only). + hide_flag_count_if_empty_symbol: false diff --git a/README.md b/README.md index 54c24c4..6824e57 100644 --- a/README.md +++ b/README.md @@ -288,15 +288,16 @@ layout: [branch, "|", flags, "|", stats] This is the list of additional configuration `options`: -| Option | Description | Default | -| :------------------- | :------------------------------------------------------------------------------ | :----------------: | -| `branch_max_len` | Maximum displayed length for local and remote branch names | `0` (no limit) | -| `branch_trim` | Trim left, right or from the center of the branch (`right`, `left` or `center`) | `right` (trailing) | -| `ellipsis` | Character to show branch name has been truncated | `…` | -| `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` | +| Option | Description | Default | +| :-------------------------------- | :------------------------------------------------------------------------------ | :----------------: | +| `branch_max_len` | Maximum displayed length for local and remote branch names | `0` (no limit) | +| `branch_trim` | Trim left, right or from the center of the branch (`right`, `left` or `center`) | `right` (trailing) | +| `ellipsis` | Character to show branch name has been truncated | `…` | +| `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` | +| `hide_flag_count_if_empty_symbol` | Hide flag count when symbol is empty (false shows count only) | `false` | ## Troubleshooting diff --git a/tmux/formater.go b/tmux/formater.go index ec246f9..20c2ffc 100644 --- a/tmux/formater.go +++ b/tmux/formater.go @@ -88,13 +88,14 @@ func (d *direction) UnmarshalYAML(value *yaml.Node) error { } type options struct { - BranchMaxLen int `yaml:"branch_max_len"` - BranchTrim direction `yaml:"branch_trim"` - Ellipsis string `yaml:"ellipsis"` - HideClean bool `yaml:"hide_clean"` - DivergenceSpace bool `yaml:"divergence_space"` - SwapDivergence bool `yaml:"swap_divergence"` - FlagsWithoutCount bool `yaml:"flags_without_count"` + BranchMaxLen int `yaml:"branch_max_len"` + BranchTrim direction `yaml:"branch_trim"` + Ellipsis string `yaml:"ellipsis"` + HideClean bool `yaml:"hide_clean"` + DivergenceSpace bool `yaml:"divergence_space"` + SwapDivergence bool `yaml:"swap_divergence"` + FlagsWithoutCount bool `yaml:"flags_without_count"` + HideFlagCountIfEmptySymbol bool `yaml:"hide_flag_count_if_empty_symbol"` } // A Formater formats git status to a tmux style string. @@ -296,22 +297,39 @@ 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 { +func (f *Formater) appendFlag(flags []string, style, symbol string, count int) []string { + // Handle empty symbol case based on hide_flag_count_if_empty_symbol option + if symbol == "" { + if f.Options.HideFlagCountIfEmptySymbol { + return flags // Hide both symbol and count + } + // Show just the count without symbol + return append(flags, fmt.Sprintf("%s%d", style, count)) + } + + // Handle flags_without_count option if f.Options.FlagsWithoutCount { - return fmt.Sprintf("%s%s", style, symbol) + return append(flags, fmt.Sprintf("%s%s", style, symbol)) } - return fmt.Sprintf("%s%s%d", style, symbol, count) + + // Default behavior: show symbol and count + return append(flags, 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)) + if f.st.NumStashed != 0 { + flags = f.appendFlag(flags, f.Styles.Stashed, f.Symbols.Stashed, f.st.NumStashed) } - if !f.Options.HideClean && f.Symbols.Clean != "" { - flags = append(flags, fmt.Sprintf("%s%s", f.Styles.Clean, f.Symbols.Clean)) + if !f.Options.HideClean { + // Handle clean symbol separately since it doesn't have a meaningful count + if f.Symbols.Clean != "" { + flags = append(flags, fmt.Sprintf("%s%s", f.Styles.Clean, f.Symbols.Clean)) + } + // Note: When clean symbol is empty, there's nothing meaningful to show + // since clean doesn't have a count, so we just skip it } if len(flags) != 0 { @@ -319,24 +337,24 @@ 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)) + if f.st.NumStaged != 0 { + flags = f.appendFlag(flags, f.Styles.Staged, f.Symbols.Staged, f.st.NumStaged) } - 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 { + flags = f.appendFlag(flags, f.Styles.Conflict, f.Symbols.Conflict, f.st.NumConflicts) } - 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 { + flags = f.appendFlag(flags, f.Styles.Modified, f.Symbols.Modified, f.st.NumModified) } - 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 { + flags = f.appendFlag(flags, f.Styles.Stashed, f.Symbols.Stashed, f.st.NumStashed) } - 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 { + flags = f.appendFlag(flags, f.Styles.Untracked, f.Symbols.Untracked, f.st.NumUntracked) } if len(flags) > 0 { diff --git a/tmux/formater_test.go b/tmux/formater_test.go index 6890740..26ba521 100644 --- a/tmux/formater_test.go +++ b/tmux/formater_test.go @@ -984,6 +984,7 @@ func TestFlagsWithEmptySymbols(t *testing.T) { name string styles styles symbols symbols + options options st *gitstatus.Status want string }{ @@ -998,6 +999,9 @@ func TestFlagsWithEmptySymbols(t *testing.T) { Modified: "SymbolMod", Stashed: "", // empty symbol should hide this flag }, + options: options{ + HideFlagCountIfEmptySymbol: true, // Use old behavior for this test + }, st: &gitstatus.Status{ NumStashed: 5, Porcelain: gitstatus.Porcelain{ @@ -1017,6 +1021,9 @@ func TestFlagsWithEmptySymbols(t *testing.T) { Modified: "", // empty symbol should hide this flag Stashed: "SymbolStash", }, + options: options{ + HideFlagCountIfEmptySymbol: true, // Use old behavior for this test + }, st: &gitstatus.Status{ NumStashed: 1, Porcelain: gitstatus.Porcelain{ @@ -1036,6 +1043,9 @@ func TestFlagsWithEmptySymbols(t *testing.T) { Staged: "", // empty symbol should hide this flag Stashed: "SymbolStash", }, + options: options{ + HideFlagCountIfEmptySymbol: true, // Use old behavior for this test + }, st: &gitstatus.Status{ NumStashed: 1, Porcelain: gitstatus.Porcelain{ @@ -1055,6 +1065,9 @@ func TestFlagsWithEmptySymbols(t *testing.T) { Untracked: "", // empty symbol should hide this flag Stashed: "SymbolStash", }, + options: options{ + HideFlagCountIfEmptySymbol: true, // Use old behavior for this test + }, st: &gitstatus.Status{ NumStashed: 1, Porcelain: gitstatus.Porcelain{ @@ -1074,6 +1087,9 @@ func TestFlagsWithEmptySymbols(t *testing.T) { Conflict: "", // empty symbol should hide this flag Stashed: "SymbolStash", }, + options: options{ + HideFlagCountIfEmptySymbol: true, // Use old behavior for this test + }, st: &gitstatus.Status{ NumStashed: 1, Porcelain: gitstatus.Porcelain{ @@ -1093,6 +1109,9 @@ func TestFlagsWithEmptySymbols(t *testing.T) { Clean: "", // empty symbol should hide this flag Stashed: "SymbolStash", }, + options: options{ + HideFlagCountIfEmptySymbol: true, // Use old behavior for this test + }, st: &gitstatus.Status{ IsClean: true, NumStashed: 1, @@ -1109,6 +1128,9 @@ func TestFlagsWithEmptySymbols(t *testing.T) { Clean: "SymbolClean", Stashed: "", // empty symbol should hide this flag }, + options: options{ + HideFlagCountIfEmptySymbol: true, // Use old behavior for this test + }, st: &gitstatus.Status{ IsClean: true, NumStashed: 1, @@ -1134,6 +1156,9 @@ func TestFlagsWithEmptySymbols(t *testing.T) { Untracked: "", Stashed: "", }, + options: options{ + HideFlagCountIfEmptySymbol: true, // Use old behavior for this test + }, st: &gitstatus.Status{ IsClean: false, NumStashed: 1, @@ -1150,7 +1175,280 @@ func TestFlagsWithEmptySymbols(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { f := &Formater{ - Config: Config{Styles: tt.styles, Symbols: tt.symbols}, + Config: Config{Styles: tt.styles, Symbols: tt.symbols, Options: tt.options}, + st: tt.st, + } + + compareStrings(t, tt.want, f.flags()) + }) + } +} +func TestFlagsWithEmptySymbolsNewBehavior(t *testing.T) { + tests := []struct { + name string + styles styles + symbols symbols + options options + st *gitstatus.Status + want string + }{ + { + name: "empty stashed symbol shows count only (new behavior)", + styles: styles{ + Clear: "StyleClear", + Modified: "StyleMod", + Stashed: "StyleStash", + }, + symbols: symbols{ + Modified: "SymbolMod", + Stashed: "", // empty symbol should show count only + }, + options: options{ + HideFlagCountIfEmptySymbol: false, // Use new behavior + }, + st: &gitstatus.Status{ + NumStashed: 5, + Porcelain: gitstatus.Porcelain{ + NumModified: 2, + }, + }, + want: "StyleClear" + "StyleModSymbolMod2 StyleStash5", + }, + { + name: "empty modified symbol shows count only (new behavior)", + styles: styles{ + Clear: "StyleClear", + Modified: "StyleMod", + Stashed: "StyleStash", + }, + symbols: symbols{ + Modified: "", // empty symbol should show count only + Stashed: "SymbolStash", + }, + options: options{ + HideFlagCountIfEmptySymbol: false, // Use new behavior + }, + st: &gitstatus.Status{ + NumStashed: 1, + Porcelain: gitstatus.Porcelain{ + NumModified: 2, + }, + }, + want: "StyleClear" + "StyleMod2 StyleStashSymbolStash1", + }, + { + name: "multiple empty symbols show counts only (new behavior)", + styles: styles{ + Clear: "StyleClear", + Staged: "StyleStaged", + Modified: "StyleMod", + Untracked: "StyleUntracked", + }, + symbols: symbols{ + Staged: "", // empty symbols should show counts only + Modified: "", + Untracked: "", + }, + options: options{ + HideFlagCountIfEmptySymbol: false, // Use new behavior + }, + st: &gitstatus.Status{ + Porcelain: gitstatus.Porcelain{ + NumStaged: 3, + NumModified: 2, + NumUntracked: 4, + }, + }, + want: "StyleClear" + "StyleStaged3 StyleMod2 StyleUntracked4", + }, + { + name: "mixed symbols and empty symbols (new behavior)", + styles: styles{ + Clear: "StyleClear", + Staged: "StyleStaged", + Modified: "StyleMod", + Stashed: "StyleStash", + }, + symbols: symbols{ + Staged: "SymbolStaged", // normal symbol + Modified: "", // empty symbol should show count only + Stashed: "SymbolStash", // normal symbol + }, + options: options{ + HideFlagCountIfEmptySymbol: false, // Use new behavior + }, + st: &gitstatus.Status{ + NumStashed: 1, + Porcelain: gitstatus.Porcelain{ + NumStaged: 3, + NumModified: 2, + }, + }, + want: "StyleClear" + "StyleStagedSymbolStaged3 StyleMod2 StyleStashSymbolStash1", + }, + } + 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 TestFlagsWithCombinedOptions(t *testing.T) { + tests := []struct { + name string + styles styles + symbols symbols + options options + st *gitstatus.Status + want string + }{ + { + name: "flags_without_count=true + hide_flag_count_if_empty_symbol=false, empty symbol shows nothing", + styles: styles{ + Clear: "StyleClear", + Modified: "StyleMod", + Stashed: "StyleStash", + }, + symbols: symbols{ + Modified: "", // empty symbol + Stashed: "SymbolStash", + }, + options: options{ + FlagsWithoutCount: true, + HideFlagCountIfEmptySymbol: false, // Should show count only for empty symbols + }, + st: &gitstatus.Status{ + NumStashed: 1, + Porcelain: gitstatus.Porcelain{ + NumModified: 2, + }, + }, + // When flags_without_count=true but symbol is empty, we still get count-only display + want: "StyleClear" + "StyleMod2 StyleStashSymbolStash", + }, + { + name: "flags_without_count=true + hide_flag_count_if_empty_symbol=true, empty symbol hides everything", + styles: styles{ + Clear: "StyleClear", + Modified: "StyleMod", + Stashed: "StyleStash", + }, + symbols: symbols{ + Modified: "", // empty symbol + Stashed: "SymbolStash", + }, + options: options{ + FlagsWithoutCount: true, + HideFlagCountIfEmptySymbol: true, // Should hide empty symbols completely + }, + st: &gitstatus.Status{ + NumStashed: 1, + Porcelain: gitstatus.Porcelain{ + NumModified: 2, + }, + }, + // Empty symbol should be hidden entirely + want: "StyleClear" + "StyleStashSymbolStash", + }, + { + name: "flags_without_count=false + hide_flag_count_if_empty_symbol=false, mixed symbols", + styles: styles{ + Clear: "StyleClear", + Modified: "StyleMod", + Stashed: "StyleStash", + Untracked: "StyleUntracked", + }, + symbols: symbols{ + Modified: "", // empty symbol + Stashed: "SymbolStash", + Untracked: "", // empty symbol + }, + options: options{ + FlagsWithoutCount: false, + HideFlagCountIfEmptySymbol: false, // Show count for empty symbols + }, + st: &gitstatus.Status{ + NumStashed: 1, + Porcelain: gitstatus.Porcelain{ + NumModified: 2, + NumUntracked: 3, + }, + }, + // Empty symbols show count only, normal symbols show symbol+count + want: "StyleClear" + "StyleMod2 StyleStashSymbolStash1 StyleUntracked3", + }, + { + name: "flags_without_count=false + hide_flag_count_if_empty_symbol=true, mixed symbols", + styles: styles{ + Clear: "StyleClear", + Modified: "StyleMod", + Stashed: "StyleStash", + Untracked: "StyleUntracked", + }, + symbols: symbols{ + Modified: "", // empty symbol + Stashed: "SymbolStash", + Untracked: "", // empty symbol + }, + options: options{ + FlagsWithoutCount: false, + HideFlagCountIfEmptySymbol: true, // Hide empty symbols completely + }, + st: &gitstatus.Status{ + NumStashed: 1, + Porcelain: gitstatus.Porcelain{ + NumModified: 2, + NumUntracked: 3, + }, + }, + // Empty symbols are hidden, normal symbols show symbol+count + want: "StyleClear" + "StyleStashSymbolStash1", + }, + { + name: "all flags with different combination settings", + styles: styles{ + Clear: "StyleClear", + Staged: "StyleStaged", + Modified: "StyleMod", + Conflict: "StyleConflict", + Stashed: "StyleStash", + Untracked: "StyleUntracked", + }, + symbols: symbols{ + Staged: "SymbolStaged", + Modified: "", // empty + Conflict: "SymbolConflict", + Stashed: "", // empty + Untracked: "SymbolUntracked", + }, + options: options{ + FlagsWithoutCount: true, // Show symbols without counts + HideFlagCountIfEmptySymbol: false, // But for empty symbols, show count only + }, + st: &gitstatus.Status{ + NumStashed: 2, + Porcelain: gitstatus.Porcelain{ + NumStaged: 1, + NumModified: 3, + NumConflicts: 1, + NumUntracked: 4, + }, + }, + // Non-empty symbols show symbol only (flags_without_count=true) + // Empty symbols show count only (hide_flag_count_if_empty_symbol=false) + want: "StyleClear" + "StyleStagedSymbolStaged StyleConflictSymbolConflict StyleMod3 StyleStash2 StyleUntrackedSymbolUntracked", + }, + } + 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, }