summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/headless
diff options
context:
space:
mode:
authorNippy <nippy@rp1.hu>2026-05-09 13:33:09 +0200
committerNippy <nippy@rp1.hu>2026-05-09 13:33:09 +0200
commita2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5 (patch)
tree1a8769217f84bfe9d6216fafaadaf8cdd876d457 /raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/headless
parent34b9c34f982b2596cfa9e368a2d7f74e9a17477c (diff)
downloadRaveOS-PKGBUILD-a2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5.tar.gz
RaveOS-PKGBUILD-a2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5.zip
raveos update
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/headless')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/headless/runner.go418
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/headless/runner_test.go459
2 files changed, 0 insertions, 877 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/headless/runner.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/headless/runner.go
deleted file mode 100644
index 7dae9fc..0000000
--- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/headless/runner.go
+++ /dev/null
@@ -1,418 +0,0 @@
-package headless
-
-import (
- "context"
- "fmt"
- "os"
- "os/exec"
- "strings"
-
- "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
- "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
- "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
- "github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
-)
-
-// ErrConfirmationRequired is returned when --yes is not set and the user
-// must explicitly confirm the operation.
-var ErrConfirmationRequired = fmt.Errorf("confirmation required: pass --yes to proceed")
-
-// validConfigNames maps lowercase CLI input to the deployer key used in
-// replaceConfigs. Keep in sync with the config types checked by
-// shouldReplaceConfig in deployer.go.
-var validConfigNames = map[string]string{
- "niri": "Niri",
- "hyprland": "Hyprland",
- "ghostty": "Ghostty",
- "kitty": "Kitty",
- "alacritty": "Alacritty",
-}
-
-// orderedConfigNames defines the canonical order for config names in output.
-// Must be kept in sync with validConfigNames.
-var orderedConfigNames = []string{"niri", "hyprland", "ghostty", "kitty", "alacritty"}
-
-// Config holds all CLI parameters for unattended installation.
-type Config struct {
- Compositor string // "niri" or "hyprland"
- Terminal string // "ghostty", "kitty", or "alacritty"
- IncludeDeps []string
- ExcludeDeps []string
- ReplaceConfigs []string // specific configs to deploy (e.g. "niri", "ghostty")
- ReplaceConfigsAll bool // deploy/replace all configurations
- Yes bool
-}
-
-// Runner orchestrates unattended (headless) installation.
-type Runner struct {
- cfg Config
- logChan chan string
-}
-
-// NewRunner creates a new headless runner.
-func NewRunner(cfg Config) *Runner {
- return &Runner{
- cfg: cfg,
- logChan: make(chan string, 1000),
- }
-}
-
-// GetLogChan returns the log channel for file logging.
-func (r *Runner) GetLogChan() <-chan string {
- return r.logChan
-}
-
-// Run executes the full unattended installation flow.
-func (r *Runner) Run() error {
- r.log("Starting headless installation")
-
- // 1. Parse compositor and terminal selections
- wm, err := r.parseWindowManager()
- if err != nil {
- return err
- }
-
- terminal, err := r.parseTerminal()
- if err != nil {
- return err
- }
-
- // 2. Build replace-configs map
- replaceConfigs, err := r.buildReplaceConfigs()
- if err != nil {
- return err
- }
-
- // 3. Detect OS
- r.log("Detecting operating system...")
- osInfo, err := distros.GetOSInfo()
- if err != nil {
- return fmt.Errorf("OS detection failed: %w", err)
- }
-
- if distros.IsUnsupportedDistro(osInfo.Distribution.ID, osInfo.VersionID) {
- return fmt.Errorf("unsupported distribution: %s %s", osInfo.PrettyName, osInfo.VersionID)
- }
-
- fmt.Fprintf(os.Stdout, "Detected: %s (%s)\n", osInfo.PrettyName, osInfo.Architecture)
-
- // 4. Create distribution instance
- distro, err := distros.NewDistribution(osInfo.Distribution.ID, r.logChan)
- if err != nil {
- return fmt.Errorf("failed to initialize distribution: %w", err)
- }
-
- // 5. Detect dependencies
- r.log("Detecting dependencies...")
- fmt.Fprintln(os.Stdout, "Detecting dependencies...")
- dependencies, err := distro.DetectDependenciesWithTerminal(context.Background(), wm, terminal)
- if err != nil {
- return fmt.Errorf("dependency detection failed: %w", err)
- }
-
- // 5. Apply include/exclude filters and build the disabled-items map.
- // Headless mode does not currently collect any explicit reinstall selections,
- // so keep reinstallItems nil instead of constructing an always-empty map.
- disabledItems, err := r.buildDisabledItems(dependencies)
- if err != nil {
- return err
- }
- var reinstallItems map[string]bool
-
- // Print dependency summary
- fmt.Fprintln(os.Stdout, "\nDependencies:")
- for _, dep := range dependencies {
- marker := " "
- status := ""
- if disabledItems[dep.Name] {
- marker = " SKIP "
- status = "(disabled)"
- } else {
- switch dep.Status {
- case deps.StatusInstalled:
- marker = " OK "
- status = "(installed)"
- case deps.StatusMissing:
- marker = " NEW "
- status = "(will install)"
- case deps.StatusNeedsUpdate:
- marker = " UPD "
- status = "(will update)"
- case deps.StatusNeedsReinstall:
- marker = " RE "
- status = "(will reinstall)"
- }
- }
- fmt.Fprintf(os.Stdout, "%s%-30s %s\n", marker, dep.Name, status)
- }
- fmt.Fprintln(os.Stdout)
-
- // 6b. Require explicit confirmation unless --yes is set
- if !r.cfg.Yes {
- if replaceConfigs == nil {
- // --replace-configs-all
- fmt.Fprintln(os.Stdout, "Packages will be installed and all configurations will be replaced.")
- fmt.Fprintln(os.Stdout, "Existing config files will be backed up before replacement.")
- } else if r.anyConfigEnabled(replaceConfigs) {
- var names []string
- for _, cliName := range orderedConfigNames {
- deployerKey := validConfigNames[cliName]
- if replaceConfigs[deployerKey] {
- names = append(names, deployerKey)
- }
- }
- fmt.Fprintf(os.Stdout, "Packages will be installed. The following configurations will be replaced (with backups): %s\n", strings.Join(names, ", "))
- } else {
- fmt.Fprintln(os.Stdout, "Packages will be installed. No configurations will be deployed.")
- }
- fmt.Fprintln(os.Stdout, "Re-run with --yes (-y) to proceed.")
- r.log("Aborted: --yes not set")
- return ErrConfirmationRequired
- }
-
- // 7. Authenticate sudo
- sudoPassword, err := r.resolveSudoPassword()
- if err != nil {
- return err
- }
-
- // 8. Install packages
- fmt.Fprintln(os.Stdout, "Installing packages...")
- r.log("Starting package installation")
-
- progressChan := make(chan distros.InstallProgressMsg, 100)
-
- installErr := make(chan error, 1)
- go func() {
- defer close(progressChan)
- installErr <- distro.InstallPackages(
- context.Background(),
- dependencies,
- wm,
- sudoPassword,
- reinstallItems,
- disabledItems,
- false, // skipGlobalUseFlags
- progressChan,
- )
- }()
-
- // Consume progress messages and print them
- for msg := range progressChan {
- if msg.Error != nil {
- fmt.Fprintf(os.Stderr, "Error: %v\n", msg.Error)
- } else if msg.Step != "" {
- fmt.Fprintf(os.Stdout, " [%3.0f%%] %s\n", msg.Progress*100, msg.Step)
- }
- if msg.LogOutput != "" {
- r.log(msg.LogOutput)
- fmt.Fprintf(os.Stdout, " %s\n", msg.LogOutput)
- }
- }
-
- if err := <-installErr; err != nil {
- return fmt.Errorf("package installation failed: %w", err)
- }
-
- // 9. Greeter setup (if dms-greeter was included)
- if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") {
- compositorName := "niri"
- if wm == deps.WindowManagerHyprland {
- compositorName = "Hyprland"
- }
- fmt.Fprintln(os.Stdout, "Configuring DMS greeter...")
- logFunc := func(line string) {
- r.log(line)
- fmt.Fprintf(os.Stdout, " greeter: %s\n", line)
- }
- if err := greeter.AutoSetupGreeter(compositorName, sudoPassword, logFunc); err != nil {
- // Non-fatal, matching TUI behavior
- fmt.Fprintf(os.Stderr, "Warning: greeter setup issue (non-fatal): %v\n", err)
- }
- }
-
- // 10. Deploy configurations
- fmt.Fprintln(os.Stdout, "Deploying configurations...")
- r.log("Starting configuration deployment")
-
- deployer := config.NewConfigDeployer(r.logChan)
- results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(
- context.Background(),
- wm,
- terminal,
- dependencies,
- replaceConfigs,
- reinstallItems,
- )
- if err != nil {
- return fmt.Errorf("configuration deployment failed: %w", err)
- }
-
- for _, result := range results {
- if result.Deployed {
- msg := fmt.Sprintf(" Deployed: %s", result.ConfigType)
- if result.BackupPath != "" {
- msg += fmt.Sprintf(" (backup: %s)", result.BackupPath)
- }
- fmt.Fprintln(os.Stdout, msg)
- }
- if result.Error != nil {
- fmt.Fprintf(os.Stderr, " Error deploying %s: %v\n", result.ConfigType, result.Error)
- }
- }
-
- fmt.Fprintln(os.Stdout, "\nInstallation complete!")
- r.log("Headless installation completed successfully")
- return nil
-}
-
-// buildDisabledItems computes the set of dependencies that should be skipped
-// during installation, applying the --include-deps and --exclude-deps filters.
-// dms-greeter is disabled by default (opt-in), matching TUI behavior.
-func (r *Runner) buildDisabledItems(dependencies []deps.Dependency) (map[string]bool, error) {
- disabledItems := make(map[string]bool)
-
- // dms-greeter is opt-in (disabled by default), matching TUI behavior
- for i := range dependencies {
- if dependencies[i].Name == "dms-greeter" {
- disabledItems["dms-greeter"] = true
- break
- }
- }
-
- // Process --include-deps (enable items that are disabled by default)
- for _, name := range r.cfg.IncludeDeps {
- name = strings.TrimSpace(name)
- if name == "" {
- continue
- }
- if !r.depExists(dependencies, name) {
- return nil, fmt.Errorf("--include-deps: unknown dependency %q", name)
- }
- delete(disabledItems, name)
- }
-
- // Process --exclude-deps (disable items)
- for _, name := range r.cfg.ExcludeDeps {
- name = strings.TrimSpace(name)
- if name == "" {
- continue
- }
- if !r.depExists(dependencies, name) {
- return nil, fmt.Errorf("--exclude-deps: unknown dependency %q", name)
- }
- // Don't allow excluding DMS itself
- if name == "dms (DankMaterialShell)" {
- return nil, fmt.Errorf("--exclude-deps: cannot exclude required package %q", name)
- }
- disabledItems[name] = true
- }
-
- return disabledItems, nil
-}
-
-// buildReplaceConfigs converts the --replace-configs / --replace-configs-all
-// flags into the map[string]bool consumed by the config deployer.
-//
-// Returns:
-// - nil when --replace-configs-all is set (deployer treats nil as "replace all")
-// - a map with all known configs set to false when neither flag is set (deploy only if config file is missing on disk)
-// - a map with requested configs true, all others false for --replace-configs
-// - an error when both flags are set or an invalid config name is given
-func (r *Runner) buildReplaceConfigs() (map[string]bool, error) {
- hasSpecific := len(r.cfg.ReplaceConfigs) > 0
- if hasSpecific && r.cfg.ReplaceConfigsAll {
- return nil, fmt.Errorf("--replace-configs and --replace-configs-all are mutually exclusive")
- }
-
- if r.cfg.ReplaceConfigsAll {
- return nil, nil
- }
-
- // Build a map with all known configs explicitly set to false
- result := make(map[string]bool, len(validConfigNames))
- for _, cliName := range orderedConfigNames {
- result[validConfigNames[cliName]] = false
- }
-
- // Enable only the requested configs
- for _, name := range r.cfg.ReplaceConfigs {
- name = strings.TrimSpace(name)
- if name == "" {
- continue
- }
- deployerKey, ok := validConfigNames[strings.ToLower(name)]
- if !ok {
- return nil, fmt.Errorf("--replace-configs: unknown config %q; valid values: niri, hyprland, ghostty, kitty, alacritty", name)
- }
- result[deployerKey] = true
- }
-
- return result, nil
-}
-
-func (r *Runner) log(message string) {
- select {
- case r.logChan <- message:
- default:
- }
-}
-
-func (r *Runner) parseWindowManager() (deps.WindowManager, error) {
- switch strings.ToLower(r.cfg.Compositor) {
- case "niri":
- return deps.WindowManagerNiri, nil
- case "hyprland":
- return deps.WindowManagerHyprland, nil
- default:
- return 0, fmt.Errorf("invalid --compositor value %q: must be 'niri' or 'hyprland'", r.cfg.Compositor)
- }
-}
-
-func (r *Runner) parseTerminal() (deps.Terminal, error) {
- switch strings.ToLower(r.cfg.Terminal) {
- case "ghostty":
- return deps.TerminalGhostty, nil
- case "kitty":
- return deps.TerminalKitty, nil
- case "alacritty":
- return deps.TerminalAlacritty, nil
- default:
- return 0, fmt.Errorf("invalid --term value %q: must be 'ghostty', 'kitty', or 'alacritty'", r.cfg.Terminal)
- }
-}
-
-func (r *Runner) resolveSudoPassword() (string, error) {
- // Check if sudo credentials are cached (via sudo -v or NOPASSWD)
- cmd := exec.Command("sudo", "-n", "true")
- if err := cmd.Run(); err == nil {
- r.log("sudo cache is valid, no password needed")
- fmt.Fprintln(os.Stdout, "sudo: using cached credentials")
- return "", nil
- }
-
- return "", fmt.Errorf(
- "sudo authentication required but no cached credentials found\n" +
- "Options:\n" +
- " 1. Run 'sudo -v' before dankinstall to cache credentials\n" +
- " 2. Configure passwordless sudo for your user",
- )
-}
-
-func (r *Runner) anyConfigEnabled(m map[string]bool) bool {
- for _, v := range m {
- if v {
- return true
- }
- }
- return false
-}
-
-func (r *Runner) depExists(dependencies []deps.Dependency, name string) bool {
- for _, dep := range dependencies {
- if dep.Name == name {
- return true
- }
- }
- return false
-}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/headless/runner_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/headless/runner_test.go
deleted file mode 100644
index 79260ae..0000000
--- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/headless/runner_test.go
+++ /dev/null
@@ -1,459 +0,0 @@
-package headless
-
-import (
- "strings"
- "testing"
-
- "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
-)
-
-func TestParseWindowManager(t *testing.T) {
- tests := []struct {
- name string
- input string
- want deps.WindowManager
- wantErr bool
- }{
- {"niri lowercase", "niri", deps.WindowManagerNiri, false},
- {"niri mixed case", "Niri", deps.WindowManagerNiri, false},
- {"hyprland lowercase", "hyprland", deps.WindowManagerHyprland, false},
- {"hyprland mixed case", "Hyprland", deps.WindowManagerHyprland, false},
- {"invalid", "sway", 0, true},
- {"empty", "", 0, true},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- r := NewRunner(Config{Compositor: tt.input})
- got, err := r.parseWindowManager()
- if (err != nil) != tt.wantErr {
- t.Errorf("parseWindowManager() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if !tt.wantErr && got != tt.want {
- t.Errorf("parseWindowManager() = %v, want %v", got, tt.want)
- }
- })
- }
-}
-
-func TestParseTerminal(t *testing.T) {
- tests := []struct {
- name string
- input string
- want deps.Terminal
- wantErr bool
- }{
- {"ghostty lowercase", "ghostty", deps.TerminalGhostty, false},
- {"ghostty mixed case", "Ghostty", deps.TerminalGhostty, false},
- {"kitty lowercase", "kitty", deps.TerminalKitty, false},
- {"alacritty lowercase", "alacritty", deps.TerminalAlacritty, false},
- {"alacritty uppercase", "ALACRITTY", deps.TerminalAlacritty, false},
- {"invalid", "wezterm", 0, true},
- {"empty", "", 0, true},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- r := NewRunner(Config{Terminal: tt.input})
- got, err := r.parseTerminal()
- if (err != nil) != tt.wantErr {
- t.Errorf("parseTerminal() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if !tt.wantErr && got != tt.want {
- t.Errorf("parseTerminal() = %v, want %v", got, tt.want)
- }
- })
- }
-}
-
-func TestDepExists(t *testing.T) {
- dependencies := []deps.Dependency{
- {Name: "niri", Status: deps.StatusInstalled},
- {Name: "ghostty", Status: deps.StatusMissing},
- {Name: "dms (DankMaterialShell)", Status: deps.StatusInstalled},
- {Name: "dms-greeter", Status: deps.StatusMissing},
- }
-
- tests := []struct {
- name string
- dep string
- want bool
- }{
- {"existing dep", "niri", true},
- {"existing dep with special chars", "dms (DankMaterialShell)", true},
- {"existing optional dep", "dms-greeter", true},
- {"non-existing dep", "firefox", false},
- {"empty name", "", false},
- }
-
- r := NewRunner(Config{})
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := r.depExists(dependencies, tt.dep); got != tt.want {
- t.Errorf("depExists(%q) = %v, want %v", tt.dep, got, tt.want)
- }
- })
- }
-}
-
-func TestNewRunner(t *testing.T) {
- cfg := Config{
- Compositor: "niri",
- Terminal: "ghostty",
- IncludeDeps: []string{"dms-greeter"},
- ExcludeDeps: []string{"some-pkg"},
- Yes: true,
- }
- r := NewRunner(cfg)
-
- if r == nil {
- t.Fatal("NewRunner returned nil")
- }
- if r.cfg.Compositor != "niri" {
- t.Errorf("cfg.Compositor = %q, want %q", r.cfg.Compositor, "niri")
- }
- if r.cfg.Terminal != "ghostty" {
- t.Errorf("cfg.Terminal = %q, want %q", r.cfg.Terminal, "ghostty")
- }
- if !r.cfg.Yes {
- t.Error("cfg.Yes = false, want true")
- }
- if r.logChan == nil {
- t.Error("logChan is nil")
- }
-}
-
-func TestGetLogChan(t *testing.T) {
- r := NewRunner(Config{})
- ch := r.GetLogChan()
- if ch == nil {
- t.Fatal("GetLogChan returned nil")
- }
-
- // Verify the channel is readable by sending a message
- go func() {
- r.logChan <- "test message"
- }()
- msg := <-ch
- if msg != "test message" {
- t.Errorf("received %q, want %q", msg, "test message")
- }
-}
-
-func TestLog(t *testing.T) {
- r := NewRunner(Config{})
-
- // log should not block even if channel is full
- for i := 0; i < 1100; i++ {
- r.log("message")
- }
- // If we reach here without hanging, the non-blocking send works
-}
-
-func TestRunRequiresYes(t *testing.T) {
- // Verify that ErrConfirmationRequired is a distinct sentinel error
- if ErrConfirmationRequired == nil {
- t.Fatal("ErrConfirmationRequired should not be nil")
- }
- expected := "confirmation required: pass --yes to proceed"
- if ErrConfirmationRequired.Error() != expected {
- t.Errorf("ErrConfirmationRequired = %q, want %q", ErrConfirmationRequired.Error(), expected)
- }
-}
-
-func TestConfigYesStoredCorrectly(t *testing.T) {
- // Yes=false (default) should be stored
- rNo := NewRunner(Config{Compositor: "niri", Terminal: "ghostty", Yes: false})
- if rNo.cfg.Yes {
- t.Error("cfg.Yes = true, want false")
- }
-
- // Yes=true should be stored
- rYes := NewRunner(Config{Compositor: "niri", Terminal: "ghostty", Yes: true})
- if !rYes.cfg.Yes {
- t.Error("cfg.Yes = false, want true")
- }
-}
-
-func TestValidConfigNamesCompleteness(t *testing.T) {
- // orderedConfigNames and validConfigNames must stay in sync.
- if len(orderedConfigNames) != len(validConfigNames) {
- t.Fatalf("orderedConfigNames has %d entries but validConfigNames has %d",
- len(orderedConfigNames), len(validConfigNames))
- }
-
- // Every entry in orderedConfigNames must exist in validConfigNames.
- for _, name := range orderedConfigNames {
- if _, ok := validConfigNames[name]; !ok {
- t.Errorf("orderedConfigNames contains %q which is missing from validConfigNames", name)
- }
- }
-
- // validConfigNames must have no extra keys not in orderedConfigNames.
- ordered := make(map[string]bool, len(orderedConfigNames))
- for _, name := range orderedConfigNames {
- ordered[name] = true
- }
- for key := range validConfigNames {
- if !ordered[key] {
- t.Errorf("validConfigNames contains %q which is missing from orderedConfigNames", key)
- }
- }
-}
-
-func TestBuildReplaceConfigs(t *testing.T) {
- allDeployerKeys := []string{"Niri", "Hyprland", "Ghostty", "Kitty", "Alacritty"}
-
- tests := []struct {
- name string
- replaceConfigs []string
- replaceAll bool
- wantNil bool // expect nil (replace all)
- wantEnabled []string // deployer keys that should be true
- wantErr bool
- }{
- {
- name: "neither flag set",
- wantNil: false,
- wantEnabled: nil, // all should be false
- },
- {
- name: "replace-configs-all",
- replaceAll: true,
- wantNil: true,
- },
- {
- name: "specific configs",
- replaceConfigs: []string{"niri", "ghostty"},
- wantNil: false,
- wantEnabled: []string{"Niri", "Ghostty"},
- },
- {
- name: "both flags set",
- replaceConfigs: []string{"niri"},
- replaceAll: true,
- wantErr: true,
- },
- {
- name: "invalid config name",
- replaceConfigs: []string{"foo"},
- wantErr: true,
- },
- {
- name: "case insensitive",
- replaceConfigs: []string{"NIRI", "Ghostty"},
- wantNil: false,
- wantEnabled: []string{"Niri", "Ghostty"},
- },
- {
- name: "single config",
- replaceConfigs: []string{"kitty"},
- wantNil: false,
- wantEnabled: []string{"Kitty"},
- },
- {
- name: "whitespace entry",
- replaceConfigs: []string{" ", "niri"},
- wantNil: false,
- wantEnabled: []string{"Niri"},
- },
- {
- name: "duplicate entry",
- replaceConfigs: []string{"niri", "niri"},
- wantNil: false,
- wantEnabled: []string{"Niri"},
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- r := NewRunner(Config{
- ReplaceConfigs: tt.replaceConfigs,
- ReplaceConfigsAll: tt.replaceAll,
- })
- got, err := r.buildReplaceConfigs()
- if (err != nil) != tt.wantErr {
- t.Fatalf("buildReplaceConfigs() error = %v, wantErr %v", err, tt.wantErr)
- }
- if tt.wantErr {
- return
- }
- if tt.wantNil {
- if got != nil {
- t.Fatalf("buildReplaceConfigs() = %v, want nil", got)
- }
- return
- }
- if got == nil {
- t.Fatal("buildReplaceConfigs() = nil, want non-nil map")
- }
-
- // All known deployer keys must be present
- for _, key := range allDeployerKeys {
- if _, exists := got[key]; !exists {
- t.Errorf("missing deployer key %q in result map", key)
- }
- }
-
- // Build enabled set for easy lookup
- enabledSet := make(map[string]bool)
- for _, k := range tt.wantEnabled {
- enabledSet[k] = true
- }
-
- for _, key := range allDeployerKeys {
- want := enabledSet[key]
- if got[key] != want {
- t.Errorf("replaceConfigs[%q] = %v, want %v", key, got[key], want)
- }
- }
- })
- }
-}
-
-func TestConfigReplaceConfigsStoredCorrectly(t *testing.T) {
- r := NewRunner(Config{
- Compositor: "niri",
- Terminal: "ghostty",
- ReplaceConfigs: []string{"niri", "ghostty"},
- ReplaceConfigsAll: false,
- })
- if len(r.cfg.ReplaceConfigs) != 2 {
- t.Errorf("len(ReplaceConfigs) = %d, want 2", len(r.cfg.ReplaceConfigs))
- }
- if r.cfg.ReplaceConfigsAll {
- t.Error("ReplaceConfigsAll = true, want false")
- }
-
- r2 := NewRunner(Config{
- Compositor: "niri",
- Terminal: "ghostty",
- ReplaceConfigsAll: true,
- })
- if !r2.cfg.ReplaceConfigsAll {
- t.Error("ReplaceConfigsAll = false, want true")
- }
- if len(r2.cfg.ReplaceConfigs) != 0 {
- t.Errorf("len(ReplaceConfigs) = %d, want 0", len(r2.cfg.ReplaceConfigs))
- }
-}
-
-func TestBuildDisabledItems(t *testing.T) {
- dependencies := []deps.Dependency{
- {Name: "niri", Status: deps.StatusInstalled},
- {Name: "ghostty", Status: deps.StatusMissing},
- {Name: "dms (DankMaterialShell)", Status: deps.StatusInstalled},
- {Name: "dms-greeter", Status: deps.StatusMissing},
- {Name: "waybar", Status: deps.StatusMissing},
- }
-
- tests := []struct {
- name string
- includeDeps []string
- excludeDeps []string
- deps []deps.Dependency // nil means use the shared fixture
- wantErr bool
- errContains string // substring expected in error message
- wantDisabled []string // dep names that should be in disabledItems
- wantEnabled []string // dep names that should NOT be in disabledItems (extra check)
- }{
- {
- name: "no flags set, dms-greeter disabled by default",
- wantDisabled: []string{"dms-greeter"},
- wantEnabled: []string{"niri", "ghostty", "waybar"},
- },
- {
- name: "include dms-greeter enables it",
- includeDeps: []string{"dms-greeter"},
- wantEnabled: []string{"dms-greeter"},
- },
- {
- name: "exclude a regular dep",
- excludeDeps: []string{"waybar"},
- wantDisabled: []string{"dms-greeter", "waybar"},
- },
- {
- name: "include unknown dep returns error",
- includeDeps: []string{"nonexistent"},
- wantErr: true,
- errContains: "--include-deps",
- },
- {
- name: "exclude unknown dep returns error",
- excludeDeps: []string{"nonexistent"},
- wantErr: true,
- errContains: "--exclude-deps",
- },
- {
- name: "exclude DMS itself is forbidden",
- excludeDeps: []string{"dms (DankMaterialShell)"},
- wantErr: true,
- errContains: "cannot exclude required package",
- },
- {
- name: "include and exclude same dep",
- includeDeps: []string{"dms-greeter"},
- excludeDeps: []string{"dms-greeter"},
- wantDisabled: []string{"dms-greeter"},
- },
- {
- name: "whitespace entries are skipped",
- includeDeps: []string{" ", "dms-greeter"},
- wantEnabled: []string{"dms-greeter"},
- },
- {
- name: "no dms-greeter in deps, nothing disabled by default",
- deps: []deps.Dependency{
- {Name: "niri", Status: deps.StatusInstalled},
- },
- wantEnabled: []string{"niri"},
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- r := NewRunner(Config{
- IncludeDeps: tt.includeDeps,
- ExcludeDeps: tt.excludeDeps,
- })
- d := tt.deps
- if d == nil {
- d = dependencies
- }
- got, err := r.buildDisabledItems(d)
- if (err != nil) != tt.wantErr {
- t.Fatalf("buildDisabledItems() error = %v, wantErr %v", err, tt.wantErr)
- }
- if tt.wantErr {
- if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
- t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
- }
- return
- }
- if got == nil {
- t.Fatal("buildDisabledItems() returned nil map, want non-nil")
- }
-
- // Check expected disabled items
- for _, name := range tt.wantDisabled {
- if !got[name] {
- t.Errorf("expected %q to be disabled, but it is not", name)
- }
- }
-
- // Check expected enabled items (should not be in the map or be false)
- for _, name := range tt.wantEnabled {
- if got[name] {
- t.Errorf("expected %q to NOT be disabled, but it is", name)
- }
- }
-
- // If wantDisabled is empty, the map should have length 0
- if len(tt.wantDisabled) == 0 && len(got) != 0 {
- t.Errorf("expected empty disabledItems map, got %v", got)
- }
- })
- }
-}