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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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" | |||
| 8 | 8 | ||
| 9 | 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 | 28 | # Skip if not running in an interactive terminal | |
| 12 | 29 | if [ ! -t 0 ]; then | |
| 13 | 30 | log "Not running in interactive terminal — skipping Dank Linux opt-in." | |
| 14 | 31 | exit 0 | |
| 15 | 32 | fi | |
| 16 | 33 | ||
| 34 | + | # Manual prompt fallback (for reinstall / re-run scenarios) | |
| 17 | 35 | echo "" | |
| 18 | 36 | echo "==============================================" | |
| 19 | 37 | echo " Dank Linux — Optional Install" | |
| @@ -27,7 +45,7 @@ echo " - Auto-theming, notifications, app launcher," | |||
| 27 | 45 | echo " lock screen, system tray, search — all" | |
| 28 | 46 | echo " integrated. One install. Everything works." | |
| 29 | 47 | echo "" | |
| 30 | - | echo " Install: curl -fsSL https://install.danklinux.com | sh" | |
| 48 | + | echo " Install: curl -fsSL https://install.danklinux.com | sh" | |
| 31 | 49 | echo " Note: This runs the official Dank Linux installer. Review at:" | |
| 32 | 50 | echo " https://github.com/AvengeMedia/DankMaterialShell" | |
| 33 | 51 | 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 terminal | 28 | # Skip if not running in an interactive terminal | ||
| 12 | if [ ! -t 0 ]; then | 29 | 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 0 | 31 | exit 0 | ||
| 15 | fi | 32 | 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 { | |||
| 7 | 7 | UserEmail string // User email for git config and SSH key (optional) | |
| 8 | 8 | RunPostBootScripts bool // Whether to run post-boot scripts | |
| 9 | 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 | } | |
| @@ -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 scripts | 8 | 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 | |||
| 59 | 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 | 71 | // Install Plymouth theme if specified | |
| 63 | 72 | if cmd.PlymouthTheme != "" { | |
| 64 | 73 | h.logger.Info("Installing Plymouth theme", "theme", cmd.PlymouthTheme) | |
| @@ -206,6 +215,16 @@ func (h *PostInstallHandler) tryReadLocal(path string) ([]byte, error) { | |||
| 206 | 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 | 228 | func (h *PostInstallHandler) downloadTemplate(path string) ([]byte, error) { | |
| 210 | 229 | url := fmt.Sprintf("%s/%s", h.rawURL, path) | |
| 211 | 230 | 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 specified | 71 | // 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) { | |||
| 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 | 214 | func TestPostInstallHandler_Handle_Everything(t *testing.T) { | |
| 166 | 215 | ctrl := gomock.NewController(t) | |
| 167 | 216 | 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 { | |||
| 33 | 33 | kernelModel *models.KernelModelImpl | |
| 34 | 34 | amdPstateModel *models.AMDPStateModelImpl | |
| 35 | 35 | gpuModel *models.GPUModelImpl | |
| 36 | + | dankLinuxModel *models.DankLinuxModelImpl | |
| 36 | 37 | installationModel *models.InstallationModelImpl | |
| 37 | 38 | progressModel *models.ProgressModelImpl | |
| 38 | 39 | ||
| @@ -57,6 +58,7 @@ const ( | |||
| 57 | 58 | ScreenKernel Screen = "kernel" | |
| 58 | 59 | ScreenAMDPState Screen = "amd-pstate" | |
| 59 | 60 | ScreenGPU Screen = "gpu" | |
| 61 | + | ScreenDankLinux Screen = "danklinux" | |
| 60 | 62 | ScreenInstalling Screen = "installing" | |
| 61 | 63 | ScreenProgress Screen = "progress" | |
| 62 | 64 | ScreenSummary Screen = "summary" | |
| @@ -82,6 +84,7 @@ func NewApp( | |||
| 82 | 84 | kernelModel: models.NewKernelModel(), | |
| 83 | 85 | amdPstateModel: models.NewAMDPStateModel(), | |
| 84 | 86 | gpuModel: models.NewGPUModel(), | |
| 87 | + | dankLinuxModel: models.NewDankLinuxModel(), | |
| 85 | 88 | installationModel: models.NewInstallationModel(), | |
| 86 | 89 | progressModel: models.NewProgressModel(), | |
| 87 | 90 | currentScreen: ScreenForm, | |
| @@ -158,6 +161,8 @@ func (a *App) View() string { | |||
| 158 | 161 | return views.RenderAMDPStateSelection(a.amdPstateModel) | |
| 159 | 162 | case ScreenGPU: | |
| 160 | 163 | return views.RenderGPUSelection(a.gpuModel) | |
| 164 | + | case ScreenDankLinux: | |
| 165 | + | return views.RenderDankLinuxSelection(a.dankLinuxModel) | |
| 161 | 166 | case ScreenInstalling, ScreenProgress: | |
| 162 | 167 | return views.RenderProgress(a.progressModel) | |
| 163 | 168 | case ScreenSummary: | |
| @@ -182,6 +187,8 @@ func (a *App) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) { | |||
| 182 | 187 | return a.handleAMDPStateInput(msg) | |
| 183 | 188 | case ScreenGPU: | |
| 184 | 189 | return a.handleGPUInput(msg) | |
| 190 | + | case ScreenDankLinux: | |
| 191 | + | return a.handleDankLinuxInput(msg) | |
| 185 | 192 | case ScreenProgress: | |
| 186 | 193 | // Limited input during installation | |
| 187 | 194 | if msg.String() == "ctrl+c" { | |
| @@ -310,12 +317,28 @@ func (a *App) handleGPUInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) { | |||
| 310 | 317 | // Update stored form data directly | |
| 311 | 318 | a.formData.GPUVendor = string(selected.Vendor) | |
| 312 | 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 | 324 | 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 | 342 | func (a *App) handleKernelInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) { | |
| 320 | 343 | switch msg.String() { | |
| 321 | 344 | case "ctrl+c": | |
| @@ -33,6 +33,7 @@ type App struct { | |||||
| 33 | kernelModel *models.KernelModelImpl | 33 | kernelModel *models.KernelModelImpl | ||
| 34 | amdPstateModel *models.AMDPStateModelImpl | 34 | amdPstateModel *models.AMDPStateModelImpl | ||
| 35 | gpuModel *models.GPUModelImpl | 35 | gpuModel *models.GPUModelImpl | ||
| 36 | + | dankLinuxModel *models.DankLinuxModelImpl | |||
| 36 | installationModel *models.InstallationModelImpl | 37 | installationModel *models.InstallationModelImpl | ||
| 37 | progressModel *models.ProgressModelImpl | 38 | 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 installation | 193 | // 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 directly | 317 | // 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, nil | 324 | 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 | |||
| 213 | 213 | UserEmail: formData.UserEmail, | |
| 214 | 214 | PlymouthTheme: config.PlymouthThemeName, | |
| 215 | 215 | RunPostBootScripts: true, | |
| 216 | + | InstallDankLinux: formData.InstallDankLinux, | |
| 216 | 217 | } | |
| 217 | 218 | if _, err := svc.RunPostInstall(ctx, postCmd); err != nil { | |
| 218 | 219 | 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 { | |||
| 39 | 39 | KernelVariant string | |
| 40 | 40 | AURHelper string | |
| 41 | 41 | Microcode bool | |
| 42 | + | InstallDankLinux bool | |
| 42 | 43 | } | |
| 43 | 44 | ||
| 44 | 45 | // FormModelImpl implements FormModel interface | |
| @@ -39,6 +39,7 @@ type FormData struct { | |||||
| 39 | KernelVariant string | 39 | KernelVariant string | ||
| 40 | AURHelper string | 40 | AURHelper string | ||
| 41 | Microcode bool | 41 | Microcode bool | ||
| 42 | + | InstallDankLinux bool | |||
| 42 | } | 43 | } | ||
| 43 | 44 | ||||
| 44 | // FormModelImpl implements FormModel interface | 45 | // 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 | + | } | |||