From b628898c6dde82dff8ab7cd5099feb21795f0a7c Mon Sep 17 00:00:00 2001 From: MDietermann Date: Tue, 14 Apr 2026 12:56:09 +0200 Subject: [PATCH] feat(theming): add theme discovery with gruvbox assets --- internal/theming/assets/gruvbox/lazygit.yml | 13 +++ internal/theming/assets/gruvbox/starship.toml | 26 +++++ internal/theming/assets/gruvbox/theme.yaml | 2 + internal/theming/assets/gruvbox/yazi.toml | 30 ++++++ internal/theming/assets/gruvbox/zellij.kdl | 35 +++++++ internal/theming/theming.go | 97 +++++++++++++++++++ internal/theming/theming_test.go | 42 ++++++++ 7 files changed, 245 insertions(+) create mode 100644 internal/theming/assets/gruvbox/lazygit.yml create mode 100644 internal/theming/assets/gruvbox/starship.toml create mode 100644 internal/theming/assets/gruvbox/theme.yaml create mode 100644 internal/theming/assets/gruvbox/yazi.toml create mode 100644 internal/theming/assets/gruvbox/zellij.kdl create mode 100644 internal/theming/theming.go create mode 100644 internal/theming/theming_test.go diff --git a/internal/theming/assets/gruvbox/lazygit.yml b/internal/theming/assets/gruvbox/lazygit.yml new file mode 100644 index 0000000..1904339 --- /dev/null +++ b/internal/theming/assets/gruvbox/lazygit.yml @@ -0,0 +1,13 @@ +gui: + theme: + activeBorderColor: + - "#fabd2f" + - bold + inactiveBorderColor: + - "#665c54" + selectedLineBgColor: + - "#3c3836" + cherryPickedCommitFgColor: + - "#b8bb26" + defaultFgColor: + - "#ebdbb2" diff --git a/internal/theming/assets/gruvbox/starship.toml b/internal/theming/assets/gruvbox/starship.toml new file mode 100644 index 0000000..d3ae401 --- /dev/null +++ b/internal/theming/assets/gruvbox/starship.toml @@ -0,0 +1,26 @@ +palette = "gruvbox_dark" +format = """$directory$git_branch$git_status$character""" + +[directory] +style = "bold fg:bright-yellow" +truncation_length = 3 + +[git_branch] +style = "bold fg:bright-green" +format = " [$branch]($style) " + +[git_status] +style = "bold fg:bright-red" + +[character] +success_symbol = "[❯](bold fg:bright-green)" +error_symbol = "[❯](bold fg:bright-red)" + +[palettes.gruvbox_dark] +bright-yellow = "#fabd2f" +bright-green = "#b8bb26" +bright-red = "#fb4934" +bright-blue = "#83a598" +bright-purple = "#d3869b" +bright-aqua = "#8ec07c" +bright-orange = "#fe8019" diff --git a/internal/theming/assets/gruvbox/theme.yaml b/internal/theming/assets/gruvbox/theme.yaml new file mode 100644 index 0000000..5a5fb54 --- /dev/null +++ b/internal/theming/assets/gruvbox/theme.yaml @@ -0,0 +1,2 @@ +display_name: "Gruvbox Dark" +nvchad: "gruvbox" diff --git a/internal/theming/assets/gruvbox/yazi.toml b/internal/theming/assets/gruvbox/yazi.toml new file mode 100644 index 0000000..c29bdbe --- /dev/null +++ b/internal/theming/assets/gruvbox/yazi.toml @@ -0,0 +1,30 @@ +[manager] +cwd = { fg = "#fabd2f" } +tab_active = { fg = "#282828", bg = "#fabd2f" } +tab_inactive = { fg = "#a89984", bg = "#3c3836" } +border_symbol = "│" +border_style = { fg = "#665c54" } +count_copied = { fg = "#282828", bg = "#b8bb26" } +count_cut = { fg = "#282828", bg = "#fb4934" } +count_selected = { fg = "#282828", bg = "#fabd2f" } + +[status] +separator_open = "" +separator_close = "" +mode_normal = { fg = "#282828", bg = "#b8bb26", bold = true } +mode_select = { fg = "#282828", bg = "#fabd2f", bold = true } +mode_unset = { fg = "#282828", bg = "#fb4934", bold = true } +progress_label = { fg = "#ebdbb2", bold = true } +progress_normal = { fg = "#458588", bg = "#3c3836" } +progress_error = { fg = "#fb4934", bg = "#3c3836" } + +[input] +border = { fg = "#fabd2f" } +title = {} +value = {} +selected = { reversed = true } + +[select] +border = { fg = "#fabd2f" } +active = { fg = "#b8bb26", bold = true } +inactive = {} diff --git a/internal/theming/assets/gruvbox/zellij.kdl b/internal/theming/assets/gruvbox/zellij.kdl new file mode 100644 index 0000000..eed4be2 --- /dev/null +++ b/internal/theming/assets/gruvbox/zellij.kdl @@ -0,0 +1,35 @@ +theme "gruvbox-dark" +default_layout "default" +default_mode "normal" +pane_frames false +simplified_ui true +mouse_mode false + +keybinds { + shared { + bind "Alt h" { MoveFocusOrTab "Left"; } + bind "Alt l" { MoveFocusOrTab "Right"; } + bind "Alt j" { MoveFocus "Down"; } + bind "Alt k" { MoveFocus "Up"; } + bind "Alt n" { NewPane "Down"; } + bind "Alt v" { NewPane "Right"; } + bind "Alt f" { ToggleFloatingPanes; } + bind "Alt t" { NewTab; } + } +} + +themes { + gruvbox-dark { + fg "#ebdbb2" + bg "#282828" + black "#1d2021" + red "#cc241d" + green "#98971a" + yellow "#d79921" + blue "#458588" + magenta "#b16286" + cyan "#689d6a" + white "#a89984" + orange "#d65d0e" + } +} diff --git a/internal/theming/theming.go b/internal/theming/theming.go new file mode 100644 index 0000000..9cffc8d --- /dev/null +++ b/internal/theming/theming.go @@ -0,0 +1,97 @@ +// Package theming applies a selected colorscheme across starship, zellij, +// yazi, lazygit, and NvChad. Themes are discovered from embedded manifest +// files under assets//theme.yaml at package init time. +package theming + +import ( + "embed" + "fmt" + "io/fs" + "path/filepath" + "sort" + + "gopkg.in/yaml.v3" +) + +//go:embed assets +var assetsFS embed.FS + +// Theme describes a colorscheme discovered under assets//theme.yaml. +type Theme struct { + ID string + DisplayName string + NvChadTheme string +} + +type manifest struct { + DisplayName string `yaml:"display_name"` + NvChad string `yaml:"nvchad"` +} + +var discovered []Theme + +func init() { + themes, err := discover() + if err != nil { + // Package is embed-only; discovery failure is a programmer error, + // not runtime input. Panic so tests surface the mistake early. + panic(fmt.Errorf("theming: discover themes: %w", err)) + } + discovered = themes +} + +func discover() ([]Theme, error) { + entries, err := fs.ReadDir(assetsFS, "assets") + if err != nil { + return nil, err + } + var out []Theme + for _, e := range entries { + if !e.IsDir() { + continue + } + id := e.Name() + m, err := readManifest(id) + if err != nil { + return nil, fmt.Errorf("theme %q: %w", id, err) + } + out = append(out, Theme{ID: id, DisplayName: m.DisplayName, NvChadTheme: m.NvChad}) + } + sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) + return out, nil +} + +func readManifest(id string) (manifest, error) { + data, err := assetsFS.ReadFile(filepath.Join("assets", id, "theme.yaml")) + if err != nil { + return manifest{}, err + } + var m manifest + if err := yaml.Unmarshal(data, &m); err != nil { + return manifest{}, err + } + if m.DisplayName == "" { + return manifest{}, fmt.Errorf("empty display_name") + } + if m.NvChad == "" { + return manifest{}, fmt.Errorf("empty nvchad") + } + return m, nil +} + +// List returns the themes sorted by ID. +func List() []Theme { + out := make([]Theme, len(discovered)) + copy(out, discovered) + return out +} + +// Get returns the theme with the given ID, if present. +func Get(id string) (Theme, bool) { + for _, t := range discovered { + if t.ID == id { + return t, true + } + } + return Theme{}, false +} diff --git a/internal/theming/theming_test.go b/internal/theming/theming_test.go new file mode 100644 index 0000000..ec3cf24 --- /dev/null +++ b/internal/theming/theming_test.go @@ -0,0 +1,42 @@ +package theming + +import ( + "testing" +) + +func TestListDiscoversThemes(t *testing.T) { + themes := List() + if len(themes) == 0 { + t.Fatal("no themes discovered") + } + for _, th := range themes { + if th.ID == "" || th.DisplayName == "" || th.NvChadTheme == "" { + t.Errorf("incomplete theme: %+v", th) + } + } +} + +func TestGetKnownThemeReturnsMatch(t *testing.T) { + got, ok := Get("gruvbox") + if !ok { + t.Fatal("gruvbox should exist") + } + if got.ID != "gruvbox" || got.DisplayName == "" || got.NvChadTheme != "gruvbox" { + t.Fatalf("got %+v", got) + } +} + +func TestGetUnknownThemeReturnsFalse(t *testing.T) { + if _, ok := Get("does-not-exist"); ok { + t.Fatal("bogus id should return ok=false") + } +} + +func TestListIsSortedByID(t *testing.T) { + themes := List() + for i := 1; i < len(themes); i++ { + if themes[i].ID < themes[i-1].ID { + t.Fatalf("not sorted: %s before %s", themes[i-1].ID, themes[i].ID) + } + } +}