From d465ede6d834ecfb4e336d06ea531306d3871b75 Mon Sep 17 00:00:00 2001 From: Ayyan <119342035+ayn2op@users.noreply.github.com> Date: Tue, 26 Mar 2024 16:06:39 +0530 Subject: [PATCH] Implement vi interface (#372) --- README.md | 80 ++++++-- cmd/attachment_image.go | 57 ------ cmd/guilds_tree.go | 26 ++- cmd/main_flex.go | 82 ++++++--- cmd/message_input.go | 175 +++++++++--------- cmd/messages_text.go | 361 ++++++++++++++++--------------------- cmd/state.go | 2 +- docs/README.md | 41 ----- docs/configuration.md | 7 - docs/faq.md | 7 - go.mod | 3 +- go.sum | 23 +-- internal/config/config.go | 105 +++-------- internal/config/config.yml | 57 ------ internal/config/keys.go | 93 ++++++++++ internal/config/theme.go | 47 +++++ internal/ui/login_form.go | 7 +- main.go | 4 +- 18 files changed, 572 insertions(+), 605 deletions(-) delete mode 100644 cmd/attachment_image.go delete mode 100644 docs/README.md delete mode 100644 docs/configuration.md delete mode 100644 docs/faq.md delete mode 100644 internal/config/config.yml create mode 100644 internal/config/keys.go create mode 100644 internal/config/theme.go diff --git a/README.md b/README.md index dd470ff8..a4b3cb11 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,6 @@ Discordo is a lightweight, secure, and feature-rich Discord terminal client. Hea ![Preview](.github/preview.png) -## Table of Contents - -- [Features](#features) -- [Installation](#installation) -- [Usage](#usage) -- [Disclaimer](#disclaimer) - -## Features - - Lightweight - Secure - Configurable @@ -41,9 +32,6 @@ You can download and install a [prebuilt binary here](https://nightly.link/ayn2o git clone https://github.com/ayn2op/discordo cd discordo go build . - -# optional -sudo mv ./discordo /usr/local/bin ``` ### Linux clipboard support @@ -55,11 +43,75 @@ sudo mv ./discordo /usr/local/bin 1. Run the `discordo` executable with no arguments. -- If you are logging in using an authentication token, provide the `token` command-line flag to the executable (eg: `--token "OTI2MDU5NTQxNDE2Nzc5ODA2.Yc2KKA.2iZ-5JxgxG-9Ub8GHzBSn-NJjNg"`). The token is stored securely in the default OS-specific keyring. +> If you are logging in using an authentication token, provide the `token` command-line flag to the executable (eg: `--token "OTI2MDU5NTQxNDE2Nzc5ODA2.Yc2KKA.2iZ-5JxgxG-9Ub8GHzBSn-NJjNg"`). The token is stored securely in the default OS-specific keyring. 2. Enter your email and password and click on the "Login" button to continue. -- Most of the Discord third-party clients store the token in a configuration file unencrypted. Discordo securely stores the token in the default OS-specific keyring. +## Configuration + +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` + +```toml +mouse = true +timestamps = false +timestamps_before_author = false +messages_limit = 50 +editor = "default" + +[keys.normal] + insert_mode = "Rune[i]" + focus_guilds_tree = "Ctrl+G" + focus_messages_text = "Ctrl+T" + toggle_guild_tree = "Ctrl+B" + + guilds_tree = { + select_current = "Enter" + select_previous = "Rune[k]" + select_next = "Rune[j]" + select_first = "Rune[g]" + select_last = "Rune[G]" + } + + messages_text = { + select_previous = "Rune[k]" + select_next = "Rune[j]" + select_first = "Rune[g]" + select_last = "Rune[G]" + select_reply = "Rune[s]" + reply = "Rune[r]" + reply_mention = "Rune[R]" + delete = "Rune[d]" + yank = "Rune[y]" + open = "Rune[o]" + } + +[keys.insert] + normal_mode = "Esc" + + message_input = { + send = "Enter" + editor = "Ctrl+E" + } + +[theme] + border = true + border_color = "default" + border_padding = [0, 0, 1, 1] + title_color = "default" + background_color = "default" + +[theme.guilds_tree] + auto_expand_folders = true + graphics = true + +[theme.messages_text] + author_color = "aqua" + reply_indicator = "╭ " +``` ## Documentation diff --git a/cmd/attachment_image.go b/cmd/attachment_image.go deleted file mode 100644 index d867ec29..00000000 --- a/cmd/attachment_image.go +++ /dev/null @@ -1,57 +0,0 @@ -package cmd - -import ( - "image" - _ "image/jpeg" - _ "image/png" - "net/http" - - "github.com/diamondburned/arikawa/v3/discord" - "github.com/gdamore/tcell/v2" - "github.com/rivo/tview" -) - -type AttachmentImage struct { - *tview.Image -} - -func newAttachmentImage(a discord.Attachment) (*AttachmentImage, error) { - ai := &AttachmentImage{ - Image: tview.NewImage(), - } - - ai.SetInputCapture(ai.onInputCapture) - ai.SetBackgroundColor(tcell.GetColor(cfg.Theme.BackgroundColor)) - ai.SetTitleColor(tcell.GetColor(cfg.Theme.TitleColor)) - ai.SetTitleAlign(tview.AlignLeft) - - p := cfg.Theme.BorderPadding - ai.SetBorder(cfg.Theme.Border) - ai.SetBorderColor(tcell.GetColor(cfg.Theme.BorderColor)) - ai.SetBorderPadding(p[0], p[1], p[2], p[3]) - - resp, err := http.Get(a.URL) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - i, _, err := image.Decode(resp.Body) - if err != nil { - return nil, err - } - - ai.SetTitle(a.Filename) - ai.SetImage(i) - return ai, nil -} - -func (ai *AttachmentImage) onInputCapture(event *tcell.EventKey) *tcell.EventKey { - if event.Name() == cfg.Keys.Cancel { - app.SetRoot(mainFlex, true) - app.SetFocus(mainFlex.messagesText) - return nil - } - - return event -} diff --git a/cmd/guilds_tree.go b/cmd/guilds_tree.go index 346441a0..26fc9462 100644 --- a/cmd/guilds_tree.go +++ b/cmd/guilds_tree.go @@ -41,6 +41,7 @@ func newGuildsTree() *GuildsTree { gt.SetBorderColor(tcell.GetColor(cfg.Theme.BorderColor)) gt.SetBorderPadding(p[0], p[1], p[2], p[3]) + gt.SetInputCapture(gt.onInputCapture) return gt } @@ -59,7 +60,7 @@ func (gt *GuildsTree) createGuildFolderNode(parent *tview.TreeNode, gf gateway.G for _, gid := range gf.GuildIDs { g, err := discordState.Cabinet.Guild(gid) if err != nil { - log.Println(err) + log.Printf("guild %v not found in state: %v", gid, err) continue } @@ -216,3 +217,26 @@ 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) + } + + // do not propagate event to the children in normal mode. + return nil + } + + return event +} diff --git a/cmd/main_flex.go b/cmd/main_flex.go index 70fa8078..4af4b66d 100644 --- a/cmd/main_flex.go +++ b/cmd/main_flex.go @@ -5,9 +5,17 @@ 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 @@ -17,14 +25,25 @@ 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 } @@ -41,32 +60,49 @@ func (mf *MainFlex) init() { } func (mf *MainFlex) onInputCapture(event *tcell.EventKey) *tcell.EventKey { - switch event.Name() { - case cfg.Keys.GuildsTree.Toggle: - // 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() + 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) + } + + return nil } - return nil - case cfg.Keys.GuildsTree.Focus: - if mf.GetItemCount() == 2 { - 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 - case cfg.Keys.MessagesText.Focus: - app.SetFocus(mf.messagesText) - return nil - case cfg.Keys.MessageInput.Focus: - app.SetFocus(mf.messageInput) - return nil } return event diff --git a/cmd/message_input.go b/cmd/message_input.go index 7fad7753..a8e5ae5c 100644 --- a/cmd/message_input.go +++ b/cmd/message_input.go @@ -22,7 +22,7 @@ type MessageInput struct { func newMessageInput() *MessageInput { mi := &MessageInput{ - TextArea: tview.NewTextArea(), + TextArea: tview.NewTextArea(), replyMessageIdx: -1, } @@ -54,95 +54,92 @@ func (mi *MessageInput) reset() { } func (mi *MessageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey { - switch event.Name() { - case cfg.Keys.MessageInput.Send: - mi.sendAction() - return nil - case "Alt+Enter": - return tcell.NewEventKey(tcell.KeyEnter, 0, tcell.ModNone) - case cfg.Keys.MessageInput.LaunchEditor: - mainFlex.messageInput.launchEditorAction() - return nil - case cfg.Keys.Cancel: - mi.replyMessageIdx = -1 - mi.reset() - return nil - } - - return event -} - -func (mi *MessageInput) sendAction() { - 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 discordState.SendMessageComplex(mainFlex.guildsTree.selectedChannelID, data) - } else { - go discordState.SendMessage(mainFlex.guildsTree.selectedChannelID, text) - } - - mi.replyMessageIdx = -1 - mainFlex.messagesText.Highlight() - mi.reset() -} - -func (mi *MessageInput) launchEditorAction() { - 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 + 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 } - }) - - msg, err := os.ReadFile(f.Name()) - if err != nil { - log.Println(err) - return } - mi.SetText(strings.TrimSpace(string(msg)), true) + return event } diff --git a/cmd/messages_text.go b/cmd/messages_text.go index 47c14b72..a764ab5d 100644 --- a/cmd/messages_text.go +++ b/cmd/messages_text.go @@ -1,10 +1,10 @@ package cmd import ( - "strings" "fmt" "io" "log" + "strings" "time" "github.com/atotto/clipboard" @@ -12,6 +12,7 @@ import ( "github.com/diamondburned/arikawa/v3/discord" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" + "github.com/skratchdot/open-golang/open" ) type MessagesText struct { @@ -132,9 +133,13 @@ func (mt *MessagesText) createBody(w io.Writer, m discord.Message, isReply bool) body = m.Content } - if isReply { fmt.Fprint(w, "[::d]") } + if isReply { + fmt.Fprint(w, "[::d]") + } fmt.Fprint(w, markdown.Parse(tview.Escape(body))) - if isReply { fmt.Fprint(w, "[::-]") } + if isReply { + fmt.Fprint(w, "[::-]") + } } func (mt *MessagesText) createFooter(w io.Writer, m discord.Message) { @@ -145,238 +150,178 @@ func (mt *MessagesText) createFooter(w io.Writer, m discord.Message) { } func (mt *MessagesText) onInputCapture(event *tcell.EventKey) *tcell.EventKey { - switch event.Name() { - case cfg.Keys.MessagesText.CopyContent: - mt.copyContentAction() - return nil - case cfg.Keys.MessagesText.Reply: - mt.replyAction(false) - return nil - case cfg.Keys.MessagesText.ReplyMention: - mt.replyAction(true) - return nil - case cfg.Keys.MessagesText.SelectPrevious: - mt.selectPreviousAction() - return nil - case cfg.Keys.MessagesText.SelectNext: - mt.selectNextAction() - return nil - case cfg.Keys.MessagesText.SelectFirst: - mt.selectFirstAction() - return nil - case cfg.Keys.MessagesText.SelectLast: - mt.selectLastAction() - return nil - case cfg.Keys.MessagesText.SelectReply: - mt.selectReplyAction() - return nil - case cfg.Keys.MessagesText.ShowImage: - mt.showImageAction() - return nil - case cfg.Keys.MessagesText.Delete: - mt.deleteAction() - return nil - case cfg.Keys.Cancel: - mainFlex.guildsTree.selectedChannelID = 0 - - mainFlex.messagesText.reset() - mainFlex.messageInput.reset() - return nil - } - - return event -} - -func (mt *MessagesText) replyAction(mention bool) { - if mt.selectedMessage == -1 { - return - } - - var title string - if mention { - title += "[@] Replying to " - } else { - title += "Replying to " - } - - ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID) - if err != nil { - log.Println(err) - return - } - - title += ms[mt.selectedMessage].Author.Tag() - mainFlex.messageInput.SetTitle(title) - mainFlex.messageInput.replyMessageIdx = mt.selectedMessage - - app.SetFocus(mainFlex.messageInput) -} - -func (mt *MessagesText) selectPreviousAction() { - ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID) - if err != nil { - log.Println(err) - return - } - - // 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 - } - } + switch mainFlex.mode { + case ModeNormal: + switch event.Name() { + case cfg.Keys.Normal.MessagesText.Yank: + if mt.selectedMessage == -1 { + return nil + } - mt.Highlight(ms[mt.selectedMessage].ID.String()) - mt.ScrollToHighlight() -} + ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID) + if err != nil { + log.Println(err) + return nil + } -func (mt *MessagesText) selectNextAction() { - ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID) - if err != nil { - log.Println(err) - return - } + err = clipboard.WriteAll(ms[mt.selectedMessage].Content) + if err != nil { + log.Println("failed to write to clipboard:", err) + return nil + } - // If no message is currently selected, do nothing - if mt.selectedMessage == -1 { return } + return nil - // Otherwise select the next message. This causes the desired - // behaviour of unselecting messages after the last one. - mt.selectedMessage-- + 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 + } - // If a message is selected, highlight it and scroll to it, otherwise remove the highlight - if mt.selectedMessage >= 0 { - mt.Highlight(ms[mt.selectedMessage].ID.String()) - mt.ScrollToHighlight() - } else { - mt.Highlight() - } -} + 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 + } + } + } + } -func (mt *MessagesText) selectFirstAction() { - ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID) - if err != nil { - log.Println(err) - return - } + mt.Highlight(ms[mt.selectedMessage].ID.String()) + mt.ScrollToHighlight() + return nil + case cfg.Keys.Normal.MessagesText.Open: + if mt.selectedMessage == -1 { + return nil + } - mt.selectedMessage = len(ms) - 1 - mt.Highlight(ms[mt.selectedMessage].ID.String()) - mt.ScrollToHighlight() -} + ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID) + if err != nil { + log.Println(err) + return nil + } -func (mt *MessagesText) selectLastAction() { - ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID) - if err != nil { - log.Println(err) - return - } + attachments := ms[mt.selectedMessage].Attachments + if len(attachments) == 0 { + return nil + } - mt.selectedMessage = 0 - mt.Highlight(ms[mt.selectedMessage].ID.String()) - mt.ScrollToHighlight() -} + for _, a := range attachments { + go func() { + if err := open.Start(a.URL); err != nil { + log.Println(err) + } + }() + } -func (mt *MessagesText) selectReplyAction() { - if mt.selectedMessage == -1 { - return - } + return nil + case cfg.Keys.Normal.MessagesText.Reply, cfg.Keys.Normal.MessagesText.ReplyMention: + if mt.selectedMessage == -1 { + return nil + } - ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID) - if err != nil { - log.Println(err) - return - } + var title string + if event.Name() == cfg.Keys.Normal.MessagesText.ReplyMention { + title += "[@] Replying to " + } else { + title += "Replying to " + } - ref := ms[mt.selectedMessage].ReferencedMessage - if ref != nil { - for i, m := range ms { - if ref.ID == m.ID { - mt.selectedMessage = i + ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID) + if err != nil { + log.Println(err) + return nil } - } - mt.Highlight(ms[mt.selectedMessage].ID.String()) - mt.ScrollToHighlight() - } -} + title += ms[mt.selectedMessage].Author.Tag() + mainFlex.messageInput.SetTitle(title) + mainFlex.messageInput.replyMessageIdx = mt.selectedMessage -func (mt *MessagesText) copyContentAction() { - if mt.selectedMessage == -1 { - return - } + 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 - } + 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(err) - return - } -} + m := ms[mt.selectedMessage] + clientID := discordState.Ready().User.ID -func (mt *MessagesText) showImageAction() { - if mt.selectedMessage == -1 { - return - } + ps, err := discordState.Permissions(mainFlex.guildsTree.selectedChannelID, discordState.Ready().User.ID) + if err != nil { + return nil + } - ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID) - if err != nil { - log.Println(err) - return - } + if m.Author.ID != clientID && !ps.Has(discord.PermissionManageMessages) { + return nil + } - as := ms[mt.selectedMessage].Attachments - if len(as) == 0 { - return - } + if err := discordState.DeleteMessage(mainFlex.guildsTree.selectedChannelID, m.ID, ""); err != nil { + log.Println(err) + } - ai, err := newAttachmentImage(as[0]) - if err != nil { - log.Println(err) - return - } + if err := discordState.MessageRemove(mainFlex.guildsTree.selectedChannelID, m.ID); err != nil { + log.Println(err) + } - app.SetRoot(ai, true) -} + ms, err = discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID) + if err != nil { + log.Println(err) + return nil + } -func (mt *MessagesText) deleteAction() { - if mt.selectedMessage == -1 { - return - } + mt.Clear() - ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID) - if err != nil { - log.Println(err) - return - } + for i := len(ms) - 1; i >= 0; i-- { + mainFlex.messagesText.createMessage(ms[i]) + } - m := ms[mt.selectedMessage] - if err := discordState.DeleteMessage(mainFlex.guildsTree.selectedChannelID, m.ID, ""); err != nil { - log.Println(err) - } + return nil + } - if err := discordState.MessageRemove(mainFlex.guildsTree.selectedChannelID, m.ID); err != nil { - log.Println(err) - } + // do not propagate event to the children in normal mode. + return nil - 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]) - } + return event } diff --git a/cmd/state.go b/cmd/state.go index 40ec4dca..61cdf604 100644 --- a/cmd/state.go +++ b/cmd/state.go @@ -43,7 +43,7 @@ func openState(token string) error { } func (s *State) onLog(err error) { - log.Printf("%s\n", err) + log.Println(err.Error()) } func (s *State) onRequest(r httpdriver.Request) error { diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 0fd16f5d..00000000 --- a/docs/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Discordo - -Discordo is a lightweight, secure, and feature-rich Discord terminal client. - -## Table of Contents - -- [FAQ](./faq.md) -- [Configuration](./configuration.md) - -## Warning - -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. - -## Authentication - -There are two ways to login: - -> In both cases, the authentication token is stored securely in the default OS-specific keyring. - -### Email login - -1. Run `discordo` without arguments. -2. Enter your email and password then click on the "Login" button to continue. - -### Token login - -Use the `--token` flag: - -``` -discordo --token "OTI2MDU5NTQxNDE2Nzc5ODA2.Yc2KKA.2iZ-5JxgxG-9Ub8GHzBSn-NJjNg" -``` - -## Logs - -The log file is created on first start-up at the following location: - -| Operating System | Log File Location | -| ---------------- | ---------------------------------------- | -| Unix | `$HOME/.cache/discordo/logs.txt` | -| Darwin | `$HOME/Library/Caches/discordo/logs.txt` | -| Windows | `%LocalAppData%/discordo/logs.txt` | diff --git a/docs/configuration.md b/docs/configuration.md deleted file mode 100644 index 23935e19..00000000 --- a/docs/configuration.md +++ /dev/null @@ -1,7 +0,0 @@ -# Configuration - -The configuration file allows you to configure and customize the behavior, keybindings, and theme of the application. It is automatically created on first start-up at the following location: - -- 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` diff --git a/docs/faq.md b/docs/faq.md deleted file mode 100644 index cd996446..00000000 --- a/docs/faq.md +++ /dev/null @@ -1,7 +0,0 @@ -# FAQ - -## How to log out from an account? - -1. Open the OS-specific keyring application. (Keychain for macOS, Credential Manager for Windows). -2. Select the `login` collection. -3. Select and delete the `discordo` service in the `login` collection. diff --git a/go.mod b/go.mod index 8468aa7e..be75614a 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,13 @@ module github.com/ayn2op/discordo go 1.22.1 require ( + github.com/BurntSushi/toml v1.3.2 github.com/atotto/clipboard v0.1.4 github.com/diamondburned/arikawa/v3 v3.3.5 github.com/gdamore/tcell/v2 v2.7.4 github.com/rivo/tview v0.0.0-20240307173318-e804876934a1 + github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/zalando/go-keyring v0.2.4 - gopkg.in/yaml.v3 v3.0.1 ) require ( diff --git a/go.sum b/go.sum index 621452dd..31283db5 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,15 @@ -github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= -github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= -github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs= github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/diamondburned/arikawa/v3 v3.3.5 h1:Z6BwetBMzPxTBLY2Ixxic2kdJJe0JhNvVrdbJ0gRcWg= github.com/diamondburned/arikawa/v3 v3.3.5/go.mod h1:KPkkWr40xmEithhd15XD2dbkVY8A5+MCmZO0gRXk3qc= -github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= @@ -20,7 +17,6 @@ github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAY github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/schema v1.2.1 h1:tjDxcmdb+siIqkTNoV+qRH2mjYdr2hHe5MKXbp61ziM= github.com/gorilla/schema v1.2.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= @@ -39,13 +35,13 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= -github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68= github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -58,8 +54,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -73,7 +67,6 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -82,7 +75,6 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= @@ -93,7 +85,6 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= @@ -102,7 +93,5 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 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/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go index 34aa90de..b613fc48 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,109 +1,55 @@ package config import ( - "bytes" - _ "embed" - "io" + "log" "os" "path/filepath" + "github.com/BurntSushi/toml" "github.com/ayn2op/discordo/internal/constants" - "gopkg.in/yaml.v3" ) -//go:embed config.yml -var defaultConfig []byte - type Config struct { - Mouse bool `yaml:"mouse"` - - Timestamps bool `yaml:"timestamps"` - TimestampsBeforeAuthor bool `yaml:"timestamps_before_author"` - - MessagesLimit uint8 `yaml:"messages_limit"` - - Editor string `yaml:"editor"` - - Keys struct { - Cancel string `yaml:"cancel"` - - GuildsTree struct { - Focus string `yaml:"focus"` - Toggle string `yaml:"toggle"` - } `yaml:"guilds_tree"` - - MessagesText struct { - Focus string `yaml:"focus"` - - ShowImage string `yaml:"show_image"` - CopyContent string `yaml:"copy_content"` - - Reply string `yaml:"reply"` - ReplyMention string `yaml:"reply_mention"` + Mouse bool `toml:"mouse"` - Delete string `yaml:"delete"` + Timestamps bool `toml:"timestamps"` + TimestampsBeforeAuthor bool `toml:"timestamps_before_author"` - SelectPrevious string `yaml:"select_previous"` - SelectNext string `yaml:"select_next"` - SelectFirst string `yaml:"select_first"` - SelectLast string `yaml:"select_last"` - SelectReply string `yaml:"select_reply"` - } `yaml:"messages_text"` + MessagesLimit uint8 `toml:"messages_limit"` - MessageInput struct { - Focus string `yaml:"focus"` + Editor string `toml:"editor"` - Send string `yaml:"send"` - LaunchEditor string `yaml:"launch_editor"` - } `yaml:"message_input"` - } `yaml:"keys"` - Theme struct { - Border bool `yaml:"border"` - BorderColor string `yaml:"border_color"` - BorderPadding [4]int `yaml:"border_padding,flow"` + Keys Keys `toml:"keys"` + Theme Theme `toml:"theme"` +} - TitleColor string `yaml:"title_color"` - BackgroundColor string `yaml:"background_color"` +func defaultConfig() Config { + return Config{ + Mouse: true, - GuildsTree struct { - AutoExpandFolders bool `yaml:"auto_expand_folders"` - Graphics bool `yaml:"graphics"` - } `yaml:"guilds_tree"` + Timestamps: false, + TimestampsBeforeAuthor: false, - MessagesText struct { - AuthorColor string `yaml:"author_color"` - ReplyIndicator string `yaml:"reply_indicator"` - } `yaml:"messages_text"` - } `yaml:"theme"` -} + MessagesLimit: 50, + Editor: "default", -// Recursively creates the configuration directory if it does not exist already and returns the path to the configuration file. -func initialize() (string, error) { - path, err := os.UserConfigDir() - if err != nil { - return "", err + Keys: defaultKeys(), + Theme: defaultTheme(), } - - path = filepath.Join(path, constants.Name) - if err := os.MkdirAll(path, os.ModePerm); err != nil { - return "", err - } - - return filepath.Join(path, "config.yml"), nil } // Reads the configuration file and parses it. func Load() (*Config, error) { - path, err := initialize() + path, err := os.UserConfigDir() if err != nil { return nil, err } + cfg := defaultConfig() + path = filepath.Join(path, constants.Name, "config.toml") f, err := os.Open(path) - reader := io.Reader(f) if os.IsNotExist(err) { - err = os.WriteFile(path, defaultConfig, os.ModePerm) - reader = bytes.NewReader(defaultConfig) + return &cfg, nil } if err != nil { @@ -111,10 +57,11 @@ func Load() (*Config, error) { } defer f.Close() - var cfg Config - if err := yaml.NewDecoder(reader).Decode(&cfg); err != nil { + if _, err := toml.NewDecoder(f).Decode(&cfg); err != nil { return nil, err } + log.Println(cfg.Theme.MessagesText.ReplyIndicator) + return &cfg, nil } diff --git a/internal/config/config.yml b/internal/config/config.yml deleted file mode 100644 index 90ec2494..00000000 --- a/internal/config/config.yml +++ /dev/null @@ -1,57 +0,0 @@ -# Whether the mouse is usable or not. -mouse: true - -# Whether to draw the timestamps of the corresponding message in front of it. -timestamps: false -# Whether to draw the timestamps before the name of author of the message or not. -timestamps_before_author: false - -# The number of messages to fetch when a text-based channel is selected. The value must be >0 and <=100. -messages_limit: 50 - -# The name of the program to launch when the launch_editor key is pressed. If the value of the field is set to "default", the `$EDITOR` environment variable is used instead. -editor: default - -keys: - cancel: Esc - - guilds_tree: - focus: Ctrl+G - toggle: Ctrl+B - - messages_text: - focus: Ctrl+T - show_image: Rune[i] - copy_content: Rune[c] - delete: Rune[d] - - reply: Rune[r] - reply_mention: Rune[R] - - select_previous: Up - select_next: Down - select_first: Home - select_last: End - select_reply: Rune[s] - - message_input: - focus: Ctrl+P - - send: Enter - launch_editor: Ctrl+E - -theme: - border: true - border_color: default - border_padding: [0, 0, 1, 1] - - title_color: default - background_color: default - - guilds_tree: - auto_expand_folders: true - graphics: true - - messages_text: - author_color: aqua - reply_indicator: ╭ diff --git a/internal/config/keys.go b/internal/config/keys.go new file mode 100644 index 00000000..a881810a --- /dev/null +++ b/internal/config/keys.go @@ -0,0 +1,93 @@ +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"` + 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"` + } + + 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"` + + Reply string `toml:"reply"` + ReplyMention string `toml:"reply_mention"` + + Delete string `toml:"delete"` + Yank string `toml:"yank"` + Open string `toml:"open"` + } + + InsertModeKeys struct { + NormalMode string `toml:"normal_mode"` + + MessageInput MessageInputInsertModeKeys `toml:"message_input"` + } + + MessageInputInsertModeKeys struct { + Send string `toml:"send"` + Editor string `toml:"editor"` + } +) + +func defaultKeys() Keys { + return Keys{ + Normal: NormalModeKeys{ + InsertMode: "Rune[i]", + + FocusGuildsTree: "Ctrl+G", + FocusMessagesText: "Ctrl+T", + ToggleGuildsTree: "Ctrl+B", + + 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]", + }, + }, + Insert: InsertModeKeys{ + NormalMode: "Esc", + MessageInput: MessageInputInsertModeKeys{ + Send: "Enter", + Editor: "Ctrl+E", + }, + }, + } +} diff --git a/internal/config/theme.go b/internal/config/theme.go new file mode 100644 index 00000000..9e8907f9 --- /dev/null +++ b/internal/config/theme.go @@ -0,0 +1,47 @@ +package config + +import "github.com/rivo/tview" + +type ( + Theme struct { + Border bool `toml:"border"` + BorderColor string `toml:"border_color"` + BorderPadding [4]int `toml:"border_padding,flow"` + + TitleColor string `toml:"title_color"` + BackgroundColor string `toml:"background_color"` + + GuildsTree GuildsTreeTheme `toml:"guilds_tree"` + MessagesText MessagesTextTheme `toml:"messages_text"` + } + + GuildsTreeTheme struct { + AutoExpandFolders bool `toml:"auto_expand_folders"` + Graphics bool `toml:"graphics"` + } + + MessagesTextTheme struct { + AuthorColor string `toml:"author_color"` + ReplyIndicator string `toml:"reply_indicator"` + } +) + +func defaultTheme() Theme { + return Theme{ + Border: true, + BorderColor: "default", + BorderPadding: [...]int{0, 0, 1, 1}, + + BackgroundColor: "default", + TitleColor: "default", + + GuildsTree: GuildsTreeTheme{ + AutoExpandFolders: true, + Graphics: true, + }, + MessagesText: MessagesTextTheme{ + AuthorColor: "aqua", + ReplyIndicator: string(tview.BoxDrawingsLightArcDownAndRight) + " ", + }, + } +} diff --git a/internal/ui/login_form.go b/internal/ui/login_form.go index 6a7954db..5f22eb7f 100644 --- a/internal/ui/login_form.go +++ b/internal/ui/login_form.go @@ -2,6 +2,7 @@ package ui import ( "errors" + "log" "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/discordo/internal/constants" @@ -83,7 +84,11 @@ func (lf *LoginForm) onLoginButtonSelected() { rememberMe := lf.GetFormItem(3).(*tview.Checkbox).IsChecked() if rememberMe { - go keyring.Set(constants.Name, "token", lr.Token) + go func() { + if err := keyring.Set(constants.Name, "token", lr.Token); err != nil { + log.Println(err) + } + }() } lf.Token <- Token{Value: lr.Token} diff --git a/main.go b/main.go index 6c99324b..71096132 100644 --- a/main.go +++ b/main.go @@ -11,14 +11,14 @@ import ( func main() { // Declare and parse all flags first - token := flag.String("token", "", "The authentication token.") + token := flag.String("token", "", "authentication token") flag.Parse() // If no token was provided, look it up in the keyring if *token == "" { t, err := keyring.Get(constants.Name, "token") if err != nil { - log.Println("Authentication token not found in keyring:", err) + log.Println("failed to get token from keyring:", err) } else { *token = t }