From 37a6394826f3b23d9d4100123bea4b5a1e5d5159 Mon Sep 17 00:00:00 2001 From: ayn2op Date: Wed, 8 Jul 2026 01:43:59 -0400 Subject: [PATCH] refactor(chat): extract mentions list --- go.mod | 6 +- go.sum | 12 +-- internal/ui/chat/composer.go | 41 ++++----- internal/ui/chat/mentions_list.go | 97 -------------------- internal/ui/chat/mentionslist/item.go | 9 ++ internal/ui/chat/mentionslist/model.go | 120 +++++++++++++++++++++++++ 6 files changed, 159 insertions(+), 126 deletions(-) delete mode 100644 internal/ui/chat/mentions_list.go create mode 100644 internal/ui/chat/mentionslist/item.go create mode 100644 internal/ui/chat/mentionslist/model.go diff --git a/go.mod b/go.mod index 0d39659..4440cad 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,9 @@ require ( github.com/andybalholm/brotli v1.2.2 github.com/ayn2op/arikawa/v3 v3.0.0-20260619211231-9a7da0c518de github.com/ayn2op/ningen/v3 v3.0.1-0.20260619214224-6a89f2062a83 - github.com/ayn2op/tview v0.0.0-20260703004135-684c7bd3e9af + github.com/ayn2op/tview v0.0.0-20260708025551-16f85f97975e github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb - github.com/gdamore/tcell/v3 v3.4.1-0.20260629051246-fe2979ab8bd1 + github.com/gdamore/tcell/v3 v3.4.1-0.20260703160154-d85b1fbd48b8 github.com/gen2brain/beeep v0.11.2 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 @@ -59,6 +59,6 @@ require ( golang.org/x/mobile v0.0.0-20260611195102-4dd8f1dbf5d2 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.38.0 // indirect + golang.org/x/text v0.39.0 // indirect golang.org/x/time v0.15.0 // indirect ) diff --git a/go.sum b/go.sum index 52a658e..1bcef80 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,8 @@ github.com/ayn2op/arikawa/v3 v3.0.0-20260619211231-9a7da0c518de h1:j5qhTMT9t09g7 github.com/ayn2op/arikawa/v3 v3.0.0-20260619211231-9a7da0c518de/go.mod h1:96vOKCn1rSdu7xJrCrKdwYzlgDZ5NmV9cMtPAeGHlGM= github.com/ayn2op/ningen/v3 v3.0.1-0.20260619214224-6a89f2062a83 h1:Dqap/VgsdiL0CNSBd+H845ZG5y2K3SJ8ji3F5oMTYIE= github.com/ayn2op/ningen/v3 v3.0.1-0.20260619214224-6a89f2062a83/go.mod h1:uMv/8i6wn0p4paqT2Tf1S4nn5dCoNIs4w1UWvODwvf0= -github.com/ayn2op/tview v0.0.0-20260703004135-684c7bd3e9af h1:TbQH/N3lTftvAL9NPELlIcp8BsVm25lpp6kFF78gHJU= -github.com/ayn2op/tview v0.0.0-20260703004135-684c7bd3e9af/go.mod h1:kBq8YDymQUJsGxUfhatCnwc1XU0z1tXjSLNpGpzcMp8= +github.com/ayn2op/tview v0.0.0-20260708025551-16f85f97975e h1:WVf0fqBMetR5F5VoA1lMvRpSX25YUyekaNwaWXieW7c= +github.com/ayn2op/tview v0.0.0-20260708025551-16f85f97975e/go.mod h1:kBq8YDymQUJsGxUfhatCnwc1XU0z1tXjSLNpGpzcMp8= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= @@ -39,8 +39,8 @@ github.com/esiqveland/notify v0.14.0 h1:LgIT/vYnziiw3d4ykaeyytc7jBUhD+Nvv3U7GIuP github.com/esiqveland/notify v0.14.0/go.mod h1:hesw/IRYTO0x99u1JPweAl4+5mwXJibQVUcP0Iu5ORE= github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= -github.com/gdamore/tcell/v3 v3.4.1-0.20260629051246-fe2979ab8bd1 h1:vWuBP/aOVrgFN81SPA8ZEOmVIMs0WQ7OZKlEPg0eZyQ= -github.com/gdamore/tcell/v3 v3.4.1-0.20260629051246-fe2979ab8bd1/go.mod h1:Ev/2PFhL0QtVmu6XPZG9NEuITAZ6XH7i7/BF3wupBdw= +github.com/gdamore/tcell/v3 v3.4.1-0.20260703160154-d85b1fbd48b8 h1:4RhCPcOmn5eM1eD0vdjd1l1IC8WqmGj/R0vVUVJ60XA= +github.com/gdamore/tcell/v3 v3.4.1-0.20260703160154-d85b1fbd48b8/go.mod h1:Ev/2PFhL0QtVmu6XPZG9NEuITAZ6XH7i7/BF3wupBdw= github.com/gen2brain/beeep v0.11.2 h1:+KfiKQBbQCuhfJFPANZuJ+oxsSKAYNe88hIpJuyKWDA= github.com/gen2brain/beeep v0.11.2/go.mod h1:jQVvuwnLuwOcdctHn/uyh8horSBNJ8uGb9Cn2W4tvoc= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= @@ -155,8 +155,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/ui/chat/composer.go b/internal/ui/chat/composer.go index 0e21d4b..43ec6cd 100644 --- a/internal/ui/chat/composer.go +++ b/internal/ui/chat/composer.go @@ -24,6 +24,7 @@ import ( "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/discordo/internal/consts" "github.com/ayn2op/discordo/internal/ui" + "github.com/ayn2op/discordo/internal/ui/chat/mentionslist" "github.com/ayn2op/ningen/v3" "github.com/ayn2op/ningen/v3/discordmd" "github.com/ayn2op/tview" @@ -53,7 +54,7 @@ type composer struct { edit bool sendMessageData *api.SendMessageData cache *cache.Cache - mentionsList *mentionsList + mentionsList *mentionslist.Model lastSearch time.Time typingTimerMu sync.Mutex @@ -71,7 +72,7 @@ func newComposer(cfg *config.Config, chat *Model) *composer { chat: chat, sendMessageData: &api.SendMessageData{}, cache: cache.NewCache(), - mentionsList: newMentionsList(cfg), + mentionsList: mentionslist.NewModel(cfg), } c.Box = ui.ConfigureBox(c.Box, &cfg.Theme) c. @@ -487,10 +488,10 @@ func (c *composer) tabComplete() tview.Cmd { } return nil } - if c.mentionsList.itemCount() == 0 { + if c.mentionsList.ItemCount() == 0 { return nil } - name, ok = c.mentionsList.selectedInsertText() + name, ok = c.mentionsList.SelectedInsertText() if !ok { return nil } @@ -509,7 +510,7 @@ func (c *composer) tabSuggest() tview.Cmd { } gID := selectedChannel.GuildID cID := selectedChannel.ID - c.mentionsList.clear() + c.mentionsList.Clear() var shown map[string]struct{} var userDone struct{} @@ -577,16 +578,16 @@ func (c *composer) tabSuggest() tview.Cmd { break } } - if c.mentionsList.itemCount() == 0 { + if c.mentionsList.ItemCount() == 0 { return tview.Batch(c.stopTabCompletion(), searchCmd) } } - if c.mentionsList.itemCount() == 0 { + if c.mentionsList.ItemCount() == 0 { return c.stopTabCompletion() } - c.mentionsList.rebuild() + c.mentionsList.Rebuild() return c.showMentionsList() } @@ -687,14 +688,14 @@ func (c *composer) showMentionsList() tview.Cmd { if t := int(c.cfg.Theme.MentionsList.MaxHeight); t != 0 { maxH = min(maxH, t) } - count := c.mentionsList.itemCount() + borders + count := c.mentionsList.ItemCount() + borders h := min(count, maxH) + borders + c.cfg.Theme.Border.Padding[1] y -= h w := int(c.cfg.Theme.MentionsList.MinWidth) if w == 0 { w = maxW } else { - w = max(w, c.mentionsList.maxDisplayWidth()) + w = max(w, c.mentionsList.MaxDisplayWidth()) w = min(w+borders*2, maxW) _, col, _, _ := c.GetCursor() @@ -734,12 +735,12 @@ func (c *composer) addMentionMember(gID discord.GuildID, m *discord.Member) bool style = style.Dim(true) } - c.mentionsList.append(mentionsListItem{ - insertText: m.User.Username, - displayText: name, - style: style, + c.mentionsList.Append(mentionslist.Item{ + InsertText: m.User.Username, + DisplayText: name, + Style: style, }) - return c.mentionsList.itemCount() > int(c.cfg.AutocompleteLimit) + return c.mentionsList.ItemCount() > int(c.cfg.AutocompleteLimit) } func (c *composer) addMentionUser(user *discord.User) { @@ -756,10 +757,10 @@ func (c *composer) addMentionUser(user *discord.User) { style = style.Dim(true) } - c.mentionsList.append(mentionsListItem{ - insertText: user.Username, - displayText: name, - style: style, + c.mentionsList.Append(mentionslist.Item{ + InsertText: user.Username, + DisplayText: name, + Style: style, }) } @@ -772,7 +773,7 @@ func (c *composer) removeMentionsList() { func (c *composer) stopTabCompletion() tview.Cmd { if c.cfg.AutocompleteLimit > 0 { - c.mentionsList.clear() + c.mentionsList.Clear() c.removeMentionsList() return tview.SetFocus(c) } diff --git a/internal/ui/chat/mentions_list.go b/internal/ui/chat/mentions_list.go deleted file mode 100644 index 3a30607..0000000 --- a/internal/ui/chat/mentions_list.go +++ /dev/null @@ -1,97 +0,0 @@ -package chat - -import ( - "github.com/ayn2op/discordo/internal/config" - "github.com/ayn2op/discordo/internal/ui" - "github.com/ayn2op/tview" - "github.com/ayn2op/tview/list" - "github.com/gdamore/tcell/v3" - "github.com/rivo/uniseg" -) - -type mentionsListItem struct { - insertText string - displayText string - style tcell.Style -} - -type mentionsList struct { - *list.Model - items []mentionsListItem -} - -func newMentionsList(cfg *config.Config) *mentionsList { - m := &mentionsList{ - Model: list.NewModel(), - } - m.SetSelectedStyle(tcell.StyleDefault.Reverse(true)) - m.SetKeybinds(list.Keybinds{ - SelectUp: cfg.Keybinds.MentionsList.SelectUp.Keybind, - SelectDown: cfg.Keybinds.MentionsList.SelectDown.Keybind, - SelectTop: cfg.Keybinds.MentionsList.SelectTop.Keybind, - SelectBottom: cfg.Keybinds.MentionsList.SelectBottom.Keybind, - }) - - m.Box = ui.ConfigureBox(m.Box, &cfg.Theme) - m.SetSnapToItems(true).SetTitle("Mentions") - - b := m.BorderSet() - b.BottomLeft, b.BottomRight = b.BottomT, b.BottomT - m.SetBorderSet(b) - - return m -} - -func (m *mentionsList) append(item mentionsListItem) { - m.items = append(m.items, item) -} - -func (m *mentionsList) clear() { - m.items = nil - m.Clear() -} - -func (m *mentionsList) rebuild() { - m.SetBuilder(func(index int) list.Item { - if index < 0 || index >= len(m.items) { - return nil - } - - item := m.items[index] - style := item.style - line := tview.NewLine(tview.NewSegment(item.displayText, style)) - - return tview.NewTextView(). - SetScrollable(false). - SetWrap(false). - SetWordWrap(false). - SetTextStyle(style). - SetLines([]tview.Line{line}) - }) - - if len(m.items) == 0 { - m.SetCursor(-1) - return - } - m.SetCursor(0) -} - -func (m *mentionsList) itemCount() int { - return len(m.items) -} - -func (m *mentionsList) selectedInsertText() (string, bool) { - index := m.Cursor() - if index < 0 || index >= len(m.items) { - return "", false - } - return m.items[index].insertText, true -} - -func (m *mentionsList) maxDisplayWidth() int { - width := 0 - for _, item := range m.items { - width = max(width, uniseg.StringWidth(item.displayText)) - } - return width -} diff --git a/internal/ui/chat/mentionslist/item.go b/internal/ui/chat/mentionslist/item.go new file mode 100644 index 0000000..b74afbf --- /dev/null +++ b/internal/ui/chat/mentionslist/item.go @@ -0,0 +1,9 @@ +package mentionslist + +import "github.com/gdamore/tcell/v3" + +type Item struct { + InsertText string + DisplayText string + Style tcell.Style +} diff --git a/internal/ui/chat/mentionslist/model.go b/internal/ui/chat/mentionslist/model.go new file mode 100644 index 0000000..9ad3869 --- /dev/null +++ b/internal/ui/chat/mentionslist/model.go @@ -0,0 +1,120 @@ +package mentionslist + +import ( + "github.com/ayn2op/discordo/internal/config" + "github.com/ayn2op/discordo/internal/ui" + "github.com/ayn2op/tview" + "github.com/ayn2op/tview/list" + "github.com/gdamore/tcell/v3" + "github.com/rivo/uniseg" +) + +type Model struct { + list *list.Model + items []Item +} + +func NewModel(cfg *config.Config) *Model { + l := list.NewModel() + l.Box = ui.ConfigureBox(l.Box, &cfg.Theme) + l. + SetSelectedStyle(tcell.StyleDefault.Reverse(true)). + SetSnapToItems(true). + SetTitle("Mentions") + + kbs := cfg.Keybinds.MentionsList + l.SetKeybinds(list.Keybinds{ + SelectUp: kbs.SelectUp.Keybind, + SelectDown: kbs.SelectDown.Keybind, + SelectTop: kbs.SelectTop.Keybind, + SelectBottom: kbs.SelectBottom.Keybind, + }) + + b := l.BorderSet() + b.BottomLeft, b.BottomRight = b.BottomT, b.BottomT + l.SetBorderSet(b) + + return &Model{list: l} +} + +func (m *Model) Append(item Item) { + m.items = append(m.items, item) +} + +func (m *Model) Clear() { + m.items = nil + m.list.Clear() +} + +func (m *Model) ItemCount() int { + return len(m.items) +} + +func (m *Model) SelectedInsertText() (string, bool) { + index := m.list.Cursor() + if index < 0 || index >= len(m.items) { + return "", false + } + return m.items[index].InsertText, true +} + +func (m *Model) MaxDisplayWidth() int { + width := 0 + for _, item := range m.items { + width = max(width, uniseg.StringWidth(item.DisplayText)) + } + return width +} + +func (m *Model) Rebuild() { + m.list.SetBuilder(func(index int) list.Item { + if index < 0 || index >= len(m.items) { + return nil + } + item := m.items[index] + style := item.Style + line := tview.NewLine(tview.NewSegment(item.DisplayText, style)) + return tview.NewTextView(). + SetScrollable(false). + SetWrap(false). + SetWordWrap(false). + SetTextStyle(style). + SetLines([]tview.Line{line}) + }) + + if len(m.items) == 0 { + m.list.SetCursor(-1) + return + } + m.list.SetCursor(0) +} + +var _ tview.Model = (*Model)(nil) + +func (m *Model) Blur() { + m.list.Blur() +} + +func (m *Model) Focus(delegate func(tview.Model)) { + m.list.Focus(delegate) +} + +func (m *Model) HasFocus() bool { + return m.list.HasFocus() +} + +func (m *Model) Rect() (int, int, int, int) { + return m.list.Rect() +} + +func (m *Model) SetRect(x int, y int, width int, height int) { + m.list.SetRect(x, y, width, height) +} + +func (m *Model) Update(msg tview.Msg) tview.Cmd { + return m.list.Update(msg) +} + +func (m *Model) View(screen tcell.Screen) { + m.list.View(screen) +}