detached 9012e23

feat(tui): add AUR helper and Chaotic-AUR selection screen

commit
9012e238bc421002a2fdf46adb92ab26aaf78005
author
brice · · 2026-02-27 05:31 UTC
parent
0f6eb90
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +231 -1
M internal/interfaces/tui/handlers/install_handler.go +1 -1
@@ -196,7 +196,7 @@ func CreateInstallationCommand(app AppContext, formData models.FormData) tea.Cmd
196196 repoCmd := commands.SetupRepositoriesCommand{
197197 MountPoint: "/mnt",
198198 EnableMultilib: true,
199- EnableChaotic: true,
199+ EnableChaotic: formData.EnableChaotic,
200200 AURHelper: parseAURHelper(formData.AURHelper),
201201 KernelVariant: parseKernelVariant(formData.KernelVariant),
202202 }
@@ -196,7 +196,7 @@ func CreateInstallationCommand(app AppContext, formData models.FormData) tea.Cmd
196 repoCmd := commands.SetupRepositoriesCommand{196 repoCmd := commands.SetupRepositoriesCommand{
197 MountPoint: "/mnt",197 MountPoint: "/mnt",
198 EnableMultilib: true,198 EnableMultilib: true,
199- EnableChaotic: true,199+ EnableChaotic: formData.EnableChaotic,
200 AURHelper: parseAURHelper(formData.AURHelper),200 AURHelper: parseAURHelper(formData.AURHelper),
201 KernelVariant: parseKernelVariant(formData.KernelVariant),201 KernelVariant: parseKernelVariant(formData.KernelVariant),
202 }202 }
A internal/interfaces/tui/models/repos.go +147 -0
@@ -0,0 +1,147 @@
1+package models
2+
3+// AURHelperOption represents a selectable AUR helper option.
4+type AURHelperOption struct {
5+ Value string
6+ Label string
7+ Description string
8+ Recommended bool
9+}
10+
11+// ChaoticOption represents a selectable Chaotic-AUR option.
12+type ChaoticOption struct {
13+ Enabled bool
14+ Label string
15+ Description string
16+ Recommended bool
17+}
18+
19+// ReposModelImpl holds AUR helper and Chaotic-AUR selection state.
20+type ReposModelImpl struct {
21+ aurOptions []AURHelperOption
22+ aurSelected int
23+ chaoticOptions []ChaoticOption
24+ chaoticSelected int
25+ // focusSection: 0 = AUR helper, 1 = Chaotic-AUR
26+ focusSection int
27+}
28+
29+// NewReposModel creates a new repos selection model.
30+func NewReposModel() *ReposModelImpl {
31+ aurOptions := []AURHelperOption{
32+ {Value: "paru", Label: "paru", Description: "Fast AUR helper written in Rust (recommended)", Recommended: true},
33+ {Value: "yay", Label: "yay", Description: "Yet Another Yogurt - AUR helper written in Go"},
34+ }
35+ aurSelected := 0
36+ for i, o := range aurOptions {
37+ if o.Recommended {
38+ aurSelected = i
39+ break
40+ }
41+ }
42+
43+ chaoticOptions := []ChaoticOption{
44+ {Enabled: true, Label: "Enabled", Description: "Pre-built AUR packages; faster installs (recommended)", Recommended: true},
45+ {Enabled: false, Label: "Disabled", Description: "Build AUR packages from source"},
46+ }
47+ chaoticSelected := 0
48+ for i, o := range chaoticOptions {
49+ if o.Recommended {
50+ chaoticSelected = i
51+ break
52+ }
53+ }
54+
55+ return &ReposModelImpl{
56+ aurOptions: aurOptions,
57+ aurSelected: aurSelected,
58+ chaoticOptions: chaoticOptions,
59+ chaoticSelected: chaoticSelected,
60+ focusSection: 0,
61+ }
62+}
63+
64+// AUROptions returns the selectable AUR helper options.
65+func (rm *ReposModelImpl) AUROptions() []AURHelperOption { return rm.aurOptions }
66+
67+// AURSelectedIndex returns the current AUR selection index.
68+func (rm *ReposModelImpl) AURSelectedIndex() int { return rm.aurSelected }
69+
70+// ChaoticOptions returns the selectable Chaotic-AUR options.
71+func (rm *ReposModelImpl) ChaoticOptions() []ChaoticOption { return rm.chaoticOptions }
72+
73+// ChaoticSelectedIndex returns the current Chaotic-AUR selection index.
74+func (rm *ReposModelImpl) ChaoticSelectedIndex() int { return rm.chaoticSelected }
75+
76+// FocusSection returns the currently focused section (0=AUR, 1=Chaotic).
77+func (rm *ReposModelImpl) FocusSection() int { return rm.focusSection }
78+
79+// SelectedAURHelper returns the selected AUR helper value string.
80+func (rm *ReposModelImpl) SelectedAURHelper() string {
81+ if len(rm.aurOptions) == 0 {
82+ return "paru"
83+ }
84+ if rm.aurSelected < 0 || rm.aurSelected >= len(rm.aurOptions) {
85+ return rm.aurOptions[0].Value
86+ }
87+ return rm.aurOptions[rm.aurSelected].Value
88+}
89+
90+// SelectedChaoticEnabled returns whether Chaotic-AUR is enabled.
91+func (rm *ReposModelImpl) SelectedChaoticEnabled() bool {
92+ if len(rm.chaoticOptions) == 0 {
93+ return true
94+ }
95+ if rm.chaoticSelected < 0 || rm.chaoticSelected >= len(rm.chaoticOptions) {
96+ return rm.chaoticOptions[0].Enabled
97+ }
98+ return rm.chaoticOptions[rm.chaoticSelected].Enabled
99+}
100+
101+// MoveUp moves selection up in the current section (wraps).
102+func (rm *ReposModelImpl) MoveUp() {
103+ if rm.focusSection == 0 {
104+ if len(rm.aurOptions) == 0 {
105+ return
106+ }
107+ if rm.aurSelected == 0 {
108+ rm.aurSelected = len(rm.aurOptions) - 1
109+ } else {
110+ rm.aurSelected--
111+ }
112+ } else {
113+ if len(rm.chaoticOptions) == 0 {
114+ return
115+ }
116+ if rm.chaoticSelected == 0 {
117+ rm.chaoticSelected = len(rm.chaoticOptions) - 1
118+ } else {
119+ rm.chaoticSelected--
120+ }
121+ }
122+}
123+
124+// MoveDown moves selection down in the current section (wraps).
125+func (rm *ReposModelImpl) MoveDown() {
126+ if rm.focusSection == 0 {
127+ if len(rm.aurOptions) == 0 {
128+ return
129+ }
130+ rm.aurSelected = (rm.aurSelected + 1) % len(rm.aurOptions)
131+ } else {
132+ if len(rm.chaoticOptions) == 0 {
133+ return
134+ }
135+ rm.chaoticSelected = (rm.chaoticSelected + 1) % len(rm.chaoticOptions)
136+ }
137+}
138+
139+// NextSection moves focus to the next section.
140+func (rm *ReposModelImpl) NextSection() {
141+ rm.focusSection = (rm.focusSection + 1) % 2
142+}
143+
144+// PrevSection moves focus to the previous section.
145+func (rm *ReposModelImpl) PrevSection() {
146+ rm.focusSection = (rm.focusSection + 1) % 2
147+}
@@ -0,0 +1,147 @@
1+package models
2+
3+// AURHelperOption represents a selectable AUR helper option.
4+type AURHelperOption struct {
5+ Value string
6+ Label string
7+ Description string
8+ Recommended bool
9+}
10+
11+// ChaoticOption represents a selectable Chaotic-AUR option.
12+type ChaoticOption struct {
13+ Enabled bool
14+ Label string
15+ Description string
16+ Recommended bool
17+}
18+
19+// ReposModelImpl holds AUR helper and Chaotic-AUR selection state.
20+type ReposModelImpl struct {
21+ aurOptions []AURHelperOption
22+ aurSelected int
23+ chaoticOptions []ChaoticOption
24+ chaoticSelected int
25+ // focusSection: 0 = AUR helper, 1 = Chaotic-AUR
26+ focusSection int
27+}
28+
29+// NewReposModel creates a new repos selection model.
30+func NewReposModel() *ReposModelImpl {
31+ aurOptions := []AURHelperOption{
32+ {Value: "paru", Label: "paru", Description: "Fast AUR helper written in Rust (recommended)", Recommended: true},
33+ {Value: "yay", Label: "yay", Description: "Yet Another Yogurt - AUR helper written in Go"},
34+ }
35+ aurSelected := 0
36+ for i, o := range aurOptions {
37+ if o.Recommended {
38+ aurSelected = i
39+ break
40+ }
41+ }
42+
43+ chaoticOptions := []ChaoticOption{
44+ {Enabled: true, Label: "Enabled", Description: "Pre-built AUR packages; faster installs (recommended)", Recommended: true},
45+ {Enabled: false, Label: "Disabled", Description: "Build AUR packages from source"},
46+ }
47+ chaoticSelected := 0
48+ for i, o := range chaoticOptions {
49+ if o.Recommended {
50+ chaoticSelected = i
51+ break
52+ }
53+ }
54+
55+ return &ReposModelImpl{
56+ aurOptions: aurOptions,
57+ aurSelected: aurSelected,
58+ chaoticOptions: chaoticOptions,
59+ chaoticSelected: chaoticSelected,
60+ focusSection: 0,
61+ }
62+}
63+
64+// AUROptions returns the selectable AUR helper options.
65+func (rm *ReposModelImpl) AUROptions() []AURHelperOption { return rm.aurOptions }
66+
67+// AURSelectedIndex returns the current AUR selection index.
68+func (rm *ReposModelImpl) AURSelectedIndex() int { return rm.aurSelected }
69+
70+// ChaoticOptions returns the selectable Chaotic-AUR options.
71+func (rm *ReposModelImpl) ChaoticOptions() []ChaoticOption { return rm.chaoticOptions }
72+
73+// ChaoticSelectedIndex returns the current Chaotic-AUR selection index.
74+func (rm *ReposModelImpl) ChaoticSelectedIndex() int { return rm.chaoticSelected }
75+
76+// FocusSection returns the currently focused section (0=AUR, 1=Chaotic).
77+func (rm *ReposModelImpl) FocusSection() int { return rm.focusSection }
78+
79+// SelectedAURHelper returns the selected AUR helper value string.
80+func (rm *ReposModelImpl) SelectedAURHelper() string {
81+ if len(rm.aurOptions) == 0 {
82+ return "paru"
83+ }
84+ if rm.aurSelected < 0 || rm.aurSelected >= len(rm.aurOptions) {
85+ return rm.aurOptions[0].Value
86+ }
87+ return rm.aurOptions[rm.aurSelected].Value
88+}
89+
90+// SelectedChaoticEnabled returns whether Chaotic-AUR is enabled.
91+func (rm *ReposModelImpl) SelectedChaoticEnabled() bool {
92+ if len(rm.chaoticOptions) == 0 {
93+ return true
94+ }
95+ if rm.chaoticSelected < 0 || rm.chaoticSelected >= len(rm.chaoticOptions) {
96+ return rm.chaoticOptions[0].Enabled
97+ }
98+ return rm.chaoticOptions[rm.chaoticSelected].Enabled
99+}
100+
101+// MoveUp moves selection up in the current section (wraps).
102+func (rm *ReposModelImpl) MoveUp() {
103+ if rm.focusSection == 0 {
104+ if len(rm.aurOptions) == 0 {
105+ return
106+ }
107+ if rm.aurSelected == 0 {
108+ rm.aurSelected = len(rm.aurOptions) - 1
109+ } else {
110+ rm.aurSelected--
111+ }
112+ } else {
113+ if len(rm.chaoticOptions) == 0 {
114+ return
115+ }
116+ if rm.chaoticSelected == 0 {
117+ rm.chaoticSelected = len(rm.chaoticOptions) - 1
118+ } else {
119+ rm.chaoticSelected--
120+ }
121+ }
122+}
123+
124+// MoveDown moves selection down in the current section (wraps).
125+func (rm *ReposModelImpl) MoveDown() {
126+ if rm.focusSection == 0 {
127+ if len(rm.aurOptions) == 0 {
128+ return
129+ }
130+ rm.aurSelected = (rm.aurSelected + 1) % len(rm.aurOptions)
131+ } else {
132+ if len(rm.chaoticOptions) == 0 {
133+ return
134+ }
135+ rm.chaoticSelected = (rm.chaoticSelected + 1) % len(rm.chaoticOptions)
136+ }
137+}
138+
139+// NextSection moves focus to the next section.
140+func (rm *ReposModelImpl) NextSection() {
141+ rm.focusSection = (rm.focusSection + 1) % 2
142+}
143+
144+// PrevSection moves focus to the previous section.
145+func (rm *ReposModelImpl) PrevSection() {
146+ rm.focusSection = (rm.focusSection + 1) % 2
147+}
A internal/interfaces/tui/views/repos.go +83 -0
@@ -0,0 +1,83 @@
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+// RenderReposSelection renders the AUR helper and Chaotic-AUR selection screen.
11+func RenderReposSelection(rm *models.ReposModelImpl) 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+ section := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("14"))
18+ activeSection := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("10"))
19+ desc := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Faint(true)
20+
21+ b.WriteString("\n")
22+ b.WriteString(title.Render("Repository Configuration"))
23+ b.WriteString("\n\n")
24+
25+ // AUR Helper section
26+ aurHeaderStyle := section
27+ if rm.FocusSection() == 0 {
28+ aurHeaderStyle = activeSection
29+ }
30+ b.WriteString(aurHeaderStyle.Render("AUR Helper"))
31+ b.WriteString("\n")
32+
33+ for i, option := range rm.AUROptions() {
34+ prefix := " "
35+ style := lipgloss.NewStyle()
36+ labelStyle := lipgloss.NewStyle()
37+
38+ if rm.FocusSection() == 0 && i == rm.AURSelectedIndex() {
39+ prefix = "> "
40+ style = active
41+ labelStyle = active
42+ }
43+
44+ b.WriteString(style.Render(prefix))
45+ b.WriteString(labelStyle.Render(option.Label))
46+ b.WriteString("\n")
47+ b.WriteString(desc.Render(" " + option.Description))
48+ b.WriteString("\n")
49+ }
50+
51+ b.WriteString("\n")
52+
53+ // Chaotic-AUR section
54+ chaoticHeaderStyle := section
55+ if rm.FocusSection() == 1 {
56+ chaoticHeaderStyle = activeSection
57+ }
58+ b.WriteString(chaoticHeaderStyle.Render("Chaotic-AUR"))
59+ b.WriteString("\n")
60+
61+ for i, option := range rm.ChaoticOptions() {
62+ prefix := " "
63+ style := lipgloss.NewStyle()
64+ labelStyle := lipgloss.NewStyle()
65+
66+ if rm.FocusSection() == 1 && i == rm.ChaoticSelectedIndex() {
67+ prefix = "> "
68+ style = active
69+ labelStyle = active
70+ }
71+
72+ b.WriteString(style.Render(prefix))
73+ b.WriteString(labelStyle.Render(option.Label))
74+ b.WriteString("\n")
75+ b.WriteString(desc.Render(" " + option.Description))
76+ b.WriteString("\n")
77+ }
78+
79+ b.WriteString("\n")
80+ b.WriteString(info.Render("↑/↓ to select • tab to switch section • enter to confirm • esc to go back"))
81+
82+ return b.String()
83+}
@@ -0,0 +1,83 @@
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+// RenderReposSelection renders the AUR helper and Chaotic-AUR selection screen.
11+func RenderReposSelection(rm *models.ReposModelImpl) 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+ section := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("14"))
18+ activeSection := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("10"))
19+ desc := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Faint(true)
20+
21+ b.WriteString("\n")
22+ b.WriteString(title.Render("Repository Configuration"))
23+ b.WriteString("\n\n")
24+
25+ // AUR Helper section
26+ aurHeaderStyle := section
27+ if rm.FocusSection() == 0 {
28+ aurHeaderStyle = activeSection
29+ }
30+ b.WriteString(aurHeaderStyle.Render("AUR Helper"))
31+ b.WriteString("\n")
32+
33+ for i, option := range rm.AUROptions() {
34+ prefix := " "
35+ style := lipgloss.NewStyle()
36+ labelStyle := lipgloss.NewStyle()
37+
38+ if rm.FocusSection() == 0 && i == rm.AURSelectedIndex() {
39+ prefix = "> "
40+ style = active
41+ labelStyle = active
42+ }
43+
44+ b.WriteString(style.Render(prefix))
45+ b.WriteString(labelStyle.Render(option.Label))
46+ b.WriteString("\n")
47+ b.WriteString(desc.Render(" " + option.Description))
48+ b.WriteString("\n")
49+ }
50+
51+ b.WriteString("\n")
52+
53+ // Chaotic-AUR section
54+ chaoticHeaderStyle := section
55+ if rm.FocusSection() == 1 {
56+ chaoticHeaderStyle = activeSection
57+ }
58+ b.WriteString(chaoticHeaderStyle.Render("Chaotic-AUR"))
59+ b.WriteString("\n")
60+
61+ for i, option := range rm.ChaoticOptions() {
62+ prefix := " "
63+ style := lipgloss.NewStyle()
64+ labelStyle := lipgloss.NewStyle()
65+
66+ if rm.FocusSection() == 1 && i == rm.ChaoticSelectedIndex() {
67+ prefix = "> "
68+ style = active
69+ labelStyle = active
70+ }
71+
72+ b.WriteString(style.Render(prefix))
73+ b.WriteString(labelStyle.Render(option.Label))
74+ b.WriteString("\n")
75+ b.WriteString(desc.Render(" " + option.Description))
76+ b.WriteString("\n")
77+ }
78+
79+ b.WriteString("\n")
80+ b.WriteString(info.Render("↑/↓ to select • tab to switch section • enter to confirm • esc to go back"))
81+
82+ return b.String()
83+}

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