feat: add help bar (#750)

This commit is contained in:
Ayyan 2026-02-23 16:03:11 -05:00 • committed by GitHub
parent dda87c5bba
commit 20ef1a7be2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 751 additions and 219 deletions

4
go.mod
View file

@ -8,9 +8,9 @@ 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-20260217063213-aa3df80a3074
github.com/ayn2op/tview v0.0.0-20260223065535-6a0c066d0bdd
github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb
github.com/diamondburned/arikawa/v3 v3.6.1-0.20260221035217-15298f718931
github.com/diamondburned/arikawa/v3 v3.6.1-0.20260221051847-b81b70d1a5cb
github.com/diamondburned/ningen/v3 v3.0.1-0.20250920191746-98fbd92e134d
github.com/gdamore/tcell/v3 v3.1.2
github.com/gen2brain/beeep v0.11.2

8
go.sum
View file

@ -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-20260217063213-aa3df80a3074 h1:16GcP8SZ7SnHvZEMu1EPxPjAlb/rwAgt556blNFdIiQ=
github.com/ayn2op/tview v0.0.0-20260217063213-aa3df80a3074/go.mod h1:lZ8RdOegQWBQafTOasGE7Ps1/Ymy4jmXoPt5vz2QsS0=
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/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=
@ -25,8 +25,8 @@ github.com/dchest/jsmin v1.0.0 h1:Y2hWXmGZiRxtl+VcTksyucgTlYxnhPzTozCwx9gy9zI=
github.com/dchest/jsmin v1.0.0/go.mod h1:AVBIund7Mr7lKXT70hKT2YgL3XEXUaUk5iw9DZ8b0Uc=
github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb h1:6S+TKObz6+Io2c8IOkcbK4Sz7nj6RpEVU7TkvmsZZcw=
github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb/go.mod h1:wf3nKtOnQqCp7kp9xB7hHnNlZ6m3NoiOxjrB9hFRq4Y=
github.com/diamondburned/arikawa/v3 v3.6.1-0.20260221035217-15298f718931 h1:7QLI6WfhnmvCzsU11+39i8IUoHyt7T7CtDjXInKLCIo=
github.com/diamondburned/arikawa/v3 v3.6.1-0.20260221035217-15298f718931/go.mod h1:TpV2GvCJIYSwAXUEAx4sutPcftyAbVidiFlG6l7K0go=
github.com/diamondburned/arikawa/v3 v3.6.1-0.20260221051847-b81b70d1a5cb h1:sBcyty++NvgR6SiAV5CuiTW3AKiAdtJ1NWE7jovZnUc=
github.com/diamondburned/arikawa/v3 v3.6.1-0.20260221051847-b81b70d1a5cb/go.mod h1:TpV2GvCJIYSwAXUEAx4sutPcftyAbVidiFlG6l7K0go=
github.com/diamondburned/ningen/v3 v3.0.1-0.20250920191746-98fbd92e134d h1:wS6HWl86RFItw38sUSdXlulj0VarV4G+9Rlv8j+p3oQ=
github.com/diamondburned/ningen/v3 v3.0.1-0.20250920191746-98fbd92e134d/go.mod h1:9PWnJlArEASrZQI89KQiBJBYOEDcsDyfhFcxRTL8eO4=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=

View file

@ -12,6 +12,7 @@ import (
"github.com/ayn2op/discordo/internal/ui/chat"
"github.com/ayn2op/discordo/internal/ui/login"
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/keybind"
"github.com/gdamore/tcell/v3"
)
@ -100,11 +101,11 @@ func (a *App) quit() {
}
func (a *App) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
switch event.Name() {
case a.cfg.Keybinds.Quit:
switch {
case keybind.Matches(event, a.cfg.Keybinds.Quit.Keybind):
a.quit()
return nil
case "Ctrl+C":
case keybind.Matches(event, keybind.NewKeybind(keybind.WithKeys("ctrl+c"))):
// https://github.com/ayn2op/tview/blob/a64fc48d7654432f71922c8b908280cdb525805c/application.go#L153
return tcell.NewEventKey(tcell.KeyCtrlC, "", tcell.ModNone)
}

View file

@ -68,6 +68,12 @@ type (
Theme string `toml:"theme"`
}
HelpConfig struct {
CompactModifiers bool `toml:"compact_modifiers"`
Padding [2]int `toml:"padding"`
Separator string `toml:"separator"`
}
Config struct {
AutoFocus bool `toml:"auto_focus"`
Mouse bool `toml:"mouse"`
@ -82,6 +88,7 @@ type (
MessagesLimit uint8 `toml:"messages_limit"`
Markdown MarkdownConfig `toml:"markdown"`
Help HelpConfig `toml:"help"`
Picker PickerConfig `toml:"picker"`
Timestamps Timestamps `toml:"timestamps"`
DateSeparator DateSeparator `toml:"date_separator"`
@ -113,7 +120,9 @@ func DefaultPath() string {
// Load reads the configuration file and parses it.
func Load(path string) (*Config, error) {
var cfg Config
cfg := Config{
Keybinds: defaultKeybinds(),
}
if err := toml.Unmarshal(defaultCfg, &cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal default config: %w", err)
}

View file

@ -26,6 +26,13 @@ enabled = true
# Theme for fenced code blocks. Available themes: https://xyproto.github.io/splash/docs
theme = "monokai"
[help]
# Show compact key modifiers in help, e.g. "^x" instead of "ctrl+x".
compact_modifiers = true
# [left, right]
padding = [1, 1]
separator = " • "
[picker]
width = 60
height = 20
@ -71,88 +78,89 @@ guild_store = "s-"
# Global shortcuts
# Esc: Reset message selection or close the channel selection popup.
[keybinds]
focus_guilds_tree = "Ctrl+G"
focus_messages_list = "Ctrl+T"
focus_message_input = "Ctrl+I"
toggle_help = "ctrl+."
focus_guilds_tree = "ctrl+g"
focus_messages_list = "ctrl+t"
focus_message_input = "ctrl+i"
# Cycle focus between the widgets.
focus_previous = "Ctrl+H"
focus_next = "Ctrl+L"
focus_previous = "ctrl+h"
focus_next = "ctrl+l"
# Hide/show the guilds tree.
toggle_guilds_tree = "Ctrl+B"
quit = "Ctrl+C"
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"
logout = "ctrl+d"
[keybinds.picker]
toggle = "Ctrl+K"
cancel = "Esc"
up = "Ctrl+P"
down = "Ctrl+N"
top = "Home"
bottom = "End"
select = "Enter"
toggle = "ctrl+k"
cancel = "esc"
up = "ctrl+p"
down = "ctrl+n"
top = "home"
bottom = "end"
select = "enter"
# Only while focusing on the guilds tree
[keybinds.guilds_tree]
up = "Rune[k]"
down = "Rune[j]"
top = "Rune[g]"
bottom = "Rune[G]"
up = "k"
down = "j"
top = "g"
bottom = "G"
# Select the currently highlighted text-based channel or expand a guild or channel.
select_current = "Enter"
yank_id = "Rune[i]"
collapse_parent_node = "Rune[-]"
move_to_parent_node = "Rune[p]"
select_current = "enter"
yank_id = "i"
collapse_parent_node = "-"
move_to_parent_node = "p"
# Only while focusing on sent messages
[keybinds.messages_list]
select_up = "Rune[k]"
select_down = "Rune[j]"
select_top = "Rune[g]"
select_bottom = "Rune[G]"
select_up = "k"
select_down = "j"
select_top = "g"
select_bottom = "G"
# Scroll the messages list without changing the selection.
scroll_up = "Rune[K]"
scroll_down = "Rune[J]"
scroll_top = "Home"
scroll_bottom = "End"
scroll_up = "K"
scroll_down = "J"
scroll_top = "home"
scroll_bottom = "end"
# Select the message reference (reply) of the selected channel.
select_reply = "Rune[s]"
select_reply = "s"
# Reply to the selected message.
reply = "Rune[R]"
reply = "R"
# Reply (with mention) to the selected message.
reply_mention = "Rune[r]"
cancel = "Esc"
edit = "Rune[e]"
delete = "Rune[D]"
delete_confirm = "Rune[d]"
reply_mention = "r"
cancel = "esc"
edit = "e"
delete = "D"
delete_confirm = "d"
# Open the selected message's attachments or hyperlinks in the message
# using the default browser application.
open = "Rune[o]"
open = "o"
# Yank (copy) the selected message's content/url/id.
yank_content = "Rune[y]"
yank_url = "Rune[u]"
yank_id = "Rune[i]"
yank_content = "y"
yank_url = "u"
yank_id = "i"
# Only while typing a message
# Alt+Enter: Insert a new line to the current text.
[keybinds.message_input]
# paste from clipboard (supports both text and images)
paste = "Ctrl+V"
send = "Enter"
paste = "ctrl+v"
send = "enter"
# Remove existing text or cancel reply.
cancel = "Esc"
cancel = "esc"
# Complete usernames when mentioning
tab_complete = "Tab"
tab_complete = "tab"
open_editor = "Ctrl+E"
open_file_picker = "Ctrl+Rune[\\]"
open_editor = "ctrl+e"
open_file_picker = "ctrl+\\"
[keybinds.mentions_list]
up = "Ctrl+P"
down = "Ctrl+N"
top = "Home"
bottom = "End"
up = "ctrl+p"
down = "ctrl+n"
top = "home"
bottom = "end"
# style = { foreground = "", background = "", attributes = "" or [""] }
[theme.title]
@ -213,3 +221,9 @@ max_height = 0
style = {}
# Background style: everything else behind the dialog
background_style = { attributes = "dim" }
[theme.help]
short_key_style = { attributes = "dim" }
short_desc_style = {}
full_key_style = { attributes = "dim" }
full_desc_style = {}

View file

@ -7,6 +7,7 @@ import (
"github.com/BurntSushi/toml"
"github.com/ayn2op/discordo/internal/consts"
tviewkeybind "github.com/ayn2op/tview/keybind"
"github.com/gdamore/tcell/v3"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
@ -84,7 +85,12 @@ func TestLoad(t *testing.T) {
}
applyDefaults(&defCfg)
if diff := cmp.Diff(defCfg, *cfg, cmpopts.EquateComparable(tcell.Style{})); diff != "" {
if diff := cmp.Diff(
defCfg,
*cfg,
cmpopts.EquateComparable(tcell.Style{}),
cmpopts.IgnoreUnexported(tviewkeybind.Keybind{}),
); diff != "" {
t.Fatalf("got = -, want = +, diff=%s", diff)
}
})

View file

@ -1,69 +1,104 @@
package config
import (
"github.com/BurntSushi/toml"
"github.com/ayn2op/tview/keybind"
)
type Keybind struct {
keybind.Keybind
}
var _ toml.Unmarshaler = (*Keybind)(nil)
func (k *Keybind) UnmarshalTOML(value any) error {
switch value := value.(type) {
case string:
k.SetKeys(value)
case []any:
keys := make([]string, 0, len(value))
for _, key := range value {
if key, ok := key.(string); ok {
keys = append(keys, key)
}
}
k.SetKeys(keys...)
}
return nil
}
func newKeybind(key, desc string) Keybind {
return Keybind{
Keybind: keybind.NewKeybind(
keybind.WithKeys(key),
keybind.WithHelp(key, desc),
),
}
}
type NavigationKeybinds struct {
Up string `toml:"up"`
Down string `toml:"down"`
Top string `toml:"top"`
Bottom string `toml:"bottom"`
Up Keybind `toml:"up"`
Down Keybind `toml:"down"`
Top Keybind `toml:"top"`
Bottom Keybind `toml:"bottom"`
}
type ScrollKeybinds struct {
ScrollUp string `toml:"scroll_up"`
ScrollDown string `toml:"scroll_down"`
ScrollTop string `toml:"scroll_top"`
ScrollBottom string `toml:"scroll_bottom"`
ScrollUp Keybind `toml:"scroll_up"`
ScrollDown Keybind `toml:"scroll_down"`
ScrollTop Keybind `toml:"scroll_top"`
ScrollBottom Keybind `toml:"scroll_bottom"`
}
type SelectionKeybinds struct {
SelectUp string `toml:"select_up"`
SelectDown string `toml:"select_down"`
SelectTop string `toml:"select_top"`
SelectBottom string `toml:"select_bottom"`
SelectUp Keybind `toml:"select_up"`
SelectDown Keybind `toml:"select_down"`
SelectTop Keybind `toml:"select_top"`
SelectBottom Keybind `toml:"select_bottom"`
}
type PickerKeybinds struct {
NavigationKeybinds
Toggle string `toml:"toggle"`
Cancel string `toml:"cancel"`
Select string `toml:"select"`
Select Keybind `toml:"select"`
Cancel Keybind `toml:"cancel"`
}
type GuildsTreeKeybinds struct {
NavigationKeybinds
SelectCurrent string `toml:"select_current"`
YankID string `toml:"yank_id"`
SelectCurrent Keybind `toml:"select_current"`
YankID Keybind `toml:"yank_id"`
CollapseParentNode string `toml:"collapse_parent_node"`
MoveToParentNode string `toml:"move_to_parent_node"`
CollapseParentNode Keybind `toml:"collapse_parent_node"`
MoveToParentNode Keybind `toml:"move_to_parent_node"`
}
type MessagesListKeybinds struct {
SelectionKeybinds
ScrollKeybinds
SelectReply string `toml:"select_reply"`
Reply string `toml:"reply"`
ReplyMention string `toml:"reply_mention"`
SelectReply Keybind `toml:"select_reply"`
Reply Keybind `toml:"reply"`
ReplyMention Keybind `toml:"reply_mention"`
Cancel string `toml:"cancel"`
Edit string `toml:"edit"`
Delete string `toml:"delete"`
DeleteConfirm string `toml:"delete_confirm"`
Open string `toml:"open"`
Cancel Keybind `toml:"cancel"`
Edit Keybind `toml:"edit"`
Delete Keybind `toml:"delete"`
DeleteConfirm Keybind `toml:"delete_confirm"`
Open Keybind `toml:"open"`
YankContent string `toml:"yank_content"`
YankURL string `toml:"yank_url"`
YankID string `toml:"yank_id"`
YankContent Keybind `toml:"yank_content"`
YankURL Keybind `toml:"yank_url"`
YankID Keybind `toml:"yank_id"`
}
type MessageInputKeybinds struct {
Paste string `toml:"paste"`
Send string `toml:"send"`
Cancel string `toml:"cancel"`
TabComplete string `toml:"tab_complete"`
Paste Keybind `toml:"paste"`
Send Keybind `toml:"send"`
Cancel Keybind `toml:"cancel"`
TabComplete Keybind `toml:"tab_complete"`
OpenEditor string `toml:"open_editor"`
OpenFilePicker string `toml:"open_file_picker"`
OpenEditor Keybind `toml:"open_editor"`
OpenFilePicker Keybind `toml:"open_file_picker"`
}
type MentionsListKeybinds struct {
@ -71,12 +106,16 @@ type MentionsListKeybinds struct {
}
type Keybinds struct {
FocusGuildsTree string `toml:"focus_guilds_tree"`
FocusMessagesList string `toml:"focus_messages_list"`
FocusMessageInput string `toml:"focus_message_input"`
FocusPrevious string `toml:"focus_previous"`
FocusNext string `toml:"focus_next"`
ToggleGuildsTree string `toml:"toggle_guilds_tree"`
ToggleGuildsTree Keybind `toml:"toggle_guilds_tree"`
ToggleChannelsPicker Keybind `toml:"toggle_channels_picker"`
ToggleHelp Keybind `toml:"toggle_help"`
FocusGuildsTree Keybind `toml:"focus_guilds_tree"`
FocusMessagesList Keybind `toml:"focus_messages_list"`
FocusMessageInput Keybind `toml:"focus_message_input"`
FocusPrevious Keybind `toml:"focus_previous"`
FocusNext Keybind `toml:"focus_next"`
Picker PickerKeybinds `toml:"picker"`
GuildsTree GuildsTreeKeybinds `toml:"guilds_tree"`
@ -84,6 +123,91 @@ type Keybinds struct {
MessageInput MessageInputKeybinds `toml:"message_input"`
MentionsList MentionsListKeybinds `toml:"mentions_list"`
Logout string `toml:"logout"`
Quit string `toml:"quit"`
Logout Keybind `toml:"logout"`
Quit Keybind `toml:"quit"`
}
func defaultKeybinds() Keybinds {
return Keybinds{
ToggleGuildsTree: newKeybind("ctrl+b", "toggle guilds"),
ToggleChannelsPicker: newKeybind("ctrl+k", "channels picker"),
ToggleHelp: newKeybind("ctrl+.", "help"),
FocusGuildsTree: newKeybind("ctrl+g", "guilds"),
FocusMessagesList: newKeybind("ctrl+t", "messages"),
FocusMessageInput: newKeybind("ctrl+i", "input"),
FocusPrevious: newKeybind("ctrl+h", "focus prev"),
FocusNext: newKeybind("ctrl+l", "focus next"),
Logout: newKeybind("ctrl+d", "logout"),
Quit: newKeybind("ctrl+c", "quit"),
Picker: PickerKeybinds{
NavigationKeybinds: NavigationKeybinds{
Up: newKeybind("ctrl+p", "up"),
Down: newKeybind("ctrl+n", "down"),
Top: newKeybind("home", "top"),
Bottom: newKeybind("end", "bottom"),
},
Cancel: newKeybind("esc", "cancel"),
Select: newKeybind("enter", "sel"),
},
GuildsTree: GuildsTreeKeybinds{
NavigationKeybinds: NavigationKeybinds{
Up: newKeybind("k", "up"),
Down: newKeybind("j", "down"),
Top: newKeybind("g", "top"),
Bottom: newKeybind("G", "bottom"),
},
SelectCurrent: newKeybind("enter", "sel"),
YankID: newKeybind("i", "copy id"),
CollapseParentNode: newKeybind("-", "collapse"),
MoveToParentNode: newKeybind("p", "parent"),
},
MessagesList: MessagesListKeybinds{
SelectionKeybinds: SelectionKeybinds{
SelectUp: newKeybind("k", "up"),
SelectDown: newKeybind("j", "down"),
SelectTop: newKeybind("g", "top"),
SelectBottom: newKeybind("G", "bottom"),
},
ScrollKeybinds: ScrollKeybinds{
ScrollUp: newKeybind("K", "scroll up"),
ScrollDown: newKeybind("J", "scroll down"),
ScrollTop: newKeybind("home", "scroll top"),
ScrollBottom: newKeybind("end", "scroll bottom"),
},
SelectReply: newKeybind("s", "sel reply"),
Reply: newKeybind("R", "reply"),
ReplyMention: newKeybind("r", "@reply"),
Cancel: newKeybind("esc", "cancel"),
Edit: newKeybind("e", "edit"),
Delete: newKeybind("D", "force delete"),
DeleteConfirm: newKeybind(
"d",
"delete",
),
Open: newKeybind("o", "open"),
YankContent: newKeybind("y", "copy text"),
YankURL: newKeybind("u", "copy url"),
YankID: newKeybind("i", "copy id"),
},
MessageInput: MessageInputKeybinds{
Paste: newKeybind("ctrl+v", "paste"),
Send: newKeybind("enter", "send"),
Cancel: newKeybind("esc", "cancel"),
TabComplete: newKeybind("tab", "complete"),
OpenEditor: newKeybind("ctrl+e", "editor"),
OpenFilePicker: newKeybind("ctrl+\\", "attach"),
},
MentionsList: MentionsListKeybinds{
NavigationKeybinds: NavigationKeybinds{
Up: newKeybind("ctrl+p", "up"),
Down: newKeybind("ctrl+n", "down"),
Top: newKeybind("home", "top"),
Bottom: newKeybind("end", "bottom"),
},
},
}
}

View file

@ -147,6 +147,13 @@ func (vw *ScrollBarVisibilityWrapper) UnmarshalTOML(val any) error {
return nil
}
type HelpTheme struct {
ShortKeyStyle StyleWrapper `toml:"short_key_style"`
ShortDescStyle StyleWrapper `toml:"short_desc_style"`
FullKeyStyle StyleWrapper `toml:"full_key_style"`
FullDescStyle StyleWrapper `toml:"full_desc_style"`
}
type (
ThemeStyle struct {
NormalStyle StyleWrapper `toml:"normal_style"`
@ -217,6 +224,7 @@ type (
MessagesList MessagesListTheme `toml:"messages_list"`
MentionsList MentionsListTheme `toml:"mentions_list"`
Dialog DialogTheme `toml:"dialog"`
Help HelpTheme `toml:"help"`
}
)

View file

@ -8,6 +8,8 @@ import (
"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/diamondburned/arikawa/v3/discord"
)
@ -16,6 +18,8 @@ type channelsPicker struct {
chatView *View
}
var _ help.KeyMap = (*channelsPicker)(nil)
func newChannelsPicker(cfg *config.Config, chatView *View) *channelsPicker {
cp := &channelsPicker{picker.New(), chatView}
cp.Box = ui.ConfigureBox(tview.NewBox(), &cfg.Theme)
@ -36,12 +40,12 @@ func newChannelsPicker(cfg *config.Config, chatView *View) *channelsPicker {
SetThumbStyle(cfg.Theme.ScrollBar.ThumbStyle.Style).
SetGlyphSet(cfg.Theme.ScrollBar.GlyphSet.GlyphSet))
cp.SetKeyMap(&picker.KeyMap{
Cancel: cfg.Keybinds.Picker.Cancel,
Up: cfg.Keybinds.Picker.Up,
Down: cfg.Keybinds.Picker.Down,
Top: cfg.Keybinds.Picker.Top,
Bottom: cfg.Keybinds.Picker.Bottom,
Select: cfg.Keybinds.Picker.Select,
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,
Select: cfg.Keybinds.Picker.Select.Keybind,
})
return cp
}
@ -121,3 +125,16 @@ func (cp *channelsPicker) addChannel(guild *discord.Guild, channel discord.Chann
name := b.String()
cp.AddItem(picker.Item{Text: name, FilterText: name, Reference: channel.ID})
}
func (cp *channelsPicker) ShortHelp() []keybind.Keybind {
cfg := cp.chatView.cfg.Keybinds.Picker
return []keybind.Keybind{cfg.Up.Keybind, cfg.Down.Keybind, cfg.Select.Keybind, cfg.Cancel.Keybind}
}
func (cp *channelsPicker) FullHelp() [][]keybind.Keybind {
cfg := cp.chatView.cfg.Keybinds.Picker
return [][]keybind.Keybind{
{cfg.Up.Keybind, cfg.Down.Keybind, cfg.Top.Keybind, cfg.Bottom.Keybind},
{cfg.Select.Keybind, cfg.Cancel.Keybind},
}
}

View file

@ -8,6 +8,8 @@ import (
"github.com/ayn2op/discordo/internal/config"
"github.com/ayn2op/discordo/internal/ui"
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/help"
"github.com/ayn2op/tview/keybind"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/gateway"
"github.com/diamondburned/ningen/v3"
@ -29,6 +31,8 @@ type guildsTree struct {
dmRootNode *tview.TreeNode
}
var _ help.KeyMap = (*guildsTree)(nil)
func newGuildsTree(cfg *config.Config, chatView *View) *guildsTree {
gt := &guildsTree{
TreeView: tview.NewTreeView(),
@ -52,6 +56,86 @@ func newGuildsTree(cfg *config.Config, chatView *View) *guildsTree {
return gt
}
func (gt *guildsTree) ShortHelp() []keybind.Keybind {
cfg := gt.cfg.Keybinds.GuildsTree
selectCurrent := cfg.SelectCurrent.Keybind
collapseParent := cfg.CollapseParentNode.Keybind
selectHelp := selectCurrent.Help()
selectDesc := selectHelp.Desc
if node := gt.GetCurrentNode(); node != nil {
if len(node.GetChildren()) > 0 {
if node.IsExpanded() {
selectDesc = "collapse"
} else {
selectDesc = "expand"
}
} else {
switch node.GetReference().(type) {
case discord.GuildID, dmNode:
selectDesc = "expand"
}
}
}
selectCurrent.SetHelp(selectHelp.Key, selectDesc)
collapseHelp := collapseParent.Help()
collapseParent.SetHelp(collapseHelp.Key, "collapse parent")
shortHelp := []keybind.Keybind{cfg.Up.Keybind, cfg.Down.Keybind, selectCurrent}
if gt.canCollapseParent(gt.GetCurrentNode()) {
shortHelp = append(shortHelp, collapseParent)
}
return shortHelp
}
func (gt *guildsTree) FullHelp() [][]keybind.Keybind {
cfg := gt.cfg.Keybinds.GuildsTree
selectCurrent := cfg.SelectCurrent.Keybind
collapseParent := cfg.CollapseParentNode.Keybind
selectHelp := selectCurrent.Help()
selectDesc := selectHelp.Desc
if node := gt.GetCurrentNode(); node != nil {
if len(node.GetChildren()) > 0 {
if node.IsExpanded() {
selectDesc = "collapse"
} else {
selectDesc = "expand"
}
} else {
switch node.GetReference().(type) {
case discord.GuildID, dmNode:
selectDesc = "expand"
}
}
}
selectCurrent.SetHelp(selectHelp.Key, selectDesc)
collapseHelp := collapseParent.Help()
collapseParent.SetHelp(collapseHelp.Key, "collapse parent")
actions := []keybind.Keybind{selectCurrent, cfg.MoveToParentNode.Keybind}
if gt.canCollapseParent(gt.GetCurrentNode()) {
actions = append(actions, collapseParent)
}
return [][]keybind.Keybind{
{cfg.Up.Keybind, cfg.Down.Keybind, cfg.Top.Keybind, cfg.Bottom.Keybind},
actions,
{cfg.YankID.Keybind},
}
}
func (gt *guildsTree) canCollapseParent(node *tview.TreeNode) bool {
if node == nil {
return false
}
path := gt.GetPath(node)
// Path layout is [root, ..., node]. A non-root parent means at least 3 nodes.
if len(path) < 3 {
return false
}
parent := path[len(path)-2]
return parent != nil && parent.GetLevel() != 0
}
func (gt *guildsTree) resetNodeIndex() {
// Keep allocated map capacity; READY can rebuild often during reconnects.
clear(gt.guildNodeByID)
@ -284,28 +368,28 @@ func (gt *guildsTree) collapseParentNode(node *tview.TreeNode) {
}
func (gt *guildsTree) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
switch event.Name() {
case gt.cfg.Keybinds.GuildsTree.CollapseParentNode:
switch {
case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.CollapseParentNode.Keybind):
gt.collapseParentNode(gt.GetCurrentNode())
return nil
case gt.cfg.Keybinds.GuildsTree.MoveToParentNode:
case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.MoveToParentNode.Keybind):
return tcell.NewEventKey(tcell.KeyRune, "K", tcell.ModNone)
case gt.cfg.Keybinds.GuildsTree.Up:
case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Up.Keybind):
return tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone)
case gt.cfg.Keybinds.GuildsTree.Down:
case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Down.Keybind):
return tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone)
case gt.cfg.Keybinds.GuildsTree.Top:
case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Top.Keybind):
gt.Move(gt.GetRowCount() * -1)
// return tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModNone)
case gt.cfg.Keybinds.GuildsTree.Bottom:
case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Bottom.Keybind):
gt.Move(gt.GetRowCount())
// return tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModNone)
case gt.cfg.Keybinds.GuildsTree.SelectCurrent:
case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.SelectCurrent.Keybind):
return tcell.NewEventKey(tcell.KeyEnter, "", tcell.ModNone)
case gt.cfg.Keybinds.GuildsTree.YankID:
case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.YankID.Keybind):
gt.yankID()
}

View file

@ -0,0 +1,71 @@
package chat
import (
"github.com/ayn2op/tview/help"
"github.com/ayn2op/tview/keybind"
)
var _ help.KeyMap = (*View)(nil)
func (v *View) ShortHelp() []keybind.Keybind {
short := make([]keybind.Keybind, 0, 16)
if active := v.activeKeyMap(); active != nil {
short = append(short, active.ShortHelp()...)
}
short = append(short, v.baseShortHelp()...)
return short
}
func (v *View) FullHelp() [][]keybind.Keybind {
full := make([][]keybind.Keybind, 0, 8)
if active := v.activeKeyMap(); active != nil {
full = append(full, active.FullHelp()...)
}
full = append(full, v.baseFullHelp()...)
return full
}
func (v *View) activeKeyMap() help.KeyMap {
if v.GetVisible(channelsPickerLayerName) {
return v.channelsPicker
}
if v.app == nil {
return nil
}
switch v.app.GetFocus() {
case v.guildsTree:
return v.guildsTree
case v.messagesList:
return v.messagesList
case v.messageInput:
return v.messageInput
default:
return nil
}
}
func (v *View) baseShortHelp() []keybind.Keybind {
cfg := v.cfg.Keybinds
short := []keybind.Keybind{cfg.FocusGuildsTree.Keybind, cfg.FocusMessagesList.Keybind}
if !v.messageInput.GetDisabled() {
short = append(short, cfg.FocusMessageInput.Keybind)
}
short = append(short, cfg.ToggleGuildsTree.Keybind, cfg.ToggleChannelsPicker.Keybind, cfg.ToggleHelp.Keybind)
return short
}
func (v *View) baseFullHelp() [][]keybind.Keybind {
cfg := v.cfg.Keybinds
focus := []keybind.Keybind{cfg.FocusGuildsTree.Keybind, cfg.FocusMessagesList.Keybind}
if !v.messageInput.GetDisabled() {
focus = append(focus, cfg.FocusMessageInput.Keybind)
}
return [][]keybind.Keybind{
focus,
{cfg.FocusPrevious.Keybind, cfg.FocusNext.Keybind},
{cfg.ToggleGuildsTree.Keybind, cfg.ToggleChannelsPicker.Keybind},
{cfg.ToggleHelp.Keybind, cfg.Logout.Keybind, cfg.Quit.Keybind},
}
}

View file

@ -19,6 +19,8 @@ import (
"github.com/ayn2op/discordo/internal/consts"
"github.com/ayn2op/discordo/internal/ui"
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/help"
"github.com/ayn2op/tview/keybind"
"github.com/ayn2op/tview/layers"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/discord"
@ -52,6 +54,8 @@ type messageInput struct {
typingTimer *time.Timer
}
var _ help.KeyMap = (*messageInput)(nil)
func newMessageInput(cfg *config.Config, chatView *View) *messageInput {
mi := &messageInput{
TextArea: tview.NewTextArea(),
@ -90,12 +94,12 @@ func (mi *messageInput) stopTypingTimer() {
}
func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
switch event.Name() {
case mi.cfg.Keybinds.MessageInput.Paste:
switch {
case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Paste.Keybind):
mi.paste()
return tcell.NewEventKey(tcell.KeyCtrlV, "", tcell.ModNone)
case mi.cfg.Keybinds.MessageInput.Send:
case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Send.Keybind):
if mi.chatView.GetVisible(mentionsListLayerName) {
mi.tabComplete()
return nil
@ -103,15 +107,15 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
mi.send()
return nil
case mi.cfg.Keybinds.MessageInput.OpenEditor:
case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.OpenEditor.Keybind):
mi.stopTabCompletion()
mi.editor()
return nil
case mi.cfg.Keybinds.MessageInput.OpenFilePicker:
case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.OpenFilePicker.Keybind):
mi.stopTabCompletion()
mi.openFilePicker()
return nil
case mi.cfg.Keybinds.MessageInput.Cancel:
case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Cancel.Keybind):
if mi.chatView.GetVisible(mentionsListLayerName) {
mi.stopTabCompletion()
} else {
@ -119,7 +123,7 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
}
return nil
case mi.cfg.Keybinds.MessageInput.TabComplete:
case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.TabComplete.Keybind):
go mi.chatView.app.QueueUpdateDraw(func() { mi.tabComplete() })
return nil
default:
@ -139,17 +143,17 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
if mi.cfg.AutocompleteLimit > 0 {
if mi.chatView.GetVisible(mentionsListLayerName) {
handler := mi.mentionsList.InputHandler()
switch event.Name() {
case mi.cfg.Keybinds.MentionsList.Up:
switch {
case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Up.Keybind):
handler(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone), nil)
return nil
case mi.cfg.Keybinds.MentionsList.Down:
case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Down.Keybind):
handler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone), nil)
return nil
case mi.cfg.Keybinds.MentionsList.Top:
case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Top.Keybind):
handler(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone), nil)
return nil
case mi.cfg.Keybinds.MentionsList.Bottom:
case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Bottom.Keybind):
handler(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone), nil)
return nil
}
@ -676,3 +680,44 @@ func (mi *messageInput) attach(name string, reader io.Reader) {
}
mi.SetFooter("Attached " + humanJoin(names))
}
func (mi *messageInput) ShortHelp() []keybind.Keybind {
if mi.chatView.GetVisible(mentionsListLayerName) {
cfg := mi.cfg.Keybinds.MentionsList
icfg := mi.cfg.Keybinds.MessageInput
short := []keybind.Keybind{cfg.Up.Keybind, cfg.Down.Keybind, icfg.Cancel.Keybind}
if selected := mi.chatView.SelectedChannel(); selected != nil && mi.chatView.state.HasPermissions(selected.ID, discord.PermissionAttachFiles) {
short = append(short, icfg.OpenFilePicker.Keybind)
}
return short
}
cfg := mi.cfg.Keybinds.MessageInput
short := []keybind.Keybind{cfg.Send.Keybind, cfg.Cancel.Keybind, cfg.Paste.Keybind, cfg.OpenEditor.Keybind}
if selected := mi.chatView.SelectedChannel(); selected != nil && mi.chatView.state.HasPermissions(selected.ID, discord.PermissionAttachFiles) {
short = append(short, cfg.OpenFilePicker.Keybind)
}
return short
}
func (mi *messageInput) FullHelp() [][]keybind.Keybind {
if mi.chatView.GetVisible(mentionsListLayerName) {
mcfg := mi.cfg.Keybinds.MentionsList
icfg := mi.cfg.Keybinds.MessageInput
return [][]keybind.Keybind{
{mcfg.Up.Keybind, mcfg.Down.Keybind, mcfg.Top.Keybind, mcfg.Bottom.Keybind},
{icfg.TabComplete.Keybind, icfg.Cancel.Keybind},
}
}
cfg := mi.cfg.Keybinds.MessageInput
openEditor := []keybind.Keybind{cfg.Paste.Keybind, cfg.OpenEditor.Keybind}
if selected := mi.chatView.SelectedChannel(); selected != nil && mi.chatView.state.HasPermissions(selected.ID, discord.PermissionAttachFiles) {
openEditor = append(openEditor, cfg.OpenFilePicker.Keybind)
}
return [][]keybind.Keybind{
{cfg.Send.Keybind, cfg.Cancel.Keybind, cfg.TabComplete.Keybind},
openEditor,
}
}

