diff options
| author | Nippy <nippy@rp1.hu> | 2026-05-09 13:33:09 +0200 |
|---|---|---|
| committer | Nippy <nippy@rp1.hu> | 2026-05-09 13:33:09 +0200 |
| commit | a2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5 (patch) | |
| tree | 1a8769217f84bfe9d6216fafaadaf8cdd876d457 /raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/headless | |
| parent | 34b9c34f982b2596cfa9e368a2d7f74e9a17477c (diff) | |
| download | RaveOS-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.go | 418 | ||||
| -rw-r--r-- | raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/headless/runner_test.go | 459 |
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) - } - }) - } -} |