Skip to content

Commit 6c04133

Browse files
authored
feat: custom theme support (#28)
* FEAT: custom theme support * FIX: don't need to write extension w/ filename * ADDED documentation for custom themes * refactored config logic to config.go * Cleaned config and model
1 parent 134066b commit 6c04133

File tree

5 files changed

+156
-3
lines changed

5 files changed

+156
-3
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.24
55
toolchain go1.24.5
66

77
require (
8+
github.com/BurntSushi/toml v1.5.0
89
github.com/charmbracelet/bubbles v0.21.0
910
github.com/charmbracelet/lipgloss v1.1.0
1011
github.com/fsnotify/fsnotify v1.9.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
2+
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
13
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
24
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
35
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=

internal/tui/config.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package tui
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
)
8+
9+
var (
10+
ConfigDirName = ".config/gitx"
11+
ConfigFileName = "config.toml"
12+
ConfigDirPath string
13+
ConfigFilePath string
14+
ConfigThemesDirPath string
15+
)
16+
17+
func initializeConfig() error {
18+
homeDir, err := os.UserHomeDir()
19+
if err != nil {
20+
return fmt.Errorf("error getting user home directory: %w", err)
21+
}
22+
23+
ConfigDirPath = filepath.Join(homeDir, ConfigDirName)
24+
ConfigFilePath = filepath.Join(ConfigDirPath, ConfigFileName)
25+
ConfigThemesDirPath = filepath.Join(ConfigDirPath, "themes")
26+
27+
err = os.MkdirAll(ConfigDirPath, 0755)
28+
if err != nil {
29+
return fmt.Errorf("error creating config directory: %w", err)
30+
}
31+
32+
err = os.MkdirAll(ConfigThemesDirPath, 0755)
33+
if err != nil {
34+
return fmt.Errorf("error creating themes directory: %w", err)
35+
}
36+
37+
if _, err := os.Stat(ConfigFilePath); err != nil {
38+
if os.IsNotExist(err) {
39+
defaultConfig := fmt.Sprintf("Theme = %q\n", DefaultThemeName)
40+
if writeErr := os.WriteFile(ConfigFilePath, []byte(defaultConfig), 0644); writeErr != nil {
41+
return fmt.Errorf("failed to create default config file: %w", writeErr)
42+
}
43+
} else {
44+
return fmt.Errorf("failed to check config file: %w", err)
45+
}
46+
}
47+
48+
return nil
49+
}
50+
51+
func init() {
52+
if err := initializeConfig(); err != nil {
53+
fmt.Fprintf(os.Stderr, "Failed to initialize config: %v\n", err)
54+
os.Exit(1)
55+
}
56+
}

internal/tui/model.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,24 @@ type Model struct {
5151

5252
// initialModel creates the initial state of the application.
5353
func initialModel() Model {
54-
themeNames := ThemeNames()
54+
themeNames := ThemeNames() //built-in themes load
55+
cfg, _ := load_config()
56+
57+
var selectedThemeName string
58+
if t, ok := Themes[cfg.Theme]; ok{
59+
selectedThemeName = cfg.Theme
60+
_ = t // to avoid unused variable warning
61+
} else{
62+
if _, err := load_custom_theme(cfg.Theme); err == nil{
63+
selectedThemeName = cfg.Theme
64+
} else{
65+
//fallback
66+
selectedThemeName = themeNames[0]
67+
}
68+
}
69+
70+
themeNames = ThemeNames() // reload
71+
5572
gc := git.NewGitCommands()
5673
repoName, branchName, _ := gc.GetRepoInfo()
5774
initialContent := initialContentLoading
@@ -77,9 +94,9 @@ func initialModel() Model {
7794
ta.SetHeight(5)
7895

7996
return Model{
80-
theme: Themes[themeNames[0]],
97+
theme: Themes[selectedThemeName],
8198
themeNames: themeNames,
82-
themeIndex: 0,
99+
themeIndex: indexOf(themeNames, selectedThemeName),
83100
focusedPanel: StatusPanel,
84101
activeSourcePanel: StatusPanel,
85102
help: help.New(),
@@ -95,6 +112,15 @@ func initialModel() Model {
95112
}
96113
}
97114

115+
func indexOf(arr []string, val string) int{
116+
for i, s := range arr{
117+
if s == val{
118+
return i
119+
}
120+
}
121+
return 0
122+
}
123+
98124
// Init is the first command that is run when the program starts.
99125
func (m Model) Init() tea.Cmd {
100126
// fetch initial content for all panels.

internal/tui/theme.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,19 @@ import (
44
"sort"
55

66
"github.com/charmbracelet/lipgloss"
7+
8+
"os"
9+
10+
"path/filepath"
11+
12+
"github.com/BurntSushi/toml"
13+
14+
"fmt"
715
)
816

17+
// DefaultThemeName is the name of the default theme.
18+
const DefaultThemeName = "GitHub Dark"
19+
920
// Palette defines a set of colors for a theme.
1021
type Palette struct {
1122
Black, Red, Green, Yellow, Blue, Magenta, Cyan, White,
@@ -139,6 +150,20 @@ type TreeStyle struct {
139150
Connector, ConnectorLast, Prefix, PrefixLast string
140151
}
141152

153+
//config.toml
154+
type themeConfig struct{
155+
Theme string `toml:"theme"`
156+
}
157+
158+
// custom_theme.toml
159+
type ThemeFile struct{
160+
Fg string `toml:"fg"`
161+
Bg string `toml:"bg"`
162+
Normal map[string]string `toml:"normal"`
163+
Bright map[string]string `toml:"bright"`
164+
Dark map[string]string `toml:"dark"`
165+
}
166+
142167
// Themes holds all the available themes, generated from palettes.
143168
var Themes = map[string]Theme{}
144169

@@ -216,3 +241,46 @@ func ThemeNames() []string {
216241
sort.Strings(names)
217242
return names
218243
}
244+
245+
func load_config() (*themeConfig, error){
246+
cfgPath := ConfigFilePath
247+
248+
var cfg themeConfig
249+
if _, err := toml.DecodeFile(cfgPath, &cfg); err != nil {
250+
return nil, err
251+
}
252+
253+
return &cfg, nil
254+
}
255+
256+
func load_custom_theme(name string) (*Palette, error){
257+
themePath := filepath.Join(ConfigThemesDirPath, name + ".toml")
258+
if _,err := os.Stat(themePath); os.IsNotExist(err) {
259+
return nil, fmt.Errorf("theme not found: %s", name)
260+
}
261+
262+
var tf ThemeFile
263+
if _, err := toml.DecodeFile(themePath, &tf); err != nil {
264+
return nil, err
265+
}
266+
267+
// Create a Palette from the ThemeFile
268+
p := Palette{
269+
Fg: tf.Fg,
270+
Bg: tf.Bg,
271+
Black: tf.Normal["Black"], Red: tf.Normal["Red"], Green: tf.Normal["Green"], Yellow: tf.Normal["Yellow"],
272+
Blue: tf.Normal["Blue"], Magenta: tf.Normal["Magenta"], Cyan: tf.Normal["Cyan"], White: tf.Normal["White"],
273+
274+
BrightBlack: tf.Bright["Black"], BrightRed: tf.Bright["Red"], BrightGreen: tf.Bright["Green"], BrightYellow: tf.Bright["Yellow"],
275+
BrightBlue: tf.Bright["Blue"], BrightMagenta: tf.Bright["Magenta"], BrightCyan: tf.Bright["Cyan"], BrightWhite: tf.Bright["White"],
276+
277+
DarkBlack: tf.Dark["Black"], DarkRed: tf.Dark["Red"], DarkGreen: tf.Dark["Green"], DarkYellow: tf.Dark["Yellow"],
278+
DarkBlue: tf.Dark["Blue"], DarkMagenta: tf.Dark["Magenta"], DarkCyan: tf.Dark["Cyan"], DarkWhite: tf.Dark["White"],
279+
280+
}
281+
282+
Palettes[name] = p // Add to Palettes map for future use
283+
Themes[name] = NewThemeFromPalette(p) // Add to Themes map
284+
285+
return &p, nil
286+
}

0 commit comments

Comments
 (0)