From 9dc509e3006b337fcb57513ce1a90704faa6c869 Mon Sep 17 00:00:00 2001 From: ayn2op Date: Mon, 28 Sep 2026 16:59:23 -0400 Subject: [PATCH] refactor(ui): adopt tview Change and Keybind APIs --- go.mod | 2 +- go.sum | 2 + internal/config/help.go | 19 +++++ internal/ui/chat/attachmentspicker/model.go | 10 +-- internal/ui/chat/channelspicker/model.go | 10 +-- internal/ui/chat/composer/model.go | 20 ++++-- internal/ui/chat/guildstree/model.go | 15 ++-- internal/ui/chat/keymap.go | 3 +- internal/ui/chat/mentionslist/model.go | 16 ++--- internal/ui/chat/messageslist/model.go | 15 ++-- internal/ui/keybind.go | 77 +++++++++++++++++++++ internal/ui/login/form/model.go | 6 +- internal/ui/login/keymap.go | 21 +++++- internal/ui/login/model.go | 4 +- internal/ui/login/qr/model.go | 6 +- internal/ui/util.go | 17 +---- 16 files changed, 163 insertions(+), 80 deletions(-) create mode 100644 internal/config/help.go create mode 100644 internal/ui/keybind.go diff --git a/go.mod b/go.mod index be1cc63..d5c7602 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/andybalholm/brotli v1.2.5 github.com/ayn2op/arikawa/v3 v3.0.0-20260904042054-7df757872273 github.com/ayn2op/ningen/v3 v3.0.1-0.20260904045623-5978c9be899f - github.com/ayn2op/tview v0.0.0-20260928003911-758a51d24cad + github.com/ayn2op/tview v0.0.0-20260928205525-fa36e0ece3f0 github.com/bogdanfinn/fhttp v0.6.9 github.com/bogdanfinn/tls-client v1.16.0 github.com/gdamore/tcell/v3 v3.5.0 diff --git a/go.sum b/go.sum index 73e68e3..f0a9a22 100644 --- a/go.sum +++ b/go.sum @@ -164,3 +164,5 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/ayn2op/tview v0.0.0-20260928205525-fa36e0ece3f0 h1:mh5nA4XFDLYLmLs++EhN2X+6LODI0tTbRRQFp4Q4F/A= +github.com/ayn2op/tview v0.0.0-20260928205525-fa36e0ece3f0/go.mod h1:oRPTSZgaEJPhv3dmmNvo6grBq/BemrLG3wFQLMTZJkY= diff --git a/internal/config/help.go b/internal/config/help.go new file mode 100644 index 0000000..e2d0ccd --- /dev/null +++ b/internal/config/help.go @@ -0,0 +1,19 @@ +package config + +import ( + "github.com/ayn2op/tview/help" + "github.com/ayn2op/tview/keybind" +) + +var _ help.KeyMap = PickerKeybinds{} + +func (k PickerKeybinds) ShortHelp() []keybind.Keybind { + return []keybind.Keybind{k.SelectUp.Keybind, k.SelectDown.Keybind, k.Select.Keybind, k.Cancel.Keybind} +} + +func (k PickerKeybinds) FullHelp() [][]keybind.Keybind { + return [][]keybind.Keybind{ + {k.SelectUp.Keybind, k.SelectDown.Keybind, k.SelectTop.Keybind, k.SelectBottom.Keybind}, + {k.Select.Keybind, k.Cancel.Keybind}, + } +} diff --git a/internal/ui/chat/attachmentspicker/model.go b/internal/ui/chat/attachmentspicker/model.go index e21dbdc..e327a4f 100644 --- a/internal/ui/chat/attachmentspicker/model.go +++ b/internal/ui/chat/attachmentspicker/model.go @@ -24,15 +24,15 @@ func NewModel(cfg *config.Config) Model { var _ tview.Model[Model] = Model{} -// actionMsg changes the picker. -type actionMsg picker.Action +// changeMsg changes the picker. +type changeMsg picker.Change func (Model) Init() tview.Cmd { return nil } // View shows the picker in a box titled Attachments. func (m Model) View() tview.Element { p := ui.Picker(m.items, &m.searchState, m.cfg). - OnAction(func(a picker.Action) tview.Msg { return actionMsg(a) }). + OnChange(func(a picker.Change) tview.Msg { return changeMsg(a) }). OnSelect(func(item picker.Item) tview.Msg { action, ok := item.Reference.(tview.Cmd) if !ok { @@ -54,8 +54,8 @@ func (m *Model) SetItems(items []Item) { } func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) { - if msg, ok := msg.(actionMsg); ok { - m.searchState.Perform(picker.Action(msg)) + if msg, ok := msg.(changeMsg); ok { + m.searchState.Apply(picker.Change(msg)) } return m, nil } diff --git a/internal/ui/chat/channelspicker/model.go b/internal/ui/chat/channelspicker/model.go index 2a125ce..32be83d 100644 --- a/internal/ui/chat/channelspicker/model.go +++ b/internal/ui/chat/channelspicker/model.go @@ -24,15 +24,15 @@ func NewModel(cfg *config.Config) Model { var _ tview.Model[Model] = Model{} -// actionMsg changes the picker. -type actionMsg picker.Action +// changeMsg changes the picker. +type changeMsg picker.Change func (Model) Init() tview.Cmd { return nil } // View shows the picker in a box titled Channels. func (m Model) View() tview.Element { p := ui.Picker(m.items, &m.searchState, m.cfg). - OnAction(func(a picker.Action) tview.Msg { return actionMsg(a) }). + OnChange(func(a picker.Change) tview.Msg { return changeMsg(a) }). OnSelect(func(item picker.Item) tview.Msg { channelID, ok := item.Reference.(discord.ChannelID) if !ok || !channelID.IsValid() { @@ -45,8 +45,8 @@ func (m Model) View() tview.Element { } func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) { - if msg, ok := msg.(actionMsg); ok { - m.searchState.Perform(picker.Action(msg)) + if msg, ok := msg.(changeMsg); ok { + m.searchState.Apply(picker.Change(msg)) } return m, nil } diff --git a/internal/ui/chat/composer/model.go b/internal/ui/chat/composer/model.go index a8cefab..58f5b56 100644 --- a/internal/ui/chat/composer/model.go +++ b/internal/ui/chat/composer/model.go @@ -50,7 +50,6 @@ var mentionRegex = regexp.MustCompile("@[a-zA-Z0-9._]+") type Model struct { title, footer string editState textarea.EditState - keybinds textarea.Keybinds placeholder string disabled bool @@ -88,7 +87,7 @@ type SentMsg struct{} type editorMsg string // editMsg edits the composer's text. -type editMsg textarea.Action +type editMsg textarea.Change var _ help.KeyMap = Model{} @@ -103,8 +102,6 @@ func NewModel(cfg *config.Config, state *ningen.State) Model { mentionsList: mentionslist.NewModel(cfg), height: 3, } - c.keybinds = textarea.DefaultKeybinds() - c.keybinds.Newline = cfg.Keybinds.Composer.Newline.Keybind return c } @@ -153,9 +150,9 @@ func (v view) Handle(msg tview.Msg, area tview.Rectangle) tview.Msg { func (c *Model) box(focused bool) box.Widget { text := textarea.New(&c.editState). Placeholder(c.placeholder). - Keybinds(c.keybinds). + Keybind(c.editAction). Focused(focused && !c.disabled). - OnAction(func(a textarea.Action) tview.Msg { return editMsg(a) }) + OnChange(func(a textarea.Change) tview.Msg { return editMsg(a) }) return ui.Box(text, &c.cfg.Theme, focused).Title(c.title).Footer(c.footer) } @@ -254,7 +251,7 @@ func (c *Model) update(msg tview.Msg) tview.Cmd { c.setText(string(msg)) return nil case editMsg: - c.editState.Perform(textarea.Action(msg)) + c.editState.Apply(textarea.Change(msg)) c.resizeForContent() typingCmd := c.sendTyping() if c.cfg.AutocompleteLimit > 0 { @@ -953,6 +950,15 @@ func (c *Model) canAttachFiles() bool { return c.channel != nil && c.state.HasPermissions(c.channel.ID, discord.PermissionAttachFiles) } +// editAction binds the configured newline key and textarea's other default keys. +func (c Model) editAction(key tview.KeyMsg) (textarea.Action, bool) { + if keybind.Matches(key, c.cfg.Keybinds.Composer.Newline.Keybind) { + return textarea.ActionNewline, true + } + action, ok := textarea.DefaultKeybind(key) + return action, ok && action != textarea.ActionNewline +} + func (c Model) ShortHelp() []keybind.Keybind { if c.mentionsVisible { cfg := c.cfg.Keybinds.MentionsList diff --git a/internal/ui/chat/guildstree/model.go b/internal/ui/chat/guildstree/model.go index 9446dc5..6207ed3 100644 --- a/internal/ui/chat/guildstree/model.go +++ b/internal/ui/chat/guildstree/model.go @@ -46,7 +46,7 @@ func (m *Model) CurrentNode() *tree.Node { } // Msg moves or scrolls the tree. -type Msg tree.Action +type Msg tree.Change // View shows the tree in a box titled Guilds. func (m Model) View(focused bool) tview.Element { @@ -66,16 +66,9 @@ func (m Model) View(focused bool) tview.Element { Graphics(theme.GuildsTree.Graphics). GraphicsSet(set). GraphicsStyle(tcell.StyleDefault.Foreground(tcell.GetColor(theme.GuildsTree.GraphicsColor))). - Keybinds(tree.Keybinds{ - Up: kbs.SelectUp.Keybind, - Down: kbs.SelectDown.Keybind, - Top: kbs.SelectTop.Keybind, - Bottom: kbs.SelectBottom.Keybind, - MoveToParent: kbs.MoveToParentNode.Keybind, - Select: kbs.SelectCurrent.Keybind, - }). + Keybind(ui.TreeKeybind(kbs)). Focused(focused). - OnAction(func(a tree.Action) tview.Msg { return Msg(a) }) + OnChange(func(a tree.Change) tview.Msg { return Msg(a) }) return ui.Box(t, theme, focused).Title("Guilds") } @@ -356,7 +349,7 @@ func (m *Model) update(msg tview.Msg) tview.Cmd { case NavigateMsg: return m.navigate(msg.ChannelID) case Msg: - m.selectionState.Perform(tree.Action(msg)) + m.selectionState.Apply(tree.Change(msg)) return nil case tree.SelectedMsg: m.selectionState.SetCurrentNode(msg.Node) diff --git a/internal/ui/chat/keymap.go b/internal/ui/chat/keymap.go index f28320a..775970a 100644 --- a/internal/ui/chat/keymap.go +++ b/internal/ui/chat/keymap.go @@ -1,7 +1,6 @@ package chat import ( - "github.com/ayn2op/discordo/internal/ui" "github.com/ayn2op/tview/help" "github.com/ayn2op/tview/keybind" ) @@ -28,7 +27,7 @@ func (m Model) FullHelp() [][]keybind.Keybind { func (m Model) activeKeyMap() help.KeyMap { if m.pickerOpen() { - return ui.PickerKeybinds(m.cfg) + return m.cfg.Keybinds.Picker } switch m.focused { diff --git a/internal/ui/chat/mentionslist/model.go b/internal/ui/chat/mentionslist/model.go index c5b8a7f..0abb682 100644 --- a/internal/ui/chat/mentionslist/model.go +++ b/internal/ui/chat/mentionslist/model.go @@ -15,22 +15,14 @@ type Model struct { selectionState list.SelectionState items []Item entries []list.Item - keybinds list.Keybinds } // Msg moves the list. -type Msg list.Action +type Msg list.Change func NewModel(cfg *config.Config) Model { m := Model{cfg: cfg, selectionState: list.NewSelectionState()} - kbs := cfg.Keybinds.MentionsList - m.keybinds = list.Keybinds{ - SelectUp: kbs.SelectUp.Keybind, - SelectDown: kbs.SelectDown.Keybind, - SelectTop: kbs.SelectTop.Keybind, - SelectBottom: kbs.SelectBottom.Keybind, - } return m } @@ -46,15 +38,15 @@ func (Model) Init() tview.Cmd { return nil } func (m Model) listView() list.Widget { return list.New(&m.selectionState, len(m.entries), func(i int) list.Item { return m.entries[i] }). SelectedStyle(tcell.StyleDefault.Reverse(true)). - Keybinds(m.keybinds). + Keybind(ui.SelectionKeybind(m.cfg.Keybinds.MentionsList.SelectionKeybinds)). // The list is only shown while mentions are being completed, so it takes its keys first. Focused(true). - OnAction(func(a list.Action) tview.Msg { return Msg(a) }) + OnChange(func(a list.Change) tview.Msg { return Msg(a) }) } func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) { if msg, ok := msg.(Msg); ok { - m.selectionState.Perform(list.Action(msg)) + m.selectionState.Apply(list.Change(msg)) } return m, nil } diff --git a/internal/ui/chat/messageslist/model.go b/internal/ui/chat/messageslist/model.go index 4bf1389..65c7de4 100644 --- a/internal/ui/chat/messageslist/model.go +++ b/internal/ui/chat/messageslist/model.go @@ -56,7 +56,6 @@ type Model struct { scrollBar scrollbar.Widget scrollBarVisibility list.ScrollBarVisibility - keybinds list.Keybinds cfg *config.Config state *ningen.State @@ -83,12 +82,6 @@ func NewModel(cfg *config.Config, state *ningen.State) Model { ml.selectionState.SetTrackEnd(true) ml.selectionState.ScrollToEnd() - ml.keybinds = 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.scrollBarVisibility = cfg.Theme.ScrollBar.Visibility.ScrollBarVisibility ml.scrollBar = scrollbar.New(). TrackStyle(cfg.Theme.ScrollBar.TrackStyle.Style). @@ -99,7 +92,7 @@ func NewModel(cfg *config.Config, state *ningen.State) Model { } // listMsg moves or scrolls the messages list. -type listMsg list.Action +type listMsg list.Change // View shows the messages in a box titled with the channel and footed with who is typing. func (ml Model) View(focused bool) tview.Element { @@ -110,9 +103,9 @@ func (ml *Model) listView(focused bool) list.Widget { return list.New(&ml.selectionState, len(ml.items), ml.buildItem). SelectedStyle(ml.cfg.Theme.MessagesList.SelectedMessageStyle.Style). ScrollBar(ml.scrollBar, ml.scrollBarVisibility). - Keybinds(ml.keybinds). + Keybind(ui.ScrollKeybind(ml.cfg.Keybinds.MessagesList.ScrollKeybinds)). Focused(focused). - OnAction(func(a list.Action) tview.Msg { return listMsg(a) }) + OnChange(func(a list.Change) tview.Msg { return listMsg(a) }) } func (ml *Model) cursor() int { @@ -903,7 +896,7 @@ func (ml *Model) update(msg tview.Msg) tview.Cmd { case attachmentActionMsg: return msg.Action case listMsg: - ml.selectionState.Perform(list.Action(msg)) + ml.selectionState.Apply(list.Change(msg)) ml.onRowCursorChanged(ml.cursor()) } return nil diff --git a/internal/ui/keybind.go b/internal/ui/keybind.go new file mode 100644 index 0000000..40f401b --- /dev/null +++ b/internal/ui/keybind.go @@ -0,0 +1,77 @@ +package ui + +import ( + "github.com/ayn2op/discordo/internal/config" + "github.com/ayn2op/tview" + "github.com/ayn2op/tview/keybind" + "github.com/ayn2op/tview/list" + "github.com/ayn2op/tview/picker" + "github.com/ayn2op/tview/tree" +) + +// SelectionKeybind binds kbs to moving a list's selection. +func SelectionKeybind(kbs config.SelectionKeybinds) func(tview.KeyMsg) (list.Action, bool) { + return func(key tview.KeyMsg) (list.Action, bool) { + switch { + case keybind.Matches(key, kbs.SelectUp.Keybind): + return list.ActionSelectUp, true + case keybind.Matches(key, kbs.SelectDown.Keybind): + return list.ActionSelectDown, true + case keybind.Matches(key, kbs.SelectTop.Keybind): + return list.ActionSelectTop, true + case keybind.Matches(key, kbs.SelectBottom.Keybind): + return list.ActionSelectBottom, true + } + return 0, false + } +} + +// ScrollKeybind binds kbs to scrolling a list. +func ScrollKeybind(kbs config.ScrollKeybinds) func(tview.KeyMsg) (list.Action, bool) { + return func(key tview.KeyMsg) (list.Action, bool) { + switch { + case keybind.Matches(key, kbs.ScrollUp.Keybind): + return list.ActionScrollUp, true + case keybind.Matches(key, kbs.ScrollDown.Keybind): + return list.ActionScrollDown, true + case keybind.Matches(key, kbs.ScrollTop.Keybind): + return list.ActionScrollTop, true + case keybind.Matches(key, kbs.ScrollBottom.Keybind): + return list.ActionScrollBottom, true + } + return 0, false + } +} + +// TreeKeybind binds kbs to moving through a tree and selecting its nodes. +func TreeKeybind(kbs config.GuildsTreeKeybinds) func(tview.KeyMsg) (tree.Action, bool) { + return func(key tview.KeyMsg) (tree.Action, bool) { + switch { + case keybind.Matches(key, kbs.SelectUp.Keybind): + return tree.ActionUp, true + case keybind.Matches(key, kbs.SelectDown.Keybind): + return tree.ActionDown, true + case keybind.Matches(key, kbs.SelectTop.Keybind): + return tree.ActionTop, true + case keybind.Matches(key, kbs.SelectBottom.Keybind): + return tree.ActionBottom, true + case keybind.Matches(key, kbs.MoveToParentNode.Keybind): + return tree.ActionMoveToParent, true + case keybind.Matches(key, kbs.SelectCurrent.Keybind): + return tree.ActionSelect, true + } + return 0, false + } +} + +func pickerKeybind(kbs config.PickerKeybinds) func(tview.KeyMsg) (picker.Action, bool) { + return func(key tview.KeyMsg) (picker.Action, bool) { + switch { + case keybind.Matches(key, kbs.Select.Keybind): + return picker.ActionSelect, true + case keybind.Matches(key, kbs.Cancel.Keybind): + return picker.ActionCancel, true + } + return 0, false + } +} diff --git a/internal/ui/login/form/model.go b/internal/ui/login/form/model.go index 8d7b642..36813b1 100644 --- a/internal/ui/login/form/model.go +++ b/internal/ui/login/form/model.go @@ -32,7 +32,7 @@ type SubmitMsg struct{} type editMsg struct { index int - action textinput.Action + change textinput.Change } type nextMsg struct{} @@ -56,7 +56,7 @@ func (m Model) Update(msg tview.Msg) (Model, tview.Cmd) { case editMsg: // Earlier copies of the model share the fields, so change a copy of them. m.fields = slices.Clone(m.fields) - m.fields[msg.index].EditState.Perform(msg.action) + m.fields[msg.index].EditState.Apply(msg.change) case nextMsg: m.focus = (m.focus + 1) % count case tview.KeyMsg: @@ -81,7 +81,7 @@ func (m Model) View() tview.Element { input := textinput.New(&f.EditState). Mask(f.Mask). Focused(m.focus == i). - OnAction(func(a textinput.Action) tview.Msg { return editMsg{index: i, action: a} }). + OnChange(func(a textinput.Change) tview.Msg { return editMsg{index: i, change: a} }). OnSubmit(nextMsg{}) layout = layout.Push(row.New(column.New(text.New(f.Label)).Width(tview.Fixed(labelWidth)), input).Height(tview.Fixed(1))) } diff --git a/internal/ui/login/keymap.go b/internal/ui/login/keymap.go index aa6b882..afc8a15 100644 --- a/internal/ui/login/keymap.go +++ b/internal/ui/login/keymap.go @@ -1,10 +1,27 @@ package login import ( + "github.com/ayn2op/tview" "github.com/ayn2op/tview/help" "github.com/ayn2op/tview/keybind" + "github.com/ayn2op/tview/tabs" ) +var ( + previousTab = keybind.NewSingleKeybind("ctrl+h", "prev tab") + nextTab = keybind.NewSingleKeybind("ctrl+l", "next tab") +) + +func tabAction(key tview.KeyMsg) (tabs.Action, bool) { + switch { + case keybind.Matches(key, previousTab): + return tabs.ActionPrevious, true + case keybind.Matches(key, nextTab): + return tabs.ActionNext, true + } + return 0, false +} + var _ help.KeyMap = Model{} func (m Model) ShortHelp() []keybind.Keybind { @@ -30,10 +47,10 @@ func (m Model) FullHelp() [][]keybind.Keybind { func (m Model) navHelp() []keybind.Keybind { var nav []keybind.Keybind if m.active > 0 { - nav = append(nav, m.keybinds.Previous) + nav = append(nav, previousTab) } if m.active < tabCount-1 { - nav = append(nav, m.keybinds.Next) + nav = append(nav, nextTab) } return nav } diff --git a/internal/ui/login/model.go b/internal/ui/login/model.go index 025335f..a0a57a5 100644 --- a/internal/ui/login/model.go +++ b/internal/ui/login/model.go @@ -30,7 +30,6 @@ type Model struct { qr qr.Model token token.Model active tab - keybinds tabs.Keybinds } var _ tview.Model[Model] = Model{} @@ -41,7 +40,6 @@ func NewModel(cfg *config.Config) Model { password: password.NewModel(), qr: qr.NewModel(), token: token.NewModel(), - keybinds: tabs.DefaultKeybinds(), } } @@ -75,7 +73,7 @@ func (m Model) View() tview.Element { t := tabs.New(m.password.Label(), m.qr.Label(), m.token.Label()). Active(int(m.active)). Content(content). - Keybinds(m.keybinds). + Keybind(tabAction). OnSelect(func(i int) tview.Msg { return selectTabMsg(i) }) return ui.Box(t, &m.cfg.Theme, false) } diff --git a/internal/ui/login/qr/model.go b/internal/ui/login/qr/model.go index 7d8e62d..851ec56 100644 --- a/internal/ui/login/qr/model.go +++ b/internal/ui/login/qr/model.go @@ -29,7 +29,7 @@ type Model struct { } // scrollMsg scrolls the code. -type scrollMsg textview.Action +type scrollMsg textview.Change // startMsg starts connecting to the Remote Auth Gateway. type startMsg struct{} @@ -68,7 +68,7 @@ func (m *Model) update(msg tview.Msg) tview.Cmd { } return nil case scrollMsg: - m.scrollState.Perform(textview.Action(msg)) + m.scrollState.Apply(textview.Change(msg)) return nil case connCreateMsg: @@ -147,7 +147,7 @@ func (m Model) View() tview.Element { Alignment(tview.AlignmentCenter). Height(tview.Fixed(len(m.code))). Focused(true). - OnAction(func(a textview.Action) tview.Msg { return scrollMsg(a) }), + OnChange(func(a textview.Change) tview.Msg { return scrollMsg(a) }), ) } diff --git a/internal/ui/util.go b/internal/ui/util.go index 024f584..4dfea56 100644 --- a/internal/ui/util.go +++ b/internal/ui/util.go @@ -45,21 +45,8 @@ func Picker(items picker.Items, searchState *picker.SearchState, cfg *config.Con Arrows(scrollbar.ArrowsBoth) return picker.New(items, searchState). ScrollBar(bar, cfg.Theme.ScrollBar.Visibility.ScrollBarVisibility). - Keybinds(PickerKeybinds(cfg)) -} - -// PickerKeybinds returns the configured picker keybinds. -func PickerKeybinds(cfg *config.Config) picker.Keybinds { - kbs := cfg.Keybinds.Picker - return picker.Keybinds{ - Cancel: kbs.Cancel.Keybind, - Select: kbs.Select.Keybind, - - SelectUp: kbs.SelectUp.Keybind, - SelectDown: kbs.SelectDown.Keybind, - SelectTop: kbs.SelectTop.Keybind, - SelectBottom: kbs.SelectBottom.Keybind, - } + Keybind(pickerKeybind(cfg.Keybinds.Picker)). + ListKeybind(SelectionKeybind(cfg.Keybinds.Picker.SelectionKeybinds)) } // ChannelToString returns how channel is shown in the UI: an icon for its type and its name, or the recipients' names for a DM.