feat(theming): add theme discovery with gruvbox assets

This commit is contained in:
MDietermann
2026-04-14 12:56:09 +02:00
parent 465d10a5ef
commit b628898c6d
7 changed files with 245 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
gui:
theme:
activeBorderColor:
- "#fabd2f"
- bold
inactiveBorderColor:
- "#665c54"
selectedLineBgColor:
- "#3c3836"
cherryPickedCommitFgColor:
- "#b8bb26"
defaultFgColor:
- "#ebdbb2"

View File

@@ -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"

View File

@@ -0,0 +1,2 @@
display_name: "Gruvbox Dark"
nvchad: "gruvbox"

View File

@@ -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 = {}

View File

@@ -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"
}
}

View File

@@ -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/<id>/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/<id>/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
}

View File

@@ -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)
}
}
}