Remove vi interface (#376)

This commit is contained in:
Ayyan 2024-03-29 16:15:08 +05:30 • committed by GitHub
parent 8692701e30
commit c87f3fffc6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 362 additions and 413 deletions

View file

@ -51,9 +51,9 @@ go build .
The configuration file allows you to configure and customize the behavior, keybindings, and theme of the application.
- Unix: `$XDG_CONFIG_HOME/discordo/config.yml` or `$HOME/.config/discordo/config.yml`
- Darwin: `$HOME/Library/Application Support/discordo/config.yml`
- Windows: `%AppData%/discordo/config.yml`
- Unix: `$XDG_CONFIG_HOME/discordo/config.toml` or `$HOME/.config/discordo/config.toml`
- Darwin: `$HOME/Library/Application Support/discordo/config.toml`
- Windows: `%AppData%/discordo/config.toml`
```toml
mouse = true
@ -62,24 +62,19 @@ timestamps_before_author = false
messages_limit = 50
editor = "default"
[keys.normal]
insert_mode = "Rune[i]"
[keys]
focus_guilds_tree = "Ctrl+G"
focus_messages_text = "Ctrl+T"
toggle_guild_tree = "Ctrl+B"
select_previous = "Rune[k]"
select_next = "Rune[j]"
select_first = "Rune[g]"
select_last = "Rune[G]"
[keys.normal.guilds_tree]
[keys.guilds_tree]
select_current = "Enter"
select_previous = "Rune[k]"
select_next = "Rune[j]"
select_first = "Rune[g]"
select_last = "Rune[G]"
[keys.normal.messages_text]
select_previous = "Rune[k]"
select_next = "Rune[j]"
select_first = "Rune[g]"
select_last = "Rune[G]"
[keys.messages_text]
select_reply = "Rune[s]"
reply = "Rune[r]"
reply_mention = "Rune[R]"
@ -87,12 +82,10 @@ delete = "Rune[d]"
yank = "Rune[y]"
open = "Rune[o]"
[keys.insert]
normal_mode = "Esc"
[keys.insert.message_input]
[keys.message_input]
send = "Enter"
editor = "Ctrl+E"
cancel = "Esc"
[theme]
border = true
@ -104,16 +97,12 @@ background_color = "default"
[theme.guilds_tree]
auto_expand_folders = true
graphics = true
[theme.messages_text]
author_color = "aqua"
reply_indicator = "╭ "
```
## Documentation
[Here.](./docs)
## Disclaimer
Automated user accounts or "self-bots" are against Discord's Terms of Service. I am not responsible for any loss caused by using "self-bots" or Discordo.

View file

@ -108,22 +108,23 @@ func (gt *GuildsTree) channelToString(c discord.Channel) string {
return s
}
func (gt *GuildsTree) createChannelNode(n *tview.TreeNode, c discord.Channel) {
func (gt *GuildsTree) createChannelNode(n *tview.TreeNode, c discord.Channel) *tview.TreeNode {
if c.Type != discord.DirectMessage && c.Type != discord.GroupDM {
ps, err := discordState.Permissions(c.ID, discordState.Ready().User.ID)
if err != nil {
log.Println(err)
return
return nil
}
if !ps.Has(discord.PermissionViewChannel) {
return
return nil
}
}
cn := tview.NewTreeNode(gt.channelToString(c))
cn.SetReference(c.ID)
n.AddChild(cn)
return cn
}
func (gt *GuildsTree) createChannelNodes(n *tview.TreeNode, cs []discord.Channel) {
@ -219,23 +220,18 @@ func (gt *GuildsTree) onSelected(n *tview.TreeNode) {
}
func (gt *GuildsTree) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
switch mainFlex.mode {
case ModeNormal:
switch event.Name() {
case cfg.Keys.Normal.GuildsTree.SelectCurrent:
return tcell.NewEventKey(tcell.KeyEnter, 0, tcell.ModNone)
case cfg.Keys.Normal.GuildsTree.SelectPrevious:
return tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone)
case cfg.Keys.Normal.GuildsTree.SelectNext:
return tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone)
case cfg.Keys.Normal.GuildsTree.SelectFirst:
return tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModNone)
case cfg.Keys.Normal.GuildsTree.SelectLast:
return tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModNone)
}
switch event.Name() {
case cfg.Keys.SelectPrevious:
return tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone)
case cfg.Keys.SelectNext:
return tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone)
case cfg.Keys.SelectFirst:
return tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModNone)
case cfg.Keys.SelectLast:
return tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModNone)
// do not propagate event to the children in normal mode.
return nil
case cfg.Keys.GuildsTree.SelectCurrent:
return tcell.NewEventKey(tcell.KeyEnter, 0, tcell.ModNone)
}
return event

