main 6af0a61

form.go RawHistoryFolder

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}

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