diff --git a/internal/ui/chat/attachments_picker.go b/internal/ui/chat/attachments_picker.go index 8a18981..ff15853 100644 --- a/internal/ui/chat/attachments_picker.go +++ b/internal/ui/chat/attachments_picker.go @@ -16,7 +16,7 @@ type attachmentItem struct { } type attachmentsPicker struct { - *picker.Picker + *picker.Model cfg *config.Config chatView *Model items []attachmentItem @@ -26,7 +26,7 @@ var _ help.KeyMap = (*attachmentsPicker)(nil) func newAttachmentsPicker(cfg *config.Config, chatView *Model) *attachmentsPicker { ap := &attachmentsPicker{ - Picker: picker.New(), + Model: picker.NewModel(), cfg: cfg, chatView: chatView, } @@ -92,7 +92,7 @@ func (ap *attachmentsPicker) HandleEvent(event tcell.Event) tview.Command { return nil } - return ap.Picker.HandleEvent(event) + return ap.Model.HandleEvent(event) } func (ap *attachmentsPicker) ShortHelp() []keybind.Keybind { diff --git a/internal/ui/chat/channels_picker.go b/internal/ui/chat/channels_picker.go index dda699c..e1446df 100644 --- a/internal/ui/chat/channels_picker.go +++ b/internal/ui/chat/channels_picker.go @@ -15,14 +15,14 @@ import ( ) type channelsPicker struct { - *picker.Picker + *picker.Model chatView *Model } var _ help.KeyMap = (*channelsPicker)(nil) func newChannelsPicker(cfg *config.Config, chatView *Model) *channelsPicker { - cp := &channelsPicker{picker.New(), chatView} + cp := &channelsPicker{picker.NewModel(), chatView} cp.Box = ui.ConfigureBox(tview.NewBox(), &cfg.Theme) // When a child of the parent flex is focused, the parent layout itself is not reported as focused. // Instead, the focused child (picker) is considered focused. @@ -84,7 +84,7 @@ func (cp *channelsPicker) HandleEvent(event tcell.Event) tview.Command { cp.chatView.closePicker() return nil } - return cp.Picker.HandleEvent(event) + return cp.Model.HandleEvent(event) } func (cp *channelsPicker) update() { diff --git a/pkg/picker/events.go b/pkg/picker/events.go index 24a2a93..2622b18 100644 --- a/pkg/picker/events.go +++ b/pkg/picker/events.go @@ -14,10 +14,10 @@ func newSelectedEvent(item Item) *SelectedEvent { return &SelectedEvent{Item: item} } -func (p *Picker) _select() tview.Command { - index := p.list.Cursor() - if index >= 0 && index < len(p.filtered) { - item := p.filtered[index] +func (m *Model) _select() tview.Command { + index := m.list.Cursor() + if index >= 0 && index < len(m.filtered) { + item := m.filtered[index] return func() tcell.Event { return newSelectedEvent(item) } diff --git a/pkg/picker/model.go b/pkg/picker/model.go new file mode 100644 index 0000000..bfeeefb --- /dev/null +++ b/pkg/picker/model.go @@ -0,0 +1,159 @@ +package picker + +import ( + "github.com/ayn2op/tview" + "github.com/ayn2op/tview/flex" + "github.com/ayn2op/tview/keybind" + "github.com/ayn2op/tview/list" + "github.com/gdamore/tcell/v3" + "github.com/sahilm/fuzzy" +) + +// bottom border + value +const inputHeight = 2 + +type Model struct { + *flex.Model + input *tview.InputField + list *list.Model + + keyMap *KeyMap + + items Items + filtered Items +} + +func NewModel() *Model { + m := &Model{ + Model: flex.NewModel(), + input: tview.NewInputField(), + list: list.NewModel(), + } + + // Show a horizontal bottom border to visually separate input from list. + var borderSet tview.BorderSet + borderSet.Bottom = tview.BoxDrawingsLightHorizontal + borderSet.BottomLeft = borderSet.Bottom + borderSet.BottomRight = borderSet.Bottom + + m.input. + SetChangedFunc(m.onInputChanged). + SetLabel("> "). + SetBorders(tview.BordersBottom). + SetBorderSet(borderSet). + SetBorderStyle(tcell.StyleDefault.Dim(true)) + + m. + SetDirection(flex.DirectionRow). + AddItem(m.input, inputHeight, 0, true). + AddItem(m.list, 0, 1, false) + + m.Update() + return m +} + +func (m *Model) setFilteredItems(filtered Items) { + m.filtered = filtered + + m.list.SetBuilder(func(index int, cursor int) list.Item { + if index < 0 || index >= len(m.filtered) { + return nil + } + style := tcell.StyleDefault + if index == cursor { + style = style.Reverse(true) + } + return tview.NewTextView(). + SetScrollable(false). + SetWrap(false). + SetWordWrap(false). + SetTextStyle(style). + SetLines([]tview.Line{{{Text: m.filtered[index].Text, Style: style}}}) + }) + + if len(filtered) == 0 { + m.list.SetCursor(-1) + } else { + m.list.SetCursor(0) + } +} + +func (m *Model) SetKeyMap(keyMap *KeyMap) { + m.keyMap = keyMap +} + +// SetScrollBarVisibility sets when the picker's list scrollBar is rendered. +func (m *Model) SetScrollBarVisibility(visibility list.ScrollBarVisibility) { + m.list.SetScrollBarVisibility(visibility) +} + +// SetScrollBar sets the scrollBar primitive used by the picker's list. +func (m *Model) SetScrollBar(scrollBar *tview.ScrollBar) { + m.list.SetScrollBar(scrollBar) +} + +func (m *Model) ClearInput() { + m.input.SetText("") +} + +func (m *Model) ClearList() { + m.filtered = nil + m.list.Clear() +} + +func (m *Model) ClearItems() { + m.items = nil + m.filtered = nil +} + +func (m *Model) AddItem(item Item) { + m.items = append(m.items, item) +} + +func (m *Model) Update() { + m.ClearInput() + m.onInputChanged("") +} + +func (m *Model) onInputChanged(text string) { + var fuzzied Items + if text == "" { + fuzzied = append(fuzzied, m.items...) + } else { + matches := fuzzy.FindFrom(text, m.items) + for _, match := range matches { + fuzzied = append(fuzzied, m.items[match.Index]) + } + } + m.setFilteredItems(fuzzied) +} + +func (m *Model) HandleEvent(event tcell.Event) tview.Command { + switch event := event.(type) { + case *tview.KeyEvent: + if m.keyMap != nil { + switch { + case keybind.Matches(event, m.keyMap.Up): + m.list.HandleEvent(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone)) + return nil + case keybind.Matches(event, m.keyMap.Down): + m.list.HandleEvent(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone)) + return nil + case keybind.Matches(event, m.keyMap.Top): + m.list.HandleEvent(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone)) + return nil + case keybind.Matches(event, m.keyMap.Bottom): + m.list.HandleEvent(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone)) + return nil + + case keybind.Matches(event, m.keyMap.Select): + return m._select() + case keybind.Matches(event, m.keyMap.Cancel): + return cancel() + } + } + + return m.Model.HandleEvent(event) + } + return m.Model.HandleEvent(event) +} diff --git a/pkg/picker/picker.go b/pkg/picker/picker.go deleted file mode 100644 index 88bcb3d..0000000 --- a/pkg/picker/picker.go +++ /dev/null @@ -1,159 +0,0 @@ -package picker - -import ( - "github.com/ayn2op/tview" - "github.com/ayn2op/tview/flex" - "github.com/ayn2op/tview/keybind" - "github.com/ayn2op/tview/list" - "github.com/gdamore/tcell/v3" - "github.com/sahilm/fuzzy" -) - -// bottom border + value -const inputHeight = 2 - -type Picker struct { - *flex.Model - input *tview.InputField - list *list.Model - - keyMap *KeyMap - - items Items - filtered Items -} - -func New() *Picker { - p := &Picker{ - Model: flex.NewModel(), - input: tview.NewInputField(), - list: list.NewModel(), - } - - // Show a horizontal bottom border to visually separate input from list. - var borderSet tview.BorderSet - borderSet.Bottom = tview.BoxDrawingsLightHorizontal - borderSet.BottomLeft = borderSet.Bottom - borderSet.BottomRight = borderSet.Bottom - - p.input. - SetChangedFunc(p.onInputChanged). - SetLabel("> "). - SetBorders(tview.BordersBottom). - SetBorderSet(borderSet). - SetBorderStyle(tcell.StyleDefault.Dim(true)) - - p. - SetDirection(flex.DirectionRow). - AddItem(p.input, inputHeight, 0, true). - AddItem(p.list, 0, 1, false) - - p.Update() - return p -} - -func (p *Picker) setFilteredItems(filtered Items) { - p.filtered = filtered - - p.list.SetBuilder(func(index int, cursor int) list.Item { - if index < 0 || index >= len(p.filtered) { - return nil - } - style := tcell.StyleDefault - if index == cursor { - style = style.Reverse(true) - } - return tview.NewTextView(). - SetScrollable(false). - SetWrap(false). - SetWordWrap(false). - SetTextStyle(style). - SetLines([]tview.Line{{{Text: p.filtered[index].Text, Style: style}}}) - }) - - if len(filtered) == 0 { - p.list.SetCursor(-1) - } else { - p.list.SetCursor(0) - } -} - -func (p *Picker) SetKeyMap(keyMap *KeyMap) { - p.keyMap = keyMap -} - -// SetScrollBarVisibility sets when the picker's list scrollBar is rendered. -func (p *Picker) SetScrollBarVisibility(visibility list.ScrollBarVisibility) { - p.list.SetScrollBarVisibility(visibility) -} - -// SetScrollBar sets the scrollBar primitive used by the picker's list. -func (p *Picker) SetScrollBar(scrollBar *tview.ScrollBar) { - p.list.SetScrollBar(scrollBar) -} - -func (p *Picker) ClearInput() { - p.input.SetText("") -} - -func (p *Picker) ClearList() { - p.filtered = nil - p.list.Clear() -} - -func (p *Picker) ClearItems() { - p.items = nil - p.filtered = nil -} - -func (p *Picker) AddItem(item Item) { - p.items = append(p.items, item) -} - -func (p *Picker) Update() { - p.ClearInput() - p.onInputChanged("") -} - -func (p *Picker) onInputChanged(text string) { - var fuzzied Items - if text == "" { - fuzzied = append(fuzzied, p.items...) - } else { - matches := fuzzy.FindFrom(text, p.items) - for _, match := range matches { - fuzzied = append(fuzzied, p.items[match.Index]) - } - } - p.setFilteredItems(fuzzied) -} - -func (p *Picker) HandleEvent(event tcell.Event) tview.Command { - switch event := event.(type) { - case *tview.KeyEvent: - if p.keyMap != nil { - switch { - case keybind.Matches(event, p.keyMap.Up): - p.list.HandleEvent(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone)) - return nil - case keybind.Matches(event, p.keyMap.Down): - p.list.HandleEvent(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone)) - return nil - case keybind.Matches(event, p.keyMap.Top): - p.list.HandleEvent(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone)) - return nil - case keybind.Matches(event, p.keyMap.Bottom): - p.list.HandleEvent(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone)) - return nil - - case keybind.Matches(event, p.keyMap.Select): - return p._select() - case keybind.Matches(event, p.keyMap.Cancel): - return cancel() - } - } - - return p.Model.HandleEvent(event) - } - return p.Model.HandleEvent(event) -}