View file

@ -5,17 +5,9 @@ import (
"github.com/rivo/tview"
)
type Mode uint
const (
ModeNormal Mode = iota
ModeInsert
)
type MainFlex struct {
*tview.Flex
mode Mode
guildsTree *GuildsTree
messagesText *MessagesText
messageInput *MessageInput
@ -25,23 +17,11 @@ func newMainFlex() *MainFlex {
mf := &MainFlex{
Flex: tview.NewFlex(),
mode: ModeNormal,
guildsTree: newGuildsTree(),
messagesText: newMessagesText(),
messageInput: newMessageInput(),
}
app.SetBeforeDrawFunc(func(screen tcell.Screen) bool {
switch mf.mode {
case ModeNormal:
mf.messageInput.SetBorderAttributes(tcell.AttrNone)
case ModeInsert:
mf.messageInput.SetBorderAttributes(tcell.AttrDim)
}
return false
})
mf.init()
mf.SetInputCapture(mf.onInputCapture)
return mf
@ -60,49 +40,29 @@ func (mf *MainFlex) init() {
}
func (mf *MainFlex) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
switch mf.mode {
case ModeNormal:
switch event.Name() {
case cfg.Keys.Normal.InsertMode:
mf.mode = ModeInsert
app.SetFocus(mf.messageInput)
return nil
case cfg.Keys.Normal.FocusGuildsTree:
app.SetFocus(mf.guildsTree)
return nil
case cfg.Keys.Normal.FocusMessagesText:
app.SetFocus(mf.messagesText)
return nil
case cfg.Keys.Normal.ToggleGuildsTree:
// The guilds tree is visible if the numbers of items is two.
if mf.GetItemCount() == 2 {
mf.RemoveItem(mf.guildsTree)
if mf.guildsTree.HasFocus() {
app.SetFocus(mf)
}
} else {
mf.init()
app.SetFocus(mf.guildsTree)
switch event.Name() {
case cfg.Keys.FocusGuildsTree:
app.SetFocus(mf.guildsTree)
return nil
case cfg.Keys.FocusMessagesText:
app.SetFocus(mf.messagesText)
return nil
case cfg.Keys.FocusMessageInput:
app.SetFocus(mf.messageInput)
return nil
case cfg.Keys.ToggleGuildsTree:
// The guilds tree is visible if the numbers of items is two.
if mf.GetItemCount() == 2 {
mf.RemoveItem(mf.guildsTree)
if mf.guildsTree.HasFocus() {
app.SetFocus(mf)
}
return nil
} else {
mf.init()
app.SetFocus(mf.guildsTree)
}
// do not propagate event to the children if the message input is focused in normal mode.
if mf.messageInput.HasFocus() {
return nil
}
case ModeInsert:
switch event.Name() {
case cfg.Keys.Insert.NormalMode:
mf.mode = ModeNormal
return nil
}
if !mf.messageInput.HasFocus() {
return nil
}
return nil
}
return event

View file

@ -49,97 +49,106 @@ func newMessageInput() *MessageInput {
}
func (mi *MessageInput) reset() {
mi.replyMessageIdx = -1
mi.SetTitle("")
mi.SetText("", true)
}
func (mi *MessageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
switch mainFlex.mode {
case ModeInsert:
switch event.Name() {
case cfg.Keys.Insert.MessageInput.Send:
if !mainFlex.guildsTree.selectedChannelID.IsValid() {
return nil
}
text := strings.TrimSpace(mi.GetText())
if text == "" {
return nil
}
if mi.replyMessageIdx != -1 {
ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
log.Println(err)
return nil
}
data := api.SendMessageData{
Content: text,
Reference: &discord.MessageReference{MessageID: ms[mi.replyMessageIdx].ID},
AllowedMentions: &api.AllowedMentions{RepliedUser: option.False},
}
if strings.HasPrefix(mi.GetTitle(), "[@]") {
data.AllowedMentions.RepliedUser = option.True
}
go func() {
if _, err := discordState.SendMessageComplex(mainFlex.guildsTree.selectedChannelID, data); err != nil {
log.Println("failed to send message:", err)
}
}()
} else {
go func() {
if _, err := discordState.SendMessage(mainFlex.guildsTree.selectedChannelID, text); err != nil {
log.Println("failed to send message:", err)
}
}()
}
mi.replyMessageIdx = -1
mainFlex.messagesText.Highlight()
mi.reset()
return nil
case cfg.Keys.Insert.MessageInput.Editor:
e := cfg.Editor
if e == "default" {
e = os.Getenv("EDITOR")
}
f, err := os.CreateTemp("", constants.TmpFilePattern)
if err != nil {
log.Println(err)
return nil
}
_, _ = f.WriteString(mi.GetText())
f.Close()
defer os.Remove(f.Name())
cmd := exec.Command(e, f.Name())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
app.Suspend(func() {
err := cmd.Run()
if err != nil {
log.Println(err)
return
}
})
msg, err := os.ReadFile(f.Name())
if err != nil {
log.Println(err)
return nil
}
mi.SetText(strings.TrimSpace(string(msg)), true)
return nil
}
switch event.Name() {
case cfg.Keys.MessageInput.Send:
mi.send()
return nil
case cfg.Keys.MessageInput.Editor:
mi.editor()
return nil
case cfg.Keys.MessageInput.Cancel:
mi.reset()
return nil
}
return event
}
func (mi *MessageInput) send() {
if !mainFlex.guildsTree.selectedChannelID.IsValid() {
return
}
text := strings.TrimSpace(mi.GetText())
if text == "" {
return
}
if mi.replyMessageIdx != -1 {
ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
log.Println(err)
return
}
data := api.SendMessageData{
Content: text,
Reference: &discord.MessageReference{MessageID: ms[mi.replyMessageIdx].ID},
AllowedMentions: &api.AllowedMentions{RepliedUser: option.False},
}
if strings.HasPrefix(mi.GetTitle(), "[@]") {
data.AllowedMentions.RepliedUser = option.True
}
go func() {
if _, err := discordState.SendMessageComplex(mainFlex.guildsTree.selectedChannelID, data); err != nil {
log.Println("failed to send message:", err)
}
}()
} else {
go func() {
if _, err := discordState.SendMessage(mainFlex.guildsTree.selectedChannelID, text); err != nil {
log.Println("failed to send message:", err)
}
}()
}
mi.replyMessageIdx = -1
mainFlex.messagesText.Highlight()
mi.reset()
}
func (mi *MessageInput) editor() {
e := cfg.Editor
if e == "default" {
e = os.Getenv("EDITOR")
}
f, err := os.CreateTemp("", constants.TmpFilePattern)
if err != nil {
log.Println(err)
return
}
_, _ = f.WriteString(mi.GetText())
f.Close()
defer os.Remove(f.Name())
cmd := exec.Command(e, f.Name())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
app.Suspend(func() {
err := cmd.Run()
if err != nil {
log.Println(err)
return
}
})
msg, err := os.ReadFile(f.Name())
if err != nil {
log.Println(err)
return
}
mi.SetText(strings.TrimSpace(string(msg)), true)
}

View file

@ -1,6 +1,7 @@
package cmd
import (
"errors"
"fmt"
"io"
"log"
@ -149,179 +150,192 @@ func (mt *MessagesText) createFooter(w io.Writer, m discord.Message) {
}
}
func (mt *MessagesText) getSelectedMessage() (*discord.Message, error) {
if mt.selectedMessage == -1 {
return nil, errors.New("no message is currently selected")
}
ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
return nil, err
}
return &ms[mt.selectedMessage], nil
}
func (mt *MessagesText) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
switch mainFlex.mode {
case ModeNormal:
switch event.Name() {
case cfg.Keys.Normal.MessagesText.Yank:
if mt.selectedMessage == -1 {
return nil
}
ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
log.Println(err)
return nil
}
err = clipboard.WriteAll(ms[mt.selectedMessage].Content)
if err != nil {
log.Println("failed to write to clipboard:", err)
return nil
}
return nil
case cfg.Keys.Normal.MessagesText.SelectFirst, cfg.Keys.Normal.MessagesText.SelectLast, cfg.Keys.Normal.MessagesText.SelectPrevious, cfg.Keys.Normal.MessagesText.SelectNext, cfg.Keys.Normal.MessagesText.SelectReply:
ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
log.Println(err)
return nil
}
switch event.Name() {
case cfg.Keys.Normal.MessagesText.SelectPrevious:
// If no message is currently selected, select the latest message.
if len(mt.GetHighlights()) == 0 {
mt.selectedMessage = 0
} else {
if mt.selectedMessage < len(ms)-1 {
mt.selectedMessage++
} else {
return nil
}
}
case cfg.Keys.Normal.MessagesText.SelectNext:
// If no message is currently selected, select the latest message.
if len(mt.GetHighlights()) == 0 {
mt.selectedMessage = 0
} else {
if mt.selectedMessage > 0 {
mt.selectedMessage--
} else {
return nil
}
}
case cfg.Keys.Normal.MessagesText.SelectFirst:
mt.selectedMessage = len(ms) - 1
case cfg.Keys.Normal.MessagesText.SelectLast:
mt.selectedMessage = 0
case cfg.Keys.Normal.MessagesText.SelectReply:
if mt.selectedMessage == -1 {
return nil
}
if ref := ms[mt.selectedMessage].ReferencedMessage; ref != nil {
for i, m := range ms {
if ref.ID == m.ID {
mt.selectedMessage = i
}
}
}
}
mt.Highlight(ms[mt.selectedMessage].ID.String())
mt.ScrollToHighlight()
return nil
case cfg.Keys.Normal.MessagesText.Open:
if mt.selectedMessage == -1 {
return nil
}
ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
log.Println(err)
return nil
}
attachments := ms[mt.selectedMessage].Attachments
if len(attachments) == 0 {
return nil
}
for _, a := range attachments {
go func() {
if err := open.Start(a.URL); err != nil {
log.Println(err)
}
}()
}
return nil
case cfg.Keys.Normal.MessagesText.Reply, cfg.Keys.Normal.MessagesText.ReplyMention:
if mt.selectedMessage == -1 {
return nil
}
var title string
if event.Name() == cfg.Keys.Normal.MessagesText.ReplyMention {
title += "[@] Replying to "
} else {
title += "Replying to "
}
ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
log.Println(err)
return nil
}
title += ms[mt.selectedMessage].Author.Tag()
mainFlex.messageInput.SetTitle(title)
mainFlex.messageInput.replyMessageIdx = mt.selectedMessage
app.SetFocus(mainFlex.messageInput)
return nil
case cfg.Keys.Normal.MessagesText.Delete:
if mt.selectedMessage == -1 {
return nil
}
ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
log.Println(err)
return nil
}
m := ms[mt.selectedMessage]
clientID := discordState.Ready().User.ID
ps, err := discordState.Permissions(mainFlex.guildsTree.selectedChannelID, discordState.Ready().User.ID)
if err != nil {
return nil
}
if m.Author.ID != clientID && !ps.Has(discord.PermissionManageMessages) {
return nil
}
if err := discordState.DeleteMessage(mainFlex.guildsTree.selectedChannelID, m.ID, ""); err != nil {
log.Println(err)
}
if err := discordState.MessageRemove(mainFlex.guildsTree.selectedChannelID, m.ID); err != nil {
log.Println(err)
}
ms, err = discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
log.Println(err)
return nil
}
mt.Clear()
for i := len(ms) - 1; i >= 0; i-- {
mainFlex.messagesText.createMessage(ms[i])
}
switch event.Name() {
case cfg.Keys.SelectPrevious, cfg.Keys.SelectNext, cfg.Keys.SelectFirst, cfg.Keys.SelectLast, cfg.Keys.MessagesText.SelectReply:
ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
log.Println(err)
return nil
}
// do not propagate event to the children in normal mode.
switch event.Name() {
case cfg.Keys.SelectPrevious:
// If no message is currently selected, select the latest message.
if len(mt.GetHighlights()) == 0 {
mt.selectedMessage = 0
} else {
if mt.selectedMessage < len(ms)-1 {
mt.selectedMessage++
} else {
return nil
}
}
case cfg.Keys.SelectNext:
// If no message is currently selected, select the latest message.
if len(mt.GetHighlights()) == 0 {
mt.selectedMessage = 0
} else {
if mt.selectedMessage > 0 {
mt.selectedMessage--
} else {
return nil
}
}
case cfg.Keys.SelectFirst:
mt.selectedMessage = len(ms) - 1
case cfg.Keys.SelectLast:
mt.selectedMessage = 0
case cfg.Keys.MessagesText.SelectReply:
if mt.selectedMessage == -1 {
return nil
}
if ref := ms[mt.selectedMessage].ReferencedMessage; ref != nil {
for i, m := range ms {
if ref.ID == m.ID {
mt.selectedMessage = i
}
}
}
}
mt.Highlight(ms[mt.selectedMessage].ID.String())
mt.ScrollToHighlight()
return nil
case cfg.Keys.MessagesText.Yank:
mt.yank()
return nil
case cfg.Keys.MessagesText.Open:
mt.open()
return nil
case cfg.Keys.MessagesText.Reply:
mt.reply(false)
return nil
case cfg.Keys.MessagesText.ReplyMention:
mt.reply(true)
return nil
case cfg.Keys.MessagesText.Delete:
mt.delete()
return nil
}
return event
}
func (mt *MessagesText) yank() {
msg, err := mt.getSelectedMessage()
if err != nil {
log.Println(err)
return
}
err = clipboard.WriteAll(msg.Content)
if err != nil {
log.Println("failed to write to clipboard:", err)
return
}
}
func (mt *MessagesText) open() {
msg, err := mt.getSelectedMessage()
if err != nil {
log.Println(err)
return
}
attachments := msg.Attachments
if len(attachments) == 0 {
return
}
for _, a := range attachments {
go func() {
if err := open.Start(a.URL); err != nil {
log.Println(err)
}
}()
}
}
func (mt *MessagesText) reply(mention bool) {
var title string
if mention {
title += "[@] Replying to "
} else {
title += "Replying to "
}
msg, err := mt.getSelectedMessage()
if err != nil {
log.Println(err)
return
}
title += msg.Author.Tag()
mainFlex.messageInput.SetTitle(title)
mainFlex.messageInput.replyMessageIdx = mt.selectedMessage
app.SetFocus(mainFlex.messageInput)
}
func (mt *MessagesText) delete() {
msg, err := mt.getSelectedMessage()
if err != nil {
log.Println(err)
return
}
clientID := discordState.Ready().User.ID
if msg.GuildID.IsValid() {
ps, err := discordState.Permissions(mainFlex.guildsTree.selectedChannelID, discordState.Ready().User.ID)
if err != nil {
return
}
if msg.Author.ID != clientID && !ps.Has(discord.PermissionManageMessages) {
return
}
} else {
if msg.Author.ID != clientID {
return
}
}
if err := discordState.DeleteMessage(mainFlex.guildsTree.selectedChannelID, msg.ID, ""); err != nil {
log.Println(err)
return
}
if err := discordState.MessageRemove(mainFlex.guildsTree.selectedChannelID, msg.ID); err != nil {
log.Println(err)
}
ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
log.Println(err)
return
}
mt.Clear()
for i := len(ms) - 1; i >= 0; i-- {
mainFlex.messagesText.createMessage(ms[i])
}
}

