diff --git a/go.mod b/go.mod index 41eedd7..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 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/config.go b/internal/tui/config.go new file mode 100644 index 0000000..4042b77 --- /dev/null +++ b/internal/tui/config.go @@ -0,0 +1,56 @@ +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) + } + + 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) + } + } + + 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/model.go b/internal/tui/model.go index 3afb472..f892ddc 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -51,7 +51,24 @@ 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 + gc := git.NewGitCommands() repoName, branchName, _ := gc.GetRepoInfo() initialContent := initialContentLoading @@ -77,9 +94,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 +112,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..6fc1c3d 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,46 @@ func ThemeNames() []string { sort.Strings(names) return names } + +func load_config() (*themeConfig, error){ + cfgPath := ConfigFilePath + + 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(ConfigThemesDirPath, name + ".toml") + 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 +}