// 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" "os" "path/filepath" "sort" "strings" "github.com/MDietermann/ArduinoArchSetup/internal/config" "github.com/MDietermann/ArduinoArchSetup/internal/platform" "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 } // toolConfig describes one tool's config file: its asset name under // assets// and its user-config destination relative to $HOME. type toolConfig struct { Asset string // e.g. "starship.toml" DestRel string // e.g. ".config/starship.toml" } var toolConfigs = []toolConfig{ {"starship.toml", ".config/starship.toml"}, {"zellij.kdl", ".config/zellij/config.kdl"}, {"yazi.toml", ".config/yazi/theme.toml"}, {"lazygit.yml", ".config/lazygit/config.yml"}, } // Apply writes each tool's theme config to the user's home directory. // Task 3 adds overlay-file update; Task 4 adds NvChad chadrc rewrite. func Apply(cfg config.Config, themeID string) error { th, ok := Get(themeID) if !ok { ids := make([]string, 0, len(discovered)) for _, t := range discovered { ids = append(ids, t.ID) } return fmt.Errorf("unknown theme %q; valid: %s", themeID, strings.Join(ids, ", ")) } home, err := os.UserHomeDir() if err != nil { return err } for _, tc := range toolConfigs { data, err := assetsFS.ReadFile(filepath.Join("assets", th.ID, tc.Asset)) if err != nil { return fmt.Errorf("read asset %s: %w", tc.Asset, err) } dst := filepath.Join(home, tc.DestRel) if err := platform.WriteUserFile(dst, data, 0o644); err != nil { return fmt.Errorf("write %s: %w", tc.DestRel, err) } } _ = cfg // overlay update lands in Task 3 return nil }