From 30890009353768159249cc39d4237b0896e25bb1 Mon Sep 17 00:00:00 2001 From: ayn2op Date: Tue, 2 Jun 2026 23:53:44 -0400 Subject: [PATCH] refactor(chat): migrate guilds tree to tview tree package --- go.mod | 2 +- go.sum | 4 +- internal/ui/chat/guilds_tree.go | 73 +++++++++++++++++---------------- internal/ui/chat/state.go | 3 +- 4 files changed, 42 insertions(+), 40 deletions(-) diff --git a/go.mod b/go.mod index 84d910b..721a8a5 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/alecthomas/chroma/v2 v2.26.1 github.com/andybalholm/brotli v1.2.1 github.com/ayn2op/clipboard v0.0.0-20260308203959-c5ad7df3fc97 - github.com/ayn2op/tview v0.0.0-20260530015500-a747daebb9d7 + github.com/ayn2op/tview v0.0.0-20260603033754-03ce043930ff github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb github.com/diamondburned/arikawa/v3 v3.6.1-0.20260518050745-b430932b3ee1 github.com/diamondburned/ningen/v3 v3.0.1-0.20260306213430-5a08d3a709b4 diff --git a/go.sum b/go.sum index 0f088a2..e98c97c 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eT github.com/andybalholm/brotli v1.2.1/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-20260530015500-a747daebb9d7 h1:LgNpUAoVkSwZXIf1wlrq/UO+TPEi3LqcGyn3nDlGw8k= -github.com/ayn2op/tview v0.0.0-20260530015500-a747daebb9d7/go.mod h1:W0lZFznN+hDXma+CT27czLM8/zbNOda6vsfVgHI1Sew= +github.com/ayn2op/tview v0.0.0-20260603033754-03ce043930ff h1:847J26ULcb5dimVXU3ymJXdlmWUY/I/hLpQ5FMiGJOY= +github.com/ayn2op/tview v0.0.0-20260603033754-03ce043930ff/go.mod h1:W0lZFznN+hDXma+CT27czLM8/zbNOda6vsfVgHI1Sew= 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= diff --git a/internal/ui/chat/guilds_tree.go b/internal/ui/chat/guilds_tree.go index fa7ca60..cb86e17 100644 --- a/internal/ui/chat/guilds_tree.go +++ b/internal/ui/chat/guilds_tree.go @@ -10,6 +10,7 @@ import ( "github.com/ayn2op/tview" "github.com/ayn2op/tview/help" "github.com/ayn2op/tview/keybind" + "github.com/ayn2op/tview/tree" "github.com/diamondburned/arikawa/v3/discord" "github.com/diamondburned/arikawa/v3/gateway" "github.com/diamondburned/ningen/v3" @@ -19,7 +20,7 @@ import ( type dmNode struct{} type guildsTree struct { - *tview.TreeView + *tree.Model chat *Model cfg *config.Config @@ -27,26 +28,26 @@ type guildsTree struct { // Fast-path indexes for frequent event handlers (read updates, picker // navigation). They mirror the current rendered tree and are rebuilt on // READY before nodes are added. - guildNodeByID map[discord.GuildID]*tview.TreeNode - channelNodeByID map[discord.ChannelID]*tview.TreeNode - dmRootNode *tview.TreeNode + guildNodeByID map[discord.GuildID]*tree.Node + channelNodeByID map[discord.ChannelID]*tree.Node + dmRootNode *tree.Node } func newGuildsTree(cfg *config.Config, chat *Model) *guildsTree { gt := &guildsTree{ - TreeView: tview.NewTreeView(), - cfg: cfg, - chat: chat, + Model: tree.NewModel(), + cfg: cfg, + chat: chat, - guildNodeByID: make(map[discord.GuildID]*tview.TreeNode), - channelNodeByID: make(map[discord.ChannelID]*tview.TreeNode), + guildNodeByID: make(map[discord.GuildID]*tree.Node), + channelNodeByID: make(map[discord.ChannelID]*tree.Node), } gt.Box = ui.ConfigureBox(gt.Box, &cfg.Theme) gt. - SetRoot(tview.NewTreeNode("")). + SetRoot(tree.NewNode("")). SetTopLevel(1). - SetMarkers(tview.TreeMarkers{ + SetMarkers(tree.Markers{ Expanded: cfg.Sidebar.Markers.Expanded, Collapsed: cfg.Sidebar.Markers.Collapsed, Leaf: cfg.Sidebar.Markers.Leaf, @@ -71,7 +72,7 @@ func (gt *guildsTree) createFolderNode(folder gateway.GuildFolder, guildsByID ma name = folder.Name } - folderNode := tview.NewTreeNode(name).SetExpanded(gt.cfg.Theme.GuildsTree.AutoExpandFolders) + folderNode := tree.NewNode(name).SetExpanded(gt.cfg.Theme.GuildsTree.AutoExpandFolders) if folder.Color != 0 { folderStyle := tcell.StyleDefault.Foreground(tcell.NewHexColor(int32(folder.Color))) gt.setNodeLineStyle(folderNode, folderStyle) @@ -133,24 +134,24 @@ func (gt *guildsTree) dmStatusStyle(status discord.Status) tcell.Style { } } -func (gt *guildsTree) createGuildNode(n *tview.TreeNode, guild discord.Guild) { - guildNode := tview.NewTreeNode(guild.Name). +func (gt *guildsTree) createGuildNode(parent *tree.Node, guild discord.Guild) { + guildNode := tree.NewNode(guild.Name). SetReference(guild.ID). SetExpandable(true). SetExpanded(false). SetIndent(gt.cfg.Sidebar.Indents.Guild) gt.setNodeLineStyle(guildNode, gt.guildNodeStyle(guild.ID)) - n.AddChild(guildNode) + parent.AddChild(guildNode) gt.guildNodeByID[guild.ID] = guildNode } -func (gt *guildsTree) createChannelNode(node *tview.TreeNode, channel discord.Channel) { +func (gt *guildsTree) createChannelNode(parent *tree.Node, channel discord.Channel) { if channel.Type != discord.DirectMessage && channel.Type != discord.GroupDM && channel.Type != discord.GuildCategory && !gt.chat.state.HasPermissions(channel.ID, discord.PermissionViewChannel) { return } indents := gt.cfg.Sidebar.Indents - channelNode := tview.NewTreeNode(ui.ChannelToString(channel, gt.cfg.Icons, gt.chat.state)).SetReference(channel.ID) + channelNode := tree.NewNode(ui.ChannelToString(channel, gt.cfg.Icons, gt.chat.state)).SetReference(channel.ID) gt.setNodeLineStyle(channelNode, gt.channelNodeStyle(channel)) switch channel.Type { case discord.DirectMessage: @@ -166,11 +167,11 @@ func (gt *guildsTree) createChannelNode(node *tview.TreeNode, channel discord.Ch default: channelNode.SetIndent(indents.Channel) } - node.AddChild(channelNode) + parent.AddChild(channelNode) gt.channelNodeByID[channel.ID] = channelNode } -func (gt *guildsTree) setNodeLineStyle(node *tview.TreeNode, style tcell.Style) { +func (gt *guildsTree) setNodeLineStyle(node *tree.Node, style tcell.Style) { line := node.GetLine() for i := range line { line[i].Style = style @@ -178,7 +179,7 @@ func (gt *guildsTree) setNodeLineStyle(node *tview.TreeNode, style tcell.Style) node.SetLine(line) } -func (gt *guildsTree) createChannelNodes(node *tview.TreeNode, channels []discord.Channel) { +func (gt *guildsTree) createChannelNodes(node *tree.Node, channels []discord.Channel) { // Preserve exact ordering semantics: // 1) top-level non-categories (in input order), // 2) categories that have at least one child in the source slice (in input order), @@ -227,7 +228,7 @@ func isThread(t discord.ChannelType) bool { } } -func (gt *guildsTree) onSelected(node *tview.TreeNode) tview.Cmd { +func (gt *guildsTree) onSelected(node *tree.Node) tview.Cmd { if len(node.GetChildren()) != 0 { node.SetExpanded(!node.IsExpanded()) return nil @@ -307,10 +308,10 @@ func (gt *guildsTree) loadChannel(channel discord.Channel) tview.Cmd { } } -func (gt *guildsTree) collapseParentNode(node *tview.TreeNode) { +func (gt *guildsTree) collapseParentNode(node *tree.Node) { gt. GetRoot(). - Walk(func(n, parent *tview.TreeNode) bool { + Walk(func(n, parent *tree.Node) bool { if n == node && parent.GetLevel() != 0 { parent.Collapse() gt.SetCurrentNode(parent) @@ -323,10 +324,10 @@ func (gt *guildsTree) collapseParentNode(node *tview.TreeNode) { func (gt *guildsTree) Update(msg tview.Msg) tview.Cmd { switch msg := msg.(type) { - case tview.TreeViewSelectedMsg: + case tree.SelectedMsg: return gt.onSelected(msg.Node) case tview.KeyMsg: - handler := gt.TreeView.Update + handler := gt.Model.Update switch { case keybind.Matches(msg, gt.cfg.Keybinds.GuildsTree.CollapseAll.Keybind): for _, node := range gt.GetRoot().GetChildren() { @@ -353,10 +354,10 @@ func (gt *guildsTree) Update(msg tview.Msg) tview.Cmd { case keybind.Matches(msg, gt.cfg.Keybinds.GuildsTree.YankID.Keybind): return gt.yankID() } - // Do not fall through to TreeView defaults for unmatched keys. + // Do not fall through to defaults for unmatched keys. return nil } - return gt.TreeView.Update(msg) + return gt.Model.Update(msg) } func (gt *guildsTree) yankID() tview.Cmd { @@ -378,7 +379,7 @@ func (gt *guildsTree) yankID() tview.Cmd { return nil } -func (gt *guildsTree) findNodeByReference(reference any) *tview.TreeNode { +func (gt *guildsTree) findNodeByReference(reference any) *tree.Node { switch ref := reference.(type) { case discord.GuildID: return gt.guildNodeByID[ref] @@ -388,8 +389,8 @@ func (gt *guildsTree) findNodeByReference(reference any) *tview.TreeNode { return gt.dmRootNode default: // Fallback keeps this helper safe for non-indexed custom references. - var found *tview.TreeNode - gt.GetRoot().Walk(func(node, _ *tview.TreeNode) bool { + var found *tree.Node + gt.GetRoot().Walk(func(node, _ *tree.Node) bool { if node.GetReference() == reference { found = node return false @@ -400,7 +401,7 @@ func (gt *guildsTree) findNodeByReference(reference any) *tview.TreeNode { } } -func (gt *guildsTree) findNodeByChannelID(channelID discord.ChannelID) *tview.TreeNode { +func (gt *guildsTree) findNodeByChannelID(channelID discord.ChannelID) *tree.Node { channel, err := gt.chat.state.Cabinet.Channel(channelID) if err != nil { slog.Error("failed to get channel", "channel_id", channelID, "err", err) @@ -413,9 +414,9 @@ func (gt *guildsTree) findNodeByChannelID(channelID discord.ChannelID) *tview.Tr } else { reference = dmNode{} } - if parentNode := gt.findNodeByReference(reference); parentNode != nil { - if len(parentNode.GetChildren()) == 0 { - gt.onSelected(parentNode) + if parent := gt.findNodeByReference(reference); parent != nil { + if len(parent.GetChildren()) == 0 { + gt.onSelected(parent) } } @@ -423,7 +424,7 @@ func (gt *guildsTree) findNodeByChannelID(channelID discord.ChannelID) *tview.Tr return node } -func (gt *guildsTree) expandPathToNode(node *tview.TreeNode) { +func (gt *guildsTree) expandPathToNode(node *tree.Node) { if node == nil { return } @@ -490,7 +491,7 @@ func (gt *guildsTree) collapseKeybinds() []keybind.Keybind { return keybinds } -func (gt *guildsTree) canCollapseParent(node *tview.TreeNode) bool { +func (gt *guildsTree) canCollapseParent(node *tree.Node) bool { if node == nil { return false } diff --git a/internal/ui/chat/state.go b/internal/ui/chat/state.go index dd6b6c2..fc80a36 100644 --- a/internal/ui/chat/state.go +++ b/internal/ui/chat/state.go @@ -6,6 +6,7 @@ import ( "github.com/ayn2op/discordo/internal/notifications" "github.com/ayn2op/tview" + "github.com/ayn2op/tview/tree" "github.com/diamondburned/arikawa/v3/discord" "github.com/diamondburned/arikawa/v3/gateway" "github.com/diamondburned/arikawa/v3/utils/httputil/httpdriver" @@ -35,7 +36,7 @@ func (m *Model) onReady(event *gateway.ReadyEvent) tview.Cmd { // retain pointers to detached tree nodes. m.guildsTree.resetNodeIndex() - dmNode := tview.NewTreeNode("Direct Messages").SetReference(dmNode{}).SetExpandable(true).SetExpanded(false) + dmNode := tree.NewNode("Direct Messages").SetReference(dmNode{}).SetExpandable(true).SetExpanded(false) m.guildsTree.dmRootNode = dmNode root := m.guildsTree.