package picker import ( "tasksquire/common" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) type Item struct { text string } func NewItem(text string) Item { return Item{text: text} } func (i Item) Title() string { return i.text } func (i Item) Description() string { return "" } func (i Item) FilterValue() string { return i.text } type Picker struct { common *common.Common list list.Model onSelect func(list.Item) tea.Cmd title string filterByDefault bool } type PickerOption func(*Picker) func WithFilterByDefault(enabled bool) PickerOption { return func(p *Picker) { p.filterByDefault = enabled } } func New( c *common.Common, title string, items []list.Item, onSelect func(list.Item) tea.Cmd, opts ...PickerOption, ) *Picker { delegate := list.NewDefaultDelegate() delegate.ShowDescription = false delegate.SetSpacing(0) l := list.New(items, delegate, 0, 0) l.SetShowTitle(false) l.SetShowHelp(false) l.SetShowStatusBar(false) l.SetFilteringEnabled(true) // Custom key for filtering (insert mode) l.KeyMap.Filter = key.NewBinding( key.WithKeys("i"), key.WithHelp("i", "filter"), ) p := &Picker{ common: c, list: l, onSelect: onSelect, title: title, } if c.TW.GetConfig().Get("uda.tasksquire.picker.filter_by_default") == "yes" { p.filterByDefault = true } for _, opt := range opts { opt(p) } return p } func (p *Picker) SetSize(width, height int) { // We do NOT set common.SetSize here, as we are a sub-component. // Set list size. The parent is responsible for providing a reasonable size. // If this component is intended to fill a page, width/height will be large. // If it's a small embedded box, they will be small. // We apply a small margin for the title if needed, but for now we just pass through // minus a header gap if we render a title. headerHeight := 2 // Title + gap p.list.SetSize(width, height-headerHeight) } func (p *Picker) Init() tea.Cmd { if p.filterByDefault { return func() tea.Msg { return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'i'}} } } return nil } func (p *Picker) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: // If filtering, let the list handle keys (including Enter to stop filtering) if p.list.FilterState() == list.Filtering { if key.Matches(msg, p.common.Keymap.Ok) { items := p.list.VisibleItems() if len(items) == 1 { return p, p.onSelect(items[0]) } } break // Pass to list.Update } switch { case key.Matches(msg, p.common.Keymap.Ok): selectedItem := p.list.SelectedItem() if selectedItem == nil { return p, nil } return p, p.onSelect(selectedItem) } } p.list, cmd = p.list.Update(msg) return p, cmd } func (p *Picker) View() string { title := p.common.Styles.Form.Focused.Title.Render(p.title) return lipgloss.JoinVertical(lipgloss.Left, title, p.list.View()) } // SelectItemByFilterValue selects the item with the given filter value func (p *Picker) SelectItemByFilterValue(filterValue string) { items := p.list.Items() for i, item := range items { if item.FilterValue() == filterValue { p.list.Select(i) break } } }