1package models
2
3import (
4 "strings"
5
6 "github.com/charmbracelet/bubbles/textinput"
7 tea "github.com/charmbracelet/bubbletea"
8 "github.com/charmbracelet/lipgloss"
9)
10
11// BaseModel provides common functionality for models
12type BaseModel struct {
13 width int
14 height int
15}
16
17// SetSize sets the terminal size for the model
18func (bm *BaseModel) SetSize(width, height int) {
19 bm.width = width
20 bm.height = height
21}
22
23// FormData contains the data from the form
24type FormData struct {
25 Hostname string
26 Username string
27 UserEmail string
28 UserPassword string
29 RootPassword string
30 TargetDisk string
31 EncryptionType string
32 AMDPState string
33 KernelParamsExtra string
34 GPUVendor string
35 GPUDrivers []string
36 Timezone string
37 Locale string
38 Keymap string
39 KernelVariant string
40 AURHelper string
41 Microcode bool
42 InstallDankLinux bool
43}
44
45// FormModelImpl implements FormModel interface
46type FormModelImpl struct {
47 BaseModel
48
49 // Form fields
50 hostname textinput.Model
51 username textinput.Model
52 userEmail textinput.Model
53 userPassword textinput.Model
54 targetDisk textinput.Model
55 timezone textinput.Model
56 locale textinput.Model
57 keymap textinput.Model
58
59 // UI state
60 focusIndex int
61 fields []textinput.Model
62 submitted bool
63 err error
64
65 // Form data
66 data FormData
67}
68
69// NewFormModel creates a new form model
70func NewFormModel() *FormModelImpl {
71 fm := &FormModelImpl{
72 fields: []textinput.Model{},
73 }
74
75 // Initialize text inputs with default values
76 fm.hostname = createTextInput("Hostname", "", "Enter system hostname")
77 fm.hostname.SetValue("arch")
78
79 fm.username = createTextInput("Username", "", "Enter regular user name")
80 fm.username.SetValue("")
81
82 fm.userEmail = createTextInput("Email", "", "Enter email for git config")
83
84 fm.userPassword = createTextInput("User Password", "", "Enter user password")
85 fm.userPassword.EchoMode = textinput.EchoPassword
86
87 // No root password - user will be sudoer
88 // targetDisk is selected via disk selection screen, not a text field
89 fm.timezone = createTextInput("Timezone", "", "Enter timezone")
90 fm.timezone.SetValue("UTC")
91
92 fm.locale = createTextInput("Locale", "", "Enter locale")
93 fm.locale.SetValue("en_US.UTF-8")
94
95 fm.keymap = createTextInput("Keymap", "", "Enter keymap")
96 fm.keymap.SetValue("us")
97
98 fm.fields = []textinput.Model{
99 fm.hostname, fm.username, fm.userEmail, fm.userPassword,
100 fm.timezone, fm.locale, fm.keymap,
101 }
102
103 return fm
104}
105
106// Init initializes the form model by focusing the first field
107func (fm *FormModelImpl) Init() tea.Cmd {
108 return fm.fields[0].Focus()
109}
110
111// Update handles input and updates the form
112// Deprecated: Use handlers.HandleFormUpdate instead
113func (fm *FormModelImpl) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
114 switch msg := msg.(type) {
115 case tea.KeyMsg:
116 switch msg.String() {
117 case "up", "shift+tab":
118 fm.focusPrevious()
119 case "down", "tab":
120 fm.focusNext()
121 case "enter":
122 if fm.focusIndex == len(fm.fields)-1 {
123 // Last field, submit
124 fm.submitted = true
125 fm.extractData()
126 return fm, nil
127 }
128 fm.focusNext()
129 case "ctrl+c":
130 return fm, tea.Quit
131 }
132 }
133
134 // Update focused field
135 cmd := fm.updateInput(msg)
136 return fm, cmd
137}
138
139// View renders the form
140// Deprecated: Use views.RenderForm instead
141func (fm *FormModelImpl) View() string {
142 var b strings.Builder
143
144 b.WriteString("\n")
145 b.WriteString(lipgloss.NewStyle().
146 Bold(true).
147 Foreground(lipgloss.Color("12")).
148 Render("ArchUp Installer Configuration"))
149 b.WriteString("\n\n")
150
151 labels := []string{
152 "Hostname:",
153 "Username:",
154 "Email(opt):",
155 "Password:",
156 "Timezone:",
157 "Locale:",
158 "Keymap:",
159 }
160
161 for i, field := range fm.fields {
162 if i == fm.focusIndex {
163 // Focused field gets highlighted rendering
164 label := lipgloss.NewStyle().
165 Width(15).
166 Foreground(lipgloss.Color("10")).
167 Bold(true).
168 Render(labels[i])
169 b.WriteString(label)
170 b.WriteString(" ")
171 b.WriteString(lipgloss.NewStyle().
172 Foreground(lipgloss.Color("10")).
173 Bold(true).
174 Render(field.View()))
175 } else {
176 // Unfocused field gets dimmed rendering
177 label := lipgloss.NewStyle().
178 Width(15).
179 Foreground(lipgloss.Color("8")).
180 Render(labels[i])
181 b.WriteString(label)
182 b.WriteString(" ")
183 b.WriteString(field.View())
184 }
185 b.WriteString("\n")
186 }
187
188 b.WriteString("\n")
189 b.WriteString(lipgloss.NewStyle().
190 Faint(true).
191 Render("↑↓ Navigate • Tab/Shift+Tab Switch • Enter Submit • Ctrl+C Quit"))
192
193 if fm.err != nil {
194 b.WriteString("\n")
195 b.WriteString(lipgloss.NewStyle().
196 Foreground(lipgloss.Color("1")).
197 Render("Error: " + fm.err.Error()))
198 }
199
200 return b.String()
201}
202
203// GetData returns the form data
204func (fm *FormModelImpl) GetData() FormData {
205 return fm.data
206}
207
208// SetData sets the form data
209func (fm *FormModelImpl) SetData(data FormData) {
210 fm.data = data
211 fm.hostname.SetValue(data.Hostname)
212 fm.username.SetValue(data.Username)
213 fm.userPassword.SetValue(data.UserPassword)
214 fm.targetDisk.SetValue(data.TargetDisk)
215 fm.timezone.SetValue(data.Timezone)
216 fm.locale.SetValue(data.Locale)
217 fm.keymap.SetValue(data.Keymap)
218}
219
220// GetFields returns the form fields
221func (fm *FormModelImpl) GetFields() []textinput.Model {
222 return fm.fields
223}
224
225// GetFocusIndex returns the currently focused field index
226func (fm *FormModelImpl) GetFocusIndex() int {
227 return fm.focusIndex
228}
229
230// GetError returns the form error if any
231func (fm *FormModelImpl) GetError() error {
232 return fm.err
233}
234
235// SetError sets an error on the form
236func (fm *FormModelImpl) SetError(err error) {
237 fm.err = err
238}
239
240// SetSubmitted marks the form as submitted
241func (fm *FormModelImpl) SetSubmitted(submitted bool) {
242 fm.submitted = submitted
243}
244
245// IsSubmitted returns whether the form was submitted
246func (fm *FormModelImpl) IsSubmitted() bool {
247 return fm.submitted
248}
249
250// FocusNext moves focus to the next field
251func (fm *FormModelImpl) FocusNext() {
252 fm.focusNext()
253}
254
255// FocusPrevious moves focus to the previous field
256func (fm *FormModelImpl) FocusPrevious() {
257 fm.focusPrevious()
258}
259
260// UpdateInput updates the currently focused input field
261func (fm *FormModelImpl) UpdateInput(msg tea.Msg) tea.Cmd {
262 return fm.updateInput(msg)
263}
264
265// ExtractData extracts form values into the data struct
266func (fm *FormModelImpl) ExtractData() {
267 fm.extractData()
268}
269
270// SetTimezone sets the timezone field value
271func (fm *FormModelImpl) SetTimezone(tz string) {
272 fm.timezone.SetValue(tz)
273}
274
275// Helper methods
276
277func (fm *FormModelImpl) focusNext() {
278 fm.fields[fm.focusIndex].Blur()
279 fm.focusIndex = (fm.focusIndex + 1) % len(fm.fields)
280 fm.fields[fm.focusIndex].Focus()
281}
282
283func (fm *FormModelImpl) focusPrevious() {
284 fm.fields[fm.focusIndex].Blur()
285 fm.focusIndex--
286 if fm.focusIndex < 0 {
287 fm.focusIndex = len(fm.fields) - 1
288 }
289 fm.fields[fm.focusIndex].Focus()
290}
291
292func (fm *FormModelImpl) updateInput(msg tea.Msg) tea.Cmd {
293 var cmd tea.Cmd
294 fm.fields[fm.focusIndex], cmd = fm.fields[fm.focusIndex].Update(msg)
295 return cmd
296}
297
298func (fm *FormModelImpl) extractData() {
299 // Use fm.fields slice values, not the original fields (which are copies)
300 // Field order: hostname(0), username(1), email(2), password(3), timezone(4), locale(5), keymap(6)
301 fm.data = FormData{
302 Hostname: fm.fields[0].Value(),
303 Username: fm.fields[1].Value(),
304 UserEmail: fm.fields[2].Value(), // Optional - for git config
305 UserPassword: fm.fields[3].Value(),
306 RootPassword: "", // No root password - root account locked, user is sudoer
307 TargetDisk: fm.targetDisk.Value(),
308 EncryptionType: fm.data.EncryptionType,
309 AMDPState: fm.data.AMDPState,
310 KernelParamsExtra: fm.data.KernelParamsExtra,
311 Timezone: fm.fields[4].Value(),
312 Locale: fm.fields[5].Value(),
313 Keymap: fm.fields[6].Value(),
314 InstallDankLinux: fm.data.InstallDankLinux,
315 }
316}
317
318// createTextInput creates a configured text input field
319func createTextInput(label, placeholder, help string) textinput.Model {
320 ti := textinput.New()
321 ti.Placeholder = placeholder
322 ti.CharLimit = 256
323 ti.Width = 30
324 return ti
325}