refactor(ui/chat): align discordo with latest tview list/picker API (#772)

This commit is contained in:
ayn2op 2026-03-18 04:52:11 -04:00 • committed by GitHub
parent 6167d7d3d8
commit 993cc53609
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 35 additions and 264 deletions

2
go.mod
View file

@ -11,7 +11,7 @@ require (
github.com/alecthomas/chroma/v2 v2.23.1
github.com/andybalholm/brotli v1.2.0
github.com/ayn2op/clipboard v0.0.0-20260308203959-c5ad7df3fc97
github.com/ayn2op/tview v0.0.0-20260318010246-0993cedfe0c2
github.com/ayn2op/tview v0.0.0-20260318080250-5bddfb2ac264
github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb
github.com/diamondburned/arikawa/v3 v3.6.1-0.20260311205148-176ad9b9440f
github.com/diamondburned/ningen/v3 v3.0.1-0.20260306213430-5a08d3a709b4

4
go.sum
View file

@ -16,8 +16,8 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/ayn2op/clipboard v0.0.0-20260308203959-c5ad7df3fc97 h1:WujETUV+v0DEJyZgjeLzQvihWyL80c0Tg4qf0dDo+Io=
github.com/ayn2op/clipboard v0.0.0-20260308203959-c5ad7df3fc97/go.mod h1:3kFnpNCa3dF6WryzOMCDao7PfZ7DTCh+pievlfuwV80=
github.com/ayn2op/tview v0.0.0-20260318010246-0993cedfe0c2 h1:AMOGrvQ1+TnDRuKC2Jnr+H1WL4bq8GSNd2GDxvy3dig=
github.com/ayn2op/tview v0.0.0-20260318010246-0993cedfe0c2/go.mod h1:IXZrCv1VAZyWPdKDUJYs9ZYFiJ1GmCQUo9tLjQWoSnU=
github.com/ayn2op/tview v0.0.0-20260318080250-5bddfb2ac264 h1:UolGESVzxrK9JyGkpoc1sLZeIWDiMtCwRA8sRckqmrA=
github.com/ayn2op/tview v0.0.0-20260318080250-5bddfb2ac264/go.mod h1:xgkca32kuEswK7Kzkfk3Pk24WHcKPjqK4xGmDi9QqOY=
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View file

@ -3,10 +3,11 @@ package chat
import (
"github.com/ayn2op/discordo/internal/config"
"github.com/ayn2op/discordo/internal/ui"
"github.com/ayn2op/discordo/pkg/picker"
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/help"
"github.com/ayn2op/tview/keybind"
"github.com/ayn2op/tview/list"
"github.com/ayn2op/tview/picker"
)
type attachmentItem struct {
@ -39,12 +40,14 @@ func newAttachmentsPicker(cfg *config.Config, chatView *Model) *attachmentsPicke
SetFooterStyle(cfg.Theme.Footer.ActiveStyle.Style)
ap.SetTitle("Attachments")
ap.SetKeyMap(&picker.KeyMap{
ap.SetKeybinds(picker.Keybinds{
Cancel: cfg.Keybinds.Picker.Cancel.Keybind,
Up: cfg.Keybinds.Picker.Up.Keybind,
Down: cfg.Keybinds.Picker.Down.Keybind,
Top: cfg.Keybinds.Picker.Top.Keybind,
Bottom: cfg.Keybinds.Picker.Bottom.Keybind,
Keybinds: list.Keybinds{
SelectUp: cfg.Keybinds.Picker.Up.Keybind,
SelectDown: cfg.Keybinds.Picker.Down.Keybind,
SelectTop: cfg.Keybinds.Picker.Top.Keybind,
SelectBottom: cfg.Keybinds.Picker.Bottom.Keybind,
},
Select: cfg.Keybinds.Picker.Select.Keybind,
})
ap.SetScrollBarVisibility(cfg.Theme.ScrollBar.Visibility.ScrollBarVisibility)

View file

@ -6,10 +6,11 @@ import (
"github.com/ayn2op/discordo/internal/config"
"github.com/ayn2op/discordo/internal/ui"
"github.com/ayn2op/discordo/pkg/picker"
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/help"
"github.com/ayn2op/tview/keybind"
"github.com/ayn2op/tview/list"
"github.com/ayn2op/tview/picker"
"github.com/diamondburned/arikawa/v3/discord"
)
@ -40,12 +41,14 @@ func newChannelsPicker(cfg *config.Config, chatView *Model) *channelsPicker {
SetTrackStyle(cfg.Theme.ScrollBar.TrackStyle.Style).
SetThumbStyle(cfg.Theme.ScrollBar.ThumbStyle.Style).
SetGlyphSet(cfg.Theme.ScrollBar.GlyphSet.GlyphSet))
cp.SetKeyMap(&picker.KeyMap{
cp.SetKeybinds(picker.Keybinds{
Cancel: cfg.Keybinds.Picker.Cancel.Keybind,
Up: cfg.Keybinds.Picker.Up.Keybind,
Down: cfg.Keybinds.Picker.Down.Keybind,
Top: cfg.Keybinds.Picker.Top.Keybind,
Bottom: cfg.Keybinds.Picker.Bottom.Keybind,
Keybinds: list.Keybinds{
SelectUp: cfg.Keybinds.Picker.Up.Keybind,
SelectDown: cfg.Keybinds.Picker.Down.Keybind,
SelectTop: cfg.Keybinds.Picker.Top.Keybind,
SelectBottom: cfg.Keybinds.Picker.Bottom.Keybind,
},
Select: cfg.Keybinds.Picker.Select.Keybind,
})
return cp

View file

@ -350,7 +350,7 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) {
gt.chat.messagesList.reset()
gt.chat.messagesList.setTitle(*channel)
gt.chat.messagesList.setMessages(messages)
gt.chat.messagesList.ScrollToEnd()
gt.chat.messagesList.ScrollBottom()
hasNoPerm := channel.Type != discord.DirectMessage && channel.Type != discord.GroupDM && !gt.chat.state.HasPermissions(channel.ID, discord.PermissionSendMessages)
gt.chat.messageInput.SetDisabled(hasNoPerm)

View file

@ -23,6 +23,10 @@ func newMentionsList(cfg *config.Config) *mentionsList {
m := &mentionsList{
Model: list.NewModel(),
}
m.SetKeybinds(list.Keybinds{
SelectUp: cfg.Keybinds.MentionsList.Up.Keybind,
SelectDown: cfg.Keybinds.MentionsList.Down.Keybind,
})
m.Box = ui.ConfigureBox(m.Box, &cfg.Theme)
m.SetSnapToItems(true).SetTitle("Mentions")

View file

@ -169,16 +169,7 @@ func (mi *messageInput) HandleEvent(event tview.Event) tview.Command {
if mi.cfg.AutocompleteLimit > 0 {
if mi.chat.GetVisible(mentionsListLayerName) {
switch {
case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Up.Keybind):
return mi.mentionsList.HandleEvent(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone))
case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Down.Keybind):
return mi.mentionsList.HandleEvent(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone))
case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Top.Keybind):
return mi.mentionsList.HandleEvent(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone))
case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Bottom.Keybind):
return mi.mentionsList.HandleEvent(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone))
}
mi.mentionsList.HandleEvent(event)
}
go mi.chat.app.QueueUpdateDraw(func() { mi.tabSuggestion() })
@ -248,7 +239,7 @@ func (mi *messageInput) send() {
}
mi.reset()
mi.chat.messagesList.clearSelection()
mi.chat.messagesList.ScrollToEnd()
mi.chat.messagesList.ScrollBottom()
}
func (mi *messageInput) processText(channel *discord.Channel, src []byte) string {

View file

@ -95,6 +95,12 @@ func newMessagesList(cfg *config.Config, chatView *Model) *messagesList {
ml.SetBuilder(ml.buildItem)
ml.SetChangedFunc(ml.onRowCursorChanged)
ml.SetTrackEnd(true)
ml.SetKeybinds(list.Keybinds{
ScrollUp: cfg.Keybinds.MessagesList.ScrollUp.Keybind,
ScrollDown: cfg.Keybinds.MessagesList.ScrollDown.Keybind,
ScrollTop: cfg.Keybinds.MessagesList.ScrollTop.Keybind,
ScrollBottom: cfg.Keybinds.MessagesList.ScrollBottom.Keybind,
})
ml.SetScrollBarVisibility(cfg.Theme.ScrollBar.Visibility.ScrollBarVisibility)
ml.SetScrollBar(tview.NewScrollBar().
SetTrackStyle(cfg.Theme.ScrollBar.TrackStyle.Style).
@ -842,18 +848,6 @@ func (ml *messagesList) HandleEvent(event tview.Event) tview.Command {
switch event := event.(type) {
case *tview.KeyEvent:
switch {
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollUp.Keybind):
ml.ScrollUp()
return nil
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollDown.Keybind):
ml.ScrollDown()
return nil
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollTop.Keybind):
ml.ScrollToStart()
return nil
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollBottom.Keybind):
ml.ScrollToEnd()
return nil
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Cancel.Keybind):
ml.clearSelection()
return nil
@ -896,8 +890,7 @@ func (ml *messagesList) HandleEvent(event tview.Event) tview.Command {
ml.confirmDelete()
return nil
}
// Do not fall through to List defaults for unmatched keys.
return nil
return ml.Model.HandleEvent(event)
}
return ml.Model.HandleEvent(event)
}

