Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199package tui
import ( "slices" "strings"
"charm.land/bubbles/v2/textinput" tea "charm.land/bubbletea/v2" "github.com/charmbracelet/x/ansi")
type PickerItem struct { ID string Label string Detail string // Note is what a detail pane says about the item, where a list has one. Note string}
type PickerSelectMsg struct { ID string}
type PickerCancelMsg struct{}
type PickerModel struct { Title string Items []PickerItem Filtered []PickerItem SearchInput textinput.Model Cursor int Width int Height int WithSearch bool}
func NewPickerModel(title string, items []PickerItem, withSearch bool, initialSelection string) PickerModel { ti := newTextInput() ti.Placeholder = "" ti.Focus() ti.Prompt = ""
m := PickerModel{ Title: title, Items: items, WithSearch: withSearch, SearchInput: ti, } m.applyFilter()
if initialSelection != "" { if i := slices.IndexFunc(m.Filtered, func(it PickerItem) bool { return it.ID == initialSelection }); i >= 0 { m.Cursor = i } }
return m}
// Highlighted returns the item under the cursor.func (m PickerModel) Highlighted() (PickerItem, bool) { if m.Cursor >= 0 && m.Cursor < len(m.Filtered) { return m.Filtered[m.Cursor], true } return PickerItem{}, false}
func (m *PickerModel) SetSize(width, height int) { m.Width = width m.Height = height m.SearchInput.SetWidth(max(1, width-8))}
// applyFilter keeps the items whose label, detail and id match every word// of the search under matchFields, in the order they were listed.func (m *PickerModel) applyFilter() { item, ok := m.Highlighted() old := "" if ok { old = item.ID } words := searchWords(m.SearchInput.Value()) m.Filtered = nil for _, item := range m.Items { if _, _, matched := matchFields(words, item.Label, item.Detail, item.ID); matched { m.Filtered = append(m.Filtered, item) } } m.Cursor = max(0, slices.IndexFunc(m.Filtered, func(item PickerItem) bool { return item.ID == old }))}
// selectableRows is a bottom-anchored list window with the selected row on// the selection surface.func selectableRows(lines []string, selected, height, limit, width int) string { if height <= 0 { height = 24 } if limit <= 0 { limit = height } available := max(1, min(limit, height-8)) first := min(max(0, selected-available+1), max(0, len(lines)-available)) if len(lines) == 0 { return DefaultStyles.Faint.Render("no matches") } var b strings.Builder bar := selectBar() + " " for i := first; i < min(len(lines), first+available); i++ { lead := " " if i == selected { lead = bar } line := lead + lines[i] if width > 0 { line = ansi.Truncate(line, width, "…") } if i == selected { line = selectedLine(line, width) } if i > first { b.WriteByte('\n') } b.WriteString(line) } return b.String()}
func pickerRow(item PickerItem) string { line := item.Label if item.Detail != "" { line += DefaultStyles.Faint.Render(" " + item.Detail) } return line}
func (m PickerModel) Init() tea.Cmd { if m.WithSearch { return textinput.Blink } return nil}
func (m PickerModel) Update(msg tea.Msg) (PickerModel, tea.Cmd) { var cmd tea.Cmd
switch msg := msg.(type) { case tea.KeyPressMsg: switch msg.String() { case "esc", "ctrl+c", "ctrl+d": return m, func() tea.Msg { return PickerCancelMsg{} } case "enter": if item, ok := m.Highlighted(); ok { return m, func() tea.Msg { return PickerSelectMsg{ID: item.ID} } } return m, nil case "up", "ctrl+p": if m.Cursor > 0 { m.Cursor-- } return m, nil case "down", "ctrl+n": if m.Cursor < len(m.Filtered)-1 { m.Cursor++ } return m, nil } }
if m.WithSearch { var tiCmd tea.Cmd m.SearchInput, tiCmd = m.SearchInput.Update(msg) m.applyFilter() cmd = tea.Batch(cmd, tiCmd) }
return m, cmd}
func (m PickerModel) View() string { var b strings.Builder if m.Title != "" { b.WriteString(m.Title) b.WriteByte('\n') } lines := make([]string, len(m.Filtered)) for i, item := range m.Filtered { lines[i] = pickerRow(item) } b.WriteString(selectableRows(lines, m.Cursor, m.Height, m.Height, m.Width)) b.WriteByte('\n') if m.WithSearch { b.WriteString(promptLead()) b.WriteString(m.SearchInput.View()) } b.WriteByte('\n') b.WriteString(keyHints(hint{"↑↓", "select"}, hint{"enter", "choose"}, hint{"esc", "cancel"})) return b.String()}