Compare commits
10 Commits
4fa580f226
...
929cee1643
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
929cee1643 | ||
|
|
fd87135f62 | ||
|
|
a203cdfe89 | ||
|
|
c710ed81df | ||
|
|
2464e70cd2 | ||
|
|
bbaa21a6bf | ||
|
|
698271a235 | ||
|
|
4f7411ca95 | ||
|
|
12c502d48a | ||
|
|
770bdcf3a2 |
@@ -2,35 +2,40 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/engine"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/modules/arduinocli"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/modules/fonts"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/modules/gittools"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/modules/neovim"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/modules/packages"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/modules/renode"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/modules/shellconfig"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/modules/starship"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/modules/themes"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/modules/yazi"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/modules/zellij"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/modules/zsh"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/privilege"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/state"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/tui"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/tui/dashboard"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/tui/wizard"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/engine"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/modules/arduinocli"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/modules/fonts"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/modules/gittools"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/modules/neovim"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/modules/packages"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/modules/renode"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/modules/shellconfig"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/modules/starship"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/modules/themes"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/modules/yazi"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/modules/zellij"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/modules/zsh"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/privilege"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/state"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/subcommands/doctor"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/subcommands/newproject"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/theming"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/tui"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/tui/dashboard"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/tui/wizard"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
@@ -57,7 +62,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 {
|
||||
@@ -77,6 +82,9 @@ func main() {
|
||||
Version: version,
|
||||
}
|
||||
root.AddCommand(newInstallCmd())
|
||||
root.AddCommand(newThemeCmd())
|
||||
root.AddCommand(newNewCmd())
|
||||
root.AddCommand(newDoctorCmd())
|
||||
|
||||
// Wire SIGINT/SIGTERM to a cancellable context so long-running phases
|
||||
// (engine.Run, sudo keepalive) observe the cancellation promptly.
|
||||
@@ -147,8 +155,9 @@ func newInstallCmd() *cobra.Command {
|
||||
|
||||
started := time.Now().UTC()
|
||||
var (
|
||||
sum engine.Summary
|
||||
runErr error
|
||||
sum engine.Summary
|
||||
runErr error
|
||||
selectedTheme = cfg.ThemeOverrides["__active"]
|
||||
)
|
||||
|
||||
if noTUI || dryRun {
|
||||
@@ -194,6 +203,9 @@ func newInstallCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return fmt.Errorf("build registry: %w", err)
|
||||
}
|
||||
if finalState.ThemeID != "" {
|
||||
selectedTheme = finalState.ThemeID
|
||||
}
|
||||
// Either the CLI flag or the wizard toggle can force halt-on-error.
|
||||
sum, runErr = dashboard.Run(ctx, reg, finalState, halt || finalState.HaltOnError)
|
||||
}
|
||||
@@ -203,6 +215,14 @@ func newInstallCmd() *cobra.Command {
|
||||
return runErr
|
||||
}
|
||||
|
||||
// Apply the wizard-selected theme (no-op if user chose a theme
|
||||
// whose configs are already on disk). Skipped on dry-run.
|
||||
if !dryRun && selectedTheme != "" {
|
||||
if err := theming.Apply(cfg, selectedTheme); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: theme apply failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
ok, fail, skipped := sum.Counts()
|
||||
fmt.Fprintf(os.Stdout, "\nsummary: %d succeeded, %d failed, %d skipped\n", ok, fail, skipped)
|
||||
for _, r := range sum.Results {
|
||||
@@ -296,3 +316,109 @@ func writeState(started, ended time.Time, sum engine.Summary) error {
|
||||
}
|
||||
return s.Save(rec, 10)
|
||||
}
|
||||
|
||||
func newThemeCmd() *cobra.Command {
|
||||
var overlayPath string
|
||||
cmd := &cobra.Command{
|
||||
Use: "theme [id]",
|
||||
Short: "Switch the active colorscheme across tools",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load(overlayPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// "list" short-circuit.
|
||||
if len(args) == 1 && args[0] == "list" {
|
||||
for _, t := range theming.List() {
|
||||
fmt.Printf("%s — %s\n", t.ID, t.DisplayName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var id string
|
||||
if len(args) == 1 {
|
||||
id = args[0]
|
||||
} else {
|
||||
themes := theming.List()
|
||||
opts := make([]huh.Option[string], 0, len(themes))
|
||||
for _, t := range themes {
|
||||
opts = append(opts, huh.NewOption(t.DisplayName, t.ID))
|
||||
}
|
||||
id = cfg.ThemeOverrides["__active"]
|
||||
if id == "" && len(themes) > 0 {
|
||||
id = themes[0].ID
|
||||
}
|
||||
form := huh.NewForm(huh.NewGroup(
|
||||
huh.NewSelect[string]().Title("Choose a theme").Options(opts...).Value(&id),
|
||||
))
|
||||
if err := form.RunWithContext(cmd.Context()); err != nil {
|
||||
if errors.Is(err, huh.ErrUserAborted) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := theming.Apply(cfg, id); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Applied theme: %s\n", id)
|
||||
fmt.Println("Restart zellij and neovim to see the new colors.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&overlayPath, "overlay", "", "path to overlay.yaml (defaults to ~/.config/archduino/overlay.yaml)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newNewCmd() *cobra.Command {
|
||||
var overlayPath string
|
||||
cmd := &cobra.Command{
|
||||
Use: "new <project> <board>",
|
||||
Short: "Scaffold a new Arduino sketch directory",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load(overlayPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
project, board := args[0], args[1]
|
||||
if err := newproject.Run(cfg, project, board); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Created %s/ (board: %s)\n", project, board)
|
||||
fmt.Printf("Next: cd %s && nvim %s.ino\n", project, project)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&overlayPath, "overlay", "", "path to overlay.yaml")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newDoctorCmd() *cobra.Command {
|
||||
var overlayPath string
|
||||
cmd := &cobra.Command{
|
||||
Use: "doctor",
|
||||
Short: "Report module convergence and pinned-tool version drift",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := config.Load(overlayPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reg, err := buildRegistry(cfg, cfg.ThemeOverrides["__active"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("build registry: %w", err)
|
||||
}
|
||||
code := doctor.Run(cmd.Context(), cfg, reg, platform.ExecRunner{}, os.Stdout)
|
||||
if code != 0 {
|
||||
os.Exit(code)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&overlayPath, "overlay", "", "path to overlay.yaml")
|
||||
return cmd
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -1,4 +1,4 @@
|
||||
module github.com/MDietermann/ArduinoArchSetup
|
||||
module gitlab.hive-brain.de/MDietermann/Archduino
|
||||
|
||||
go 1.25.0
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
)
|
||||
|
||||
// TopoSort returns module IDs in a valid dependency order.
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
)
|
||||
|
||||
func TestTopoSort(t *testing.T) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
)
|
||||
|
||||
// RunOptions controls a single engine run.
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
)
|
||||
|
||||
// scriptedModule lets tests control what each phase returns.
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
)
|
||||
|
||||
// Event is a single item streamed from a module during Apply.
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
)
|
||||
|
||||
type fakeRunner struct {
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type fakeRunner struct {
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type fakeRunner struct {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
)
|
||||
|
||||
const defaultRelPath = ".cache/archduino/hello.ok"
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/engine"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/engine"
|
||||
)
|
||||
|
||||
func TestHelloFullLifecycle(t *testing.T) {
|
||||
|
||||
@@ -14,15 +14,22 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/tui"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/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,13 +85,48 @@ 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 {
|
||||
return t.NvChadTheme
|
||||
}
|
||||
if len(m.themes) > 0 {
|
||||
return m.themes[0].NvChadTheme
|
||||
return theming.Default().NvChadTheme
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
return "gruvbox"
|
||||
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) {
|
||||
|
||||
@@ -7,10 +7,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/tui"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
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{})
|
||||
@@ -161,20 +160,51 @@ 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}}
|
||||
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"))
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
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{}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type fakeRunner struct {
|
||||
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
//go:embed assets/arduino-sim.tmpl
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type fakeRunner struct {
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type Module struct{ cfg config.ShellConfigSection }
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
)
|
||||
|
||||
type nop struct{}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type fakeRunner struct {
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
)
|
||||
|
||||
type Module struct{ cfg config.ThemesConfig }
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
)
|
||||
|
||||
type nop struct{}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type fakeRunner struct {
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type fakeRunner struct {
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
type fakeRunner struct {
|
||||
|
||||
104
internal/subcommands/doctor/doctor.go
Normal file
104
internal/subcommands/doctor/doctor.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Package doctor reports module convergence and pinned-tool version drift.
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/platform"
|
||||
)
|
||||
|
||||
// Run inspects every module in the registry plus pinned tool versions and
|
||||
// writes a human-readable report to w. Returns 0 when everything is
|
||||
// converged with no drift; 1 otherwise. Never panics on module errors.
|
||||
func Run(ctx context.Context, cfg config.Config, reg *module.Registry, runner platform.Runner, w io.Writer) int {
|
||||
var converged, pending, errs, drift int
|
||||
|
||||
for _, m := range reg.All() {
|
||||
id := m.Meta().ID
|
||||
st, err := m.Detect(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "[!] %s — detect failed: %v\n", id, err)
|
||||
errs++
|
||||
continue
|
||||
}
|
||||
pl, err := m.Plan(ctx, st)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "[!] %s — plan failed: %v\n", id, err)
|
||||
errs++
|
||||
continue
|
||||
}
|
||||
if pl.IsEmpty() {
|
||||
fmt.Fprintf(w, "[✓] %s\n", id)
|
||||
converged++
|
||||
} else {
|
||||
fmt.Fprintf(w, "[✗] %s (%d actions pending)\n", id, len(pl.Actions))
|
||||
pending++
|
||||
}
|
||||
}
|
||||
|
||||
drift += checkArduinoCLI(ctx, runner, cfg.ArduinoCLI.Version, w)
|
||||
drift += checkWokwiCLI(ctx, runner, cfg.Renode.WokwiVersion, w)
|
||||
|
||||
fmt.Fprintf(w, "\n%d converged, %d need-install, %d drift, %d errors\n", converged, pending, drift, errs)
|
||||
|
||||
if pending > 0 || drift > 0 || errs > 0 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type arduinoCLIVersion struct {
|
||||
VersionString string `json:"VersionString"`
|
||||
}
|
||||
|
||||
func checkArduinoCLI(ctx context.Context, runner platform.Runner, want string, w io.Writer) int {
|
||||
if want == "" {
|
||||
return 0
|
||||
}
|
||||
out, err := runner.Output(ctx, "arduino-cli", "version", "--format", "json")
|
||||
if err != nil {
|
||||
return 0 // not installed → silent skip
|
||||
}
|
||||
var v arduinoCLIVersion
|
||||
if err := json.Unmarshal(out, &v); err != nil || v.VersionString == "" {
|
||||
fmt.Fprintf(w, "[!] arduino-cli — version parse failed\n")
|
||||
return 1
|
||||
}
|
||||
if v.VersionString != want {
|
||||
fmt.Fprintf(w, "[!] arduino-cli drift: got %s, want %s\n", v.VersionString, want)
|
||||
return 1
|
||||
}
|
||||
fmt.Fprintf(w, "[✓] arduino-cli %s\n", v.VersionString)
|
||||
return 0
|
||||
}
|
||||
|
||||
var wokwiVersionRE = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
|
||||
|
||||
func checkWokwiCLI(ctx context.Context, runner platform.Runner, want string, w io.Writer) int {
|
||||
if want == "" {
|
||||
return 0
|
||||
}
|
||||
out, err := runner.Output(ctx, "wokwi-cli", "--version")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
m := wokwiVersionRE.FindStringSubmatch(strings.TrimSpace(string(out)))
|
||||
if len(m) < 2 {
|
||||
fmt.Fprintf(w, "[!] wokwi-cli — version parse failed\n")
|
||||
return 1
|
||||
}
|
||||
got := m[1]
|
||||
if got != want {
|
||||
fmt.Fprintf(w, "[!] wokwi-cli drift: got %s, want %s\n", got, want)
|
||||
return 1
|
||||
}
|
||||
fmt.Fprintf(w, "[✓] wokwi-cli %s\n", got)
|
||||
return 0
|
||||
}
|
||||
135
internal/subcommands/doctor/doctor_test.go
Normal file
135
internal/subcommands/doctor/doctor_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
)
|
||||
|
||||
// convergedModule returns an empty Plan — always converged.
|
||||
type convergedModule struct{ id string }
|
||||
|
||||
func (c convergedModule) Meta() module.Meta { return module.Meta{ID: c.id, Name: c.id} }
|
||||
func (c convergedModule) Detect(context.Context) (module.State, error) { return nil, nil }
|
||||
func (c convergedModule) Plan(context.Context, module.State) (module.Plan, error) {
|
||||
return module.Plan{}, nil
|
||||
}
|
||||
func (c convergedModule) Apply(context.Context, module.Plan, module.Reporter) error { return nil }
|
||||
func (c convergedModule) Verify(context.Context) error { return nil }
|
||||
|
||||
type pendingModule struct{ id string }
|
||||
|
||||
func (p pendingModule) Meta() module.Meta { return module.Meta{ID: p.id} }
|
||||
func (p pendingModule) Detect(context.Context) (module.State, error) { return nil, nil }
|
||||
func (p pendingModule) Plan(context.Context, module.State) (module.Plan, error) {
|
||||
return module.Plan{Actions: []module.Action{{Description: "do a thing"}}}, nil
|
||||
}
|
||||
func (p pendingModule) Apply(context.Context, module.Plan, module.Reporter) error { return nil }
|
||||
func (p pendingModule) Verify(context.Context) error { return nil }
|
||||
|
||||
type detectFailModule struct{ id string }
|
||||
|
||||
func (d detectFailModule) Meta() module.Meta { return module.Meta{ID: d.id} }
|
||||
func (d detectFailModule) Detect(context.Context) (module.State, error) { return nil, errors.New("boom") }
|
||||
func (d detectFailModule) Plan(context.Context, module.State) (module.Plan, error) {
|
||||
return module.Plan{}, nil
|
||||
}
|
||||
func (d detectFailModule) Apply(context.Context, module.Plan, module.Reporter) error { return nil }
|
||||
func (d detectFailModule) Verify(context.Context) error { return nil }
|
||||
|
||||
// notFoundRunner simulates "command not found" for version checks.
|
||||
type notFoundRunner struct{}
|
||||
|
||||
func (notFoundRunner) Run(context.Context, string, ...string) error { return errors.New("not found") }
|
||||
func (notFoundRunner) Output(context.Context, string, ...string) ([]byte, error) {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
|
||||
// fixedRunner returns a preset version string for a matching command.
|
||||
type fixedRunner struct {
|
||||
arduinoOut []byte
|
||||
arduinoErr error
|
||||
wokwiOut []byte
|
||||
wokwiErr error
|
||||
}
|
||||
|
||||
func (f fixedRunner) Run(context.Context, string, ...string) error { return nil }
|
||||
func (f fixedRunner) Output(_ context.Context, name string, args ...string) ([]byte, error) {
|
||||
switch name {
|
||||
case "arduino-cli":
|
||||
return f.arduinoOut, f.arduinoErr
|
||||
case "wokwi-cli":
|
||||
return f.wokwiOut, f.wokwiErr
|
||||
}
|
||||
return nil, errors.New("no match")
|
||||
}
|
||||
|
||||
func TestDoctorAllConvergedExitsZero(t *testing.T) {
|
||||
reg := module.NewRegistry()
|
||||
_ = reg.Register(convergedModule{id: "foo"})
|
||||
var buf bytes.Buffer
|
||||
code := Run(context.Background(), config.Config{}, reg, notFoundRunner{}, &buf)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit %d, want 0; output:\n%s", code, buf.String())
|
||||
}
|
||||
if !strings.Contains(buf.String(), "[✓] foo") {
|
||||
t.Errorf("missing converged mark: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoctorDivergedExitsOne(t *testing.T) {
|
||||
reg := module.NewRegistry()
|
||||
_ = reg.Register(pendingModule{id: "bar"})
|
||||
var buf bytes.Buffer
|
||||
code := Run(context.Background(), config.Config{}, reg, notFoundRunner{}, &buf)
|
||||
if code != 1 {
|
||||
t.Fatalf("exit %d, want 1", code)
|
||||
}
|
||||
if !strings.Contains(buf.String(), "[✗] bar") {
|
||||
t.Errorf("missing diverged mark: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoctorDetectErrorIsReported(t *testing.T) {
|
||||
reg := module.NewRegistry()
|
||||
_ = reg.Register(detectFailModule{id: "baz"})
|
||||
var buf bytes.Buffer
|
||||
code := Run(context.Background(), config.Config{}, reg, notFoundRunner{}, &buf)
|
||||
if code != 1 {
|
||||
t.Fatalf("exit %d, want 1", code)
|
||||
}
|
||||
if !strings.Contains(buf.String(), "[!] baz") {
|
||||
t.Errorf("missing error mark: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoctorArduinoCLIDriftIsReported(t *testing.T) {
|
||||
reg := module.NewRegistry()
|
||||
_ = reg.Register(convergedModule{id: "foo"})
|
||||
cfg := config.Config{ArduinoCLI: config.ArduinoCLIConfig{Version: "0.35.3"}}
|
||||
runner := fixedRunner{arduinoOut: []byte(`{"VersionString":"0.36.0"}`)}
|
||||
var buf bytes.Buffer
|
||||
code := Run(context.Background(), cfg, reg, runner, &buf)
|
||||
if code != 1 {
|
||||
t.Fatalf("exit %d, want 1 (drift)", code)
|
||||
}
|
||||
if !strings.Contains(buf.String(), "drift") {
|
||||
t.Errorf("missing drift report: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoctorMissingBinariesSkipVersionCheck(t *testing.T) {
|
||||
reg := module.NewRegistry()
|
||||
_ = reg.Register(convergedModule{id: "foo"})
|
||||
cfg := config.Config{ArduinoCLI: config.ArduinoCLIConfig{Version: "0.35.3"}}
|
||||
var buf bytes.Buffer
|
||||
code := Run(context.Background(), cfg, reg, notFoundRunner{}, &buf)
|
||||
if code != 0 {
|
||||
t.Fatalf("exit %d, want 0; output:\n%s", code, buf.String())
|
||||
}
|
||||
}
|
||||
9
internal/subcommands/newproject/assets/default.ino
Normal file
9
internal/subcommands/newproject/assets/default.ino
Normal file
@@ -0,0 +1,9 @@
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial) { ; }
|
||||
Serial.println("Hello from Arduino!");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Your code here
|
||||
}
|
||||
22
internal/subcommands/newproject/assets/esp32.ino
Normal file
22
internal/subcommands/newproject/assets/esp32.ino
Normal file
@@ -0,0 +1,22 @@
|
||||
#include <WiFi.h>
|
||||
|
||||
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);
|
||||
}
|
||||
9
internal/subcommands/newproject/assets/esp32s3.ino
Normal file
9
internal/subcommands/newproject/assets/esp32s3.ino
Normal file
@@ -0,0 +1,9 @@
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial) { ; }
|
||||
Serial.println("Hello from Arduino!");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Your code here
|
||||
}
|
||||
22
internal/subcommands/newproject/assets/esp8266.ino
Normal file
22
internal/subcommands/newproject/assets/esp8266.ino
Normal file
@@ -0,0 +1,22 @@
|
||||
#include <ESP8266WiFi.h>
|
||||
|
||||
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);
|
||||
}
|
||||
9
internal/subcommands/newproject/assets/mega.ino
Normal file
9
internal/subcommands/newproject/assets/mega.ino
Normal file
@@ -0,0 +1,9 @@
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial) { ; }
|
||||
Serial.println("Hello from Arduino!");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Your code here
|
||||
}
|
||||
9
internal/subcommands/newproject/assets/nano.ino
Normal file
9
internal/subcommands/newproject/assets/nano.ino
Normal file
@@ -0,0 +1,9 @@
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial) { ; }
|
||||
Serial.println("Hello from Arduino!");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Your code here
|
||||
}
|
||||
9
internal/subcommands/newproject/assets/uno.ino
Normal file
9
internal/subcommands/newproject/assets/uno.ino
Normal file
@@ -0,0 +1,9 @@
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
while (!Serial) { ; }
|
||||
Serial.println("Hello from Arduino!");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Your code here
|
||||
}
|
||||
75
internal/subcommands/newproject/newproject.go
Normal file
75
internal/subcommands/newproject/newproject.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// Package newproject scaffolds a new Arduino sketch directory.
|
||||
package newproject
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/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 <template>.ino, else default.ino.
|
||||
inoData, err := assetsFS.ReadFile(filepath.Join("assets", b.Template+".ino"))
|
||||
if err != nil {
|
||||
inoData, err = assetsFS.ReadFile("assets/default.ino")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := platform.WriteUserFile(filepath.Join(project, project+".ino"), inoData, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sketch := fmt.Sprintf("default_fqbn: %s\ndefault_port: auto\n", b.FQBN)
|
||||
if err := platform.WriteUserFile(filepath.Join(project, "sketch.yaml"), []byte(sketch), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := platform.WriteUserFile(filepath.Join(project, ".clangd"), []byte(clangdContent), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
84
internal/subcommands/newproject/newproject_test.go
Normal file
84
internal/subcommands/newproject/newproject_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package newproject
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
)
|
||||
|
||||
func sampleCfg() config.Config {
|
||||
return config.Config{
|
||||
Scaffolding: config.Scaffolding{Boards: []config.Board{
|
||||
{Name: "uno", FQBN: "arduino:avr:uno", Template: "uno"},
|
||||
{Name: "xyz", FQBN: "fake:fqbn:xyz", Template: "does-not-exist"},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCreatesProject(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Chdir(dir)
|
||||
if err := Run(sampleCfg(), "blinky", "uno"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ino, err := os.ReadFile(filepath.Join("blinky", "blinky.ino"))
|
||||
if err != nil {
|
||||
t.Fatalf("blinky.ino missing: %v", err)
|
||||
}
|
||||
if len(ino) == 0 {
|
||||
t.Error("blinky.ino empty")
|
||||
}
|
||||
sketch, err := os.ReadFile(filepath.Join("blinky", "sketch.yaml"))
|
||||
if err != nil {
|
||||
t.Fatalf("sketch.yaml missing: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(sketch), "arduino:avr:uno") {
|
||||
t.Errorf("sketch.yaml missing fqbn: %s", sketch)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join("blinky", ".clangd")); err != nil {
|
||||
t.Errorf(".clangd missing: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRefusesExistingDir(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Chdir(dir)
|
||||
os.Mkdir("blinky", 0o755)
|
||||
if err := Run(sampleCfg(), "blinky", "uno"); err == nil {
|
||||
t.Fatal("want error on existing dir")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunFallsBackToDefaultTemplate(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Chdir(dir)
|
||||
if err := Run(sampleCfg(), "thing", "xyz"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join("thing", "thing.ino")); err != nil {
|
||||
t.Fatalf("thing.ino missing: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunUnknownBoardErrors(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Chdir(dir)
|
||||
err := Run(sampleCfg(), "thing", "bogus")
|
||||
if err == nil {
|
||||
t.Fatal("want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "uno") {
|
||||
t.Errorf("error should list valid boards: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRejectsSlashInProjectName(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Chdir(dir)
|
||||
if err := Run(sampleCfg(), "bad/name", "uno"); err == nil {
|
||||
t.Fatal("want error on slashes")
|
||||
}
|
||||
}
|
||||
13
internal/theming/assets/catppuccin/lazygit.yml
Normal file
13
internal/theming/assets/catppuccin/lazygit.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
gui:
|
||||
theme:
|
||||
activeBorderColor:
|
||||
- "#f9e2af"
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- "#585b70"
|
||||
selectedLineBgColor:
|
||||
- "#313244"
|
||||
cherryPickedCommitFgColor:
|
||||
- "#a6e3a1"
|
||||
defaultFgColor:
|
||||
- "#cdd6f4"
|
||||
26
internal/theming/assets/catppuccin/starship.toml
Normal file
26
internal/theming/assets/catppuccin/starship.toml
Normal file
@@ -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"
|
||||
2
internal/theming/assets/catppuccin/theme.yaml
Normal file
2
internal/theming/assets/catppuccin/theme.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
display_name: "Catppuccin Mocha"
|
||||
nvchad: "catppuccin"
|
||||
30
internal/theming/assets/catppuccin/yazi.toml
Normal file
30
internal/theming/assets/catppuccin/yazi.toml
Normal file
@@ -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 = {}
|
||||
35
internal/theming/assets/catppuccin/zellij.kdl
Normal file
35
internal/theming/assets/catppuccin/zellij.kdl
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
13
internal/theming/assets/dracula/lazygit.yml
Normal file
13
internal/theming/assets/dracula/lazygit.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
gui:
|
||||
theme:
|
||||
activeBorderColor:
|
||||
- "#f1fa8c"
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- "#6272a4"
|
||||
selectedLineBgColor:
|
||||
- "#44475a"
|
||||
cherryPickedCommitFgColor:
|
||||
- "#50fa7b"
|
||||
defaultFgColor:
|
||||
- "#f8f8f2"
|
||||
26
internal/theming/assets/dracula/starship.toml
Normal file
26
internal/theming/assets/dracula/starship.toml
Normal file
@@ -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"
|
||||
2
internal/theming/assets/dracula/theme.yaml
Normal file
2
internal/theming/assets/dracula/theme.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
display_name: "Dracula"
|
||||
nvchad: "dracula"
|
||||
30
internal/theming/assets/dracula/yazi.toml
Normal file
30
internal/theming/assets/dracula/yazi.toml
Normal file
@@ -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 = {}
|
||||
35
internal/theming/assets/dracula/zellij.kdl
Normal file
35
internal/theming/assets/dracula/zellij.kdl
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
13
internal/theming/assets/nord/lazygit.yml
Normal file
13
internal/theming/assets/nord/lazygit.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
gui:
|
||||
theme:
|
||||
activeBorderColor:
|
||||
- "#ebcb8b"
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- "#4c566a"
|
||||
selectedLineBgColor:
|
||||
- "#3b4252"
|
||||
cherryPickedCommitFgColor:
|
||||
- "#a3be8c"
|
||||
defaultFgColor:
|
||||
- "#d8dee9"
|
||||
26
internal/theming/assets/nord/starship.toml
Normal file
26
internal/theming/assets/nord/starship.toml
Normal file
@@ -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"
|
||||
2
internal/theming/assets/nord/theme.yaml
Normal file
2
internal/theming/assets/nord/theme.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
display_name: "Nord"
|
||||
nvchad: "nord"
|
||||
30
internal/theming/assets/nord/yazi.toml
Normal file
30
internal/theming/assets/nord/yazi.toml
Normal file
@@ -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 = {}
|
||||
35
internal/theming/assets/nord/zellij.kdl
Normal file
35
internal/theming/assets/nord/zellij.kdl
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
13
internal/theming/assets/tokyonight/lazygit.yml
Normal file
13
internal/theming/assets/tokyonight/lazygit.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
gui:
|
||||
theme:
|
||||
activeBorderColor:
|
||||
- "#e0af68"
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- "#3b4261"
|
||||
selectedLineBgColor:
|
||||
- "#283457"
|
||||
cherryPickedCommitFgColor:
|
||||
- "#9ece6a"
|
||||
defaultFgColor:
|
||||
- "#c0caf5"
|
||||
26
internal/theming/assets/tokyonight/starship.toml
Normal file
26
internal/theming/assets/tokyonight/starship.toml
Normal file
@@ -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"
|
||||
2
internal/theming/assets/tokyonight/theme.yaml
Normal file
2
internal/theming/assets/tokyonight/theme.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
display_name: "Tokyo Night"
|
||||
nvchad: "tokyonight"
|
||||
30
internal/theming/assets/tokyonight/yazi.toml
Normal file
30
internal/theming/assets/tokyonight/yazi.toml
Normal file
@@ -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 = {}
|
||||
35
internal/theming/assets/tokyonight/zellij.kdl
Normal file
35
internal/theming/assets/tokyonight/zellij.kdl
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,18 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/platform"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/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
|
||||
|
||||
@@ -90,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 {
|
||||
@@ -144,6 +166,17 @@ func Apply(cfg config.Config, themeID string) error {
|
||||
if err := updateOverlayActive(th.ID); err != nil {
|
||||
return fmt.Errorf("update overlay: %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
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
)
|
||||
|
||||
func TestListDiscoversThemes(t *testing.T) {
|
||||
@@ -117,3 +117,84 @@ 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultIsGruvbox(t *testing.T) {
|
||||
if got := Default(); got.ID != "gruvbox" {
|
||||
t.Fatalf("Default().ID = %q, want gruvbox", got.ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/engine"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/modules/hello"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/tui"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/engine"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/modules/hello"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/tui"
|
||||
)
|
||||
|
||||
// TestDashboardRunsHelloModule exercises the whole reporter → model loop
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/engine"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/tui"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/engine"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/tui"
|
||||
)
|
||||
|
||||
// DoneMsg is sent from the engine goroutine once engine.Run returns.
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/engine"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/tui"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/engine"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/tui"
|
||||
)
|
||||
|
||||
func TestProgressUpdatesRow(t *testing.T) {
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/engine"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/tui"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/engine"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/tui"
|
||||
)
|
||||
|
||||
// Run displays the dashboard while engine.Run executes the selected modules.
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/x/exp/teatest"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/engine"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/tui"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/engine"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/tui"
|
||||
)
|
||||
|
||||
// TestModelEndToEnd drives the Model through teatest, feeding in the same
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
)
|
||||
|
||||
// ProgressMsg is emitted when a module reports progress during Apply.
|
||||
|
||||
@@ -3,37 +3,17 @@ package tui
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/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,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 })
|
||||
|
||||
theme := BuiltinThemes[0].ID
|
||||
theme := theming.Default().ID
|
||||
if override, ok := cfg.ThemeOverrides["__active"]; ok && override != "" {
|
||||
theme = override
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/config"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/config"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/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.Default().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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ import (
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/tui"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/theming"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/tui"
|
||||
)
|
||||
|
||||
// Run presents the six-screen wizard. Returns the final state and whether
|
||||
@@ -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))
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/module"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/modules/hello"
|
||||
"github.com/MDietermann/ArduinoArchSetup/internal/tui"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/module"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/modules/hello"
|
||||
"gitlab.hive-brain.de/MDietermann/Archduino/internal/tui"
|
||||
)
|
||||
|
||||
func TestSyncSelected(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user