From a203cdfe8927486bd33cf497214ea1c074d0992c Mon Sep 17 00:00:00 2001 From: MDietermann Date: Tue, 14 Apr 2026 14:22:33 +0200 Subject: [PATCH] feat(cmd): add theme/new/doctor subcommands; install applies theme --- cmd/archduino/main.go | 130 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 2 deletions(-) diff --git a/cmd/archduino/main.go b/cmd/archduino/main.go index a9eab4e..aa77686 100644 --- a/cmd/archduino/main.go +++ b/cmd/archduino/main.go @@ -2,12 +2,14 @@ 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" @@ -28,6 +30,9 @@ import ( "github.com/MDietermann/ArduinoArchSetup/internal/platform" "github.com/MDietermann/ArduinoArchSetup/internal/privilege" "github.com/MDietermann/ArduinoArchSetup/internal/state" + "github.com/MDietermann/ArduinoArchSetup/internal/subcommands/doctor" + "github.com/MDietermann/ArduinoArchSetup/internal/subcommands/newproject" + "github.com/MDietermann/ArduinoArchSetup/internal/theming" "github.com/MDietermann/ArduinoArchSetup/internal/tui" "github.com/MDietermann/ArduinoArchSetup/internal/tui/dashboard" "github.com/MDietermann/ArduinoArchSetup/internal/tui/wizard" @@ -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 ", + 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 +}