View file

@ -22,6 +22,8 @@ import (
"github.com/ayn2op/discordo/internal/markdown"
"github.com/ayn2op/discordo/internal/ui"
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/help"
"github.com/ayn2op/tview/keybind"
"github.com/diamondburned/arikawa/v3/api"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/gateway"
@ -58,6 +60,8 @@ type messagesList struct {
}
}
var _ help.KeyMap = (*messagesList)(nil)
type messagesListRowKind uint8
const (
@ -523,52 +527,64 @@ func (ml *messagesList) selectedMessage() (*discord.Message, error) {
}
func (ml *messagesList) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
switch event.Name() {
case ml.cfg.Keybinds.MessagesList.ScrollUp:
switch {
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollUp.Keybind):
ml.ScrollUp()
return nil
case ml.cfg.Keybinds.MessagesList.ScrollDown:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollDown.Keybind):
ml.ScrollDown()
return nil
case ml.cfg.Keybinds.MessagesList.ScrollTop:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollTop.Keybind):
ml.ScrollToStart()
return nil
case ml.cfg.Keybinds.MessagesList.ScrollBottom:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollBottom.Keybind):
ml.ScrollToEnd()
return nil
case ml.cfg.Keybinds.MessagesList.Cancel:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Cancel.Keybind):
ml.clearSelection()
return nil
case ml.cfg.Keybinds.MessagesList.SelectUp, ml.cfg.Keybinds.MessagesList.SelectDown, ml.cfg.Keybinds.MessagesList.SelectTop, ml.cfg.Keybinds.MessagesList.SelectBottom, ml.cfg.Keybinds.MessagesList.SelectReply:
ml._select(event.Name())
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectUp.Keybind):
ml.selectUp()
return nil
case ml.cfg.Keybinds.MessagesList.YankID:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectDown.Keybind):
ml.selectDown()
return nil
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectTop.Keybind):
ml.selectTop()
return nil
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectBottom.Keybind):
ml.selectBottom()
return nil
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectReply.Keybind):
ml.selectReply()
return nil
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.YankID.Keybind):
ml.yankID()
return nil
case ml.cfg.Keybinds.MessagesList.YankContent:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.YankContent.Keybind):
ml.yankContent()
return nil
case ml.cfg.Keybinds.MessagesList.YankURL:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.YankURL.Keybind):
ml.yankURL()
return nil
case ml.cfg.Keybinds.MessagesList.Open:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Open.Keybind):
ml.open()
return nil
case ml.cfg.Keybinds.MessagesList.Reply:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Reply.Keybind):
ml.reply(false)
return nil
case ml.cfg.Keybinds.MessagesList.ReplyMention:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ReplyMention.Keybind):
ml.reply(true)
return nil
case ml.cfg.Keybinds.MessagesList.Edit:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Edit.Keybind):
ml.edit()
return nil
case ml.cfg.Keybinds.MessagesList.Delete:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Delete.Keybind):
ml.delete()
return nil
case ml.cfg.Keybinds.MessagesList.DeleteConfirm:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.DeleteConfirm.Keybind):
ml.confirmDelete()
return nil
}
@ -576,57 +592,81 @@ func (ml *messagesList) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
return event
}
func (ml *messagesList) _select(name string) {
func (ml *messagesList) selectUp() {
messages := ml.messages
if len(messages) == 0 {
return
}
cursor := ml.Cursor()
switch name {
case ml.cfg.Keybinds.MessagesList.SelectUp:
switch {
case cursor == -1:
cursor = len(messages) - 1
case cursor > 0:
cursor--
case cursor == 0:
added := ml.prependOlderMessages()
if added == 0 {
return
}
cursor = added - 1
}
case ml.cfg.Keybinds.MessagesList.SelectDown:
switch {
case cursor == -1:
cursor = len(messages) - 1
case cursor < len(messages)-1:
cursor++
}
case ml.cfg.Keybinds.MessagesList.SelectTop:
cursor = 0
case ml.cfg.Keybinds.MessagesList.SelectBottom:
switch {
case cursor == -1:
cursor = len(messages) - 1
case ml.cfg.Keybinds.MessagesList.SelectReply:
if cursor == -1 || cursor >= len(messages) {
case cursor > 0:
cursor--
case cursor == 0:
added := ml.prependOlderMessages()
if added == 0 {
return
}
if ref := messages[cursor].ReferencedMessage; ref != nil {
refIdx := slices.IndexFunc(messages, func(m discord.Message) bool {
return m.ID == ref.ID
})
if refIdx != -1 {
cursor = refIdx
}
}
cursor = added - 1
}
ml.SetCursor(cursor)
}
func (ml *messagesList) selectDown() {
messages := ml.messages
if len(messages) == 0 {
return
}
cursor := ml.Cursor()
switch {
case cursor == -1:
cursor = len(messages) - 1
case cursor < len(messages)-1:
cursor++
}
ml.SetCursor(cursor)
}
func (ml *messagesList) selectTop() {
if len(ml.messages) == 0 {
return
}
ml.SetCursor(0)
}
func (ml *messagesList) selectBottom() {
if len(ml.messages) == 0 {
return
}
ml.SetCursor(len(ml.messages) - 1)
}
func (ml *messagesList) selectReply() {
messages := ml.messages
if len(messages) == 0 {
return
}
cursor := ml.Cursor()
if cursor == -1 || cursor >= len(messages) {
return
}
if ref := messages[cursor].ReferencedMessage; ref != nil {
refIdx := slices.IndexFunc(messages, func(m discord.Message) bool {
return m.ID == ref.ID
})
if refIdx != -1 {
ml.SetCursor(refIdx)
}
}
}
func (ml *messagesList) prependOlderMessages() int {
selectedChannel := ml.chatView.SelectedChannel()
if selectedChannel == nil {
@ -815,16 +855,16 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord
list.SetCursor(0)
}
list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Name() {
case ml.cfg.Keybinds.MessagesList.SelectUp:
switch {
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectUp.Keybind):
return tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone)
case ml.cfg.Keybinds.MessagesList.SelectDown:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectDown.Keybind):
return tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone)
case ml.cfg.Keybinds.MessagesList.SelectTop:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectTop.Keybind):
return tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone)
case ml.cfg.Keybinds.MessagesList.SelectBottom:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectBottom.Keybind):
return tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone)
case ml.cfg.Keybinds.MessagesList.Cancel:
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Cancel.Keybind):
closeList()
return nil
}
@ -1061,3 +1101,72 @@ func (ml *messagesList) waitForChunkEvent() uint {
<-ml.fetchingMembers.done
return ml.fetchingMembers.count
}
func (ml *messagesList) ShortHelp() []keybind.Keybind {
cfg := ml.cfg.Keybinds.MessagesList
help := []keybind.Keybind{
cfg.SelectUp.Keybind,
cfg.SelectDown.Keybind,
cfg.Cancel.Keybind,
}
if msg, err := ml.selectedMessage(); err == nil {
me, _ := ml.chatView.state.Cabinet.Me()
if msg.Author.ID != me.ID {
help = append(help, cfg.Reply.Keybind)
}
}
return help
}
func (ml *messagesList) FullHelp() [][]keybind.Keybind {
cfg := ml.cfg.Keybinds.MessagesList
canSelectReply := false
canReply := false
canEdit := false
canDelete := false
canOpen := false
if msg, err := ml.selectedMessage(); err == nil {
canSelectReply = msg.ReferencedMessage != nil
canOpen = len(extractURLs(msg.Content)) != 0 || len(msg.Attachments) != 0
me, _ := ml.chatView.state.Cabinet.Me()
canReply = msg.Author.ID != me.ID
canEdit = msg.Author.ID == me.ID
canDelete = canEdit
if !canDelete {
selected := ml.chatView.SelectedChannel()
canDelete = selected != nil && ml.chatView.state.HasPermissions(selected.ID, discord.PermissionManageMessages)
}
}
actions := make([]keybind.Keybind, 0, 4)
if canReply {
actions = append(actions, cfg.Reply.Keybind, cfg.ReplyMention.Keybind)
}
if canSelectReply {
actions = append(actions, cfg.SelectReply.Keybind)
}
actions = append(actions, cfg.Cancel.Keybind)
manage := make([]keybind.Keybind, 0, 4)
if canEdit {
manage = append(manage, cfg.Edit.Keybind)
}
if canDelete {
manage = append(manage, cfg.DeleteConfirm.Keybind, cfg.Delete.Keybind)
}
if canOpen {
manage = append(manage, cfg.Open.Keybind)
}
return [][]keybind.Keybind{
{cfg.SelectUp.Keybind, cfg.SelectDown.Keybind, cfg.SelectTop.Keybind, cfg.SelectBottom.Keybind},
{cfg.ScrollUp.Keybind, cfg.ScrollDown.Keybind, cfg.ScrollTop.Keybind, cfg.ScrollBottom.Keybind},
actions,
manage,
{cfg.YankContent.Keybind, cfg.YankURL.Keybind, cfg.YankID.Keybind},
}
}

