okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
3922552
main
1package tui 2 3import ( 4"slices" 5"strings" 6 7"charm.land/bubbles/v2/textinput" 8 tea"charm.land/bubbletea/v2" 9"github.com/charmbracelet/x/ansi" 10) 11 12// form is a column of text fields with one focused; the MCP and webhook 13// forms both build on it. 14type form struct { 15Inputs map [string ]* textinput.Model 16Focus int 17} 18 19// newField is a text input that follows a label instead of a prompt. 20func newField () textinput.Model { 21input := newTextInput () 22input .Prompt = "" 23return input 24} 25 26// ask re-arms input for the next question, masking a secret, and reports 27// the blink command that shows its cursor. 28func ask (input * textinput.Model ,value string ,secret bool ) tea.Cmd { 29input .Reset () 30input .EchoMode = echo (secret ) 31input .SetValue (value ) 32input .Focus () 33return textinput .Blink 34} 35 36func echo (secret bool ) textinput.EchoMode { 37if secret { 38return textinput .EchoPassword 39 } 40return textinput .EchoNormal 41} 42 43// newForm makes a field for each key, the masked ones echoing dots, with a 44// length limit no pasted secret reaches. 45func newForm (keys []string ,masked ... string )form { 46f := form {Inputs :map [string ]* textinput.Model {}} 47for _ ,key := range keys { 48input := newField () 49input .CharLimit ,input .EchoMode = 4096 ,echo (slices .Contains (masked ,key )) 50f .Inputs [key ]= & input 51 } 52return f 53} 54 55// focus focuses the field named current and blurs the rest. 56func (f * form )focus (current string ) { 57for key ,input := range f .Inputs { 58if key == current { 59input .Focus () 60 }else { 61input .Blur () 62 } 63 } 64} 65 66// key applies the movement keys every form shares, cycling through fields 67// from first on; submit reports that the form should be saved. 68func (f * form )key (msg tea.Msg ,fields []string ,first int ) (submit ,handled bool ) { 69key ,ok := msg .(tea.KeyPressMsg ) 70if ! ok { 71return false ,false 72 } 73delta := 0 74switch key .String () { 75case "tab" ,"down" : 76delta = 1 77case "shift+tab" ,"up" : 78delta = - 1 79case "ctrl+s" : 80return true ,true 81case "enter" : 82if f .Focus == len (fields )- 1 { 83return true ,true 84 } 85delta = 1 86default : 87return false ,false 88 } 89n := len (fields )- first 90f .Focus = first + (f .Focus - first + delta + n )% n 91f .focus (fields [f .Focus ]) 92return false ,true 93} 94 95// value is a field's text without surrounding space. 96func (f form )value (key string )string {return strings .TrimSpace (f .Inputs [key ].Value ()) } 97 98// fit sizes the fields to the columns their rows leave for a value; a 99// textinput without a width shows only the first rune of its placeholder. 100func (f form )fit (width int ) { 101for _ ,input := range f .Inputs { 102input .SetWidth (max (1 ,width )) 103 } 104} 105 106// formRow is one labelled field, marked when focused. 107func formRow (focused bool ,label string ,pad int ,value string ,width int )string { 108lead := " " 109if focused { 110lead = promptLead () 111 } 112return ansi .Truncate (lead + padRight (label ,pad )+ value ,width ,"…" ) 113} 114 115// formFooter explains the focused field, then offers the keys every form 116// shares after any of its own. 117func formFooter (note string ,width int ,own ... hint )string { 118line := keyHints (append (own ,hint {"tab/↑↓" ,"move" },hint {"enter" ,"next" },hint {"ctrl+s" ,"save" },hint {"esc" ,"cancel" })... ) 119if note != "" { 120line = DefaultStyles .Faint .Render (note )+ DefaultStyles .Decor .Render (" · " )+ line 121 } 122return ansi .Truncate (line ,width ,"…" ) 123} 124 125func padRight (s string ,n int )string { 126if len (s )>= n { 127return s + " " 128 } 129return s + strings .Repeat (" " ,n - len (s )) 130}