View file

@ -22,7 +22,7 @@ type Config struct {
Theme Theme `toml:"theme"`
}
func defaultConfig() Config {
func DefaultConfig() Config {
return Config{
Mouse: true,
@ -44,7 +44,7 @@ func Load() (*Config, error) {
return nil, err
}
cfg := defaultConfig()
cfg := DefaultConfig()
path = filepath.Join(path, constants.Name, "config.toml")
f, err := os.Open(path)
if os.IsNotExist(err) {

View file

@ -2,35 +2,27 @@ package config
type (
Keys struct {
Normal NormalModeKeys `toml:"normal"`
Insert InsertModeKeys `toml:"insert"`
}
NormalModeKeys struct {
InsertMode string `toml:"insert_mode"`
FocusGuildsTree string `toml:"focus_guilds_tree"`
FocusMessagesText string `toml:"focus_messages_text"`
FocusMessageInput string `toml:"focus_message_input"`
ToggleGuildsTree string `toml:"toggle_guild_tree"`
GuildsTree GuildsTreeNormalModeKeys `toml:"guilds_tree"`
MessagesText MessagesTextNormalModeKeys `toml:"messages_text"`
}
GuildsTreeNormalModeKeys struct {
SelectCurrent string `toml:"select_current"`
SelectPrevious string `toml:"select_previous"`
SelectNext string `toml:"select_next"`
SelectFirst string `toml:"select_first"`
SelectLast string `toml:"select_last"`
GuildsTree GuildsTreeKeys `toml:"guilds_tree"`
MessagesText MessagesTextKeys `toml:"messages_text"`
MessageInput MessageInputKeys `toml:"message_input"`
}
MessagesTextNormalModeKeys struct {
SelectPrevious string `toml:"select_previous"`
SelectNext string `toml:"select_next"`
SelectFirst string `toml:"select_first"`
SelectLast string `toml:"select_last"`
SelectReply string `toml:"select_reply"`
GuildsTreeKeys struct {
SelectCurrent string `toml:"select_current"`
}
MessagesTextKeys struct {
SelectReply string `toml:"select_reply"`
Reply string `toml:"reply"`
ReplyMention string `toml:"reply_mention"`
@ -39,55 +31,44 @@ type (
Open string `toml:"open"`
}
InsertModeKeys struct {
NormalMode string `toml:"normal_mode"`
MessageInput MessageInputInsertModeKeys `toml:"message_input"`
}
MessageInputInsertModeKeys struct {
MessageInputKeys struct {
Send string `toml:"send"`
Editor string `toml:"editor"`
Cancel string `toml:"cancel"`
}
)
func defaultKeys() Keys {
return Keys{
Normal: NormalModeKeys{
InsertMode: "Rune[i]",
FocusGuildsTree: "Ctrl+G",
FocusMessagesText: "Ctrl+T",
FocusMessageInput: "Ctrl+P",
ToggleGuildsTree: "Ctrl+B",
FocusGuildsTree: "Ctrl+G",
FocusMessagesText: "Ctrl+T",
ToggleGuildsTree: "Ctrl+B",
SelectPrevious: "Rune[k]",
SelectNext: "Rune[j]",
SelectFirst: "Rune[g]",
SelectLast: "Rune[G]",
GuildsTree: GuildsTreeNormalModeKeys{
SelectCurrent: "Enter",
SelectPrevious: "Rune[k]",
SelectNext: "Rune[j]",
SelectFirst: "Rune[g]",
SelectLast: "Rune[G]",
},
MessagesText: MessagesTextNormalModeKeys{
SelectPrevious: "Rune[k]",
SelectNext: "Rune[j]",
SelectFirst: "Rune[g]",
SelectLast: "Rune[G]",
SelectReply: "Rune[s]",
Reply: "Rune[r]",
ReplyMention: "Rune[R]",
Delete: "Rune[d]",
Yank: "Rune[y]",
Open: "Rune[o]",
},
GuildsTree: GuildsTreeKeys{
SelectCurrent: "Enter",
},
Insert: InsertModeKeys{
NormalMode: "Esc",
MessageInput: MessageInputInsertModeKeys{
Send: "Enter",
Editor: "Ctrl+E",
},
MessagesText: MessagesTextKeys{
SelectReply: "Rune[s]",
Reply: "Rune[r]",
ReplyMention: "Rune[R]",
Delete: "Rune[d]",
Yank: "Rune[y]",
Open: "Rune[o]",
},
MessageInput: MessageInputKeys{
Send: "Enter",
Editor: "Ctrl+E",
Cancel: "Esc",
},
}
}