View file

@ -2,7 +2,6 @@ package chat
import (
"fmt"
"github.com/ayn2op/tview/layers"
"log/slog"
"sync"
"time"
@ -11,6 +10,9 @@ import (
"github.com/ayn2op/discordo/internal/keyring"
"github.com/ayn2op/discordo/internal/ui"
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/help"
"github.com/ayn2op/tview/keybind"
"github.com/ayn2op/tview/layers"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/ningen/v3"
"github.com/diamondburned/ningen/v3/states/read"
@ -30,6 +32,7 @@ const (
type View struct {
*layers.Layers
rootFlex *tview.Flex
mainFlex *tview.Flex
rightFlex *tview.Flex
@ -37,6 +40,7 @@ type View struct {
messagesList *messagesList
messageInput *messageInput
channelsPicker *channelsPicker
help *help.Help
selectedChannel *discord.Channel
selectedChannelMu sync.RWMutex
@ -55,6 +59,7 @@ func NewView(app *tview.Application, cfg *config.Config, onLogout func()) *View
v := &View{
Layers: layers.New(),
rootFlex: tview.NewFlex(),
mainFlex: tview.NewFlex(),
rightFlex: tview.NewFlex(),
@ -71,6 +76,20 @@ func NewView(app *tview.Application, cfg *config.Config, onLogout func()) *View
v.channelsPicker = newChannelsPicker(cfg, v)
v.channelsPicker.SetCancelFunc(v.closePicker)
v.help = help.New()
styles := help.DefaultStyles()
styles.ShortKeyStyle = cfg.Theme.Help.ShortKeyStyle.Style
styles.ShortDescStyle = cfg.Theme.Help.ShortDescStyle.Style
styles.FullKeyStyle = cfg.Theme.Help.FullKeyStyle.Style
styles.FullDescStyle = cfg.Theme.Help.FullDescStyle.Style
v.help.SetStyles(styles)
v.help.SetKeyMap(v)
v.help.SetCompactModifiers(cfg.Help.CompactModifiers)
v.help.SetShortSeparator(cfg.Help.Separator)
v.help.SetBorderPadding(0, 0, cfg.Help.Padding[0], cfg.Help.Padding[1])
v.SetBackgroundLayerStyle(v.cfg.Theme.Dialog.BackgroundStyle.Style)
v.SetInputCapture(v.onInputCapture)
v.buildLayout()
@ -91,6 +110,7 @@ func (v *View) SetSelectedChannel(channel *discord.Channel) {
func (v *View) buildLayout() {
v.Clear()
v.rootFlex.Clear()
v.rightFlex.Clear()
v.mainFlex.Clear()
@ -103,7 +123,13 @@ func (v *View) buildLayout() {
AddItem(v.guildsTree, 0, 1, true).
AddItem(v.rightFlex, 0, 4, false)
v.AddLayer(v.mainFlex, layers.WithName(flexLayerName), layers.WithResize(true), layers.WithVisible(true))
v.rootFlex.
SetDirection(tview.FlexRow).
AddItem(v.mainFlex, 0, 1, true).
AddItem(v.help, 1, 0, false)
v.updateHelpHeight()
v.AddLayer(v.rootFlex, layers.WithName(flexLayerName), layers.WithResize(true), layers.WithVisible(true))
}
func (v *View) togglePicker() {
@ -197,25 +223,29 @@ func (v *View) focusNext() {
}
func (v *View) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
switch event.Name() {
case v.cfg.Keybinds.FocusGuildsTree:
switch {
case keybind.Matches(event, v.cfg.Keybinds.ToggleHelp.Keybind):
v.help.SetShowAll(!v.help.ShowAll())
v.updateHelpHeight()
return nil
case keybind.Matches(event, v.cfg.Keybinds.FocusGuildsTree.Keybind):
v.messageInput.removeMentionsList()
v.focusGuildsTree()
return nil
case v.cfg.Keybinds.FocusMessagesList:
case keybind.Matches(event, v.cfg.Keybinds.FocusMessagesList.Keybind):
v.messageInput.removeMentionsList()
v.app.SetFocus(v.messagesList)
return nil
case v.cfg.Keybinds.FocusMessageInput:
case keybind.Matches(event, v.cfg.Keybinds.FocusMessageInput.Keybind):
v.focusMessageInput()
return nil
case v.cfg.Keybinds.FocusPrevious:
case keybind.Matches(event, v.cfg.Keybinds.FocusPrevious.Keybind):
v.focusPrevious()
return nil
case v.cfg.Keybinds.FocusNext:
case keybind.Matches(event, v.cfg.Keybinds.FocusNext.Keybind):
v.focusNext()
return nil
case v.cfg.Keybinds.Logout:
case keybind.Matches(event, v.cfg.Keybinds.Logout.Keybind):
if v.onLogout != nil {
v.onLogout()
}
@ -226,10 +256,10 @@ func (v *View) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
}
return nil
case v.cfg.Keybinds.ToggleGuildsTree:
case keybind.Matches(event, v.cfg.Keybinds.ToggleGuildsTree.Keybind):
v.toggleGuildsTree()
return nil
case v.cfg.Keybinds.Picker.Toggle:
case keybind.Matches(event, v.cfg.Keybinds.ToggleChannelsPicker.Keybind):
v.togglePicker()
return nil
}
@ -237,6 +267,17 @@ func (v *View) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
return event
}
func (v *View) updateHelpHeight() {
height := 1
if v.help.ShowAll() {
height = len(v.help.FullHelpLines(v.FullHelp(), 0))
if height < 1 {
height = 1
}
}
v.rootFlex.ResizeItem(v.help, height, 0)
}
func (v *View) showConfirmModal(prompt string, buttons []string, onDone func(label string)) {
previousFocus := v.app.GetFocus()

View file

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

View file

@ -2,6 +2,7 @@ package picker
import (
"github.com/ayn2op/tview"
"github.com/ayn2op/tview/keybind"
"github.com/gdamore/tcell/v3"
"github.com/sahilm/fuzzy"
)
@ -154,23 +155,23 @@ func (p *Picker) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
}
handler := p.list.InputHandler()
switch event.Name() {
case p.keyMap.Up:
switch {
case keybind.Matches(event, p.keyMap.Up):
handler(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone), nil)
return nil
case p.keyMap.Down:
case keybind.Matches(event, p.keyMap.Down):
handler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone), nil)
return nil
case p.keyMap.Top:
case keybind.Matches(event, p.keyMap.Top):
handler(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone), nil)
return nil
case p.keyMap.Bottom:
case keybind.Matches(event, p.keyMap.Bottom):
handler(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone), nil)
case p.keyMap.Select:
case keybind.Matches(event, p.keyMap.Select):
p.onListSelected(p.list.Cursor())
return nil
case p.keyMap.Cancel:
case keybind.Matches(event, p.keyMap.Cancel):
if p.onCancel != nil {
p.onCancel()
}