1package rules23import (4 "strings"56 "charm.land/lipgloss/v2"7 "github.com/charmbracelet/x/ansi"89 "src.rest/cat/012/internal/locale"10 "src.rest/cat/012/internal/sheet"11 "src.rest/cat/012/internal/ui/theme"12)1314// A form is a list of rows in the manner of lazygit's panels: Up and15// Down pick a row, Left and Right change a choice, Space flips a check,16// and a text row is typed into (in the host's edit line) while it's17// picked.1819type rowKind uint82021const (22 rowText rowKind = iota23 rowChoice24 rowCheck25 rowSep26 rowPreview27)2829// row is one line of a form, pointing into the form's state.30type row struct {31 kind rowKind32 label string33 hint string // what the row sets, for the status line34 text *string35 placeholder string36 choices []string37 at *int38 // swatch, when set, draws a sample before each choice's name.39 swatch func(i int) string40 on *bool41 preview func(w int) string42}4344// picks reports whether the row can be picked.45func (r row) picks() bool { return r.kind != rowSep && r.kind != rowPreview }4647// labelW is the width of the label column.48const labelW = 124950// draw lays out the row in w columns; sel marks it as picked, and51// editing is the text being typed into it.52func (r row) draw(th *theme.Theme, w int, sel bool, editing string) string {53 base, dim := th.MenuBar, th.Muted54 if sel {55 base, dim = th.MenuSelected, th.MenuSelected56 }57 lead := base.Render(" " + theme.PadRight(r.label, labelW-1))58 var val string59 switch r.kind {60 case rowSep:61 return theme.SepRow62 case rowCheck:63 box := "[ ] "64 if *r.on {65 box = "[x] "66 }67 return theme.Cells(base, " "+box+r.label, w)68 case rowPreview:69 return spread(base, lead+r.preview(w-labelW-1), "", w)70 case rowChoice:71 chip := th.KeyChip72 if sel {73 chip = th.Indicator74 }75 i := min(max(*r.at, 0), len(r.choices)-1)76 val = chip.Render("‹ " + r.choices[i] + " ›")77 if r.swatch != nil {78 val = r.swatch(i) + base.Render(" ") + val79 }80 case rowText:81 text := *r.text82 if sel {83 text = editing84 }85 switch {86 case text == "" && !sel:87 val = dim.Render(r.placeholder)88 case sel:89 val = base.Render(text)90 default:91 val = th.Key.Render(text)92 }93 }94 return spread(base, lead+base.Render(" ")+val, "", w)95}9697// spread lays left and right out in w columns on base, cutting left to98// make room for right.99func spread(base lipgloss.Style, left, right string, w int) string {100 rw := ansi.StringWidth(right)101 left = ansi.Truncate(left, max(w-rw-1, 1), "…")102 gap := max(w-ansi.StringWidth(left)-rw, 0)103 return ansi.Truncate(left+base.Render(strings.Repeat(" ", gap))+right, w, "")104}105106// cycle moves a choice row d steps, wrapping around.107func (r row) cycle(d int) {108 n := len(r.choices)109 *r.at = ((*r.at+d)%n + n) % n110}111112// textX is where a text row's value starts, from the box's inner left.113func textX() int { return labelW + 1 }114115// canonicalArgs are a rule's two arguments typed in loc, trimmed, as the116// rule stores them (see sheet.CanonicalArg).117func canonicalArgs(args [2]string, loc *locale.Locale) [2]string {118 return [2]string{sheet.CanonicalArg(strings.TrimSpace(args[0]), loc), sheet.CanonicalArg(strings.TrimSpace(args[1]), loc)}119}120121// localArgs are a rule's two arguments as typed in loc.122func localArgs(args [2]string, loc *locale.Locale) [2]string {123 return [2]string{sheet.LocalArg(args[0], loc), sheet.LocalArg(args[1], loc)}124}