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