diff --git a/go.mod b/go.mod index 7762333..98fe4dc 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/BurntSushi/toml v1.6.0 github.com/alecthomas/chroma/v2 v2.23.1 github.com/andybalholm/brotli v1.2.0 - github.com/ayn2op/tview v0.0.0-20260223065535-6a0c066d0bdd + github.com/ayn2op/tview v0.0.0-20260223211434-6859308d597f github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb github.com/diamondburned/arikawa/v3 v3.6.1-0.20260221051847-b81b70d1a5cb github.com/diamondburned/ningen/v3 v3.0.1-0.20250920191746-98fbd92e134d diff --git a/go.sum b/go.sum index 686376a..5392dd1 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/ayn2op/tview v0.0.0-20260223065535-6a0c066d0bdd h1:DdR19f4xNbd3XM9vxmC9LtF5XY1nv0rDMMzn0FSTwd0= -github.com/ayn2op/tview v0.0.0-20260223065535-6a0c066d0bdd/go.mod h1:lZ8RdOegQWBQafTOasGE7Ps1/Ymy4jmXoPt5vz2QsS0= +github.com/ayn2op/tview v0.0.0-20260223211434-6859308d597f h1:ZxDODg1E/RPoTWlXhnhZOKITe1ebBM0Cpy+GiGaa+xk= +github.com/ayn2op/tview v0.0.0-20260223211434-6859308d597f/go.mod h1:lZ8RdOegQWBQafTOasGE7Ps1/Ymy4jmXoPt5vz2QsS0= 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= diff --git a/internal/app/app.go b/internal/app/app.go index 3a1834a..a726181 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -102,6 +102,9 @@ func (a *App) quit() { func (a *App) onInputCapture(event *tcell.EventKey) *tcell.EventKey { switch { + case keybind.Matches(event, a.cfg.Keybinds.Suspend.Keybind): + a.suspend() + return nil case keybind.Matches(event, a.cfg.Keybinds.Quit.Keybind): a.quit() return nil diff --git a/internal/app/suspend_default.go b/internal/app/suspend_default.go new file mode 100644 index 0000000..c020fba --- /dev/null +++ b/internal/app/suspend_default.go @@ -0,0 +1,5 @@ +//go:build !unix + +package app + +func (a *App) suspend() {} diff --git a/internal/app/suspend_unix.go b/internal/app/suspend_unix.go new file mode 100644 index 0000000..baff2d6 --- /dev/null +++ b/internal/app/suspend_unix.go @@ -0,0 +1,20 @@ +//go:build unix + +package app + +import ( + "os" + "os/signal" + "syscall" +) + +func (a *App) suspend() { + a.inner.Suspend(func() { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGCONT) + defer signal.Stop(c) + + _ = syscall.Kill(0, syscall.SIGTSTP) + <-c + }) +} diff --git a/internal/config/config.go b/internal/config/config.go index 0384fde..affbc96 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -74,6 +74,16 @@ type ( Separator string `toml:"separator"` } + SidebarMarkersConfig struct { + Expanded string `toml:"expanded"` + Collapsed string `toml:"collapsed"` + Leaf string `toml:"leaf"` + } + + SidebarConfig struct { + Markers SidebarMarkersConfig `toml:"markers"` + } + Config struct { AutoFocus bool `toml:"auto_focus"` Mouse bool `toml:"mouse"` @@ -94,6 +104,7 @@ type ( DateSeparator DateSeparator `toml:"date_separator"` Notifications Notifications `toml:"notifications"` TypingIndicator TypingIndicator `toml:"typing_indicator"` + Sidebar SidebarConfig `toml:"sidebar"` Icons Icons `toml:"icons"` diff --git a/internal/config/config.toml b/internal/config/config.toml index 9433729..41d196a 100644 --- a/internal/config/config.toml +++ b/internal/config/config.toml @@ -63,6 +63,11 @@ send = true # Whether to receive typing status or not. receive = true +[sidebar.markers] +expanded = "▾ " +collapsed = "▸ " +leaf = "" + [icons] guild_category = "" guild_text = "#" @@ -87,10 +92,11 @@ focus_previous = "ctrl+h" focus_next = "ctrl+l" # Hide/show the guilds tree. toggle_guilds_tree = "ctrl+b" -quit = "ctrl+c" # Log out and remove the authentication token from keyring. # Requires re-login upon restart. logout = "ctrl+d" +suspend = "ctrl+z" +quit = "ctrl+c" [keybinds.picker] toggle = "ctrl+k" @@ -152,6 +158,7 @@ send = "enter" cancel = "esc" # Complete usernames when mentioning tab_complete = "tab" +undo = "ctrl+u" open_editor = "ctrl+e" open_file_picker = "ctrl+\\" diff --git a/internal/config/keybinds.go b/internal/config/keybinds.go index 44711c4..4c39f5d 100644 --- a/internal/config/keybinds.go +++ b/internal/config/keybinds.go @@ -96,6 +96,7 @@ type MessageInputKeybinds struct { Send Keybind `toml:"send"` Cancel Keybind `toml:"cancel"` TabComplete Keybind `toml:"tab_complete"` + Undo Keybind `toml:"undo"` OpenEditor Keybind `toml:"open_editor"` OpenFilePicker Keybind `toml:"open_file_picker"` @@ -109,6 +110,7 @@ type Keybinds struct { ToggleGuildsTree Keybind `toml:"toggle_guilds_tree"` ToggleChannelsPicker Keybind `toml:"toggle_channels_picker"` ToggleHelp Keybind `toml:"toggle_help"` + Suspend Keybind `toml:"suspend"` FocusGuildsTree Keybind `toml:"focus_guilds_tree"` FocusMessagesList Keybind `toml:"focus_messages_list"` @@ -132,6 +134,7 @@ func defaultKeybinds() Keybinds { ToggleGuildsTree: newKeybind("ctrl+b", "toggle guilds"), ToggleChannelsPicker: newKeybind("ctrl+k", "channels picker"), ToggleHelp: newKeybind("ctrl+.", "help"), + Suspend: newKeybind("ctrl+z", "suspend"), FocusGuildsTree: newKeybind("ctrl+g", "guilds"), FocusMessagesList: newKeybind("ctrl+t", "messages"), @@ -198,6 +201,7 @@ func defaultKeybinds() Keybinds { Send: newKeybind("enter", "send"), Cancel: newKeybind("esc", "cancel"), TabComplete: newKeybind("tab", "complete"), + Undo: newKeybind("ctrl+u", "undo"), OpenEditor: newKeybind("ctrl+e", "editor"), OpenFilePicker: newKeybind("ctrl+\\", "attach"), }, diff --git a/internal/ui/chat/guilds_tree.go b/internal/ui/chat/guilds_tree.go index 34261d6..53e7c27 100644 --- a/internal/ui/chat/guilds_tree.go +++ b/internal/ui/chat/guilds_tree.go @@ -47,6 +47,11 @@ func newGuildsTree(cfg *config.Config, chatView *View) *guildsTree { gt. SetRoot(tview.NewTreeNode("")). SetTopLevel(1). + SetMarkers(tview.TreeMarkers{ + Expanded: cfg.Sidebar.Markers.Expanded, + Collapsed: cfg.Sidebar.Markers.Collapsed, + Leaf: cfg.Sidebar.Markers.Leaf, + }). SetGraphics(cfg.Theme.GuildsTree.Graphics). SetGraphicsColor(tcell.GetColor(cfg.Theme.GuildsTree.GraphicsColor)). SetSelectedFunc(gt.onSelected). @@ -189,7 +194,7 @@ func (gt *guildsTree) getChannelNodeStyle(channelID discord.ChannelID) tcell.Sty } func (gt *guildsTree) createGuildNode(n *tview.TreeNode, guild discord.Guild) { - guildNode := tview.NewTreeNode(guild.Name).SetReference(guild.ID) + guildNode := tview.NewTreeNode(guild.Name).SetReference(guild.ID).SetExpandable(true) gt.setNodeLineStyle(guildNode, gt.getGuildNodeStyle(guild.ID)) n.AddChild(guildNode) gt.guildNodeByID[guild.ID] = guildNode @@ -201,6 +206,9 @@ func (gt *guildsTree) createChannelNode(node *tview.TreeNode, channel discord.Ch } channelNode := tview.NewTreeNode(ui.ChannelToString(channel, gt.cfg.Icons)).SetReference(channel.ID) + if channel.Type == discord.GuildForum { + channelNode.SetExpandable(true) + } gt.setNodeLineStyle(channelNode, gt.getChannelNodeStyle(channel.ID)) node.AddChild(channelNode) gt.channelNodeByID[channel.ID] = channelNode diff --git a/internal/ui/chat/keybinds.go b/internal/ui/chat/keybinds.go index 719525d..ef7900a 100644 --- a/internal/ui/chat/keybinds.go +++ b/internal/ui/chat/keybinds.go @@ -66,6 +66,6 @@ func (v *View) baseFullHelp() [][]keybind.Keybind { focus, {cfg.FocusPrevious.Keybind, cfg.FocusNext.Keybind}, {cfg.ToggleGuildsTree.Keybind, cfg.ToggleChannelsPicker.Keybind}, - {cfg.ToggleHelp.Keybind, cfg.Logout.Keybind, cfg.Quit.Keybind}, + {cfg.ToggleHelp.Keybind, cfg.Suspend.Keybind, cfg.Logout.Keybind, cfg.Quit.Keybind}, } } diff --git a/internal/ui/chat/message_input.go b/internal/ui/chat/message_input.go index 16e03b0..02f62b7 100644 --- a/internal/ui/chat/message_input.go +++ b/internal/ui/chat/message_input.go @@ -126,6 +126,8 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey { case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.TabComplete.Keybind): go mi.chatView.app.QueueUpdateDraw(func() { mi.tabComplete() }) return nil + case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Undo.Keybind): + return tcell.NewEventKey(tcell.KeyCtrlZ, "", tcell.ModNone) default: if mi.cfg.TypingIndicator.Send && mi.typingTimer == nil { mi.typingTimer = time.AfterFunc(typingDuration, func() { @@ -717,7 +719,7 @@ func (mi *messageInput) FullHelp() [][]keybind.Keybind { } return [][]keybind.Keybind{ - {cfg.Send.Keybind, cfg.Cancel.Keybind, cfg.TabComplete.Keybind}, + {cfg.Send.Keybind, cfg.Cancel.Keybind, cfg.TabComplete.Keybind, cfg.Undo.Keybind}, openEditor, } } diff --git a/internal/ui/chat/state.go b/internal/ui/chat/state.go index db6337e..57dc95b 100644 --- a/internal/ui/chat/state.go +++ b/internal/ui/chat/state.go @@ -85,7 +85,7 @@ func (v *View) onReady(event *gateway.ReadyEvent) { // retain stale pointers to detached tree nodes. v.guildsTree.resetNodeIndex() - dmNode := tview.NewTreeNode("Direct Messages").SetReference(dmNode{}) + dmNode := tview.NewTreeNode("Direct Messages").SetReference(dmNode{}).SetExpandable(true) v.guildsTree.dmRootNode = dmNode root := v.guildsTree.