package cli import ( "strings" "time" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // Two small widgets, written here rather than taken from bubbles: its // packages ask the terminal for its colours as they load, so every llmgw // command — llmgw --version included — sent a query and, in a terminal that // never answers one, waited five seconds for it. Only the screens need to // know the colours, and lipgloss asks when they first draw. // spinTickMsg advances a spinner. type spinTickMsg struct{} var spinFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} type spinnerModel struct { frame int Style lipgloss.Style } func newSpinner() spinnerModel { return spinnerModel{Style: lipgloss.NewStyle().Foreground(tuiAccent)} } // Tick starts the spinner turning; it is a tea.Cmd. func (s spinnerModel) Tick() tea.Msg { return spinTickMsg{} } func (s spinnerModel) Update(msg tea.Msg) (spinnerModel, tea.Cmd) { if _, ok := msg.(spinTickMsg); !ok { return s, nil } s.frame = (s.frame + 1) % len(spinFrames) return s, tea.Tick(100*time.Millisecond, func(time.Time) tea.Msg { return spinTickMsg{} }) } func (s spinnerModel) View() string { return s.Style.Render(spinFrames[s.frame]) } // textField is one line of text typed at its end: all a form here needs. type textField struct { value []rune focused bool Prompt string Placeholder string CharLimit int Width int // Mask shows a dot for every character: a password. Mask bool } func newTextField() textField { return textField{Prompt: "> "} } func (f *textField) SetValue(s string) { f.value = []rune(s) } func (f textField) Value() string { return string(f.value) } func (f *textField) Focus() { f.focused = true } func (f *textField) Blur() { f.focused = false } func (f textField) Update(msg tea.Msg) (textField, tea.Cmd) { k, ok := msg.(tea.KeyMsg) if !ok || !f.focused { return f, nil } switch k.Type { case tea.KeyRunes, tea.KeySpace: r := k.Runes if k.Type == tea.KeySpace { r = []rune{' '} } for _, c := range r { if f.CharLimit == 0 || len(f.value) < f.CharLimit { f.value = append(f.value, c) } } case tea.KeyBackspace: if len(f.value) > 0 { f.value = f.value[:len(f.value)-1] } case tea.KeyCtrlU: f.value = nil } return f, nil } func (f textField) View() string { text := string(f.value) if f.Mask { text = strings.Repeat("•", len(f.value)) } shown := text if text == "" && f.Placeholder != "" { shown = sDim.Render(f.Placeholder) } if f.focused { shown += "█" } if pad := f.Width - len([]rune(text)); pad > 0 && text != "" { shown += strings.Repeat(" ", pad) } return f.Prompt + shown }