detached 74791d1

feat(tui): add Dank Linux opt-in toggle screen before installation, auto-run via flag file on first boot

commit
74791d132bf5c9fab188f232c83fc42bcb9ce132
author
bnema · · 2026-02-27 03:53 UTC
committer
bnema · · 2026-02-27 03:57 UTC
parent
3095135
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
9 files · +236 -2
M install/mandatory/post-boot/dms-opt-in.sh +19 -1
@@ -8,12 +8,30 @@ LOG_FILE="/var/log/archup-first-boot.log"
88
99 log() { echo "[dms-opt-in] $*" | tee -a "$LOG_FILE"; }
1010
11+FLAG_FILE="/var/lib/archup-install-danklinux"
12+
13+# Auto-install if flag was set during installation
14+if [ -f "$FLAG_FILE" ]; then
15+ log "Dank Linux install flag detected — running installer automatically."
16+ echo ""
17+ echo "Installing Dank Linux (selected during system installation)..."
18+ if curl -fsSL https://install.danklinux.com | sh; then
19+ rm -f "$FLAG_FILE"
20+ log "Dank Linux installation complete."
21+ else
22+ log "Dank Linux installer failed — flag preserved at $FLAG_FILE for retry."
23+ exit 1
24+ fi
25+ exit 0
26+fi
27+
1128 # Skip if not running in an interactive terminal
1229 if [ ! -t 0 ]; then
1330 log "Not running in interactive terminal — skipping Dank Linux opt-in."
1431 exit 0
1532 fi
1633
34+# Manual prompt fallback (for reinstall / re-run scenarios)
1735 echo ""
1836 echo "=============================================="
1937 echo " Dank Linux — Optional Install"
@@ -27,7 +45,7 @@ echo " - Auto-theming, notifications, app launcher,"
2745 echo " lock screen, system tray, search — all"
2846 echo " integrated. One install. Everything works."
2947 echo ""
30- echo " Install: curl -fsSL https://install.danklinux.com | sh"
48+echo " Install: curl -fsSL https://install.danklinux.com | sh"
3149 echo " Note: This runs the official Dank Linux installer. Review at:"
3250 echo " https://github.com/AvengeMedia/DankMaterialShell"
3351 echo ""
@@ -8,12 +8,30 @@ LOG_FILE="/var/log/archup-first-boot.log"
8 8
9 log() { echo "[dms-opt-in] $*" | tee -a "$LOG_FILE"; }9 log() { echo "[dms-opt-in] $*" | tee -a "$LOG_FILE"; }
10 10
11+FLAG_FILE="/var/lib/archup-install-danklinux"
12+
13+# Auto-install if flag was set during installation
14+if [ -f "$FLAG_FILE" ]; then
15+ log "Dank Linux install flag detected — running installer automatically."
16+ echo ""
17+ echo "Installing Dank Linux (selected during system installation)..."
18+ if curl -fsSL https://install.danklinux.com | sh; then
19+ rm -f "$FLAG_FILE"
20+ log "Dank Linux installation complete."
21+ else
22+ log "Dank Linux installer failed — flag preserved at $FLAG_FILE for retry."
23+ exit 1
24+ fi
25+ exit 0
26+fi
27+
11 # Skip if not running in an interactive terminal28 # Skip if not running in an interactive terminal
12 if [ ! -t 0 ]; then29 if [ ! -t 0 ]; then
13 log "Not running in interactive terminal — skipping Dank Linux opt-in."30 log "Not running in interactive terminal — skipping Dank Linux opt-in."
14 exit 031 exit 0
15 fi32 fi
16 33
34+# Manual prompt fallback (for reinstall / re-run scenarios)
17 echo ""35 echo ""
18 echo "=============================================="36 echo "=============================================="
19 echo " Dank Linux — Optional Install"37 echo " Dank Linux — Optional Install"
@@ -27,7 +45,7 @@ echo " - Auto-theming, notifications, app launcher,"
27 echo " lock screen, system tray, search — all"45 echo " lock screen, system tray, search — all"
28 echo " integrated. One install. Everything works."46 echo " integrated. One install. Everything works."
29 echo ""47 echo ""
30- echo " Install: curl -fsSL https://install.danklinux.com | sh"48+echo " Install: curl -fsSL https://install.danklinux.com | sh"
31 echo " Note: This runs the official Dank Linux installer. Review at:"49 echo " Note: This runs the official Dank Linux installer. Review at:"
32 echo " https://github.com/AvengeMedia/DankMaterialShell"50 echo " https://github.com/AvengeMedia/DankMaterialShell"
33 echo ""51 echo ""
M internal/application/commands/post_install.go +1 -0
@@ -7,4 +7,5 @@ type PostInstallCommand struct {
77 UserEmail string // User email for git config and SSH key (optional)
88 RunPostBootScripts bool // Whether to run post-boot scripts
99 PlymouthTheme string // Plymouth theme to install (optional)
10+ InstallDankLinux bool // Whether to write the Dank Linux flag file for first-boot auto-install
1011 }
@@ -7,4 +7,5 @@ type PostInstallCommand struct {
7 UserEmail string // User email for git config and SSH key (optional)7 UserEmail string // User email for git config and SSH key (optional)
8 RunPostBootScripts bool // Whether to run post-boot scripts8 RunPostBootScripts bool // Whether to run post-boot scripts
9 PlymouthTheme string // Plymouth theme to install (optional)9 PlymouthTheme string // Plymouth theme to install (optional)
10+ InstallDankLinux bool // Whether to write the Dank Linux flag file for first-boot auto-install
10 }11 }
M internal/application/handlers/post_install_handler.go +19 -0
@@ -59,6 +59,15 @@ func (h *PostInstallHandler) Handle(ctx context.Context, cmd commands.PostInstal
5959 h.logger.Info("Post-boot scripts prepared")
6060 }
6161
62+ // Write Dank Linux flag file if user opted in
63+ if cmd.InstallDankLinux {
64+ if err := h.writeDankLinuxFlag(cmd.MountPoint); err != nil {
65+ return result, fmt.Errorf("failed to write dank linux flag: %w", err)
66+ }
67+ result.TasksRun = append(result.TasksRun, "dank-linux-flag")
68+ h.logger.Info("Dank Linux flag file written")
69+ }
70+
6271 // Install Plymouth theme if specified
6372 if cmd.PlymouthTheme != "" {
6473 h.logger.Info("Installing Plymouth theme", "theme", cmd.PlymouthTheme)
@@ -206,6 +215,16 @@ func (h *PostInstallHandler) tryReadLocal(path string) ([]byte, error) {
206215 return nil, fmt.Errorf("local template not found")
207216 }
208217
218+// writeDankLinuxFlag writes a flag file to the installed system so that
219+// dms-opt-in.sh auto-runs without prompting on first boot.
220+func (h *PostInstallHandler) writeDankLinuxFlag(mountPoint string) error {
221+ if err := h.fs.MkdirAll(filepath.Join(mountPoint, "var", "lib"), 0755); err != nil {
222+ return fmt.Errorf("failed to create /var/lib: %w", err)
223+ }
224+ flagPath := filepath.Join(mountPoint, "var", "lib", "archup-install-danklinux")
225+ return h.fs.WriteFile(flagPath, []byte(""), 0644)
226+}
227+
209228 func (h *PostInstallHandler) downloadTemplate(path string) ([]byte, error) {
210229 url := fmt.Sprintf("%s/%s", h.rawURL, path)
211230 h.logger.Info("Downloading template", "url", url)
@@ -59,6 +59,15 @@ func (h *PostInstallHandler) Handle(ctx context.Context, cmd commands.PostInstal
59 h.logger.Info("Post-boot scripts prepared")59 h.logger.Info("Post-boot scripts prepared")
60 }60 }
61 61
62+ // Write Dank Linux flag file if user opted in
63+ if cmd.InstallDankLinux {
64+ if err := h.writeDankLinuxFlag(cmd.MountPoint); err != nil {
65+ return result, fmt.Errorf("failed to write dank linux flag: %w", err)
66+ }
67+ result.TasksRun = append(result.TasksRun, "dank-linux-flag")
68+ h.logger.Info("Dank Linux flag file written")
69+ }
70+
62 // Install Plymouth theme if specified71 // Install Plymouth theme if specified
63 if cmd.PlymouthTheme != "" {72 if cmd.PlymouthTheme != "" {
64 h.logger.Info("Installing Plymouth theme", "theme", cmd.PlymouthTheme)73 h.logger.Info("Installing Plymouth theme", "theme", cmd.PlymouthTheme)
@@ -206,6 +215,16 @@ func (h *PostInstallHandler) tryReadLocal(path string) ([]byte, error) {
206 return nil, fmt.Errorf("local template not found")215 return nil, fmt.Errorf("local template not found")
207 }216 }
208 217
218+// writeDankLinuxFlag writes a flag file to the installed system so that
219+// dms-opt-in.sh auto-runs without prompting on first boot.
220+func (h *PostInstallHandler) writeDankLinuxFlag(mountPoint string) error {
221+ if err := h.fs.MkdirAll(filepath.Join(mountPoint, "var", "lib"), 0755); err != nil {
222+ return fmt.Errorf("failed to create /var/lib: %w", err)
223+ }
224+ flagPath := filepath.Join(mountPoint, "var", "lib", "archup-install-danklinux")
225+ return h.fs.WriteFile(flagPath, []byte(""), 0644)
226+}
227+
209 func (h *PostInstallHandler) downloadTemplate(path string) ([]byte, error) {228 func (h *PostInstallHandler) downloadTemplate(path string) ([]byte, error) {
210 url := fmt.Sprintf("%s/%s", h.rawURL, path)229 url := fmt.Sprintf("%s/%s", h.rawURL, path)
211 h.logger.Info("Downloading template", "url", url)230 h.logger.Info("Downloading template", "url", url)
M internal/application/handlers/post_install_handler_test.go +49 -0
@@ -162,6 +162,55 @@ func TestPostInstallHandler_Handle_WithPlymouth(t *testing.T) {
162162 }
163163 }
164164
165+func TestPostInstallHandler_Handle_WithDankLinux(t *testing.T) {
166+ ctrl := gomock.NewController(t)
167+ defer ctrl.Finish()
168+
169+ mockFS := mocks.NewMockFileSystem(ctrl)
170+ mockHTTP := mocks.NewMockHTTPClient(ctrl)
171+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
172+ mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
173+ mockLogger := mocks.NewMockLogger(ctrl)
174+
175+ mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
176+ mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
177+ mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
178+ mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes()
179+ mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes()
180+ mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
181+ mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
182+ mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
183+ mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
184+
185+ handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
186+
187+ cmd := commands.PostInstallCommand{
188+ MountPoint: "/mnt",
189+ Username: "testuser",
190+ RunPostBootScripts: false,
191+ PlymouthTheme: "",
192+ InstallDankLinux: true,
193+ }
194+
195+ result, err := handler.Handle(context.Background(), cmd)
196+
197+ if err != nil {
198+ t.Fatalf("expected no error, got %v", err)
199+ }
200+
201+ if !result.Success {
202+ t.Error("expected success")
203+ }
204+
205+ if len(result.TasksRun) != 1 {
206+ t.Errorf("expected 1 task run, got %d", len(result.TasksRun))
207+ }
208+
209+ if result.TasksRun[0] != "dank-linux-flag" {
210+ t.Errorf("expected task 'dank-linux-flag', got %s", result.TasksRun[0])
211+ }
212+}
213+
165214 func TestPostInstallHandler_Handle_Everything(t *testing.T) {
166215 ctrl := gomock.NewController(t)
167216 defer ctrl.Finish()
@@ -162,6 +162,55 @@ func TestPostInstallHandler_Handle_WithPlymouth(t *testing.T) {
162 }162 }
163 }163 }
164 164
165+func TestPostInstallHandler_Handle_WithDankLinux(t *testing.T) {
166+ ctrl := gomock.NewController(t)
167+ defer ctrl.Finish()
168+
169+ mockFS := mocks.NewMockFileSystem(ctrl)
170+ mockHTTP := mocks.NewMockHTTPClient(ctrl)
171+ mockChrExec := mocks.NewMockChrootExecutor(ctrl)
172+ mockScriptExec := mocks.NewMockScriptExecutor(ctrl)
173+ mockLogger := mocks.NewMockLogger(ctrl)
174+
175+ mockLogger.EXPECT().Info(gomock.Any(), gomock.Any()).AnyTimes()
176+ mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes()
177+ mockLogger.EXPECT().LogPath().Return("/var/log/archup-install.log").AnyTimes()
178+ mockFS.EXPECT().Exists(gomock.Any()).Return(false, nil).AnyTimes()
179+ mockFS.EXPECT().ReadFile(gomock.Any()).Return([]byte("graphics: yes"), nil).AnyTimes()
180+ mockFS.EXPECT().WriteFile(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
181+ mockFS.EXPECT().MkdirAll(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
182+ mockHTTP.EXPECT().Get(gomock.Any()).Return(newMockResponse(ctrl, http.StatusOK, []byte("content")), nil).AnyTimes()
183+ mockChrExec.EXPECT().ChrootSystemctl(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
184+
185+ handler := NewPostInstallHandler(mockFS, mockHTTP, mockChrExec, mockScriptExec, mockLogger, "https://raw.githubusercontent.com/bnema/archup/dev")
186+
187+ cmd := commands.PostInstallCommand{
188+ MountPoint: "/mnt",
189+ Username: "testuser",
190+ RunPostBootScripts: false,
191+ PlymouthTheme: "",
192+ InstallDankLinux: true,
193+ }
194+
195+ result, err := handler.Handle(context.Background(), cmd)
196+
197+ if err != nil {
198+ t.Fatalf("expected no error, got %v", err)
199+ }
200+
201+ if !result.Success {
202+ t.Error("expected success")
203+ }
204+
205+ if len(result.TasksRun) != 1 {
206+ t.Errorf("expected 1 task run, got %d", len(result.TasksRun))
207+ }
208+
209+ if result.TasksRun[0] != "dank-linux-flag" {
210+ t.Errorf("expected task 'dank-linux-flag', got %s", result.TasksRun[0])
211+ }
212+}
213+
165 func TestPostInstallHandler_Handle_Everything(t *testing.T) {214 func TestPostInstallHandler_Handle_Everything(t *testing.T) {
166 ctrl := gomock.NewController(t)215 ctrl := gomock.NewController(t)
167 defer ctrl.Finish()216 defer ctrl.Finish()
M internal/interfaces/tui/app.go +24 -1
@@ -33,6 +33,7 @@ type App struct {
3333 kernelModel *models.KernelModelImpl
3434 amdPstateModel *models.AMDPStateModelImpl
3535 gpuModel *models.GPUModelImpl
36+ dankLinuxModel *models.DankLinuxModelImpl
3637 installationModel *models.InstallationModelImpl
3738 progressModel *models.ProgressModelImpl
3839
@@ -57,6 +58,7 @@ const (
5758 ScreenKernel Screen = "kernel"
5859 ScreenAMDPState Screen = "amd-pstate"
5960 ScreenGPU Screen = "gpu"
61+ ScreenDankLinux Screen = "danklinux"
6062 ScreenInstalling Screen = "installing"
6163 ScreenProgress Screen = "progress"
6264 ScreenSummary Screen = "summary"
@@ -82,6 +84,7 @@ func NewApp(
8284 kernelModel: models.NewKernelModel(),
8385 amdPstateModel: models.NewAMDPStateModel(),
8486 gpuModel: models.NewGPUModel(),
87+ dankLinuxModel: models.NewDankLinuxModel(),
8588 installationModel: models.NewInstallationModel(),
8689 progressModel: models.NewProgressModel(),
8790 currentScreen: ScreenForm,
@@ -158,6 +161,8 @@ func (a *App) View() string {
158161 return views.RenderAMDPStateSelection(a.amdPstateModel)
159162 case ScreenGPU:
160163 return views.RenderGPUSelection(a.gpuModel)
164+ case ScreenDankLinux:
165+ return views.RenderDankLinuxSelection(a.dankLinuxModel)
161166 case ScreenInstalling, ScreenProgress:
162167 return views.RenderProgress(a.progressModel)
163168 case ScreenSummary:
@@ -182,6 +187,8 @@ func (a *App) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
182187 return a.handleAMDPStateInput(msg)
183188 case ScreenGPU:
184189 return a.handleGPUInput(msg)
190+ case ScreenDankLinux:
191+ return a.handleDankLinuxInput(msg)
185192 case ScreenProgress:
186193 // Limited input during installation
187194 if msg.String() == "ctrl+c" {
@@ -310,12 +317,28 @@ func (a *App) handleGPUInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
310317 // Update stored form data directly
311318 a.formData.GPUVendor = string(selected.Vendor)
312319 a.formData.GPUDrivers = append([]string{}, selected.Drivers...)
313- return a.startInstallation()
320+ a.currentScreen = ScreenDankLinux
321+ return a, nil
314322 }
315323
316324 return a, nil
317325 }
318326
327+func (a *App) handleDankLinuxInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
328+ switch msg.String() {
329+ case "up", "shift+tab":
330+ a.dankLinuxModel.MoveUp()
331+ case "down", "tab":
332+ a.dankLinuxModel.MoveDown()
333+ case "enter":
334+ a.formData.InstallDankLinux = a.dankLinuxModel.SelectedOption().Value
335+ return a.startInstallation()
336+ case "q", "ctrl+c":
337+ return a, tea.Quit
338+ }
339+ return a, nil
340+}
341+
319342 func (a *App) handleKernelInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
320343 switch msg.String() {
321344 case "ctrl+c":
@@ -33,6 +33,7 @@ type App struct {
33 kernelModel *models.KernelModelImpl33 kernelModel *models.KernelModelImpl
34 amdPstateModel *models.AMDPStateModelImpl34 amdPstateModel *models.AMDPStateModelImpl
35 gpuModel *models.GPUModelImpl35 gpuModel *models.GPUModelImpl
36+ dankLinuxModel *models.DankLinuxModelImpl
36 installationModel *models.InstallationModelImpl37 installationModel *models.InstallationModelImpl
37 progressModel *models.ProgressModelImpl38 progressModel *models.ProgressModelImpl
38 39
@@ -57,6 +58,7 @@ const (
57 ScreenKernel Screen = "kernel"58 ScreenKernel Screen = "kernel"
58 ScreenAMDPState Screen = "amd-pstate"59 ScreenAMDPState Screen = "amd-pstate"
59 ScreenGPU Screen = "gpu"60 ScreenGPU Screen = "gpu"
61+ ScreenDankLinux Screen = "danklinux"
60 ScreenInstalling Screen = "installing"62 ScreenInstalling Screen = "installing"
61 ScreenProgress Screen = "progress"63 ScreenProgress Screen = "progress"
62 ScreenSummary Screen = "summary"64 ScreenSummary Screen = "summary"
@@ -82,6 +84,7 @@ func NewApp(
82 kernelModel: models.NewKernelModel(),84 kernelModel: models.NewKernelModel(),
83 amdPstateModel: models.NewAMDPStateModel(),85 amdPstateModel: models.NewAMDPStateModel(),
84 gpuModel: models.NewGPUModel(),86 gpuModel: models.NewGPUModel(),
87+ dankLinuxModel: models.NewDankLinuxModel(),
85 installationModel: models.NewInstallationModel(),88 installationModel: models.NewInstallationModel(),
86 progressModel: models.NewProgressModel(),89 progressModel: models.NewProgressModel(),
87 currentScreen: ScreenForm,90 currentScreen: ScreenForm,
@@ -158,6 +161,8 @@ func (a *App) View() string {
158 return views.RenderAMDPStateSelection(a.amdPstateModel)161 return views.RenderAMDPStateSelection(a.amdPstateModel)
159 case ScreenGPU:162 case ScreenGPU:
160 return views.RenderGPUSelection(a.gpuModel)163 return views.RenderGPUSelection(a.gpuModel)
164+ case ScreenDankLinux:
165+ return views.RenderDankLinuxSelection(a.dankLinuxModel)
161 case ScreenInstalling, ScreenProgress:166 case ScreenInstalling, ScreenProgress:
162 return views.RenderProgress(a.progressModel)167 return views.RenderProgress(a.progressModel)
163 case ScreenSummary:168 case ScreenSummary:
@@ -182,6 +187,8 @@ func (a *App) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
182 return a.handleAMDPStateInput(msg)187 return a.handleAMDPStateInput(msg)
183 case ScreenGPU:188 case ScreenGPU:
184 return a.handleGPUInput(msg)189 return a.handleGPUInput(msg)
190+ case ScreenDankLinux:
191+ return a.handleDankLinuxInput(msg)
185 case ScreenProgress:192 case ScreenProgress:
186 // Limited input during installation193 // Limited input during installation
187 if msg.String() == "ctrl+c" {194 if msg.String() == "ctrl+c" {
@@ -310,12 +317,28 @@ func (a *App) handleGPUInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
310 // Update stored form data directly317 // Update stored form data directly
311 a.formData.GPUVendor = string(selected.Vendor)318 a.formData.GPUVendor = string(selected.Vendor)
312 a.formData.GPUDrivers = append([]string{}, selected.Drivers...)319 a.formData.GPUDrivers = append([]string{}, selected.Drivers...)
313- return a.startInstallation()320+ a.currentScreen = ScreenDankLinux
321+ return a, nil
314 }322 }
315 323
316 return a, nil324 return a, nil
317 }325 }
318 326
327+func (a *App) handleDankLinuxInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
328+ switch msg.String() {
329+ case "up", "shift+tab":
330+ a.dankLinuxModel.MoveUp()
331+ case "down", "tab":
332+ a.dankLinuxModel.MoveDown()
333+ case "enter":
334+ a.formData.InstallDankLinux = a.dankLinuxModel.SelectedOption().Value
335+ return a.startInstallation()
336+ case "q", "ctrl+c":
337+ return a, tea.Quit
338+ }
339+ return a, nil
340+}
341+
319 func (a *App) handleKernelInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {342 func (a *App) handleKernelInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
320 switch msg.String() {343 switch msg.String() {
321 case "ctrl+c":344 case "ctrl+c":
M internal/interfaces/tui/handlers/install_handler.go +1 -0
@@ -213,6 +213,7 @@ func CreateInstallationCommand(app AppContext, formData models.FormData) tea.Cmd
213213 UserEmail: formData.UserEmail,
214214 PlymouthTheme: config.PlymouthThemeName,
215215 RunPostBootScripts: true,
216+ InstallDankLinux: formData.InstallDankLinux,
216217 }
217218 if _, err := svc.RunPostInstall(ctx, postCmd); err != nil {
218219 logger.Error("Post-installation failed", "error", err)
@@ -213,6 +213,7 @@ func CreateInstallationCommand(app AppContext, formData models.FormData) tea.Cmd
213 UserEmail: formData.UserEmail,213 UserEmail: formData.UserEmail,
214 PlymouthTheme: config.PlymouthThemeName,214 PlymouthTheme: config.PlymouthThemeName,
215 RunPostBootScripts: true,215 RunPostBootScripts: true,
216+ InstallDankLinux: formData.InstallDankLinux,
216 }217 }
217 if _, err := svc.RunPostInstall(ctx, postCmd); err != nil {218 if _, err := svc.RunPostInstall(ctx, postCmd); err != nil {
218 logger.Error("Post-installation failed", "error", err)219 logger.Error("Post-installation failed", "error", err)
A internal/interfaces/tui/models/dank_linux.go +72 -0
@@ -0,0 +1,72 @@
1+package models
2+
3+// DankLinuxOption represents a yes/no option for installing Dank Linux
4+type DankLinuxOption struct {
5+ Value bool
6+ Label string
7+ Description string
8+}
9+
10+// DankLinuxModelImpl is the model for the Dank Linux opt-in toggle
11+type DankLinuxModelImpl struct {
12+ options []DankLinuxOption
13+ selected int
14+}
15+
16+// NewDankLinuxModel creates a new Dank Linux opt-in selection model.
17+func NewDankLinuxModel() *DankLinuxModelImpl {
18+ return &DankLinuxModelImpl{
19+ options: []DankLinuxOption{
20+ {
21+ Value: false,
22+ Label: "No, keep barebone Bash",
23+ Description: "Barebone system with modern CLI tools. Install Dank Linux later with: curl -fsSL https://install.danklinux.com | sh",
24+ },
25+ {
26+ Value: true,
27+ Label: "Yes, install Dank Linux",
28+ Description: "Full Wayland desktop: niri or Hyprland + DankMaterialShell + Ghostty + auto-theming. Runs on first boot.",
29+ },
30+ },
31+ selected: 0,
32+ }
33+}
34+
35+// Options returns the selectable options.
36+func (d *DankLinuxModelImpl) Options() []DankLinuxOption {
37+ return d.options
38+}
39+
40+// SelectedIndex returns the current selection index.
41+func (d *DankLinuxModelImpl) SelectedIndex() int {
42+ return d.selected
43+}
44+
45+// SelectedOption returns the currently selected option.
46+func (d *DankLinuxModelImpl) SelectedOption() DankLinuxOption {
47+ if len(d.options) == 0 {
48+ return DankLinuxOption{}
49+ }
50+ if d.selected < 0 || d.selected >= len(d.options) {
51+ return d.options[0]
52+ }
53+ return d.options[d.selected]
54+}
55+
56+// MoveUp moves selection up, wrapping to the last option.
57+func (d *DankLinuxModelImpl) MoveUp() {
58+ if d.selected > 0 {
59+ d.selected--
60+ } else {
61+ d.selected = len(d.options) - 1
62+ }
63+}
64+
65+// MoveDown moves selection down, wrapping to the first option.
66+func (d *DankLinuxModelImpl) MoveDown() {
67+ if d.selected < len(d.options)-1 {
68+ d.selected++
69+ } else {
70+ d.selected = 0
71+ }
72+}
@@ -0,0 +1,72 @@
1+package models
2+
3+// DankLinuxOption represents a yes/no option for installing Dank Linux
4+type DankLinuxOption struct {
5+ Value bool
6+ Label string
7+ Description string
8+}
9+
10+// DankLinuxModelImpl is the model for the Dank Linux opt-in toggle
11+type DankLinuxModelImpl struct {
12+ options []DankLinuxOption
13+ selected int
14+}
15+
16+// NewDankLinuxModel creates a new Dank Linux opt-in selection model.
17+func NewDankLinuxModel() *DankLinuxModelImpl {
18+ return &DankLinuxModelImpl{
19+ options: []DankLinuxOption{
20+ {
21+ Value: false,
22+ Label: "No, keep barebone Bash",
23+ Description: "Barebone system with modern CLI tools. Install Dank Linux later with: curl -fsSL https://install.danklinux.com | sh",
24+ },
25+ {
26+ Value: true,
27+ Label: "Yes, install Dank Linux",
28+ Description: "Full Wayland desktop: niri or Hyprland + DankMaterialShell + Ghostty + auto-theming. Runs on first boot.",
29+ },
30+ },
31+ selected: 0,
32+ }
33+}
34+
35+// Options returns the selectable options.
36+func (d *DankLinuxModelImpl) Options() []DankLinuxOption {
37+ return d.options
38+}
39+
40+// SelectedIndex returns the current selection index.
41+func (d *DankLinuxModelImpl) SelectedIndex() int {
42+ return d.selected
43+}
44+
45+// SelectedOption returns the currently selected option.
46+func (d *DankLinuxModelImpl) SelectedOption() DankLinuxOption {
47+ if len(d.options) == 0 {
48+ return DankLinuxOption{}
49+ }
50+ if d.selected < 0 || d.selected >= len(d.options) {
51+ return d.options[0]
52+ }
53+ return d.options[d.selected]
54+}
55+
56+// MoveUp moves selection up, wrapping to the last option.
57+func (d *DankLinuxModelImpl) MoveUp() {
58+ if d.selected > 0 {
59+ d.selected--
60+ } else {
61+ d.selected = len(d.options) - 1
62+ }
63+}
64+
65+// MoveDown moves selection down, wrapping to the first option.
66+func (d *DankLinuxModelImpl) MoveDown() {
67+ if d.selected < len(d.options)-1 {
68+ d.selected++
69+ } else {
70+ d.selected = 0
71+ }
72+}
M internal/interfaces/tui/models/form.go +1 -0
@@ -39,6 +39,7 @@ type FormData struct {
3939 KernelVariant string
4040 AURHelper string
4141 Microcode bool
42+ InstallDankLinux bool
4243 }
4344
4445 // FormModelImpl implements FormModel interface
@@ -39,6 +39,7 @@ type FormData struct {
39 KernelVariant string39 KernelVariant string
40 AURHelper string40 AURHelper string
41 Microcode bool41 Microcode bool
42+ InstallDankLinux bool
42 }43 }
43 44
44 // FormModelImpl implements FormModel interface45 // FormModelImpl implements FormModel interface
A internal/interfaces/tui/views/dank_linux.go +50 -0
@@ -0,0 +1,50 @@
1+package views
2+
3+import (
4+ "strings"
5+
6+ "github.com/bnema/archup/internal/interfaces/tui/models"
7+ "github.com/charmbracelet/lipgloss"
8+)
9+
10+// RenderDankLinuxSelection renders the Dank Linux opt-in screen.
11+func RenderDankLinuxSelection(dm *models.DankLinuxModelImpl) string {
12+ var b strings.Builder
13+
14+ title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
15+ info := lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
16+ active := lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true)
17+ desc := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Faint(true)
18+
19+ b.WriteString("\n")
20+ b.WriteString(title.Render("Dank Linux Desktop"))
21+ b.WriteString("\n\n")
22+
23+ b.WriteString(info.Render("Optional: install Dank Linux — a full Wayland desktop suite."))
24+ b.WriteString("\n")
25+ b.WriteString(info.Render("Includes: niri or Hyprland, DankMaterialShell, Ghostty, and auto-theming."))
26+ b.WriteString("\n\n")
27+
28+ for i, option := range dm.Options() {
29+ prefix := " "
30+ style := lipgloss.NewStyle()
31+ labelStyle := lipgloss.NewStyle()
32+
33+ if i == dm.SelectedIndex() {
34+ prefix = "> "
35+ style = active
36+ labelStyle = active
37+ }
38+
39+ b.WriteString(style.Render(prefix))
40+ b.WriteString(labelStyle.Render(option.Label))
41+ b.WriteString("\n")
42+ b.WriteString(desc.Render(" " + option.Description))
43+ b.WriteString("\n")
44+ }
45+
46+ b.WriteString("\n")
47+ b.WriteString(info.Render("↑/↓ to select • enter to confirm"))
48+
49+ return b.String()
50+}
@@ -0,0 +1,50 @@
1+package views
2+
3+import (
4+ "strings"
5+
6+ "github.com/bnema/archup/internal/interfaces/tui/models"
7+ "github.com/charmbracelet/lipgloss"
8+)
9+
10+// RenderDankLinuxSelection renders the Dank Linux opt-in screen.
11+func RenderDankLinuxSelection(dm *models.DankLinuxModelImpl) string {
12+ var b strings.Builder
13+
14+ title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
15+ info := lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
16+ active := lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true)
17+ desc := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Faint(true)
18+
19+ b.WriteString("\n")
20+ b.WriteString(title.Render("Dank Linux Desktop"))
21+ b.WriteString("\n\n")
22+
23+ b.WriteString(info.Render("Optional: install Dank Linux — a full Wayland desktop suite."))
24+ b.WriteString("\n")
25+ b.WriteString(info.Render("Includes: niri or Hyprland, DankMaterialShell, Ghostty, and auto-theming."))
26+ b.WriteString("\n\n")
27+
28+ for i, option := range dm.Options() {
29+ prefix := " "
30+ style := lipgloss.NewStyle()
31+ labelStyle := lipgloss.NewStyle()
32+
33+ if i == dm.SelectedIndex() {
34+ prefix = "> "
35+ style = active
36+ labelStyle = active
37+ }
38+
39+ b.WriteString(style.Render(prefix))
40+ b.WriteString(labelStyle.Render(option.Label))
41+ b.WriteString("\n")
42+ b.WriteString(desc.Render(" " + option.Description))
43+ b.WriteString("\n")
44+ }
45+
46+ b.WriteString("\n")
47+ b.WriteString(info.Render("↑/↓ to select • enter to confirm"))
48+
49+ return b.String()
50+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 0

    ↑↓ moveenter openesc close