main 6af0a61

install_handler.go RawHistoryFolder

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}

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