1package handlers
2
3import (
4 "strings"
5
6 "github.com/bnema/archup/internal/application/commands"
7 "github.com/bnema/archup/internal/application/dto"
8 "github.com/bnema/archup/internal/config"
9 "github.com/bnema/archup/internal/domain/bootloader"
10 "github.com/bnema/archup/internal/domain/disk"
11 "github.com/bnema/archup/internal/domain/packages"
12 "github.com/bnema/archup/internal/interfaces/tui/models"
13 tea "github.com/charmbracelet/bubbletea"
14)
15
16// NOTE: This package uses AppContext interface to avoid importing parent tui package
17// This breaks the circular dependency: tui → tui/handlers → tui
18// Messages are typed by the caller (app.go), not here
19
20// ProgressUpdateMsg wraps progress update for bubbletea
21type ProgressUpdateMsg struct {
22 Update *dto.ProgressUpdate
23}
24
25// InstallationErrorMsg wraps installation error for bubbletea
26type InstallationErrorMsg struct {
27 Err error
28}
29
30// InstallationCompleteMsg signals installation completion
31type InstallationCompleteMsg struct {
32 Duration int
33}
34
35// HandleProgressUpdate processes progress update messages
36func HandleProgressUpdate(app AppContext, msg interface{}, progressModel *models.ProgressModelImpl) (*models.ProgressModelImpl, tea.Cmd) {
37 if m, ok := msg.(ProgressUpdateMsg); ok {
38 progressModel.UpdateProgress(m.Update)
39 }
40 return progressModel, nil
41}
42
43// HandleInstallationError processes installation error messages
44func HandleInstallationError(app AppContext, msg interface{}, installModel *models.InstallationModelImpl) (*models.InstallationModelImpl, tea.Cmd) {
45 if errMsg, ok := msg.(InstallationErrorMsg); ok {
46 app.GetLogger().Error("Installation error", "error", errMsg.Err)
47 installModel.SetError(errMsg.Err.Error())
48 }
49 return installModel, nil
50}
51
52// HandleInstallationComplete processes installation completion messages
53func HandleInstallationComplete(app AppContext, msg interface{}, installModel *models.InstallationModelImpl) (*models.InstallationModelImpl, tea.Cmd) {
54 app.GetLogger().Info("Installation completed successfully")
55 installModel.SetComplete()
56 // Update status with final timestamps for duration display
57 if svc := app.GetInstallService(); svc != nil {
58 installModel.SetStatus(svc.GetStatus())
59 }
60 return installModel, nil
61}
62
63// CreateInstallationCommand creates an installation command from form data
64// Runs the full installation in a goroutine and sends progress via p.Send()
65func CreateInstallationCommand(app AppContext, formData models.FormData) tea.Cmd {
66 return func() tea.Msg {
67 ctx := app.GetContext()
68 svc := app.GetInstallService()
69 logger := app.GetLogger()
70 program := app.GetProgram()
71
72 encryptionType := normalizeEncryptionType(formData.EncryptionType)
73
74 // Start installation via service
75 if err := svc.Start(ctx, formData.Hostname, formData.Username, formData.TargetDisk, encryptionType); err != nil {
76 logger.Error("Failed to start installation", "error", err)
77 return InstallationErrorMsg{Err: err}
78 }
79
80 // Subscribe to progress updates and forward to bubbletea
81 progressChan := app.GetProgressTracker().Subscribe()
82 go func() {
83 for {
84 select {
85 case update, ok := <-progressChan:
86 if !ok {
87 return
88 }
89 if program != nil {
90 program.Send(ProgressUpdateMsg{Update: update})
91 }
92 case <-ctx.Done():
93 logger.Info("Installation context cancelled")
94 return
95 }
96 }
97 }()
98
99 // Run installation phases in background goroutine
100 go func() {
101 // Phase 1: Preflight
102 if _, err := svc.RunPreflight(ctx); err != nil {
103 logger.Error("Preflight checks failed", "error", err)
104 if program != nil {
105 program.Send(InstallationErrorMsg{Err: err})
106 }
107 return
108 }
109
110 // Phase 1.5: Bootstrap (clone repo / download files)
111 if _, err := svc.RunBootstrap(ctx); err != nil {
112 logger.Error("Bootstrap failed", "error", err)
113 if program != nil {
114 program.Send(InstallationErrorMsg{Err: err})
115 }
116 return
117 }
118
119 // Phase 2: Partition disk with chosen encryption using user password
120 isEncrypted := parseEncryptionType(formData.EncryptionType) != disk.EncryptionTypeNone
121 partitionCmd := commands.PartitionDiskCommand{
122 TargetDisk: formData.TargetDisk,
123 RootSizeGB: 0, // Use all available space
124 BootSizeGB: 4, // 4GB for limine-snapper-sync
125 EncryptionType: parseEncryptionType(formData.EncryptionType),
126 EncryptionPassword: formData.UserPassword, // Use account password for disk encryption
127 FilesystemType: disk.FilesystemBtrfs,
128 WipeDisks: true,
129 }
130 partitionResult, err := svc.RunPartition(ctx, partitionCmd)
131 if err != nil {
132 logger.Error("Partitioning failed", "error", err)
133 if program != nil {
134 program.Send(InstallationErrorMsg{Err: err})
135 }
136 return
137 }
138
139 // Phase 3: Base install
140 baseCmd := commands.InstallBaseCommand{
141 TargetDisk: formData.TargetDisk,
142 MountPoint: "/mnt",
143 KernelVariant: parseKernelVariant(formData.KernelVariant),
144 IncludeMicrocode: formData.Microcode,
145 Encrypted: isEncrypted,
146 }
147 if _, err := svc.RunBaseInstall(ctx, baseCmd); err != nil {
148 logger.Error("Base installation failed", "error", err)
149 if program != nil {
150 program.Send(InstallationErrorMsg{Err: err})
151 }
152 return
153 }
154
155 // Phase 4: Configure system
156 configCmd := commands.ConfigureSystemCommand{
157 MountPoint: "/mnt",
158 Hostname: formData.Hostname,
159 Timezone: formData.Timezone,
160 Locale: formData.Locale,
161 Keymap: formData.Keymap,
162 Username: formData.Username,
163 UserShell: "/bin/bash",
164 UserPassword: formData.UserPassword,
165 RootPassword: formData.RootPassword,
166 }
167 if _, err := svc.RunConfigSystem(ctx, configCmd); err != nil {
168 logger.Error("System configuration failed", "error", err)
169 if program != nil {
170 program.Send(InstallationErrorMsg{Err: err})
171 }
172 return
173 }
174
175 // Phase 5: Bootloader
176 bootCmd := commands.InstallBootloaderCommand{
177 MountPoint: "/mnt",
178 BootloaderType: bootloader.BootloaderTypeLimine,
179 TimeoutSeconds: 5,
180 Branding: "Arch Linux",
181 KernelVariant: parseKernelVariant(formData.KernelVariant),
182 RootPartition: partitionResult.RootPartition,
183 EncryptionType: partitionCmd.EncryptionType,
184 EFIPartition: partitionResult.EFIPartition,
185 TargetDisk: formData.TargetDisk,
186 KernelParamsExtra: formData.KernelParamsExtra,
187 GPUVendor: formData.GPUVendor,
188 }
189 if _, err := svc.RunBootloaderSetup(ctx, bootCmd); err != nil {
190 logger.Error("Bootloader setup failed", "error", err)
191 if program != nil {
192 program.Send(InstallationErrorMsg{Err: err})
193 }
194 return
195 }
196
197 // Phase 6: Repository setup
198 repoCmd := commands.SetupRepositoriesCommand{
199 MountPoint: "/mnt",
200 EnableMultilib: true,
201 AURHelper: parseAURHelper(formData.AURHelper),
202 KernelVariant: parseKernelVariant(formData.KernelVariant),
203 }
204 if _, err := svc.RunRepositorySetup(ctx, repoCmd); err != nil {
205 logger.Error("Repository setup failed", "error", err)
206 if program != nil {
207 program.Send(InstallationErrorMsg{Err: err})
208 }
209 return
210 }
211
212 // Phase 7: Post-install
213 postCmd := commands.PostInstallCommand{
214 MountPoint: "/mnt",
215 Username: formData.Username,
216 UserEmail: formData.UserEmail,
217 PlymouthTheme: config.PlymouthThemeName,
218 RunPostBootScripts: true,
219 InstallDankLinux: formData.InstallDankLinux,
220 TargetDisk: formData.TargetDisk,
221 Encrypted: isEncrypted,
222 }
223 if _, err := svc.RunPostInstall(ctx, postCmd); err != nil {
224 logger.Error("Post-installation failed", "error", err)
225 if program != nil {
226 program.Send(InstallationErrorMsg{Err: err})
227 }
228 return
229 }
230 // Complete installation
231 if err := svc.Complete(ctx); err != nil {
232 logger.Error("Failed to complete installation", "error", err)
233 if program != nil {
234 program.Send(InstallationErrorMsg{Err: err})
235 }
236 return
237 }
238
239 // Send completion message
240 if program != nil {
241 program.Send(InstallationCompleteMsg{Duration: 0})
242 }
243 }()
244
245 return nil
246 }
247}
248
249// parseKernelVariant converts string to KernelVariant
250func parseKernelVariant(s string) packages.KernelVariant {
251 switch s {
252 case "linux-zen", "zen":
253 return packages.KernelZen
254 case "linux-lts", "lts":
255 return packages.KernelLTS
256 case "linux-hardened", "hardened":
257 return packages.KernelHardened
258 case "linux-cachyos", "cachyos":
259 return packages.KernelCachyOS
260 default:
261 return packages.KernelStable
262 }
263}
264
265// parseAURHelper converts string to AURHelper
266func parseAURHelper(s string) packages.AURHelper {
267 switch s {
268 case "yay":
269 return packages.AURHelperYay
270 default:
271 return packages.AURHelperParu
272 }
273}
274
275func normalizeEncryptionType(value string) string {
276 if value == "" {
277 return "luks"
278 }
279
280 switch strings.ToLower(value) {
281 case "none":
282 return "none"
283 case "luks", "luks+", "luks+standard":
284 return "luks"
285 case "luks-lvm", "luks+lvm":
286 return "luks-lvm"
287 default:
288 return "luks"
289 }
290}
291
292func parseEncryptionType(value string) disk.EncryptionType {
293 switch normalizeEncryptionType(value) {
294 case "none":
295 return disk.EncryptionTypeNone
296 case "luks-lvm":
297 return disk.EncryptionTypeLUKSLVM
298 default:
299 return disk.EncryptionTypeLUKS
300 }
301}