Files
2026-07-29 06:51:43 -04:00

228 lines
4.9 KiB
Go

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())
}