feat(theming): add theme discovery with gruvbox assets
This commit is contained in:
13
internal/theming/assets/gruvbox/lazygit.yml
Normal file
13
internal/theming/assets/gruvbox/lazygit.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
gui:
|
||||||
|
theme:
|
||||||
|
activeBorderColor:
|
||||||
|
- "#fabd2f"
|
||||||
|
- bold
|
||||||
|
inactiveBorderColor:
|
||||||
|
- "#665c54"
|
||||||
|
selectedLineBgColor:
|
||||||
|
- "#3c3836"
|
||||||
|
cherryPickedCommitFgColor:
|
||||||
|
- "#b8bb26"
|
||||||
|
defaultFgColor:
|
||||||
|
- "#ebdbb2"
|
||||||
26
internal/theming/assets/gruvbox/starship.toml
Normal file
26
internal/theming/assets/gruvbox/starship.toml
Normal 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"
|
||||||
2
internal/theming/assets/gruvbox/theme.yaml
Normal file
2
internal/theming/assets/gruvbox/theme.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
display_name: "Gruvbox Dark"
|
||||||
|
nvchad: "gruvbox"
|
||||||
30
internal/theming/assets/gruvbox/yazi.toml
Normal file
30
internal/theming/assets/gruvbox/yazi.toml
Normal 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 = {}
|
||||||
35
internal/theming/assets/gruvbox/zellij.kdl
Normal file
35
internal/theming/assets/gruvbox/zellij.kdl
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
97
internal/theming/theming.go
Normal file
97
internal/theming/theming.go
Normal 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
|
||||||
|
}
|
||||||
42
internal/theming/theming_test.go
Normal file
42
internal/theming/theming_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user