View file

@ -1,34 +0,0 @@
package picker
import (
"github.com/ayn2op/tview"
"github.com/gdamore/tcell/v3"
)
type SelectedEvent struct {
tcell.EventTime
Item
}
func newSelectedEvent(item Item) *SelectedEvent {
return &SelectedEvent{Item: item}
}
func (m *Model) _select() tview.Command {
index := m.list.Cursor()
if index >= 0 && index < len(m.filtered) {
item := m.filtered[index]
return func() tview.Event {
return newSelectedEvent(item)
}
}
return nil
}
type CancelEvent struct{ tcell.EventTime }
func cancel() tview.Command {
return func() tview.Event {
return &CancelEvent{}
}
}

View file

@ -1,17 +0,0 @@
package picker
type Item struct {
Text string
FilterText string
Reference any
}
type Items []Item
func (is Items) String(index int) string {
return is[index].FilterText
}
func (is Items) Len() int {
return len(is)
}

View file

@ -1,13 +0,0 @@
package picker
import "github.com/ayn2op/tview/keybind"
type KeyMap struct {
Cancel keybind.Keybind
Up keybind.Keybind
Down keybind.Keybind
Top keybind.Keybind
Bottom keybind.Keybind
Select keybind.Keybind
}

View file

@ -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 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 tview.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)
}