Implement vi interface (#372)

This commit is contained in:
Ayyan 2024-03-26 16:06:39 +05:30 • committed by GitHub
parent ff7dbd15c5
commit d465ede6d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 596 additions and 629 deletions

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
switch mf.mode {
case ModeNormal:
switch event.Name() {
case cfg.Keys.Normal.InsertMode:
mf.mode = ModeInsert
app.SetFocus(mf.messageInput)
return nil
if mf.guildsTree.HasFocus() {
app.SetFocus(mf)
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)
}
} 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

View file

@ -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
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
}
}
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
}
})
msg, err := os.ReadFile(f.Name())
if err != nil {
log.Println(err)
return
}
mi.SetText(strings.TrimSpace(string(msg)), true)
}

View file

@ -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
switch mainFlex.mode {
case ModeNormal:
switch event.Name() {
case cfg.Keys.Normal.MessagesText.Yank:
if mt.selectedMessage == -1 {
return nil
}
mainFlex.messagesText.reset()
mainFlex.messageInput.reset()
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])
}
return nil
}
// do not propagate event to the children in normal mode.
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
}
}
mt.Highlight(ms[mt.selectedMessage].ID.String())
mt.ScrollToHighlight()
}
func (mt *MessagesText) selectNextAction() {
ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
log.Println(err)
return
}
// If no message is currently selected, do nothing
if mt.selectedMessage == -1 { return }
// Otherwise select the next message. This causes the desired
// behaviour of unselecting messages after the last one.
mt.selectedMessage--
// 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()
}
}
func (mt *MessagesText) selectFirstAction() {
ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
log.Println(err)
return
}
mt.selectedMessage = len(ms) - 1
mt.Highlight(ms[mt.selectedMessage].ID.String())
mt.ScrollToHighlight()
}
func (mt *MessagesText) selectLastAction() {
ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
log.Println(err)
return
}
mt.selectedMessage = 0
mt.Highlight(ms[mt.selectedMessage].ID.String())
mt.ScrollToHighlight()
}
func (mt *MessagesText) selectReplyAction() {
if mt.selectedMessage == -1 {
return
}
ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
log.Println(err)
return
}
ref := ms[mt.selectedMessage].ReferencedMessage
if ref != nil {
for i, m := range ms {
if ref.ID == m.ID {
mt.selectedMessage = i
}
}
mt.Highlight(ms[mt.selectedMessage].ID.String())
mt.ScrollToHighlight()
}
}
func (mt *MessagesText) copyContentAction() {
if mt.selectedMessage == -1 {
return
}
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(err)
return
}
}
func (mt *MessagesText) showImageAction() {
if mt.selectedMessage == -1 {
return
}
ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
log.Println(err)
return
}
as := ms[mt.selectedMessage].Attachments
if len(as) == 0 {
return
}
ai, err := newAttachmentImage(as[0])
if err != nil {
log.Println(err)
return
}
app.SetRoot(ai, true)
}
func (mt *MessagesText) deleteAction() {
if mt.selectedMessage == -1 {
return
}
ms, err := discordState.Cabinet.Messages(mainFlex.guildsTree.selectedChannelID)
if err != nil {
log.Println(err)
return
}
m := ms[mt.selectedMessage]
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
}
mt.Clear()
for i := len(ms) - 1; i >= 0; i-- {
mainFlex.messagesText.createMessage(ms[i])
}
}

View file

@ -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 {

View file

@ -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` |

View file

@ -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`

View file

@ -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.

3
go.mod
View file

@ -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 (

23
go.sum
View file

@ -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=

View file

@ -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"`
Mouse bool `toml:"mouse"`
Timestamps bool `yaml:"timestamps"`
TimestampsBeforeAuthor bool `yaml:"timestamps_before_author"`
Timestamps bool `toml:"timestamps"`
TimestampsBeforeAuthor bool `toml:"timestamps_before_author"`
MessagesLimit uint8 `yaml:"messages_limit"`
MessagesLimit uint8 `toml:"messages_limit"`
Editor string `yaml:"editor"`
Editor string `toml:"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"`
Delete string `yaml:"delete"`
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"`
MessageInput struct {
Focus string `yaml:"focus"`
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"`
TitleColor string `yaml:"title_color"`
BackgroundColor string `yaml:"background_color"`
GuildsTree struct {
AutoExpandFolders bool `yaml:"auto_expand_folders"`
Graphics bool `yaml:"graphics"`
} `yaml:"guilds_tree"`
MessagesText struct {
AuthorColor string `yaml:"author_color"`
ReplyIndicator string `yaml:"reply_indicator"`
} `yaml:"messages_text"`
} `yaml:"theme"`
Keys Keys `toml:"keys"`
Theme Theme `toml:"theme"`
}
// 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
}
func defaultConfig() Config {
return Config{
Mouse: true,
path = filepath.Join(path, constants.Name)
if err := os.MkdirAll(path, os.ModePerm); err != nil {
return "", err
}
Timestamps: false,
TimestampsBeforeAuthor: false,
return filepath.Join(path, "config.yml"), nil
MessagesLimit: 50,
Editor: "default",
Keys: defaultKeys(),
Theme: defaultTheme(),
}
}
// 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
}

View file

@ -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: ╭

93
internal/config/keys.go Normal file
View file

@ -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",
},
},
}
}

47
internal/config/theme.go Normal file
View file

@ -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) + " ",
},
}
}

View file

@ -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}

View file

@ -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
}