From 899ca856aac20943ec2a4f4d1a1fc8090fbf02bd Mon Sep 17 00:00:00 2001 From: Divy13ansh Date: Sun, 5 Oct 2025 18:37:14 +0000 Subject: [PATCH 1/5] FEAT: custom theme support --- go.mod | 1 + go.sum | 2 ++ internal/tui/model.go | 34 +++++++++++++++++++-- internal/tui/theme.go | 71 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 41eedd7..0392091 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( ) require ( + github.com/BurntSushi/toml v1.5.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.3.2 // indirect diff --git a/go.sum b/go.sum index beffb1d..e349ae8 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= diff --git a/internal/tui/model.go b/internal/tui/model.go index 3afb472..d9771d9 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -51,7 +51,26 @@ type Model struct { // initialModel creates the initial state of the application. func initialModel() Model { - themeNames := ThemeNames() + themeNames := ThemeNames() //built-in themes load + cfg, _ := load_config() + + var selectedThemeName string + if t, ok := Themes[cfg.Theme]; ok{ + selectedThemeName = cfg.Theme + _ = t // to avoid unused variable warning + } else{ + if _, err := load_custom_theme(cfg.Theme); err == nil{ + selectedThemeName = cfg.Theme + } else{ + //fallback + selectedThemeName = themeNames[0] + } + } + + themeNames = ThemeNames() // reload + + // fmt.Println("All themes:", themeNames) + gc := git.NewGitCommands() repoName, branchName, _ := gc.GetRepoInfo() initialContent := initialContentLoading @@ -77,9 +96,9 @@ func initialModel() Model { ta.SetHeight(5) return Model{ - theme: Themes[themeNames[0]], + theme: Themes[selectedThemeName], themeNames: themeNames, - themeIndex: 0, + themeIndex: indexOf(themeNames, selectedThemeName), focusedPanel: StatusPanel, activeSourcePanel: StatusPanel, help: help.New(), @@ -95,6 +114,15 @@ func initialModel() Model { } } +func indexOf(arr []string, val string) int{ + for i, s := range arr{ + if s == val{ + return i + } + } + return 0 +} + // Init is the first command that is run when the program starts. func (m Model) Init() tea.Cmd { // fetch initial content for all panels. diff --git a/internal/tui/theme.go b/internal/tui/theme.go index ecce002..a5322c1 100644 --- a/internal/tui/theme.go +++ b/internal/tui/theme.go @@ -4,8 +4,19 @@ import ( "sort" "github.com/charmbracelet/lipgloss" + + "os" + + "path/filepath" + + "github.com/BurntSushi/toml" + + "fmt" ) +// DefaultThemeName is the name of the default theme. +const DefaultThemeName = "GitHub Dark" + // Palette defines a set of colors for a theme. type Palette struct { Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, @@ -139,6 +150,20 @@ type TreeStyle struct { Connector, ConnectorLast, Prefix, PrefixLast string } +//config.toml +type themeConfig struct{ + Theme string `toml:"theme"` +} + +// custom_theme.toml +type ThemeFile struct{ + Fg string `toml:"fg"` + Bg string `toml:"bg"` + Normal map[string]string `toml:"normal"` + Bright map[string]string `toml:"bright"` + Dark map[string]string `toml:"dark"` +} + // Themes holds all the available themes, generated from palettes. var Themes = map[string]Theme{} @@ -216,3 +241,49 @@ func ThemeNames() []string { sort.Strings(names) return names } + +func load_config() (*themeConfig, error){ + cfgPath := filepath.Join(os.Getenv("HOME"), ".config", "gitx", "config.toml") + if _,err := os.Stat(cfgPath); os.IsNotExist(err) { + return &themeConfig{Theme: DefaultThemeName}, nil //fallback + } + + var cfg themeConfig + if _, err := toml.DecodeFile(cfgPath, &cfg); err != nil { + return nil, err + } + + return &cfg, nil +} + +func load_custom_theme(name string) (*Palette, error){ + themePath := filepath.Join(os.Getenv("HOME"), ".config", "gitx", "themes", name) + if _,err := os.Stat(themePath); os.IsNotExist(err) { + return nil, fmt.Errorf("theme not found: %s", name) + } + + var tf ThemeFile + if _, err := toml.DecodeFile(themePath, &tf); err != nil { + return nil, err + } + + // Create a Palette from the ThemeFile + p := Palette{ + Fg: tf.Fg, + Bg: tf.Bg, + Black: tf.Normal["Black"], Red: tf.Normal["Red"], Green: tf.Normal["Green"], Yellow: tf.Normal["Yellow"], + Blue: tf.Normal["Blue"], Magenta: tf.Normal["Magenta"], Cyan: tf.Normal["Cyan"], White: tf.Normal["White"], + + BrightBlack: tf.Bright["Black"], BrightRed: tf.Bright["Red"], BrightGreen: tf.Bright["Green"], BrightYellow: tf.Bright["Yellow"], + BrightBlue: tf.Bright["Blue"], BrightMagenta: tf.Bright["Magenta"], BrightCyan: tf.Bright["Cyan"], BrightWhite: tf.Bright["White"], + + DarkBlack: tf.Dark["Black"], DarkRed: tf.Dark["Red"], DarkGreen: tf.Dark["Green"], DarkYellow: tf.Dark["Yellow"], + DarkBlue: tf.Dark["Blue"], DarkMagenta: tf.Dark["Magenta"], DarkCyan: tf.Dark["Cyan"], DarkWhite: tf.Dark["White"], + + } + + Palettes[name] = p // Add to Palettes map for future use + Themes[name] = NewThemeFromPalette(p) // Add to Themes map + + return &p, nil +} From 2da0db23fef31e9a1600b9699520ef213732444b Mon Sep 17 00:00:00 2001 From: Divy13ansh Date: Sun, 5 Oct 2025 19:22:41 +0000 Subject: [PATCH 2/5] FIX: don't need to write extension w/ filename --- internal/tui/theme.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tui/theme.go b/internal/tui/theme.go index a5322c1..c18602e 100644 --- a/internal/tui/theme.go +++ b/internal/tui/theme.go @@ -257,7 +257,7 @@ func load_config() (*themeConfig, error){ } func load_custom_theme(name string) (*Palette, error){ - themePath := filepath.Join(os.Getenv("HOME"), ".config", "gitx", "themes", name) + themePath := filepath.Join(os.Getenv("HOME"), ".config", "gitx", "themes", name+".toml") if _,err := os.Stat(themePath); os.IsNotExist(err) { return nil, fmt.Errorf("theme not found: %s", name) } From 3ca584f8f39e2166c3a02409e1a0b45ab9bfaac9 Mon Sep 17 00:00:00 2001 From: Divy13ansh Date: Sun, 5 Oct 2025 19:34:13 +0000 Subject: [PATCH 3/5] ADDED documentation for custom themes --- docs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs b/docs index 8ca9e5b..7255813 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 8ca9e5b5b30999da084dc689ef1b1927ad6709f7 +Subproject commit 725581311d723cc7d91400f072d4a996fc3295a1 From c8bf845db137a6a4f6ed8ea173f219ff6a6d1602 Mon Sep 17 00:00:00 2001 From: Divy13ansh Date: Thu, 9 Oct 2025 00:07:48 +0000 Subject: [PATCH 4/5] refactored config logic to config.go --- internal/tui/config.go | 61 ++++++++++++++++++++++++++++++++++++++++++ internal/tui/theme.go | 7 ++--- 2 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 internal/tui/config.go diff --git a/internal/tui/config.go b/internal/tui/config.go new file mode 100644 index 0000000..7bfa77f --- /dev/null +++ b/internal/tui/config.go @@ -0,0 +1,61 @@ +package tui + +import ( + "fmt" + "os" + "path/filepath" +) + +var ( + ConfigDirName = ".config/gitx" + ConfigFileName = "config.toml" + ConfigDirPath string + ConfigFilePath string + ConfigThemesDirPath string +) + +func initializeConfig() error { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("error getting user home directory: %w", err) + } + + ConfigDirPath = filepath.Join(homeDir, ConfigDirName) + ConfigFilePath = filepath.Join(ConfigDirPath, ConfigFileName) + ConfigThemesDirPath = filepath.Join(ConfigDirPath, "themes") + + err = os.MkdirAll(ConfigDirPath, 0755) + if err != nil { + return fmt.Errorf("error creating config directory: %w", err) + } + + err = os.MkdirAll(ConfigThemesDirPath, 0755) + if err != nil { + return fmt.Errorf("error creating themes directory: %w", err) + } + + _, err = os.Stat(ConfigFilePath) + if err == nil { + // File exists + // fmt.Println("Config file exists:", ConfigFilePath) + } else if os.IsNotExist(err) { + // File does not exist + defaultConfigContent := fmt.Sprintf("Theme = \"%s\"\n", DefaultThemeName) + err = os.WriteFile(ConfigFilePath, []byte(defaultConfigContent), 0644) + if err != nil { + return fmt.Errorf("error creating default config file: %w", err) + } + } else { + // Some other error + return fmt.Errorf("error checking config file: %w", err) + } + + return nil +} + +func init() { + if err := initializeConfig(); err != nil { + fmt.Fprintf(os.Stderr, "Failed to initialize config: %v\n", err) + os.Exit(1) + } +} \ No newline at end of file diff --git a/internal/tui/theme.go b/internal/tui/theme.go index c18602e..6fc1c3d 100644 --- a/internal/tui/theme.go +++ b/internal/tui/theme.go @@ -243,10 +243,7 @@ func ThemeNames() []string { } func load_config() (*themeConfig, error){ - cfgPath := filepath.Join(os.Getenv("HOME"), ".config", "gitx", "config.toml") - if _,err := os.Stat(cfgPath); os.IsNotExist(err) { - return &themeConfig{Theme: DefaultThemeName}, nil //fallback - } + cfgPath := ConfigFilePath var cfg themeConfig if _, err := toml.DecodeFile(cfgPath, &cfg); err != nil { @@ -257,7 +254,7 @@ func load_config() (*themeConfig, error){ } func load_custom_theme(name string) (*Palette, error){ - themePath := filepath.Join(os.Getenv("HOME"), ".config", "gitx", "themes", name+".toml") + themePath := filepath.Join(ConfigThemesDirPath, name + ".toml") if _,err := os.Stat(themePath); os.IsNotExist(err) { return nil, fmt.Errorf("theme not found: %s", name) } From 173247336e431861aaa00fa59627528f59a51889 Mon Sep 17 00:00:00 2001 From: Divy13ansh Date: Thu, 9 Oct 2025 08:53:02 +0000 Subject: [PATCH 5/5] Cleaned config and model --- go.mod | 2 +- internal/tui/config.go | 21 ++++++++------------- internal/tui/model.go | 2 -- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 0392091..75107fa 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24 toolchain go1.24.5 require ( + github.com/BurntSushi/toml v1.5.0 github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/lipgloss v1.1.0 github.com/fsnotify/fsnotify v1.9.0 @@ -12,7 +13,6 @@ require ( ) require ( - github.com/BurntSushi/toml v1.5.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.3.2 // indirect diff --git a/internal/tui/config.go b/internal/tui/config.go index 7bfa77f..4042b77 100644 --- a/internal/tui/config.go +++ b/internal/tui/config.go @@ -34,20 +34,15 @@ func initializeConfig() error { return fmt.Errorf("error creating themes directory: %w", err) } - _, err = os.Stat(ConfigFilePath) - if err == nil { - // File exists - // fmt.Println("Config file exists:", ConfigFilePath) - } else if os.IsNotExist(err) { - // File does not exist - defaultConfigContent := fmt.Sprintf("Theme = \"%s\"\n", DefaultThemeName) - err = os.WriteFile(ConfigFilePath, []byte(defaultConfigContent), 0644) - if err != nil { - return fmt.Errorf("error creating default config file: %w", err) + if _, err := os.Stat(ConfigFilePath); err != nil { + if os.IsNotExist(err) { + defaultConfig := fmt.Sprintf("Theme = %q\n", DefaultThemeName) + if writeErr := os.WriteFile(ConfigFilePath, []byte(defaultConfig), 0644); writeErr != nil { + return fmt.Errorf("failed to create default config file: %w", writeErr) + } + } else { + return fmt.Errorf("failed to check config file: %w", err) } - } else { - // Some other error - return fmt.Errorf("error checking config file: %w", err) } return nil diff --git a/internal/tui/model.go b/internal/tui/model.go index d9771d9..f892ddc 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -69,8 +69,6 @@ func initialModel() Model { themeNames = ThemeNames() // reload - // fmt.Println("All themes:", themeNames) - gc := git.NewGitCommands() repoName, branchName, _ := gc.GetRepoInfo() initialContent := initialContentLoading