Implement vi interface (#372)
This commit is contained in:
parent
ff7dbd15c5
commit
d465ede6d8
18 changed files with 596 additions and 629 deletions
80
README.md
80
README.md
|
|
@ -4,15 +4,6 @@ Discordo is a lightweight, secure, and feature-rich Discord terminal client. Hea
|
|||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
|
@ -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`
|
||||
|
|
@ -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
3
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 (
|
||||
|
|
|
|||
23
go.sum
23
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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
93
internal/config/keys.go
Normal 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
47
internal/config/theme.go
Normal 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) + " ",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
4
main.go
4
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue