detached 0f6eb90

feat(tui): add encryption selection screen between disk and kernel steps

commit
0f6eb900c19ccd1194a4bcbcda5ce7376b9cb01b
author
brice · · 2026-02-27 05:31 UTC
parent
81998aa
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +198 -2
M internal/interfaces/tui/app.go +70 -2
@@ -30,9 +30,11 @@ type App struct {
3030 // UI models (using concrete types for compatibility with views/handlers)
3131 formModel *models.FormModelImpl
3232 diskModel *models.DiskModelImpl
33+ encryptionModel *models.EncryptionModelImpl
3334 kernelModel *models.KernelModelImpl
3435 amdPstateModel *models.AMDPStateModelImpl
3536 gpuModel *models.GPUModelImpl
37+ reposModel *models.ReposModelImpl
3638 dankLinuxModel *models.DankLinuxModelImpl
3739 installationModel *models.InstallationModelImpl
3840 progressModel *models.ProgressModelImpl
@@ -55,9 +57,11 @@ type Screen string
5557 const (
5658 ScreenForm Screen = "form"
5759 ScreenDisk Screen = "disk"
60+ ScreenEncryption Screen = "encryption"
5861 ScreenKernel Screen = "kernel"
5962 ScreenAMDPState Screen = "amd-pstate"
6063 ScreenGPU Screen = "gpu"
64+ ScreenRepos Screen = "repos"
6165 ScreenDankLinux Screen = "danklinux"
6266 ScreenInstalling Screen = "installing"
6367 ScreenProgress Screen = "progress"
@@ -81,9 +85,11 @@ func NewApp(
8185 logger: logger,
8286 formModel: models.NewFormModel(),
8387 diskModel: models.NewDiskModel(),
88+ encryptionModel: models.NewEncryptionModel(),
8489 kernelModel: models.NewKernelModel(),
8590 amdPstateModel: models.NewAMDPStateModel(),
8691 gpuModel: models.NewGPUModel(),
92+ reposModel: models.NewReposModel(),
8793 dankLinuxModel: models.NewDankLinuxModel(),
8894 installationModel: models.NewInstallationModel(),
8995 progressModel: models.NewProgressModel(),
@@ -155,12 +161,16 @@ func (a *App) View() string {
155161 return views.RenderForm(a.formModel)
156162 case ScreenDisk:
157163 return views.RenderDiskSelection(a.diskModel)
164+ case ScreenEncryption:
165+ return views.RenderEncryptionSelection(a.encryptionModel)
158166 case ScreenKernel:
159167 return views.RenderKernelSelection(a.kernelModel)
160168 case ScreenAMDPState:
161169 return views.RenderAMDPStateSelection(a.amdPstateModel)
162170 case ScreenGPU:
163171 return views.RenderGPUSelection(a.gpuModel)
172+ case ScreenRepos:
173+ return views.RenderReposSelection(a.reposModel)
164174 case ScreenDankLinux:
165175 return views.RenderDankLinuxSelection(a.dankLinuxModel)
166176 case ScreenInstalling, ScreenProgress:
@@ -181,12 +191,16 @@ func (a *App) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
181191 return a.handleFormInput(msg)
182192 case ScreenDisk:
183193 return a.handleDiskInput(msg)
194+ case ScreenEncryption:
195+ return a.handleEncryptionInput(msg)
184196 case ScreenKernel:
185197 return a.handleKernelInput(msg)
186198 case ScreenAMDPState:
187199 return a.handleAMDPStateInput(msg)
188200 case ScreenGPU:
189201 return a.handleGPUInput(msg)
202+ case ScreenRepos:
203+ return a.handleReposInput(msg)
190204 case ScreenDankLinux:
191205 return a.handleDankLinuxInput(msg)
192206 case ScreenProgress:
@@ -317,8 +331,7 @@ func (a *App) handleGPUInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
317331 // Update stored form data directly
318332 a.formData.GPUVendor = string(selected.Vendor)
319333 a.formData.GPUDrivers = append([]string{}, selected.Drivers...)
320- a.currentScreen = ScreenDankLinux
321- return a, nil
334+ return a.startReposSelection()
322335 }
323336
324337 return a, nil
@@ -326,6 +339,8 @@ func (a *App) handleGPUInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
326339
327340 func (a *App) handleDankLinuxInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
328341 switch msg.String() {
342+ case "esc", "backspace":
343+ return a.startReposSelection()
329344 case "up", "shift+tab":
330345 a.dankLinuxModel.MoveUp()
331346 return a, nil
@@ -341,6 +356,35 @@ func (a *App) handleDankLinuxInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
341356 return a, nil
342357 }
343358
359+func (a *App) startReposSelection() (tea.Model, tea.Cmd) {
360+ a.currentScreen = ScreenRepos
361+ return a, nil
362+}
363+
364+func (a *App) handleReposInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
365+ switch msg.String() {
366+ case "ctrl+c", "q":
367+ return a, tea.Quit
368+ case "esc", "backspace":
369+ return a.startGPUSelection()
370+ case "up":
371+ a.reposModel.MoveUp()
372+ return a, nil
373+ case "down":
374+ a.reposModel.MoveDown()
375+ return a, nil
376+ case "tab", "shift+tab":
377+ a.reposModel.NextSection()
378+ return a, nil
379+ case "enter":
380+ a.formData.AURHelper = a.reposModel.SelectedAURHelper()
381+ a.formData.EnableChaotic = a.reposModel.SelectedChaoticEnabled()
382+ a.currentScreen = ScreenDankLinux
383+ return a, nil
384+ }
385+ return a, nil
386+}
387+
344388 func (a *App) handleKernelInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
345389 switch msg.String() {
346390 case "ctrl+c":
@@ -536,6 +580,30 @@ func (a *App) handleDiskInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
536580 }
537581 // Update stored form data directly
538582 a.formData.TargetDisk = selected.Path
583+ return a.startEncryptionSelection()
584+ }
585+ return a, nil
586+}
587+
588+func (a *App) startEncryptionSelection() (tea.Model, tea.Cmd) {
589+ a.currentScreen = ScreenEncryption
590+ return a, nil
591+}
592+
593+func (a *App) handleEncryptionInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
594+ switch msg.String() {
595+ case "ctrl+c":
596+ return a, tea.Quit
597+ case "esc", "backspace":
598+ return a.startDiskSelection()
599+ case "up", "shift+tab":
600+ a.encryptionModel.MoveUp()
601+ return a, nil
602+ case "down", "tab":
603+ a.encryptionModel.MoveDown()
604+ return a, nil
605+ case "enter":
606+ a.formData.EncryptionType = a.encryptionModel.SelectedOption().Value
539607 return a.startKernelSelection()
540608 }
541609 return a, nil
@@ -30,9 +30,11 @@ type App struct {
30 // UI models (using concrete types for compatibility with views/handlers)30 // UI models (using concrete types for compatibility with views/handlers)
31 formModel *models.FormModelImpl31 formModel *models.FormModelImpl
32 diskModel *models.DiskModelImpl32 diskModel *models.DiskModelImpl
33+ encryptionModel *models.EncryptionModelImpl
33 kernelModel *models.KernelModelImpl34 kernelModel *models.KernelModelImpl
34 amdPstateModel *models.AMDPStateModelImpl35 amdPstateModel *models.AMDPStateModelImpl
35 gpuModel *models.GPUModelImpl36 gpuModel *models.GPUModelImpl
37+ reposModel *models.ReposModelImpl
36 dankLinuxModel *models.DankLinuxModelImpl38 dankLinuxModel *models.DankLinuxModelImpl
37 installationModel *models.InstallationModelImpl39 installationModel *models.InstallationModelImpl
38 progressModel *models.ProgressModelImpl40 progressModel *models.ProgressModelImpl
@@ -55,9 +57,11 @@ type Screen string
55 const (57 const (
56 ScreenForm Screen = "form"58 ScreenForm Screen = "form"
57 ScreenDisk Screen = "disk"59 ScreenDisk Screen = "disk"
60+ ScreenEncryption Screen = "encryption"
58 ScreenKernel Screen = "kernel"61 ScreenKernel Screen = "kernel"
59 ScreenAMDPState Screen = "amd-pstate"62 ScreenAMDPState Screen = "amd-pstate"
60 ScreenGPU Screen = "gpu"63 ScreenGPU Screen = "gpu"
64+ ScreenRepos Screen = "repos"
61 ScreenDankLinux Screen = "danklinux"65 ScreenDankLinux Screen = "danklinux"
62 ScreenInstalling Screen = "installing"66 ScreenInstalling Screen = "installing"
63 ScreenProgress Screen = "progress"67 ScreenProgress Screen = "progress"
@@ -81,9 +85,11 @@ func NewApp(
81 logger: logger,85 logger: logger,
82 formModel: models.NewFormModel(),86 formModel: models.NewFormModel(),
83 diskModel: models.NewDiskModel(),87 diskModel: models.NewDiskModel(),
88+ encryptionModel: models.NewEncryptionModel(),
84 kernelModel: models.NewKernelModel(),89 kernelModel: models.NewKernelModel(),
85 amdPstateModel: models.NewAMDPStateModel(),90 amdPstateModel: models.NewAMDPStateModel(),
86 gpuModel: models.NewGPUModel(),91 gpuModel: models.NewGPUModel(),
92+ reposModel: models.NewReposModel(),
87 dankLinuxModel: models.NewDankLinuxModel(),93 dankLinuxModel: models.NewDankLinuxModel(),
88 installationModel: models.NewInstallationModel(),94 installationModel: models.NewInstallationModel(),
89 progressModel: models.NewProgressModel(),95 progressModel: models.NewProgressModel(),
@@ -155,12 +161,16 @@ func (a *App) View() string {
155 return views.RenderForm(a.formModel)161 return views.RenderForm(a.formModel)
156 case ScreenDisk:162 case ScreenDisk:
157 return views.RenderDiskSelection(a.diskModel)163 return views.RenderDiskSelection(a.diskModel)
164+ case ScreenEncryption:
165+ return views.RenderEncryptionSelection(a.encryptionModel)
158 case ScreenKernel:166 case ScreenKernel:
159 return views.RenderKernelSelection(a.kernelModel)167 return views.RenderKernelSelection(a.kernelModel)
160 case ScreenAMDPState:168 case ScreenAMDPState:
161 return views.RenderAMDPStateSelection(a.amdPstateModel)169 return views.RenderAMDPStateSelection(a.amdPstateModel)
162 case ScreenGPU:170 case ScreenGPU:
163 return views.RenderGPUSelection(a.gpuModel)171 return views.RenderGPUSelection(a.gpuModel)
172+ case ScreenRepos:
173+ return views.RenderReposSelection(a.reposModel)
164 case ScreenDankLinux:174 case ScreenDankLinux:
165 return views.RenderDankLinuxSelection(a.dankLinuxModel)175 return views.RenderDankLinuxSelection(a.dankLinuxModel)
166 case ScreenInstalling, ScreenProgress:176 case ScreenInstalling, ScreenProgress:
@@ -181,12 +191,16 @@ func (a *App) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
181 return a.handleFormInput(msg)191 return a.handleFormInput(msg)
182 case ScreenDisk:192 case ScreenDisk:
183 return a.handleDiskInput(msg)193 return a.handleDiskInput(msg)
194+ case ScreenEncryption:
195+ return a.handleEncryptionInput(msg)
184 case ScreenKernel:196 case ScreenKernel:
185 return a.handleKernelInput(msg)197 return a.handleKernelInput(msg)
186 case ScreenAMDPState:198 case ScreenAMDPState:
187 return a.handleAMDPStateInput(msg)199 return a.handleAMDPStateInput(msg)
188 case ScreenGPU:200 case ScreenGPU:
189 return a.handleGPUInput(msg)201 return a.handleGPUInput(msg)
202+ case ScreenRepos:
203+ return a.handleReposInput(msg)
190 case ScreenDankLinux:204 case ScreenDankLinux:
191 return a.handleDankLinuxInput(msg)205 return a.handleDankLinuxInput(msg)
192 case ScreenProgress:206 case ScreenProgress:
@@ -317,8 +331,7 @@ func (a *App) handleGPUInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
317 // Update stored form data directly331 // Update stored form data directly
318 a.formData.GPUVendor = string(selected.Vendor)332 a.formData.GPUVendor = string(selected.Vendor)
319 a.formData.GPUDrivers = append([]string{}, selected.Drivers...)333 a.formData.GPUDrivers = append([]string{}, selected.Drivers...)
320- a.currentScreen = ScreenDankLinux334+ return a.startReposSelection()
321- return a, nil
322 }335 }
323 336
324 return a, nil337 return a, nil
@@ -326,6 +339,8 @@ func (a *App) handleGPUInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
326 339
327 func (a *App) handleDankLinuxInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {340 func (a *App) handleDankLinuxInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
328 switch msg.String() {341 switch msg.String() {
342+ case "esc", "backspace":
343+ return a.startReposSelection()
329 case "up", "shift+tab":344 case "up", "shift+tab":
330 a.dankLinuxModel.MoveUp()345 a.dankLinuxModel.MoveUp()
331 return a, nil346 return a, nil
@@ -341,6 +356,35 @@ func (a *App) handleDankLinuxInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
341 return a, nil356 return a, nil
342 }357 }
343 358
359+func (a *App) startReposSelection() (tea.Model, tea.Cmd) {
360+ a.currentScreen = ScreenRepos
361+ return a, nil
362+}
363+
364+func (a *App) handleReposInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
365+ switch msg.String() {
366+ case "ctrl+c", "q":
367+ return a, tea.Quit
368+ case "esc", "backspace":
369+ return a.startGPUSelection()
370+ case "up":
371+ a.reposModel.MoveUp()
372+ return a, nil
373+ case "down":
374+ a.reposModel.MoveDown()
375+ return a, nil
376+ case "tab", "shift+tab":
377+ a.reposModel.NextSection()
378+ return a, nil
379+ case "enter":
380+ a.formData.AURHelper = a.reposModel.SelectedAURHelper()
381+ a.formData.EnableChaotic = a.reposModel.SelectedChaoticEnabled()
382+ a.currentScreen = ScreenDankLinux
383+ return a, nil
384+ }
385+ return a, nil
386+}
387+
344 func (a *App) handleKernelInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {388 func (a *App) handleKernelInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
345 switch msg.String() {389 switch msg.String() {
346 case "ctrl+c":390 case "ctrl+c":
@@ -536,6 +580,30 @@ func (a *App) handleDiskInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
536 }580 }
537 // Update stored form data directly581 // Update stored form data directly
538 a.formData.TargetDisk = selected.Path582 a.formData.TargetDisk = selected.Path
583+ return a.startEncryptionSelection()
584+ }
585+ return a, nil
586+}
587+
588+func (a *App) startEncryptionSelection() (tea.Model, tea.Cmd) {
589+ a.currentScreen = ScreenEncryption
590+ return a, nil
591+}
592+
593+func (a *App) handleEncryptionInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
594+ switch msg.String() {
595+ case "ctrl+c":
596+ return a, tea.Quit
597+ case "esc", "backspace":
598+ return a.startDiskSelection()
599+ case "up", "shift+tab":
600+ a.encryptionModel.MoveUp()
601+ return a, nil
602+ case "down", "tab":
603+ a.encryptionModel.MoveDown()
604+ return a, nil
605+ case "enter":
606+ a.formData.EncryptionType = a.encryptionModel.SelectedOption().Value
539 return a.startKernelSelection()607 return a.startKernelSelection()
540 }608 }
541 return a, nil609 return a, nil
A internal/interfaces/tui/models/encryption.go +78 -0
@@ -0,0 +1,78 @@
1+package models
2+
3+// EncryptionOption represents a selectable encryption option.
4+type EncryptionOption struct {
5+ Value string
6+ Label string
7+ Description string
8+ Recommended bool
9+}
10+
11+// EncryptionModelImpl holds encryption selection state.
12+type EncryptionModelImpl struct {
13+ options []EncryptionOption
14+ selected int
15+}
16+
17+// NewEncryptionModel creates a new encryption selection model.
18+func NewEncryptionModel() *EncryptionModelImpl {
19+ options := []EncryptionOption{
20+ {Value: "luks", Label: "LUKS2", Description: "Full disk encryption (recommended)", Recommended: true},
21+ {Value: "none", Label: "None", Description: "No encryption"},
22+ }
23+ selected := 0
24+ for i, o := range options {
25+ if o.Recommended {
26+ selected = i
27+ break
28+ }
29+ }
30+ return &EncryptionModelImpl{options: options, selected: selected}
31+}
32+
33+// Options returns the selectable encryption options.
34+func (em *EncryptionModelImpl) Options() []EncryptionOption { return em.options }
35+
36+// SelectedIndex returns the current selection index.
37+func (em *EncryptionModelImpl) SelectedIndex() int { return em.selected }
38+
39+// SelectedOption returns the currently selected option.
40+func (em *EncryptionModelImpl) SelectedOption() EncryptionOption {
41+ if len(em.options) == 0 {
42+ return EncryptionOption{}
43+ }
44+ if em.selected < 0 || em.selected >= len(em.options) {
45+ return em.options[0]
46+ }
47+ return em.options[em.selected]
48+}
49+
50+// MoveUp moves selection up (wraps).
51+func (em *EncryptionModelImpl) MoveUp() {
52+ if len(em.options) == 0 {
53+ return
54+ }
55+ if em.selected == 0 {
56+ em.selected = len(em.options) - 1
57+ return
58+ }
59+ em.selected--
60+}
61+
62+// MoveDown moves selection down (wraps).
63+func (em *EncryptionModelImpl) MoveDown() {
64+ if len(em.options) == 0 {
65+ return
66+ }
67+ em.selected = (em.selected + 1) % len(em.options)
68+}
69+
70+// SetSelectedValue selects an option by value string.
71+func (em *EncryptionModelImpl) SetSelectedValue(v string) {
72+ for i, o := range em.options {
73+ if o.Value == v {
74+ em.selected = i
75+ return
76+ }
77+ }
78+}
@@ -0,0 +1,78 @@
1+package models
2+
3+// EncryptionOption represents a selectable encryption option.
4+type EncryptionOption struct {
5+ Value string
6+ Label string
7+ Description string
8+ Recommended bool
9+}
10+
11+// EncryptionModelImpl holds encryption selection state.
12+type EncryptionModelImpl struct {
13+ options []EncryptionOption
14+ selected int
15+}
16+
17+// NewEncryptionModel creates a new encryption selection model.
18+func NewEncryptionModel() *EncryptionModelImpl {
19+ options := []EncryptionOption{
20+ {Value: "luks", Label: "LUKS2", Description: "Full disk encryption (recommended)", Recommended: true},
21+ {Value: "none", Label: "None", Description: "No encryption"},
22+ }
23+ selected := 0
24+ for i, o := range options {
25+ if o.Recommended {
26+ selected = i
27+ break
28+ }
29+ }
30+ return &EncryptionModelImpl{options: options, selected: selected}
31+}
32+
33+// Options returns the selectable encryption options.
34+func (em *EncryptionModelImpl) Options() []EncryptionOption { return em.options }
35+
36+// SelectedIndex returns the current selection index.
37+func (em *EncryptionModelImpl) SelectedIndex() int { return em.selected }
38+
39+// SelectedOption returns the currently selected option.
40+func (em *EncryptionModelImpl) SelectedOption() EncryptionOption {
41+ if len(em.options) == 0 {
42+ return EncryptionOption{}
43+ }
44+ if em.selected < 0 || em.selected >= len(em.options) {
45+ return em.options[0]
46+ }
47+ return em.options[em.selected]
48+}
49+
50+// MoveUp moves selection up (wraps).
51+func (em *EncryptionModelImpl) MoveUp() {
52+ if len(em.options) == 0 {
53+ return
54+ }
55+ if em.selected == 0 {
56+ em.selected = len(em.options) - 1
57+ return
58+ }
59+ em.selected--
60+}
61+
62+// MoveDown moves selection down (wraps).
63+func (em *EncryptionModelImpl) MoveDown() {
64+ if len(em.options) == 0 {
65+ return
66+ }
67+ em.selected = (em.selected + 1) % len(em.options)
68+}
69+
70+// SetSelectedValue selects an option by value string.
71+func (em *EncryptionModelImpl) SetSelectedValue(v string) {
72+ for i, o := range em.options {
73+ if o.Value == v {
74+ em.selected = i
75+ return
76+ }
77+ }
78+}
M internal/interfaces/tui/models/form.go +2 -0
@@ -40,6 +40,7 @@ type FormData struct {
4040 AURHelper string
4141 Microcode bool
4242 InstallDankLinux bool
43+ EnableChaotic bool
4344 }
4445
4546 // FormModelImpl implements FormModel interface
@@ -312,6 +313,7 @@ func (fm *FormModelImpl) extractData() {
312313 Locale: fm.fields[5].Value(),
313314 Keymap: fm.fields[6].Value(),
314315 InstallDankLinux: fm.data.InstallDankLinux,
316+ EnableChaotic: fm.data.EnableChaotic,
315317 }
316318 }
317319
@@ -40,6 +40,7 @@ type FormData struct {
40 AURHelper string40 AURHelper string
41 Microcode bool41 Microcode bool
42 InstallDankLinux bool42 InstallDankLinux bool
43+ EnableChaotic bool
43 }44 }
44 45
45 // FormModelImpl implements FormModel interface46 // FormModelImpl implements FormModel interface
@@ -312,6 +313,7 @@ func (fm *FormModelImpl) extractData() {
312 Locale: fm.fields[5].Value(),313 Locale: fm.fields[5].Value(),
313 Keymap: fm.fields[6].Value(),314 Keymap: fm.fields[6].Value(),
314 InstallDankLinux: fm.data.InstallDankLinux,315 InstallDankLinux: fm.data.InstallDankLinux,
316+ EnableChaotic: fm.data.EnableChaotic,
315 }317 }
316 }318 }
317 319
A internal/interfaces/tui/views/encryption.go +48 -0
@@ -0,0 +1,48 @@
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+// RenderEncryptionSelection renders the encryption selection screen.
11+func RenderEncryptionSelection(em *models.EncryptionModelImpl) 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("Disk Encryption"))
21+ b.WriteString("\n\n")
22+
23+ b.WriteString(info.Render("Choose how to encrypt your installation disk."))
24+ b.WriteString("\n\n")
25+
26+ for i, option := range em.Options() {
27+ prefix := " "
28+ style := lipgloss.NewStyle()
29+ labelStyle := lipgloss.NewStyle()
30+
31+ if i == em.SelectedIndex() {
32+ prefix = "> "
33+ style = active
34+ labelStyle = active
35+ }
36+
37+ b.WriteString(style.Render(prefix))
38+ b.WriteString(labelStyle.Render(option.Label))
39+ b.WriteString("\n")
40+ b.WriteString(desc.Render(" " + option.Description))
41+ b.WriteString("\n")
42+ }
43+
44+ b.WriteString("\n")
45+ b.WriteString(info.Render("↑/↓ to select • enter to confirm • esc to go back"))
46+
47+ return b.String()
48+}
@@ -0,0 +1,48 @@
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+// RenderEncryptionSelection renders the encryption selection screen.
11+func RenderEncryptionSelection(em *models.EncryptionModelImpl) 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("Disk Encryption"))
21+ b.WriteString("\n\n")
22+
23+ b.WriteString(info.Render("Choose how to encrypt your installation disk."))
24+ b.WriteString("\n\n")
25+
26+ for i, option := range em.Options() {
27+ prefix := " "
28+ style := lipgloss.NewStyle()
29+ labelStyle := lipgloss.NewStyle()
30+
31+ if i == em.SelectedIndex() {
32+ prefix = "> "
33+ style = active
34+ labelStyle = active
35+ }
36+
37+ b.WriteString(style.Render(prefix))
38+ b.WriteString(labelStyle.Render(option.Label))
39+ b.WriteString("\n")
40+ b.WriteString(desc.Render(" " + option.Description))
41+ b.WriteString("\n")
42+ }
43+
44+ b.WriteString("\n")
45+ b.WriteString(info.Render("↑/↓ to select • enter to confirm • esc to go back"))
46+
47+ return b.String()
48+}

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