228 lines
4.9 KiB
Go
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())
|
|
}
|