Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,26 +155,38 @@ curl -fsSL https://install.danklinux.com | sh

**Headless (unattended):**

Headless mode requires cached sudo credentials. Run `sudo -v` first:
Headless mode requires cached credentials or passwordless privilege escalation (sudo/doas/run0):

```bash
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c niri -t ghostty -y
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t kitty --include-deps dms-greeter -y
curl -fsSL https://raw.githubusercontent.com/JDKamalakar/DankMaterialShell/master/core/install.sh | sh -s -- -c niri -t ghostty -p sudo -y
Comment thread
JDKamalakar marked this conversation as resolved.
Outdated
curl -fsSL https://raw.githubusercontent.com/JDKamalakar/DankMaterialShell/master/core/install.sh | sh -s -- -c hyprland -t kitty --wm-git --quickshell-git -a -y
curl -fsSL https://raw.githubusercontent.com/JDKamalakar/DankMaterialShell/master/core/install.sh | sh -s -- -c mango -t alacritty --git --danksearch --dankcalendar -y
```

| Flag | Short | Description |
|------|-------|-------------|
| `--compositor <niri|hyprland>` | `-c` | Compositor/WM to install (required for headless) |
| `--term <ghostty|kitty|alacritty>` | `-t` | Terminal emulator (required for headless) |
| `--include-deps <name,...>` | | Enable optional dependencies (e.g. `dms-greeter`) |
| `--compositor <niri\|hyprland\|mango>` | `-c` | Compositor/WM to install (required for headless) |
| `--term <ghostty\|kitty\|alacritty>` | `-t` | Terminal emulator (required for headless) |
| `--privesc <sudo\|doas\|run0>` | `-p` | Explicit privilege escalation tool |
| `--wm-git` | | Use git/development version of selected window manager |
| `--quickshell-git` | | Use git/development version of quickshell |
| `--dms-git` | | Use git/development version of DankMaterialShell |
| `--git` / `--git-all` | | Use git/development versions for all supported components |
| `--git-deps <name,...>` | | Specify dependencies to install git versions of |
| `--all` / `--all-features` | `-a` | Install all optional features and dependencies |
| `--no-features` | | Skip all optional features and dependencies |
| `--dms-greeter` / `--greeter` | | Install dms-greeter optional package |
| `--danksearch` | | Install danksearch and enable user indexing service |
| `--dankcalendar` | | Install dankcalendar package |
| `--include-deps <name,...>` | | Enable specific optional dependencies |
| `--exclude-deps <name,...>` | | Skip specific dependencies |
| `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) |
| `--replace-configs-all` | | Replace all configuration files (mutually exclusive with `--replace-configs`) |
| `--yes` | `-y` | Required for headless mode - confirms installation without interactive prompts |

Headless mode requires `--yes` to proceed; without it, the installer exits with an error.
Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified.
`dms-greeter` is disabled by default; use `--include-deps dms-greeter` to enable it.
Use `--all` / `-a` to automatically enable all optional packages (`dms-greeter`, `danksearch`, `dankcalendar`).

When no flags are provided, `dankinstall` launches the interactive TUI.

Expand Down
51 changes: 48 additions & 3 deletions core/cmd/dankinstall/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ var Version = "dev"
var (
compositor string
term string
privescTool string
wmGit bool
quickshellGit bool
dmsGit bool
gitAll bool
gitDeps []string
allFeatures bool
noFeatures bool
dmsGreeter bool
includeDeps []string
excludeDeps []string
replaceConfigs []string
Expand All @@ -35,8 +44,7 @@ var rootCmd = &cobra.Command{
Without flags, it launches an interactive TUI. Providing either --compositor
or --term activates headless (unattended) mode, which requires both flags.

Headless mode requires cached sudo credentials. Run 'sudo -v' beforehand, or
configure passwordless sudo for your user.`,
Headless mode requires cached credentials or passwordless privilege escalation.`,
Args: cobra.NoArgs,
RunE: runDankinstall,
SilenceErrors: true,
Expand All @@ -46,7 +54,19 @@ configure passwordless sudo for your user.`,
func init() {
rootCmd.Flags().StringVarP(&compositor, "compositor", "c", "", "Compositor/WM to install: niri, hyprland, or mango (enables headless mode)")
rootCmd.Flags().StringVarP(&term, "term", "t", "", "Terminal emulator to install: ghostty, kitty, or alacritty (enables headless mode)")
rootCmd.Flags().StringSliceVar(&includeDeps, "include-deps", []string{}, "Optional deps to enable (e.g. dms-greeter)")
rootCmd.Flags().StringVarP(&privescTool, "privesc", "p", "", "Privilege escalation tool: sudo, doas, or run0")
rootCmd.Flags().BoolVar(&wmGit, "wm-git", false, "Use git/development version of selected window manager")
rootCmd.Flags().BoolVar(&quickshellGit, "quickshell-git", false, "Use git/development version of quickshell")
rootCmd.Flags().BoolVar(&dmsGit, "dms-git", false, "Use git/development version of DankMaterialShell")
Comment thread
JDKamalakar marked this conversation as resolved.
Outdated
rootCmd.Flags().BoolVar(&gitAll, "git-all", false, "Use git/development versions for all supported components")
rootCmd.Flags().BoolVar(&gitAll, "git", false, "Use git/development versions for all supported components (alias for --git-all)")
rootCmd.Flags().StringSliceVar(&gitDeps, "git-deps", []string{}, "Comma-separated list of dependencies to use git versions for")
rootCmd.Flags().BoolVarP(&allFeatures, "all-features", "a", false, "Install all optional features and dependencies")
rootCmd.Flags().BoolVar(&allFeatures, "all", false, "Install all optional features and dependencies (alias for --all-features)")
rootCmd.Flags().BoolVar(&noFeatures, "no-features", false, "Skip all optional features and dependencies")
rootCmd.Flags().BoolVar(&dmsGreeter, "dms-greeter", false, "Install dms-greeter optional package")
rootCmd.Flags().BoolVar(&dmsGreeter, "greeter", false, "Install dms-greeter optional package (alias for --dms-greeter)")
rootCmd.Flags().StringSliceVar(&includeDeps, "include-deps", []string{}, "Optional deps to enable (e.g. dms-greeter, danksearch)")
rootCmd.Flags().StringSliceVar(&excludeDeps, "exclude-deps", []string{}, "Deps to skip during installation")
rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)")
rootCmd.Flags().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations")
Expand All @@ -73,6 +93,18 @@ func runDankinstall(cmd *cobra.Command, args []string) error {
if !headlessMode {
// Reject headless-only flags when running in TUI mode.
headlessOnly := []string{
"privesc",
"wm-git",
"quickshell-git",
"dms-git",
"git-all",
"git",
"git-deps",
"all-features",
"all",
"no-features",
"dms-greeter",
"greeter",
"include-deps",
"exclude-deps",
"replace-configs",
Expand Down Expand Up @@ -107,9 +139,22 @@ func runHeadless() error {
return fmt.Errorf("--term is required for headless mode (ghostty, kitty, or alacritty)")
}

if allFeatures && noFeatures {
return fmt.Errorf("cannot specify both --all-features/--all and --no-features")
Comment thread
JDKamalakar marked this conversation as resolved.
Outdated
}

cfg := headless.Config{
Compositor: compositor,
Terminal: term,
PrivescTool: privescTool,
WMGit: wmGit,
QuickshellGit: quickshellGit,
DMSGit: dmsGit,
GitAll: gitAll,
GitDeps: gitDeps,
AllFeatures: allFeatures,
NoFeatures: noFeatures,
DmsGreeter: dmsGreeter,
IncludeDeps: includeDeps,
ExcludeDeps: excludeDeps,
ReplaceConfigs: replaceConfigs,
Expand Down
12 changes: 7 additions & 5 deletions core/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ aarch64)
;;
esac

REPO_SLUG="${DMS_REPO:-JDKamalakar/DankMaterialShell}"
Comment thread
JDKamalakar marked this conversation as resolved.
Outdated

# Get the latest release version
LATEST_VERSION=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
LATEST_VERSION=$(curl -s "https://api.github.com/repos/${REPO_SLUG}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')

if [ -z "$LATEST_VERSION" ]; then
printf "%bError: Could not fetch latest version%b\n" "$RED" "$NC"
printf "%bError: Could not fetch latest version from %s%b\n" "$RED" "$REPO_SLUG" "$NC"
exit 1
fi

Expand All @@ -51,8 +53,8 @@ cd "$TEMP_DIR" || exit 1

# Download the gzipped binary and its checksum
printf "%bDownloading installer...%b\n" "$GREEN" "$NC"
curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz" -o "installer.gz"
curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz.sha256" -o "expected.sha256"
curl -L "https://github.com/${REPO_SLUG}/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz" -o "installer.gz"
curl -L "https://github.com/${REPO_SLUG}/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz.sha256" -o "expected.sha256"

# Get the expected checksum
EXPECTED_CHECKSUM=$(awk '{print $1}' expected.sha256)
Expand All @@ -79,7 +81,7 @@ chmod +x installer

# Execute the installer
printf "%bRunning installer...%b\n" "$GREEN" "$NC"
./installer
./installer "$@"

# Cleanup
cd - >/dev/null
Expand Down
91 changes: 89 additions & 2 deletions core/internal/headless/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,17 @@ var orderedConfigNames = []string{"niri", "hyprland", "ghostty", "kitty", "alacr

// Config holds all CLI parameters for unattended installation.
type Config struct {
Compositor string // "niri" or "hyprland"
Terminal string // "ghostty", "kitty", or "alacritty"
Compositor string // "niri", "hyprland", or "mango"
Terminal string // "ghostty", "kitty", or "alacritty"
PrivescTool string // "sudo", "doas", or "run0"
WMGit bool // install git version of selected window manager
QuickshellGit bool // install git version of quickshell
DMSGit bool // install git version of DMS
GitAll bool // install git version of all supported components
GitDeps []string // specific dependencies to force git version
AllFeatures bool // enable all optional features
NoFeatures bool // disable all optional features
DmsGreeter bool // install dms-greeter
IncludeDeps []string
ExcludeDeps []string
ReplaceConfigs []string // specific configs to deploy (e.g. "niri", "ghostty")
Expand Down Expand Up @@ -68,6 +77,18 @@ func (r *Runner) GetLogChan() <-chan string {
func (r *Runner) Run() error {
r.log("Starting headless installation")

if r.cfg.PrivescTool != "" {
Comment thread
JDKamalakar marked this conversation as resolved.
tool := privesc.Tool(strings.ToLower(r.cfg.PrivescTool))
switch tool {
case privesc.ToolSudo, privesc.ToolDoas, privesc.ToolRun0:
if err := privesc.SetTool(tool); err != nil {
return fmt.Errorf("failed to set privilege escalation tool %q: %w", r.cfg.PrivescTool, err)
}
default:
return fmt.Errorf("invalid --privesc value %q: must be 'sudo', 'doas', or 'run0'", r.cfg.PrivescTool)
}
}

// 1. Parse compositor and terminal selections
wm, err := r.parseWindowManager()
if err != nil {
Expand Down Expand Up @@ -112,6 +133,8 @@ func (r *Runner) Run() error {
return fmt.Errorf("dependency detection failed: %w", err)
}

r.applyGitVariants(dependencies)

// 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.
Expand Down Expand Up @@ -283,12 +306,70 @@ func (r *Runner) Run() error {
return nil
}

// applyGitVariants sets VariantGit on dependencies matching git flags or --git-deps.
func (r *Runner) applyGitVariants(dependencies []deps.Dependency) {
gitDepsMap := make(map[string]bool)
Comment thread
JDKamalakar marked this conversation as resolved.
for _, raw := range r.cfg.GitDeps {
for _, item := range strings.Split(raw, ",") {
item = strings.TrimSpace(strings.ToLower(item))
if item != "" {
gitDepsMap[item] = true
}
}
}

wmName := strings.ToLower(r.cfg.Compositor)

for i := range dependencies {
depNameLower := strings.ToLower(dependencies[i].Name)
isWM := depNameLower == wmName
isQuickshell := depNameLower == "quickshell"
isDMS := strings.HasPrefix(depNameLower, "dms") && strings.Contains(depNameLower, "dankmaterialshell")
Comment thread
JDKamalakar marked this conversation as resolved.
Outdated

shouldUseGit := r.cfg.GitAll ||
(isWM && (r.cfg.WMGit || gitDepsMap[depNameLower])) ||
Comment thread
JDKamalakar marked this conversation as resolved.
Outdated
(isQuickshell && (r.cfg.QuickshellGit || gitDepsMap["quickshell"])) ||
(isDMS && (r.cfg.DMSGit || gitDepsMap["dms"] || gitDepsMap["dms (dankmaterialshell)"])) ||
gitDepsMap[depNameLower]

if shouldUseGit && dependencies[i].CanToggle {
Comment thread
JDKamalakar marked this conversation as resolved.
Outdated
dependencies[i].Variant = deps.VariantGit
}
}
}

// buildDisabledItems computes the set of dependencies that should be skipped
// during installation. Optional components are opt-in (disabled by default),
// then re-enabled by the dedicated flags and --include-deps.
func (r *Runner) buildDisabledItems(dependencies []deps.Dependency) (map[string]bool, error) {
disabledItems := make(map[string]bool)

if r.cfg.AllFeatures {
Comment thread
JDKamalakar marked this conversation as resolved.
Outdated
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)
}
if name == "dms (DankMaterialShell)" {
return nil, fmt.Errorf("--exclude-deps: cannot exclude required package %q", name)
}
disabledItems[name] = true
}
return disabledItems, nil
}

if r.cfg.NoFeatures {
for i := range dependencies {
if !dependencies[i].Required {
disabledItems[dependencies[i].Name] = true
}
}
return disabledItems, nil
}

for i := range dependencies {
if !dependencies[i].Required {
disabledItems[dependencies[i].Name] = true
Expand All @@ -308,6 +389,12 @@ func (r *Runner) buildDisabledItems(dependencies []deps.Dependency) (map[string]
}
delete(disabledItems, "dankcalendar")
}
if r.cfg.DmsGreeter {
if !r.depExists(dependencies, "dms-greeter") {
return nil, fmt.Errorf("--dms-greeter: not available on this distribution")
}
delete(disabledItems, "dms-greeter")
}

// Process --include-deps (enable items that are disabled by default)
for _, name := range r.cfg.IncludeDeps {
Expand Down
48 changes: 43 additions & 5 deletions core/internal/headless/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,16 @@ func TestBuildDisabledItems(t *testing.T) {
wantErr: true,
errContains: "--dankcalendar",
},
{
name: "allFeatures enables all optional features",
allFeatures: true,
wantEnabled: []string{"dms-greeter", "danksearch", "dankcalendar"},
},
{
name: "noFeatures disables all optional features",
noFeatures: true,
wantDisabled: []string{"dms-greeter", "danksearch", "dankcalendar"},
},
}

for _, tt := range tests {
Expand All @@ -451,6 +461,8 @@ func TestBuildDisabledItems(t *testing.T) {
ExcludeDeps: tt.excludeDeps,
DankSearch: tt.dankSearch,
DankCalendar: tt.dankCalendar,
AllFeatures: tt.allFeatures,
NoFeatures: tt.noFeatures,
})
d := tt.deps
if d == nil {
Expand Down Expand Up @@ -483,11 +495,37 @@ func TestBuildDisabledItems(t *testing.T) {
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)
}
})
}
}

func TestApplyGitVariants(t *testing.T) {
r := NewRunner(Config{
Compositor: "niri",
WMGit: true,
QuickshellGit: true,
GitDeps: []string{"matugen"},
})

dependencies := []deps.Dependency{
{Name: "niri", Variant: deps.VariantStable, CanToggle: true},
{Name: "quickshell", Variant: deps.VariantStable, CanToggle: true},
{Name: "matugen", Variant: deps.VariantStable, CanToggle: true},
{Name: "ghostty", Variant: deps.VariantStable, CanToggle: false},
}

r.applyGitVariants(dependencies)

if dependencies[0].Variant != deps.VariantGit {
t.Errorf("expected niri variant to be VariantGit, got %v", dependencies[0].Variant)
}
if dependencies[1].Variant != deps.VariantGit {
t.Errorf("expected quickshell variant to be VariantGit, got %v", dependencies[1].Variant)
}
if dependencies[2].Variant != deps.VariantGit {
t.Errorf("expected matugen variant to be VariantGit, got %v", dependencies[2].Variant)
}
if dependencies[3].Variant != deps.VariantStable {
t.Errorf("expected ghostty variant to stay VariantStable (CanToggle=false), got %v", dependencies[3].Variant)
}
}
Loading