package gitctl import ( "strconv" "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" ) type submitMsg struct { action action values map[string]string perm string } type backMsg struct{} type formModel struct { action action inputs []textinput.Model focus int perm string permIdx int width int err string } func newForm(a action, width int, prefill map[string]string, focus int) (formModel, tea.Cmd) { f := formModel{action: a, perm: perms[0], permIdx: 0, width: width} f.inputs = make([]textinput.Model, len(a.fields)) for i, fld := range a.fields { ti := textinput.New() ti.Prompt = "" ti.Placeholder = fld.hint if fld.secret { ti.EchoMode = textinput.EchoPassword } ti.Width = formInputWidth(width) if v := prefill[fld.name]; v != "" { ti.SetValue(v) } f.inputs[i] = ti } var cmd tea.Cmd if n := f.nFocusable(); n > 0 { if focus < 0 || focus >= n { focus = 0 } f.focus = focus if f.focus < len(f.inputs) { cmd = f.inputs[f.focus].Focus() } } return f, cmd } func formInputWidth(termWidth int) int { w := max(termWidth-32, 24) return w } func (f formModel) nFocusable() int { n := len(f.inputs) if f.action.hasPerm { n++ } return n } func (f formModel) permIndex() int { return len(f.inputs) } func (f formModel) Update(msg tea.Msg) (formModel, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: f.width = msg.Width for i := range f.inputs { f.inputs[i].Width = formInputWidth(msg.Width) } return f, nil case tea.KeyMsg: f.err = "" switch msg.String() { case "esc": return f, func() tea.Msg { return backMsg{} } case "enter": if f.focus == f.nFocusable()-1 { if e := f.validate(); e != "" { f.err = e return f, nil } action := f.action values := f.values() perm := f.perm return f, func() tea.Msg { return submitMsg{action: action, values: values, perm: perm} } } return f.advance() case "tab", "down": return f.advance() case "shift+tab", "up": return f.retreat() case "left": if f.focus == f.permIndex() && f.action.hasPerm { f.permIdx = (f.permIdx + len(perms) - 1) % len(perms) f.perm = perms[f.permIdx] } return f, nil case "right": if f.focus == f.permIndex() && f.action.hasPerm { f.permIdx = (f.permIdx + 1) % len(perms) f.perm = perms[f.permIdx] } return f, nil } if f.focus < len(f.inputs) { var cmd tea.Cmd f.inputs[f.focus], cmd = f.inputs[f.focus].Update(msg) return f, cmd } } return f, nil } func (f formModel) advance() (formModel, tea.Cmd) { n := f.nFocusable() if n == 0 { return f, nil } old := f.focus f.focus = (old + 1) % n return f.refocus(old) } func (f formModel) retreat() (formModel, tea.Cmd) { n := f.nFocusable() if n == 0 { return f, nil } old := f.focus f.focus = (old - 1 + n) % n return f.refocus(old) } func (f formModel) refocus(old int) (formModel, tea.Cmd) { if old < len(f.inputs) { f.inputs[old].Blur() } var cmd tea.Cmd if f.focus < len(f.inputs) { cmd = f.inputs[f.focus].Focus() } return f, cmd } func (f formModel) values() map[string]string { vals := make(map[string]string, len(f.inputs)) for i, fld := range f.action.fields { vals[fld.name] = strings.TrimSpace(f.inputs[i].Value()) } return vals } func (f formModel) validate() string { for i, fld := range f.action.fields { v := strings.TrimSpace(f.inputs[i].Value()) if v == "" && !fld.optional { return fld.name + " is required" } if fld.numeric { if _, err := strconv.ParseInt(v, 10, 64); err != nil { return fld.name + " must be an integer" } } } return "" } func (f formModel) View() string { var b strings.Builder b.WriteString(titleStyle.Render("gitctl · " + f.action.label)) b.WriteString("\n\n") for i, fld := range f.action.fields { focused := i == f.focus lbl := labelStyle.Render(fld.name + ":") if focused { lbl = focusedLabelStyle.Render(fld.name + ":") } val := f.inputs[i].View() if focused { val = focusedInputStyle.Render(val) } else { val = inputStyle.Render(val) } b.WriteString(" ") b.WriteString(lbl) b.WriteString(" ") b.WriteString(val) b.WriteString("\n") } if f.action.hasPerm { focused := f.focus == f.permIndex() b.WriteString(" ") if focused { b.WriteString(focusedLabelStyle.Render("perm:")) } else { b.WriteString(labelStyle.Render("perm:")) } b.WriteString(" ") for i, p := range perms { if i == f.permIdx { b.WriteString(permActiveStyle.Render("["+p+"]") + " ") } else { b.WriteString(permStyle.Render(p) + " ") } } b.WriteString(hintStyle.Render("(←/→)")) b.WriteString("\n") } if f.err != "" { b.WriteString("\n " + errStyle.Render("✗ "+f.err) + "\n") } b.WriteString("\n ") b.WriteString(hintStyle.Render("tab/↑↓ move · ←/→ perm · enter submit · esc cancel")) return frame.Render(b.String()) }