From b628898c6dde82dff8ab7cd5099feb21795f0a7c Mon Sep 17 00:00:00 2001 From: MDietermann Date: Tue, 14 Apr 2026 12:56:09 +0200 Subject: [PATCH 01/11] feat(theming): add theme discovery with gruvbox assets --- internal/theming/assets/gruvbox/lazygit.yml | 13 +++ internal/theming/assets/gruvbox/starship.toml | 26 +++++ internal/theming/assets/gruvbox/theme.yaml | 2 + internal/theming/assets/gruvbox/yazi.toml | 30 ++++++ internal/theming/assets/gruvbox/zellij.kdl | 35 +++++++ internal/theming/theming.go | 97 +++++++++++++++++++ internal/theming/theming_test.go | 42 ++++++++ 7 files changed, 245 insertions(+) create mode 100644 internal/theming/assets/gruvbox/lazygit.yml create mode 100644 internal/theming/assets/gruvbox/starship.toml create mode 100644 internal/theming/assets/gruvbox/theme.yaml create mode 100644 internal/theming/assets/gruvbox/yazi.toml create mode 100644 internal/theming/assets/gruvbox/zellij.kdl create mode 100644 internal/theming/theming.go create mode 100644 internal/theming/theming_test.go diff --git a/internal/theming/assets/gruvbox/lazygit.yml b/internal/theming/assets/gruvbox/lazygit.yml new file mode 100644 index 0000000..1904339 --- /dev/null +++ b/internal/theming/assets/gruvbox/lazygit.yml @@ -0,0 +1,13 @@ +gui: + theme: + activeBorderColor: + - "#fabd2f" + - bold + inactiveBorderColor: + - "#665c54" + selectedLineBgColor: + - "#3c3836" + cherryPickedCommitFgColor: + - "#b8bb26" + defaultFgColor: + - "#ebdbb2" diff --git a/internal/theming/assets/gruvbox/starship.toml b/internal/theming/assets/gruvbox/starship.toml new file mode 100644 index 0000000..d3ae401 --- /dev/null +++ b/internal/theming/assets/gruvbox/starship.toml @@ -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" diff --git a/internal/theming/assets/gruvbox/theme.yaml b/internal/theming/assets/gruvbox/theme.yaml new file mode 100644 index 0000000..5a5fb54 --- /dev/null +++ b/internal/theming/assets/gruvbox/theme.yaml @@ -0,0 +1,2 @@ +display_name: "Gruvbox Dark" +nvchad: "gruvbox" diff --git a/internal/theming/assets/gruvbox/yazi.toml b/internal/theming/assets/gruvbox/yazi.toml new file mode 100644 index 0000000..c29bdbe --- /dev/null +++ b/internal/theming/assets/gruvbox/yazi.toml @@ -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 = {} diff --git a/internal/theming/assets/gruvbox/zellij.kdl b/internal/theming/assets/gruvbox/zellij.kdl new file mode 100644 index 0000000..eed4be2 --- /dev/null +++ b/internal/theming/assets/gruvbox/zellij.kdl @@ -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" + } +} diff --git a/internal/theming/theming.go b/internal/theming/theming.go new file mode 100644 index 0000000..9cffc8d --- /dev/null +++ b/internal/theming/theming.go @@ -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//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//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 +} diff --git a/internal/theming/theming_test.go b/internal/theming/theming_test.go new file mode 100644 index 0000000..ec3cf24 --- /dev/null +++ b/internal/theming/theming_test.go @@ -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) + } + } +} From b422402ffc3b3e48f859c72df0353900ecd800b6 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Tue, 14 Apr 2026 13:07:08 +0200 Subject: [PATCH 02/11] feat(theming): apply writes four tool configs to $HOME --- internal/theming/theming.go | 48 ++++++++++++++++++++++++++++++++ internal/theming/theming_test.go | 39 ++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/internal/theming/theming.go b/internal/theming/theming.go index 9cffc8d..bc176bd 100644 --- a/internal/theming/theming.go +++ b/internal/theming/theming.go @@ -7,9 +7,13 @@ 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" ) @@ -95,3 +99,47 @@ func Get(id string) (Theme, bool) { } 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 +} diff --git a/internal/theming/theming_test.go b/internal/theming/theming_test.go index ec3cf24..e7366cf 100644 --- a/internal/theming/theming_test.go +++ b/internal/theming/theming_test.go @@ -1,7 +1,12 @@ package theming import ( + "os" + "path/filepath" + "strings" "testing" + + "github.com/MDietermann/ArduinoArchSetup/internal/config" ) func TestListDiscoversThemes(t *testing.T) { @@ -40,3 +45,37 @@ func TestListIsSortedByID(t *testing.T) { } } } + +func TestApplyWritesFourToolConfigs(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + if err := Apply(config.Config{}, "gruvbox"); err != nil { + t.Fatal(err) + } + + checks := []struct{ rel, wantPrefix string }{ + {".config/starship.toml", "palette"}, + {".config/zellij/config.kdl", "theme"}, + {".config/yazi/theme.toml", ""}, + {".config/lazygit/config.yml", "gui"}, + } + for _, c := range checks { + data, err := os.ReadFile(filepath.Join(home, c.rel)) + if err != nil { + t.Errorf("%s missing: %v", c.rel, err) + continue + } + if len(data) == 0 { + t.Errorf("%s empty", c.rel) + } + if c.wantPrefix != "" && !strings.HasPrefix(strings.TrimSpace(string(data)), c.wantPrefix) { + t.Errorf("%s: want prefix %q, got %q", c.rel, c.wantPrefix, string(data[:min(len(data), 40)])) + } + } +} + +func TestApplyUnknownThemeErrors(t *testing.T) { + if err := Apply(config.Config{}, "does-not-exist"); err == nil { + t.Fatal("want error on unknown theme") + } +} From 4fa580f226bc1951a6d45c7a70f3b166355ffffd Mon Sep 17 00:00:00 2001 From: MDietermann Date: Tue, 14 Apr 2026 13:11:34 +0200 Subject: [PATCH 03/11] feat(theming): apply updates overlay theme_overrides.__active --- internal/theming/theming.go | 48 +++++++++++++++++++++++++++++++- internal/theming/theming_test.go | 38 +++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/internal/theming/theming.go b/internal/theming/theming.go index bc176bd..b1369b4 100644 --- a/internal/theming/theming.go +++ b/internal/theming/theming.go @@ -140,6 +140,52 @@ func Apply(cfg config.Config, themeID string) error { return fmt.Errorf("write %s: %w", tc.DestRel, err) } } - _ = cfg // overlay update lands in Task 3 + _ = cfg // overlay carries config state; overlay file update below + if err := updateOverlayActive(th.ID); err != nil { + return fmt.Errorf("update overlay: %w", err) + } 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) +} diff --git a/internal/theming/theming_test.go b/internal/theming/theming_test.go index e7366cf..0e5f02c 100644 --- a/internal/theming/theming_test.go +++ b/internal/theming/theming_test.go @@ -79,3 +79,41 @@ func TestApplyUnknownThemeErrors(t *testing.T) { t.Fatal("want error on unknown theme") } } + +func TestApplyUpdatesOverlayActive(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + if err := Apply(config.Config{}, "gruvbox"); err != nil { + t.Fatal(err) + } + data, err := os.ReadFile(filepath.Join(home, ".config", "archduino", "overlay.yaml")) + if err != nil { + t.Fatalf("overlay missing: %v", err) + } + if !strings.Contains(string(data), "__active: gruvbox") { + t.Fatalf("overlay missing active theme: %s", data) + } +} + +func TestApplyPreservesOtherOverlayKeys(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + op := filepath.Join(home, ".config", "archduino", "overlay.yaml") + if err := os.MkdirAll(filepath.Dir(op), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(op, []byte("version: 1\nextra_packages:\n - foo\n"), 0o644); err != nil { + t.Fatal(err) + } + + if err := Apply(config.Config{}, "gruvbox"); err != nil { + t.Fatal(err) + } + data, _ := os.ReadFile(op) + if !strings.Contains(string(data), "- foo") { + t.Fatalf("extra_packages lost: %s", data) + } + if !strings.Contains(string(data), "__active: gruvbox") { + t.Fatalf("active theme missing after preserve: %s", data) + } +} From 770bdcf3a24e521cd2f67206e53c913bd686f13b Mon Sep 17 00:00:00 2001 From: MDietermann Date: Tue, 14 Apr 2026 13:14:47 +0200 Subject: [PATCH 04/11] feat(theming): integrate NvChad chadrc rewrite into Apply --- internal/modules/neovim/neovim.go | 38 ++++++++++++++++++++++++++ internal/modules/neovim/neovim_test.go | 30 ++++++++++++++++++++ internal/theming/theming.go | 10 +++++++ internal/theming/theming_test.go | 31 +++++++++++++++++++++ 4 files changed, 109 insertions(+) diff --git a/internal/modules/neovim/neovim.go b/internal/modules/neovim/neovim.go index e61c51f..0f89cca 100644 --- a/internal/modules/neovim/neovim.go +++ b/internal/modules/neovim/neovim.go @@ -88,6 +88,44 @@ func (m Module) resolvedTheme() string { return "gruvbox" } +// WriteChadrc renders chadrc.lua for the given NvChad theme name and writes +// it to ~/.config/nvim/lua/chadrc.lua. If the file does not already exist +// and the surrounding nvim config dir does not exist, the function returns +// nil without writing anything (the user hasn't installed neovim yet). +// +// Exported so the theming package can re-render chadrc on theme switches +// without re-running the full Module pipeline. +func WriteChadrc(nvChadThemeName string) error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + cfg := filepath.Join(home, ".config", "nvim") + chadrcPath := filepath.Join(cfg, "lua", "chadrc.lua") + + // If chadrc doesn't exist, only write when the parent dir exists. A + // fresh system where nvim was never installed should be a no-op. + if _, err := os.Stat(chadrcPath); err != nil { + if _, err := os.Stat(cfg); err != nil { + return nil + } + } + + tmplBytes, err := assets.ReadFile("assets/chadrc.lua.tmpl") + if err != nil { + return err + } + tmpl, err := template.New("chadrc").Parse(string(tmplBytes)) + if err != nil { + return err + } + var buf bytes.Buffer + if err := tmpl.Execute(&buf, struct{ NvChadTheme string }{NvChadTheme: nvChadThemeName}); err != nil { + return err + } + return platform.WriteUserFile(chadrcPath, buf.Bytes(), 0o644) +} + func (m Module) renderChadrc() ([]byte, error) { tmplBytes, err := assets.ReadFile("assets/chadrc.lua.tmpl") if err != nil { diff --git a/internal/modules/neovim/neovim_test.go b/internal/modules/neovim/neovim_test.go index 6ad7510..862d107 100644 --- a/internal/modules/neovim/neovim_test.go +++ b/internal/modules/neovim/neovim_test.go @@ -177,4 +177,34 @@ func TestUnknownThemeFallsBackToFirst(t *testing.T) { } } +func TestWriteChadrcWritesFile(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + cfg := filepath.Join(home, ".config", "nvim") + if err := os.MkdirAll(cfg, 0o755); err != nil { + t.Fatal(err) + } + if err := WriteChadrc("dracula"); err != nil { + t.Fatal(err) + } + data, err := os.ReadFile(filepath.Join(cfg, "lua", "chadrc.lua")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(data), `theme = "dracula"`) { + t.Fatalf("theme not applied: %s", data) + } +} + +func TestWriteChadrcNoopWithoutConfigDir(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + if err := WriteChadrc("gruvbox"); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(home, ".config", "nvim", "lua", "chadrc.lua")); !os.IsNotExist(err) { + t.Fatal("chadrc should not be created on a bare system") + } +} + var _ module.Module = Module{} diff --git a/internal/theming/theming.go b/internal/theming/theming.go index b1369b4..cf5493a 100644 --- a/internal/theming/theming.go +++ b/internal/theming/theming.go @@ -13,6 +13,7 @@ import ( "strings" "github.com/MDietermann/ArduinoArchSetup/internal/config" + "github.com/MDietermann/ArduinoArchSetup/internal/modules/neovim" "github.com/MDietermann/ArduinoArchSetup/internal/platform" "gopkg.in/yaml.v3" ) @@ -144,6 +145,15 @@ func Apply(cfg config.Config, themeID string) error { 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 } diff --git a/internal/theming/theming_test.go b/internal/theming/theming_test.go index 0e5f02c..d018b6d 100644 --- a/internal/theming/theming_test.go +++ b/internal/theming/theming_test.go @@ -117,3 +117,34 @@ func TestApplyPreservesOtherOverlayKeys(t *testing.T) { t.Fatalf("active theme missing after preserve: %s", data) } } + +func TestApplyRewritesChadrcIfPresent(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + nvimCfg := filepath.Join(home, ".config", "nvim", "lua") + if err := os.MkdirAll(nvimCfg, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(nvimCfg, "chadrc.lua"), + []byte(`M.base46 = { theme = "onedark" }`), 0o644); err != nil { + t.Fatal(err) + } + if err := Apply(config.Config{}, "gruvbox"); err != nil { + t.Fatal(err) + } + data, _ := os.ReadFile(filepath.Join(nvimCfg, "chadrc.lua")) + if !strings.Contains(string(data), `theme = "gruvbox"`) { + t.Fatalf("chadrc not rewritten: %s", data) + } + if strings.Contains(string(data), `theme = "onedark"`) { + t.Fatalf("old theme still present: %s", data) + } +} + +func TestApplyNoChadrcIsFine(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + if err := Apply(config.Config{}, "gruvbox"); err != nil { + t.Fatal(err) + } +} From 12c502d48a535c17942320a03372d23fe8f8d9c7 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Tue, 14 Apr 2026 13:25:58 +0200 Subject: [PATCH 05/11] feat(theming): add catppuccin, dracula, nord, tokyonight themes --- .../theming/assets/catppuccin/lazygit.yml | 13 +++++++ .../theming/assets/catppuccin/starship.toml | 26 ++++++++++++++ internal/theming/assets/catppuccin/theme.yaml | 2 ++ internal/theming/assets/catppuccin/yazi.toml | 30 ++++++++++++++++ internal/theming/assets/catppuccin/zellij.kdl | 35 +++++++++++++++++++ internal/theming/assets/dracula/lazygit.yml | 13 +++++++ internal/theming/assets/dracula/starship.toml | 26 ++++++++++++++ internal/theming/assets/dracula/theme.yaml | 2 ++ internal/theming/assets/dracula/yazi.toml | 30 ++++++++++++++++ internal/theming/assets/dracula/zellij.kdl | 35 +++++++++++++++++++ internal/theming/assets/nord/lazygit.yml | 13 +++++++ internal/theming/assets/nord/starship.toml | 26 ++++++++++++++ internal/theming/assets/nord/theme.yaml | 2 ++ internal/theming/assets/nord/yazi.toml | 30 ++++++++++++++++ internal/theming/assets/nord/zellij.kdl | 35 +++++++++++++++++++ .../theming/assets/tokyonight/lazygit.yml | 13 +++++++ .../theming/assets/tokyonight/starship.toml | 26 ++++++++++++++ internal/theming/assets/tokyonight/theme.yaml | 2 ++ internal/theming/assets/tokyonight/yazi.toml | 30 ++++++++++++++++ internal/theming/assets/tokyonight/zellij.kdl | 35 +++++++++++++++++++ internal/theming/theming_test.go | 33 +++++++++++++++++ 21 files changed, 457 insertions(+) create mode 100644 internal/theming/assets/catppuccin/lazygit.yml create mode 100644 internal/theming/assets/catppuccin/starship.toml create mode 100644 internal/theming/assets/catppuccin/theme.yaml create mode 100644 internal/theming/assets/catppuccin/yazi.toml create mode 100644 internal/theming/assets/catppuccin/zellij.kdl create mode 100644 internal/theming/assets/dracula/lazygit.yml create mode 100644 internal/theming/assets/dracula/starship.toml create mode 100644 internal/theming/assets/dracula/theme.yaml create mode 100644 internal/theming/assets/dracula/yazi.toml create mode 100644 internal/theming/assets/dracula/zellij.kdl create mode 100644 internal/theming/assets/nord/lazygit.yml create mode 100644 internal/theming/assets/nord/starship.toml create mode 100644 internal/theming/assets/nord/theme.yaml create mode 100644 internal/theming/assets/nord/yazi.toml create mode 100644 internal/theming/assets/nord/zellij.kdl create mode 100644 internal/theming/assets/tokyonight/lazygit.yml create mode 100644 internal/theming/assets/tokyonight/starship.toml create mode 100644 internal/theming/assets/tokyonight/theme.yaml create mode 100644 internal/theming/assets/tokyonight/yazi.toml create mode 100644 internal/theming/assets/tokyonight/zellij.kdl diff --git a/internal/theming/assets/catppuccin/lazygit.yml b/internal/theming/assets/catppuccin/lazygit.yml new file mode 100644 index 0000000..800f0df --- /dev/null +++ b/internal/theming/assets/catppuccin/lazygit.yml @@ -0,0 +1,13 @@ +gui: + theme: + activeBorderColor: + - "#f9e2af" + - bold + inactiveBorderColor: + - "#585b70" + selectedLineBgColor: + - "#313244" + cherryPickedCommitFgColor: + - "#a6e3a1" + defaultFgColor: + - "#cdd6f4" diff --git a/internal/theming/assets/catppuccin/starship.toml b/internal/theming/assets/catppuccin/starship.toml new file mode 100644 index 0000000..d0c4174 --- /dev/null +++ b/internal/theming/assets/catppuccin/starship.toml @@ -0,0 +1,26 @@ +palette = "catppuccin_mocha" +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.catppuccin_mocha] +bright-yellow = "#f9e2af" +bright-green = "#a6e3a1" +bright-red = "#f38ba8" +bright-blue = "#89b4fa" +bright-purple = "#cba6f7" +bright-aqua = "#94e2d5" +bright-orange = "#fab387" diff --git a/internal/theming/assets/catppuccin/theme.yaml b/internal/theming/assets/catppuccin/theme.yaml new file mode 100644 index 0000000..58dac4e --- /dev/null +++ b/internal/theming/assets/catppuccin/theme.yaml @@ -0,0 +1,2 @@ +display_name: "Catppuccin Mocha" +nvchad: "catppuccin" diff --git a/internal/theming/assets/catppuccin/yazi.toml b/internal/theming/assets/catppuccin/yazi.toml new file mode 100644 index 0000000..91ed567 --- /dev/null +++ b/internal/theming/assets/catppuccin/yazi.toml @@ -0,0 +1,30 @@ +[manager] +cwd = { fg = "#f9e2af" } +tab_active = { fg = "#1e1e2e", bg = "#f9e2af" } +tab_inactive = { fg = "#bac2de", bg = "#313244" } +border_symbol = "│" +border_style = { fg = "#585b70" } +count_copied = { fg = "#1e1e2e", bg = "#a6e3a1" } +count_cut = { fg = "#1e1e2e", bg = "#f38ba8" } +count_selected = { fg = "#1e1e2e", bg = "#f9e2af" } + +[status] +separator_open = "" +separator_close = "" +mode_normal = { fg = "#1e1e2e", bg = "#a6e3a1", bold = true } +mode_select = { fg = "#1e1e2e", bg = "#f9e2af", bold = true } +mode_unset = { fg = "#1e1e2e", bg = "#f38ba8", bold = true } +progress_label = { fg = "#cdd6f4", bold = true } +progress_normal = { fg = "#89b4fa", bg = "#313244" } +progress_error = { fg = "#f38ba8", bg = "#313244" } + +[input] +border = { fg = "#f9e2af" } +title = {} +value = {} +selected = { reversed = true } + +[select] +border = { fg = "#f9e2af" } +active = { fg = "#a6e3a1", bold = true } +inactive = {} diff --git a/internal/theming/assets/catppuccin/zellij.kdl b/internal/theming/assets/catppuccin/zellij.kdl new file mode 100644 index 0000000..aa4974d --- /dev/null +++ b/internal/theming/assets/catppuccin/zellij.kdl @@ -0,0 +1,35 @@ +theme "catppuccin-mocha" +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 { + catppuccin-mocha { + fg "#cdd6f4" + bg "#1e1e2e" + black "#181825" + red "#f38ba8" + green "#a6e3a1" + yellow "#f9e2af" + blue "#89b4fa" + magenta "#cba6f7" + cyan "#94e2d5" + white "#bac2de" + orange "#fab387" + } +} diff --git a/internal/theming/assets/dracula/lazygit.yml b/internal/theming/assets/dracula/lazygit.yml new file mode 100644 index 0000000..d2125aa --- /dev/null +++ b/internal/theming/assets/dracula/lazygit.yml @@ -0,0 +1,13 @@ +gui: + theme: + activeBorderColor: + - "#f1fa8c" + - bold + inactiveBorderColor: + - "#6272a4" + selectedLineBgColor: + - "#44475a" + cherryPickedCommitFgColor: + - "#50fa7b" + defaultFgColor: + - "#f8f8f2" diff --git a/internal/theming/assets/dracula/starship.toml b/internal/theming/assets/dracula/starship.toml new file mode 100644 index 0000000..6df3708 --- /dev/null +++ b/internal/theming/assets/dracula/starship.toml @@ -0,0 +1,26 @@ +palette = "dracula" +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.dracula] +bright-yellow = "#f1fa8c" +bright-green = "#50fa7b" +bright-red = "#ff5555" +bright-blue = "#bd93f9" +bright-purple = "#ff79c6" +bright-aqua = "#8be9fd" +bright-orange = "#ffb86c" diff --git a/internal/theming/assets/dracula/theme.yaml b/internal/theming/assets/dracula/theme.yaml new file mode 100644 index 0000000..77b3985 --- /dev/null +++ b/internal/theming/assets/dracula/theme.yaml @@ -0,0 +1,2 @@ +display_name: "Dracula" +nvchad: "dracula" diff --git a/internal/theming/assets/dracula/yazi.toml b/internal/theming/assets/dracula/yazi.toml new file mode 100644 index 0000000..476abe4 --- /dev/null +++ b/internal/theming/assets/dracula/yazi.toml @@ -0,0 +1,30 @@ +[manager] +cwd = { fg = "#f1fa8c" } +tab_active = { fg = "#282a36", bg = "#f1fa8c" } +tab_inactive = { fg = "#bfbfbf", bg = "#44475a" } +border_symbol = "│" +border_style = { fg = "#6272a4" } +count_copied = { fg = "#282a36", bg = "#50fa7b" } +count_cut = { fg = "#282a36", bg = "#ff5555" } +count_selected = { fg = "#282a36", bg = "#f1fa8c" } + +[status] +separator_open = "" +separator_close = "" +mode_normal = { fg = "#282a36", bg = "#50fa7b", bold = true } +mode_select = { fg = "#282a36", bg = "#f1fa8c", bold = true } +mode_unset = { fg = "#282a36", bg = "#ff5555", bold = true } +progress_label = { fg = "#f8f8f2", bold = true } +progress_normal = { fg = "#bd93f9", bg = "#44475a" } +progress_error = { fg = "#ff5555", bg = "#44475a" } + +[input] +border = { fg = "#f1fa8c" } +title = {} +value = {} +selected = { reversed = true } + +[select] +border = { fg = "#f1fa8c" } +active = { fg = "#50fa7b", bold = true } +inactive = {} diff --git a/internal/theming/assets/dracula/zellij.kdl b/internal/theming/assets/dracula/zellij.kdl new file mode 100644 index 0000000..2dfa81e --- /dev/null +++ b/internal/theming/assets/dracula/zellij.kdl @@ -0,0 +1,35 @@ +theme "dracula" +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 { + dracula { + fg "#f8f8f2" + bg "#282a36" + black "#21222c" + red "#ff5555" + green "#50fa7b" + yellow "#f1fa8c" + blue "#bd93f9" + magenta "#ff79c6" + cyan "#8be9fd" + white "#bfbfbf" + orange "#ffb86c" + } +} diff --git a/internal/theming/assets/nord/lazygit.yml b/internal/theming/assets/nord/lazygit.yml new file mode 100644 index 0000000..5a865cc --- /dev/null +++ b/internal/theming/assets/nord/lazygit.yml @@ -0,0 +1,13 @@ +gui: + theme: + activeBorderColor: + - "#ebcb8b" + - bold + inactiveBorderColor: + - "#4c566a" + selectedLineBgColor: + - "#3b4252" + cherryPickedCommitFgColor: + - "#a3be8c" + defaultFgColor: + - "#d8dee9" diff --git a/internal/theming/assets/nord/starship.toml b/internal/theming/assets/nord/starship.toml new file mode 100644 index 0000000..566e243 --- /dev/null +++ b/internal/theming/assets/nord/starship.toml @@ -0,0 +1,26 @@ +palette = "nord" +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.nord] +bright-yellow = "#ebcb8b" +bright-green = "#a3be8c" +bright-red = "#bf616a" +bright-blue = "#81a1c1" +bright-purple = "#b48ead" +bright-aqua = "#88c0d0" +bright-orange = "#d08770" diff --git a/internal/theming/assets/nord/theme.yaml b/internal/theming/assets/nord/theme.yaml new file mode 100644 index 0000000..0b6aad7 --- /dev/null +++ b/internal/theming/assets/nord/theme.yaml @@ -0,0 +1,2 @@ +display_name: "Nord" +nvchad: "nord" diff --git a/internal/theming/assets/nord/yazi.toml b/internal/theming/assets/nord/yazi.toml new file mode 100644 index 0000000..67a073d --- /dev/null +++ b/internal/theming/assets/nord/yazi.toml @@ -0,0 +1,30 @@ +[manager] +cwd = { fg = "#ebcb8b" } +tab_active = { fg = "#2e3440", bg = "#ebcb8b" } +tab_inactive = { fg = "#e5e9f0", bg = "#3b4252" } +border_symbol = "│" +border_style = { fg = "#4c566a" } +count_copied = { fg = "#2e3440", bg = "#a3be8c" } +count_cut = { fg = "#2e3440", bg = "#bf616a" } +count_selected = { fg = "#2e3440", bg = "#ebcb8b" } + +[status] +separator_open = "" +separator_close = "" +mode_normal = { fg = "#2e3440", bg = "#a3be8c", bold = true } +mode_select = { fg = "#2e3440", bg = "#ebcb8b", bold = true } +mode_unset = { fg = "#2e3440", bg = "#bf616a", bold = true } +progress_label = { fg = "#d8dee9", bold = true } +progress_normal = { fg = "#81a1c1", bg = "#3b4252" } +progress_error = { fg = "#bf616a", bg = "#3b4252" } + +[input] +border = { fg = "#ebcb8b" } +title = {} +value = {} +selected = { reversed = true } + +[select] +border = { fg = "#ebcb8b" } +active = { fg = "#a3be8c", bold = true } +inactive = {} diff --git a/internal/theming/assets/nord/zellij.kdl b/internal/theming/assets/nord/zellij.kdl new file mode 100644 index 0000000..ecdc217 --- /dev/null +++ b/internal/theming/assets/nord/zellij.kdl @@ -0,0 +1,35 @@ +theme "nord" +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 { + nord { + fg "#d8dee9" + bg "#2e3440" + black "#3b4252" + red "#bf616a" + green "#a3be8c" + yellow "#ebcb8b" + blue "#81a1c1" + magenta "#b48ead" + cyan "#88c0d0" + white "#e5e9f0" + orange "#d08770" + } +} diff --git a/internal/theming/assets/tokyonight/lazygit.yml b/internal/theming/assets/tokyonight/lazygit.yml new file mode 100644 index 0000000..3bd6c23 --- /dev/null +++ b/internal/theming/assets/tokyonight/lazygit.yml @@ -0,0 +1,13 @@ +gui: + theme: + activeBorderColor: + - "#e0af68" + - bold + inactiveBorderColor: + - "#3b4261" + selectedLineBgColor: + - "#283457" + cherryPickedCommitFgColor: + - "#9ece6a" + defaultFgColor: + - "#c0caf5" diff --git a/internal/theming/assets/tokyonight/starship.toml b/internal/theming/assets/tokyonight/starship.toml new file mode 100644 index 0000000..896eae2 --- /dev/null +++ b/internal/theming/assets/tokyonight/starship.toml @@ -0,0 +1,26 @@ +palette = "tokyonight" +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.tokyonight] +bright-yellow = "#e0af68" +bright-green = "#9ece6a" +bright-red = "#f7768e" +bright-blue = "#7aa2f7" +bright-purple = "#bb9af7" +bright-aqua = "#7dcfff" +bright-orange = "#ff9e64" diff --git a/internal/theming/assets/tokyonight/theme.yaml b/internal/theming/assets/tokyonight/theme.yaml new file mode 100644 index 0000000..97e78bd --- /dev/null +++ b/internal/theming/assets/tokyonight/theme.yaml @@ -0,0 +1,2 @@ +display_name: "Tokyo Night" +nvchad: "tokyonight" diff --git a/internal/theming/assets/tokyonight/yazi.toml b/internal/theming/assets/tokyonight/yazi.toml new file mode 100644 index 0000000..919d472 --- /dev/null +++ b/internal/theming/assets/tokyonight/yazi.toml @@ -0,0 +1,30 @@ +[manager] +cwd = { fg = "#e0af68" } +tab_active = { fg = "#1a1b26", bg = "#e0af68" } +tab_inactive = { fg = "#a9b1d6", bg = "#283457" } +border_symbol = "│" +border_style = { fg = "#3b4261" } +count_copied = { fg = "#1a1b26", bg = "#9ece6a" } +count_cut = { fg = "#1a1b26", bg = "#f7768e" } +count_selected = { fg = "#1a1b26", bg = "#e0af68" } + +[status] +separator_open = "" +separator_close = "" +mode_normal = { fg = "#1a1b26", bg = "#9ece6a", bold = true } +mode_select = { fg = "#1a1b26", bg = "#e0af68", bold = true } +mode_unset = { fg = "#1a1b26", bg = "#f7768e", bold = true } +progress_label = { fg = "#c0caf5", bold = true } +progress_normal = { fg = "#7aa2f7", bg = "#283457" } +progress_error = { fg = "#f7768e", bg = "#283457" } + +[input] +border = { fg = "#e0af68" } +title = {} +value = {} +selected = { reversed = true } + +[select] +border = { fg = "#e0af68" } +active = { fg = "#9ece6a", bold = true } +inactive = {} diff --git a/internal/theming/assets/tokyonight/zellij.kdl b/internal/theming/assets/tokyonight/zellij.kdl new file mode 100644 index 0000000..3fa988e --- /dev/null +++ b/internal/theming/assets/tokyonight/zellij.kdl @@ -0,0 +1,35 @@ +theme "tokyonight" +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 { + tokyonight { + fg "#c0caf5" + bg "#1a1b26" + black "#15161e" + red "#f7768e" + green "#9ece6a" + yellow "#e0af68" + blue "#7aa2f7" + magenta "#bb9af7" + cyan "#7dcfff" + white "#a9b1d6" + orange "#ff9e64" + } +} diff --git a/internal/theming/theming_test.go b/internal/theming/theming_test.go index d018b6d..b2bfc41 100644 --- a/internal/theming/theming_test.go +++ b/internal/theming/theming_test.go @@ -148,3 +148,36 @@ func TestApplyNoChadrcIsFine(t *testing.T) { t.Fatal(err) } } + +func TestAllFiveThemesPresent(t *testing.T) { + want := []string{"catppuccin", "dracula", "gruvbox", "nord", "tokyonight"} + themes := List() + if len(themes) != len(want) { + t.Fatalf("want %d themes, got %d", len(want), len(themes)) + } + for i, id := range want { + if themes[i].ID != id { + t.Fatalf("themes[%d].ID = %q, want %q", i, themes[i].ID, id) + } + } +} + +func TestApplyEachThemeWritesStarship(t *testing.T) { + for _, id := range []string{"catppuccin", "dracula", "nord", "tokyonight"} { + id := id + t.Run(id, func(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + if err := Apply(config.Config{}, id); err != nil { + t.Fatal(err) + } + data, err := os.ReadFile(filepath.Join(home, ".config", "starship.toml")) + if err != nil { + t.Fatal(err) + } + if len(data) == 0 { + t.Fatal("empty starship.toml") + } + }) + } +} From 4f7411ca951e5b5351cb3d9f5f520ca0d8dcb6f9 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Tue, 14 Apr 2026 13:35:10 +0200 Subject: [PATCH 06/11] refactor: retire tui.BuiltinThemes in favor of theming.List MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make internal/theming.List the single source of truth for themes: - Drop tui.BuiltinThemes, tui.ThemeByID, and the NvChadTheme field on tui.Theme. tui.Theme is now a thin {ID, Name} display value. - Wizard populates its dropdown from theming.List() and uses DisplayName for the label. - neovim.resolvedTheme uses theming.Get (with theming.List()[0] as a fallback) instead of a caller-supplied []tui.Theme slice. - neovim.New signature loses the themes []tui.Theme parameter; update cmd/archduino/main.go and all neovim tests to match. Break the resulting neovim→theming import cycle (theming previously imported neovim for WriteChadrc) by inverting the dependency: theming exposes a ChadrcWriter function hook, and neovim registers its WriteChadrc into it via package init. The theming test stubs ChadrcWriter directly to exercise the chadrc-rewrite branch without pulling neovim in and re-creating the cycle. Delete the now-obsolete TestBuiltinThemesCarryNvChadName / TestThemeByID* tests in internal/tui/state_test.go — their coverage lives in the theming package's own tests. --- cmd/archduino/main.go | 2 +- internal/modules/neovim/neovim.go | 20 +++++++++----- internal/modules/neovim/neovim_test.go | 15 ++++++----- internal/theming/theming.go | 14 +++++++--- internal/theming/theming_test.go | 11 ++++++++ internal/tui/state.go | 37 +++++++------------------- internal/tui/state_test.go | 30 ++------------------- internal/tui/wizard/wizard.go | 8 +++--- 8 files changed, 61 insertions(+), 76 deletions(-) diff --git a/cmd/archduino/main.go b/cmd/archduino/main.go index bbcc835..a9eab4e 100644 --- a/cmd/archduino/main.go +++ b/cmd/archduino/main.go @@ -57,7 +57,7 @@ func buildRegistry(cfg config.Config, themeID string) (*module.Registry, error) themes.New(cfg.Themes), arduinocli.New(cfg.ArduinoCLI, runner), shellconfig.New(cfg.ShellConfig), - neovim.New(cfg.Neovim, themeID, tui.BuiltinThemes, pm, git), + neovim.New(cfg.Neovim, themeID, pm, git), renode.New(cfg.Renode, pm, yay), } for _, m := range mods { diff --git a/internal/modules/neovim/neovim.go b/internal/modules/neovim/neovim.go index 0f89cca..2a3f5a1 100644 --- a/internal/modules/neovim/neovim.go +++ b/internal/modules/neovim/neovim.go @@ -17,12 +17,19 @@ import ( "github.com/MDietermann/ArduinoArchSetup/internal/config" "github.com/MDietermann/ArduinoArchSetup/internal/module" "github.com/MDietermann/ArduinoArchSetup/internal/platform" - "github.com/MDietermann/ArduinoArchSetup/internal/tui" + "github.com/MDietermann/ArduinoArchSetup/internal/theming" ) //go:embed assets/* var assets embed.FS +// Register WriteChadrc with the theming package so theming.Apply can +// rewrite chadrc.lua on theme changes without importing this package +// (which would be a cycle: neovim imports theming). +func init() { + theming.ChadrcWriter = WriteChadrc +} + // assetMap lists each asset and its destination under ~/.config/nvim. // Order matters: Detect iterates in this order and Apply writes in this order. var assetMap = []struct { @@ -40,14 +47,13 @@ var assetMap = []struct { type Module struct { cfg config.NeovimConfig themeID string - themes []tui.Theme pm platform.PackageManager git platform.GitClient } // New constructs a Neovim Module. -func New(cfg config.NeovimConfig, themeID string, themes []tui.Theme, pm platform.PackageManager, git platform.GitClient) Module { - return Module{cfg: cfg, themeID: themeID, themes: themes, pm: pm, git: git} +func New(cfg config.NeovimConfig, themeID string, pm platform.PackageManager, git platform.GitClient) Module { + return Module{cfg: cfg, themeID: themeID, pm: pm, git: git} } type state struct { @@ -79,11 +85,11 @@ func nvimCfg() (string, error) { } func (m Module) resolvedTheme() string { - if t, ok := tui.ThemeByID(m.themes, m.themeID); ok && t.NvChadTheme != "" { + if t, ok := theming.Get(m.themeID); ok && t.NvChadTheme != "" { return t.NvChadTheme } - if len(m.themes) > 0 { - return m.themes[0].NvChadTheme + if list := theming.List(); len(list) > 0 && list[0].NvChadTheme != "" { + return list[0].NvChadTheme } return "gruvbox" } diff --git a/internal/modules/neovim/neovim_test.go b/internal/modules/neovim/neovim_test.go index 862d107..bd43a96 100644 --- a/internal/modules/neovim/neovim_test.go +++ b/internal/modules/neovim/neovim_test.go @@ -10,7 +10,6 @@ import ( "github.com/MDietermann/ArduinoArchSetup/internal/config" "github.com/MDietermann/ArduinoArchSetup/internal/module" "github.com/MDietermann/ArduinoArchSetup/internal/platform" - "github.com/MDietermann/ArduinoArchSetup/internal/tui" ) type fakeRunner struct { @@ -58,7 +57,7 @@ func TestDetectFreshSystem(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) r := &fakeRunner{installed: map[string]bool{}} - m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "gruvbox", tui.BuiltinThemes, platform.NewPacman(r), platform.GitClient{Runner: r}) + m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "gruvbox", platform.NewPacman(r), platform.GitClient{Runner: r}) st, err := m.Detect(context.Background()) if err != nil { t.Fatal(err) @@ -81,7 +80,7 @@ func TestDetectNonNvChadTriggersBackup(t *testing.T) { os.MkdirAll(filepath.Join(home, ".config", "nvim", "lua"), 0o755) os.WriteFile(filepath.Join(home, ".config", "nvim", "lua", "plugins.lua"), []byte("-- lazyvim"), 0o644) r := &fakeRunner{installed: map[string]bool{}} - m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "gruvbox", tui.BuiltinThemes, platform.NewPacman(r), platform.GitClient{Runner: r}) + m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "gruvbox", platform.NewPacman(r), platform.GitClient{Runner: r}) st, _ := m.Detect(context.Background()) if !st.(state).BackupNeeded { t.Fatal("expected BackupNeeded=true") @@ -92,7 +91,7 @@ func TestPlanApplyInstallsEverything(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) r := &fakeRunner{installed: map[string]bool{}} - m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "dracula", tui.BuiltinThemes, platform.NewPacman(r), platform.GitClient{Runner: r}) + m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "dracula", platform.NewPacman(r), platform.GitClient{Runner: r}) st, _ := m.Detect(context.Background()) p, _ := m.Plan(context.Background(), st) if len(p.Actions) == 0 { @@ -150,7 +149,7 @@ func TestPlanAfterApplyIsEmpty(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) r := &fakeRunner{installed: map[string]bool{"neovim": true}} - m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "gruvbox", tui.BuiltinThemes, platform.NewPacman(r), platform.GitClient{Runner: r}) + m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "gruvbox", platform.NewPacman(r), platform.GitClient{Runner: r}) st, _ := m.Detect(context.Background()) p, _ := m.Plan(context.Background(), st) _ = m.Apply(context.Background(), p, nopReporter{}) @@ -165,14 +164,16 @@ func TestUnknownThemeFallsBackToFirst(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) r := &fakeRunner{installed: map[string]bool{"neovim": true}} - m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "does-not-exist", tui.BuiltinThemes, platform.NewPacman(r), platform.GitClient{Runner: r}) + m := New(config.NeovimConfig{NvChadStarterRepo: "https://x/starter"}, "does-not-exist", platform.NewPacman(r), platform.GitClient{Runner: r}) st, _ := m.Detect(context.Background()) p, _ := m.Plan(context.Background(), st) if err := m.Apply(context.Background(), p, nopReporter{}); err != nil { t.Fatal(err) } chadrc, _ := os.ReadFile(filepath.Join(home, ".config", "nvim", "lua", "chadrc.lua")) - if !strings.Contains(string(chadrc), `theme = "gruvbox"`) { + // Unknown theme IDs fall back to the first entry of theming.List(), + // which is sorted alphabetically by ID (currently "catppuccin"). + if !strings.Contains(string(chadrc), `theme = "catppuccin"`) { t.Fatalf("fallback theme not applied: %s", chadrc) } } diff --git a/internal/theming/theming.go b/internal/theming/theming.go index cf5493a..904a2c1 100644 --- a/internal/theming/theming.go +++ b/internal/theming/theming.go @@ -13,11 +13,17 @@ import ( "strings" "github.com/MDietermann/ArduinoArchSetup/internal/config" - "github.com/MDietermann/ArduinoArchSetup/internal/modules/neovim" "github.com/MDietermann/ArduinoArchSetup/internal/platform" "gopkg.in/yaml.v3" ) +// ChadrcWriter is the hook used by Apply to rewrite NvChad's chadrc.lua +// after a theme change. The neovim module registers its WriteChadrc here +// via an init() function to avoid a theming→neovim import cycle (neovim +// now imports theming to look up the NvChad theme name). If nil, Apply +// skips the chadrc update silently. +var ChadrcWriter func(nvChadThemeName string) error + //go:embed assets var assetsFS embed.FS @@ -146,8 +152,10 @@ func Apply(cfg config.Config, themeID string) error { return fmt.Errorf("update overlay: %w", err) } - if err := neovim.WriteChadrc(th.NvChadTheme); err != nil { - return fmt.Errorf("update chadrc: %w", err) + if ChadrcWriter != nil { + if err := ChadrcWriter(th.NvChadTheme); err != nil { + return fmt.Errorf("update chadrc: %w", err) + } } // Clear NvChad's base46 theme cache so the new theme activates on next diff --git a/internal/theming/theming_test.go b/internal/theming/theming_test.go index b2bfc41..57785a4 100644 --- a/internal/theming/theming_test.go +++ b/internal/theming/theming_test.go @@ -129,6 +129,17 @@ func TestApplyRewritesChadrcIfPresent(t *testing.T) { []byte(`M.base46 = { theme = "onedark" }`), 0o644); err != nil { t.Fatal(err) } + + // The real ChadrcWriter lives in the neovim package, which cannot be + // imported here without creating an import cycle (neovim→theming). + // Stub it with a minimal writer that covers what the test asserts. + prev := ChadrcWriter + t.Cleanup(func() { ChadrcWriter = prev }) + ChadrcWriter = func(name string) error { + return os.WriteFile(filepath.Join(nvimCfg, "chadrc.lua"), + []byte(`theme = "`+name+`"`), 0o644) + } + if err := Apply(config.Config{}, "gruvbox"); err != nil { t.Fatal(err) } diff --git a/internal/tui/state.go b/internal/tui/state.go index 63b5965..a75380b 100644 --- a/internal/tui/state.go +++ b/internal/tui/state.go @@ -5,35 +5,15 @@ import ( "github.com/MDietermann/ArduinoArchSetup/internal/config" "github.com/MDietermann/ArduinoArchSetup/internal/module" + "github.com/MDietermann/ArduinoArchSetup/internal/theming" ) -// Theme is a named colorscheme presented in the wizard. Each entry carries -// per-tool theme names; Plan 3b uses NvChadTheme, later plans add more. +// Theme is a named colorscheme presented in the wizard. It is a thin +// display value for the dropdown; the authoritative theme catalogue lives +// in the theming package (theming.List / theming.Get). type Theme struct { - ID string - Name string - NvChadTheme string -} - -// BuiltinThemes is the fixed list the wizard shows. Keep in sync with the -// themes shipped by Plan 3's theme module. -var BuiltinThemes = []Theme{ - {ID: "gruvbox", Name: "Gruvbox Dark", NvChadTheme: "gruvbox"}, - {ID: "catppuccin", Name: "Catppuccin Mocha", NvChadTheme: "catppuccin"}, - {ID: "tokyonight", Name: "Tokyo Night", NvChadTheme: "tokyonight"}, - {ID: "nord", Name: "Nord", NvChadTheme: "nord"}, - {ID: "dracula", Name: "Dracula", NvChadTheme: "dracula"}, -} - -// ThemeByID returns the Theme with the given ID. The bool is false when no -// match is found; callers decide whether to fall back or error. -func ThemeByID(themes []Theme, id string) (Theme, bool) { - for _, t := range themes { - if t.ID == id { - return t, true - } - } - return Theme{}, false + ID string + Name string } // ModuleChoice is one row in the module selector. @@ -74,7 +54,10 @@ func NewWizardState(cfg config.Config, reg *module.Registry) WizardState { } sort.Slice(choices, func(i, j int) bool { return choices[i].ID < choices[j].ID }) - theme := BuiltinThemes[0].ID + var theme string + if list := theming.List(); len(list) > 0 { + theme = list[0].ID + } if override, ok := cfg.ThemeOverrides["__active"]; ok && override != "" { theme = override } diff --git a/internal/tui/state_test.go b/internal/tui/state_test.go index 90d5dc7..c212492 100644 --- a/internal/tui/state_test.go +++ b/internal/tui/state_test.go @@ -6,6 +6,7 @@ import ( "github.com/MDietermann/ArduinoArchSetup/internal/config" "github.com/MDietermann/ArduinoArchSetup/internal/module" + "github.com/MDietermann/ArduinoArchSetup/internal/theming" ) type fakeModule struct{ id, name, desc string } @@ -34,7 +35,7 @@ func TestNewWizardStatePreTicksAllByDefault(t *testing.T) { t.Errorf("%s not selected", m.ID) } } - if s.ThemeID != BuiltinThemes[0].ID { + if s.ThemeID != theming.List()[0].ID { t.Errorf("theme = %q", s.ThemeID) } } @@ -71,30 +72,3 @@ func TestSelectedIDsOnlyReturnsChecked(t *testing.T) { } } -func TestBuiltinThemesCarryNvChadName(t *testing.T) { - for _, th := range BuiltinThemes { - if th.NvChadTheme == "" { - t.Errorf("theme %q has empty NvChadTheme", th.ID) - } - } -} - -func TestThemeByIDReturnsMatch(t *testing.T) { - got, ok := ThemeByID(BuiltinThemes, "dracula") - if !ok { - t.Fatal("dracula should exist") - } - if got.ID != "dracula" { - t.Fatalf("got id=%q", got.ID) - } - if got.NvChadTheme == "" { - t.Fatal("NvChadTheme empty") - } -} - -func TestThemeByIDFallsBackWhenUnknown(t *testing.T) { - _, ok := ThemeByID(BuiltinThemes, "does-not-exist") - if ok { - t.Fatal("unknown id should report ok=false") - } -} diff --git a/internal/tui/wizard/wizard.go b/internal/tui/wizard/wizard.go index a3ec35a..77ad965 100644 --- a/internal/tui/wizard/wizard.go +++ b/internal/tui/wizard/wizard.go @@ -10,6 +10,7 @@ import ( "github.com/charmbracelet/huh" "github.com/MDietermann/ArduinoArchSetup/internal/module" + "github.com/MDietermann/ArduinoArchSetup/internal/theming" "github.com/MDietermann/ArduinoArchSetup/internal/tui" ) @@ -23,9 +24,10 @@ func Run(ctx context.Context, reg *module.Registry, initial tui.WizardState) (tu return s, false, nil } - themeOptions := make([]huh.Option[string], 0, len(tui.BuiltinThemes)) - for _, t := range tui.BuiltinThemes { - themeOptions = append(themeOptions, huh.NewOption(t.Name, t.ID)) + themes := theming.List() + themeOptions := make([]huh.Option[string], 0, len(themes)) + for _, t := range themes { + themeOptions = append(themeOptions, huh.NewOption(t.DisplayName, t.ID)) } moduleOptions := make([]huh.Option[string], 0, len(s.Modules)) From 698271a2355d4a57f1bca465b1d00ced1cf916a7 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Tue, 14 Apr 2026 13:42:58 +0200 Subject: [PATCH 07/11] fix(theming): restore gruvbox as default via Default() helper --- internal/modules/neovim/neovim.go | 7 ++----- internal/modules/neovim/neovim_test.go | 7 +++---- internal/theming/theming.go | 15 +++++++++++++++ internal/theming/theming_test.go | 6 ++++++ internal/tui/state.go | 5 +---- internal/tui/state_test.go | 2 +- 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/internal/modules/neovim/neovim.go b/internal/modules/neovim/neovim.go index 2a3f5a1..fc1dbd4 100644 --- a/internal/modules/neovim/neovim.go +++ b/internal/modules/neovim/neovim.go @@ -85,13 +85,10 @@ func nvimCfg() (string, error) { } func (m Module) resolvedTheme() string { - if t, ok := theming.Get(m.themeID); ok && t.NvChadTheme != "" { + if t, ok := theming.Get(m.themeID); ok { return t.NvChadTheme } - if list := theming.List(); len(list) > 0 && list[0].NvChadTheme != "" { - return list[0].NvChadTheme - } - return "gruvbox" + return theming.Default().NvChadTheme } // WriteChadrc renders chadrc.lua for the given NvChad theme name and writes diff --git a/internal/modules/neovim/neovim_test.go b/internal/modules/neovim/neovim_test.go index bd43a96..7c91180 100644 --- a/internal/modules/neovim/neovim_test.go +++ b/internal/modules/neovim/neovim_test.go @@ -160,7 +160,7 @@ func TestPlanAfterApplyIsEmpty(t *testing.T) { } } -func TestUnknownThemeFallsBackToFirst(t *testing.T) { +func TestUnknownThemeFallsBackToDefault(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) r := &fakeRunner{installed: map[string]bool{"neovim": true}} @@ -171,9 +171,8 @@ func TestUnknownThemeFallsBackToFirst(t *testing.T) { t.Fatal(err) } chadrc, _ := os.ReadFile(filepath.Join(home, ".config", "nvim", "lua", "chadrc.lua")) - // Unknown theme IDs fall back to the first entry of theming.List(), - // which is sorted alphabetically by ID (currently "catppuccin"). - if !strings.Contains(string(chadrc), `theme = "catppuccin"`) { + // Unknown theme IDs fall back to theming.Default(), which is gruvbox. + if !strings.Contains(string(chadrc), `theme = "gruvbox"`) { t.Fatalf("fallback theme not applied: %s", chadrc) } } diff --git a/internal/theming/theming.go b/internal/theming/theming.go index 904a2c1..3cb961b 100644 --- a/internal/theming/theming.go +++ b/internal/theming/theming.go @@ -97,6 +97,21 @@ func List() []Theme { return out } +// Default returns the archduino default theme. Used by the wizard's fresh- +// install default and by callers that resolve an unknown theme ID. +// The identity here is stable policy: gruvbox is the project's long-standing +// default (carried over from the pre-Go installer). Changing this changes +// the out-of-box experience on fresh systems. +func Default() Theme { + if t, ok := Get("gruvbox"); ok { + return t + } + if len(discovered) > 0 { + return discovered[0] + } + return Theme{ID: "gruvbox", DisplayName: "Gruvbox Dark", NvChadTheme: "gruvbox"} +} + // Get returns the theme with the given ID, if present. func Get(id string) (Theme, bool) { for _, t := range discovered { diff --git a/internal/theming/theming_test.go b/internal/theming/theming_test.go index 57785a4..ee04c62 100644 --- a/internal/theming/theming_test.go +++ b/internal/theming/theming_test.go @@ -192,3 +192,9 @@ func TestApplyEachThemeWritesStarship(t *testing.T) { }) } } + +func TestDefaultIsGruvbox(t *testing.T) { + if got := Default(); got.ID != "gruvbox" { + t.Fatalf("Default().ID = %q, want gruvbox", got.ID) + } +} diff --git a/internal/tui/state.go b/internal/tui/state.go index a75380b..710ec84 100644 --- a/internal/tui/state.go +++ b/internal/tui/state.go @@ -54,10 +54,7 @@ func NewWizardState(cfg config.Config, reg *module.Registry) WizardState { } sort.Slice(choices, func(i, j int) bool { return choices[i].ID < choices[j].ID }) - var theme string - if list := theming.List(); len(list) > 0 { - theme = list[0].ID - } + theme := theming.Default().ID if override, ok := cfg.ThemeOverrides["__active"]; ok && override != "" { theme = override } diff --git a/internal/tui/state_test.go b/internal/tui/state_test.go index c212492..a9307a2 100644 --- a/internal/tui/state_test.go +++ b/internal/tui/state_test.go @@ -35,7 +35,7 @@ func TestNewWizardStatePreTicksAllByDefault(t *testing.T) { t.Errorf("%s not selected", m.ID) } } - if s.ThemeID != theming.List()[0].ID { + if s.ThemeID != theming.Default().ID { t.Errorf("theme = %q", s.ThemeID) } } From bbaa21a6bf5475689590dcb8935cc28b58074eae Mon Sep 17 00:00:00 2001 From: MDietermann Date: Tue, 14 Apr 2026 14:08:54 +0200 Subject: [PATCH 08/11] feat(config): add Scaffolding.Boards typed input with 6 defaults --- internal/config/config.go | 14 ++++++++++++++ internal/config/config_test.go | 23 +++++++++++++++++++++++ internal/config/defaults.yaml | 9 +++++++++ 3 files changed, 46 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index 42d2274..f23c512 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -61,6 +61,16 @@ type RenodeConfig struct { Platforms []RenodePlatform `yaml:"platforms"` } +type Board struct { + Name string `yaml:"name"` + FQBN string `yaml:"fqbn"` + Template string `yaml:"template"` +} + +type Scaffolding struct { + Boards []Board `yaml:"boards"` +} + // Config is the fully-resolved runtime configuration. type Config struct { Version int `yaml:"version"` @@ -76,6 +86,7 @@ type Config struct { ShellConfig ShellConfigSection `yaml:"shell_config"` Neovim NeovimConfig `yaml:"neovim"` Renode RenodeConfig `yaml:"renode"` + Scaffolding Scaffolding `yaml:"scaffolding"` } const currentVersion = 1 @@ -161,5 +172,8 @@ func merge(base, over Config) Config { if over.Renode.WokwiVersion != "" || len(over.Renode.Platforms) > 0 { out.Renode = over.Renode } + if len(over.Scaffolding.Boards) > 0 { + out.Scaffolding = over.Scaffolding + } return out } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 4becd3c..d2b31cd 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -108,3 +108,26 @@ func TestLoadRejectsUnknownVersion(t *testing.T) { t.Fatal("expected version error") } } + +func TestDefaultsCarryScaffoldingBoards(t *testing.T) { + cfg, err := Load("/nonexistent/overlay.yaml") + if err != nil { + t.Fatal(err) + } + if len(cfg.Scaffolding.Boards) < 6 { + t.Fatalf("want >= 6 boards, got %d", len(cfg.Scaffolding.Boards)) + } + want := map[string]string{ + "uno": "arduino:avr:uno", "mega": "arduino:avr:mega", "nano": "arduino:avr:nano", + "esp32": "esp32:esp32:esp32", "esp32s3": "esp32:esp32:esp32s3", "esp8266": "esp8266:esp8266:nodemcuv2", + } + got := map[string]string{} + for _, b := range cfg.Scaffolding.Boards { + got[b.Name] = b.FQBN + } + for name, fqbn := range want { + if got[name] != fqbn { + t.Errorf("board %q: got fqbn %q, want %q", name, got[name], fqbn) + } + } +} diff --git a/internal/config/defaults.yaml b/internal/config/defaults.yaml index 51da5bf..b84c562 100644 --- a/internal/config/defaults.yaml +++ b/internal/config/defaults.yaml @@ -97,3 +97,12 @@ renode: - name: nrf52840 repl_path: "platforms/cpus/nrf52840.repl" machine: "nRF52840" + +scaffolding: + boards: + - {name: uno, fqbn: "arduino:avr:uno", template: uno} + - {name: mega, fqbn: "arduino:avr:mega", template: mega} + - {name: nano, fqbn: "arduino:avr:nano", template: nano} + - {name: esp32, fqbn: "esp32:esp32:esp32", template: esp32} + - {name: esp32s3, fqbn: "esp32:esp32:esp32s3", template: esp32s3} + - {name: esp8266, fqbn: "esp8266:esp8266:nodemcuv2", template: esp8266} From 2464e70cd20861ff9ac077950673536f4f22c003 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Tue, 14 Apr 2026 14:16:07 +0200 Subject: [PATCH 09/11] feat(newproject): add archduino new project scaffolder --- .../subcommands/newproject/assets/default.ino | 9 ++ .../subcommands/newproject/assets/esp32.ino | 22 +++++ .../subcommands/newproject/assets/esp32s3.ino | 9 ++ .../subcommands/newproject/assets/esp8266.ino | 22 +++++ .../subcommands/newproject/assets/mega.ino | 9 ++ .../subcommands/newproject/assets/nano.ino | 9 ++ .../subcommands/newproject/assets/uno.ino | 9 ++ internal/subcommands/newproject/newproject.go | 75 +++++++++++++++++ .../subcommands/newproject/newproject_test.go | 84 +++++++++++++++++++ 9 files changed, 248 insertions(+) create mode 100644 internal/subcommands/newproject/assets/default.ino create mode 100644 internal/subcommands/newproject/assets/esp32.ino create mode 100644 internal/subcommands/newproject/assets/esp32s3.ino create mode 100644 internal/subcommands/newproject/assets/esp8266.ino create mode 100644 internal/subcommands/newproject/assets/mega.ino create mode 100644 internal/subcommands/newproject/assets/nano.ino create mode 100644 internal/subcommands/newproject/assets/uno.ino create mode 100644 internal/subcommands/newproject/newproject.go create mode 100644 internal/subcommands/newproject/newproject_test.go diff --git a/internal/subcommands/newproject/assets/default.ino b/internal/subcommands/newproject/assets/default.ino new file mode 100644 index 0000000..2d0cebb --- /dev/null +++ b/internal/subcommands/newproject/assets/default.ino @@ -0,0 +1,9 @@ +void setup() { + Serial.begin(115200); + while (!Serial) { ; } + Serial.println("Hello from Arduino!"); +} + +void loop() { + // Your code here +} diff --git a/internal/subcommands/newproject/assets/esp32.ino b/internal/subcommands/newproject/assets/esp32.ino new file mode 100644 index 0000000..12bc573 --- /dev/null +++ b/internal/subcommands/newproject/assets/esp32.ino @@ -0,0 +1,22 @@ +#include + +const char* ssid = "YOUR_SSID"; +const char* password = "YOUR_PASS"; + +void setup() { + Serial.begin(115200); + delay(1000); + WiFi.begin(ssid, password); + Serial.print("Connecting to WiFi"); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println(); + Serial.print("Connected! IP: "); + Serial.println(WiFi.localIP()); +} + +void loop() { + delay(1000); +} diff --git a/internal/subcommands/newproject/assets/esp32s3.ino b/internal/subcommands/newproject/assets/esp32s3.ino new file mode 100644 index 0000000..2d0cebb --- /dev/null +++ b/internal/subcommands/newproject/assets/esp32s3.ino @@ -0,0 +1,9 @@ +void setup() { + Serial.begin(115200); + while (!Serial) { ; } + Serial.println("Hello from Arduino!"); +} + +void loop() { + // Your code here +} diff --git a/internal/subcommands/newproject/assets/esp8266.ino b/internal/subcommands/newproject/assets/esp8266.ino new file mode 100644 index 0000000..fb8694c --- /dev/null +++ b/internal/subcommands/newproject/assets/esp8266.ino @@ -0,0 +1,22 @@ +#include + +const char* ssid = "YOUR_SSID"; +const char* password = "YOUR_PASS"; + +void setup() { + Serial.begin(115200); + delay(1000); + WiFi.begin(ssid, password); + Serial.print("Connecting to WiFi"); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println(); + Serial.print("Connected! IP: "); + Serial.println(WiFi.localIP()); +} + +void loop() { + delay(1000); +} diff --git a/internal/subcommands/newproject/assets/mega.ino b/internal/subcommands/newproject/assets/mega.ino new file mode 100644 index 0000000..2d0cebb --- /dev/null +++ b/internal/subcommands/newproject/assets/mega.ino @@ -0,0 +1,9 @@ +void setup() { + Serial.begin(115200); + while (!Serial) { ; } + Serial.println("Hello from Arduino!"); +} + +void loop() { + // Your code here +} diff --git a/internal/subcommands/newproject/assets/nano.ino b/internal/subcommands/newproject/assets/nano.ino new file mode 100644 index 0000000..2d0cebb --- /dev/null +++ b/internal/subcommands/newproject/assets/nano.ino @@ -0,0 +1,9 @@ +void setup() { + Serial.begin(115200); + while (!Serial) { ; } + Serial.println("Hello from Arduino!"); +} + +void loop() { + // Your code here +} diff --git a/internal/subcommands/newproject/assets/uno.ino b/internal/subcommands/newproject/assets/uno.ino new file mode 100644 index 0000000..2d0cebb --- /dev/null +++ b/internal/subcommands/newproject/assets/uno.ino @@ -0,0 +1,9 @@ +void setup() { + Serial.begin(115200); + while (!Serial) { ; } + Serial.println("Hello from Arduino!"); +} + +void loop() { + // Your code here +} diff --git a/internal/subcommands/newproject/newproject.go b/internal/subcommands/newproject/newproject.go new file mode 100644 index 0000000..4f44faa --- /dev/null +++ b/internal/subcommands/newproject/newproject.go @@ -0,0 +1,75 @@ +// Package newproject scaffolds a new Arduino sketch directory. +package newproject + +import ( + "embed" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/MDietermann/ArduinoArchSetup/internal/config" + "github.com/MDietermann/ArduinoArchSetup/internal/platform" +) + +//go:embed assets +var assetsFS embed.FS + +const clangdContent = `CompileFlags: + Add: + - -xc++ + - -std=c++17 +` + +// Run creates a new Arduino project directory. It errors if the project +// directory already exists, if the board is unknown, or if the project name +// contains a path separator. +func Run(cfg config.Config, project, board string) error { + if project == "" { + return fmt.Errorf("project name required") + } + if strings.ContainsAny(project, "/\\") || strings.HasPrefix(project, ".") { + return fmt.Errorf("invalid project name %q", project) + } + + var b *config.Board + var valid []string + for i := range cfg.Scaffolding.Boards { + bp := &cfg.Scaffolding.Boards[i] + valid = append(valid, bp.Name) + if bp.Name == board { + b = bp + } + } + if b == nil { + return fmt.Errorf("unknown board %q; valid: %s", board, strings.Join(valid, ", ")) + } + + if _, err := os.Stat(project); err == nil { + return fmt.Errorf("project directory %q already exists", project) + } + if err := os.MkdirAll(project, 0o755); err != nil { + return err + } + + // Pick .ino template: try