From aca5b7516efdb5a769f62e883e75ee5a2445654a Mon Sep 17 00:00:00 2001 From: PeterTimCousins Date: Fri, 3 Apr 2026 14:31:51 +0100 Subject: [PATCH] feat: add per-file-status theme colors for the files panel Add configurable colors for each git file status character (modified, deleted, untracked, added, renamed, copied) in the theme config. Each color falls back to unstagedChangesColor when not set, preserving existing behavior. New theme config keys: - modifiedColor: color for modified files (M) - deletedColor: color for deleted files (D) - untrackedColor: color for untracked files (?) - addedColor: color for added/staged new files (A) - renamedColor: color for renamed files (R) - copiedColor: color for copied files (C) Example config: gui: theme: modifiedColor: - '#e0af68' deletedColor: - '#f7768e' untrackedColor: - '#9ece6a' --- pkg/config/user_config.go | 20 +++++++++++++- pkg/gui/presentation/files.go | 51 ++++++++++++++++++++++++++--------- pkg/theme/theme.go | 39 +++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 14 deletions(-) diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index fde680d12e1..798d613f430 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -233,8 +233,20 @@ type ThemeConfig struct { MarkedBaseCommitFgColor []string `yaml:"markedBaseCommitFgColor"` // Background color of marked base commit (for rebase) MarkedBaseCommitBgColor []string `yaml:"markedBaseCommitBgColor"` - // Color for file with unstaged changes + // Color for file with unstaged changes (used as fallback when per-status colors are not set) UnstagedChangesColor []string `yaml:"unstagedChangesColor" jsonschema:"minItems=1,uniqueItems=true"` + // Color for modified files. Defaults to unstagedChangesColor if empty. + ModifiedColor []string `yaml:"modifiedColor"` + // Color for deleted files. Defaults to unstagedChangesColor if empty. + DeletedColor []string `yaml:"deletedColor"` + // Color for untracked (new) files. Defaults to unstagedChangesColor if empty. + UntrackedColor []string `yaml:"untrackedColor"` + // Color for added (staged new) files. Defaults to green if empty. + AddedColor []string `yaml:"addedColor"` + // Color for renamed files. Defaults to unstagedChangesColor if empty. + RenamedColor []string `yaml:"renamedColor"` + // Color for copied files. Defaults to cyan if empty. + CopiedColor []string `yaml:"copiedColor"` // Default text color DefaultFgColor []string `yaml:"defaultFgColor" jsonschema:"minItems=1,uniqueItems=true"` } @@ -796,6 +808,12 @@ func GetDefaultConfig() *UserConfig { MarkedBaseCommitBgColor: []string{"yellow"}, MarkedBaseCommitFgColor: []string{"blue"}, UnstagedChangesColor: []string{"red"}, + ModifiedColor: []string{}, + DeletedColor: []string{}, + UntrackedColor: []string{}, + AddedColor: []string{}, + RenamedColor: []string{}, + CopiedColor: []string{}, DefaultFgColor: []string{"default"}, }, CommitLength: CommitLengthConfig{Show: true}, diff --git a/pkg/gui/presentation/files.go b/pkg/gui/presentation/files.go index cc0a9388972..0d2afa47436 100644 --- a/pkg/gui/presentation/files.go +++ b/pkg/gui/presentation/files.go @@ -183,23 +183,46 @@ func getFileLine( func formatFileStatus(file *models.File, restColor style.TextStyle) string { firstChar := file.ShortStatus[0:1] - firstCharCl := style.FgGreen - switch firstChar { - case "?": - firstCharCl = theme.UnstagedChangesColor - case " ": + firstCharCl := colorForStatusChar(firstChar, true) + if firstCharCl == (style.TextStyle{}) { firstCharCl = restColor } secondChar := file.ShortStatus[1:2] - secondCharCl := theme.UnstagedChangesColor - if secondChar == " " { + secondCharCl := colorForStatusChar(secondChar, false) + if secondCharCl == (style.TextStyle{}) { secondCharCl = restColor } return firstCharCl.Sprint(firstChar) + secondCharCl.Sprint(secondChar) } +// colorForStatusChar returns the color for a single status character from git short status. +// isStagedColumn indicates whether this is the first (index/staged) or second (worktree) column. +func colorForStatusChar(ch string, isStagedColumn bool) style.TextStyle { + switch ch { + case "M": + return theme.ModifiedColor + case "D": + return theme.DeletedColor + case "?": + return theme.UntrackedColor + case "A": + return theme.AddedColor + case "R": + return theme.RenamedColor + case "C": + return theme.CopiedColor + case "T": + return theme.ModifiedColor + case " ": + // space means no change in this column — use rest/name color (caller handles this) + return style.TextStyle{} + default: + return theme.UnstagedChangesColor + } +} + func formatLineChanges(linesAdded, linesDeleted int) string { output := "" @@ -285,15 +308,17 @@ func getCommitFileLine( func getColorForChangeStatus(changeStatus string) style.TextStyle { switch changeStatus { case "A": - return style.FgGreen - case "M", "R": - return style.FgYellow + return theme.AddedColor + case "M": + return theme.ModifiedColor + case "R": + return theme.RenamedColor case "D": - return theme.UnstagedChangesColor + return theme.DeletedColor case "C": - return style.FgCyan + return theme.CopiedColor case "T": - return style.FgMagenta + return theme.ModifiedColor default: return theme.DefaultTextColor } diff --git a/pkg/theme/theme.go b/pkg/theme/theme.go index acd8ebf7161..f1c9c2fb972 100644 --- a/pkg/theme/theme.go +++ b/pkg/theme/theme.go @@ -45,6 +45,13 @@ var ( DiffTerminalColor = style.FgMagenta UnstagedChangesColor = style.New() + + ModifiedColor = style.New() + DeletedColor = style.New() + UntrackedColor = style.New() + AddedColor = style.New() + RenamedColor = style.New() + CopiedColor = style.New() ) // UpdateTheme updates all theme variables @@ -66,6 +73,38 @@ func UpdateTheme(themeConfig config.ThemeConfig) { unstagedChangesTextStyle := GetTextStyle(themeConfig.UnstagedChangesColor, false) UnstagedChangesColor = unstagedChangesTextStyle + // Per-status colors — fall back to UnstagedChangesColor or hardcoded defaults if not set + if len(themeConfig.ModifiedColor) > 0 { + ModifiedColor = GetTextStyle(themeConfig.ModifiedColor, false) + } else { + ModifiedColor = UnstagedChangesColor + } + if len(themeConfig.DeletedColor) > 0 { + DeletedColor = GetTextStyle(themeConfig.DeletedColor, false) + } else { + DeletedColor = UnstagedChangesColor + } + if len(themeConfig.UntrackedColor) > 0 { + UntrackedColor = GetTextStyle(themeConfig.UntrackedColor, false) + } else { + UntrackedColor = UnstagedChangesColor + } + if len(themeConfig.AddedColor) > 0 { + AddedColor = GetTextStyle(themeConfig.AddedColor, false) + } else { + AddedColor = style.FgGreen + } + if len(themeConfig.RenamedColor) > 0 { + RenamedColor = GetTextStyle(themeConfig.RenamedColor, false) + } else { + RenamedColor = UnstagedChangesColor + } + if len(themeConfig.CopiedColor) > 0 { + CopiedColor = GetTextStyle(themeConfig.CopiedColor, false) + } else { + CopiedColor = style.FgCyan + } + GocuiSelectedLineBgColor = GetGocuiStyle(themeConfig.SelectedLineBgColor) GocuiInactiveViewSelectedLineBgColor = GetGocuiStyle(themeConfig.InactiveViewSelectedLineBgColor) OptionsColor = GetGocuiStyle(themeConfig.OptionsTextColor)