202 lines
5.2 KiB
Go
202 lines
5.2 KiB
Go
// 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"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
|
"github.com/MDietermann/ArduinoArchSetup/internal/modules/neovim"
|
|
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
|
"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
|
|
}
|
|
|
|
// toolConfig describes one tool's config file: its asset name under
|
|
// assets/<theme>/ 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 carries config state; overlay file update below
|
|
if err := updateOverlayActive(th.ID); err != nil {
|
|
return fmt.Errorf("update overlay: %w", err)
|
|
}
|
|
|
|
if err := neovim.WriteChadrc(th.NvChadTheme); err != nil {
|
|
return fmt.Errorf("update chadrc: %w", err)
|
|
}
|
|
|
|
// Clear NvChad's base46 theme cache so the new theme activates on next
|
|
// nvim launch. Errors ignored — the cache may not exist.
|
|
_ = os.RemoveAll(filepath.Join(home, ".cache", "nvim", "base46"))
|
|
_ = os.RemoveAll(filepath.Join(home, ".local", "share", "nvim", "lazy", "base46", "lua", "base46", "themes"))
|
|
return nil
|
|
}
|
|
|
|
// updateOverlayActive rewrites ~/.config/archduino/overlay.yaml so that
|
|
// theme_overrides.__active reflects the selected theme, preserving all
|
|
// other keys via a generic map round-trip.
|
|
func updateOverlayActive(themeID string) error {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path := filepath.Join(home, ".config", "archduino", "overlay.yaml")
|
|
|
|
var root map[string]any
|
|
if data, err := os.ReadFile(path); err == nil {
|
|
if err := yaml.Unmarshal(data, &root); err != nil {
|
|
return fmt.Errorf("parse overlay: %w", err)
|
|
}
|
|
}
|
|
if root == nil {
|
|
root = map[string]any{}
|
|
}
|
|
|
|
if _, ok := root["version"]; !ok {
|
|
root["version"] = 1
|
|
}
|
|
|
|
var overrides map[string]any
|
|
if raw, ok := root["theme_overrides"]; ok {
|
|
if m, ok := raw.(map[string]any); ok {
|
|
overrides = m
|
|
}
|
|
}
|
|
if overrides == nil {
|
|
overrides = map[string]any{}
|
|
}
|
|
overrides["__active"] = themeID
|
|
root["theme_overrides"] = overrides
|
|
|
|
out, err := yaml.Marshal(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return platform.WriteUserFile(path, out, 0o644)
|
|
}
|