Implement scripting interface (#194)
This commit is contained in:
parent
57471984ac
commit
41ed0bc604
15 changed files with 745 additions and 494 deletions
|
|
@ -72,7 +72,7 @@ sudo mv ./discordo /usr/local/bin
|
|||
|
||||
### Configuration
|
||||
|
||||
A default configuration file is created on first start-up at `$HOME/.config/discordo/config.toml` on Unix, `$HOME/Library/Application Support/discordo/config.toml` on Darwin, and `%AppData%/discordo/config.toml` on Windows. You can configure the default configuration path using the `config` command-line flag.
|
||||
A default configuration file is created on first start-up at `$HOME/.config/discordo/config.lua` on Unix, `$HOME/Library/Application Support/discordo/config.lua` on Darwin, and `%AppData%/discordo/config.lua` on Windows. You can configure the default configuration path using the `config` command-line flag.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
|
|
|||
150
config/config.go
150
config/config.go
|
|
@ -1,141 +1,77 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
_ "embed"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
type IdentifyConfig struct {
|
||||
UserAgent string `toml:"user_agent"`
|
||||
Browser string `toml:"browser"`
|
||||
BrowserVersion string `toml:"browser_version"`
|
||||
Os string `toml:"os"`
|
||||
}
|
||||
|
||||
type KeysConfig struct {
|
||||
ToggleGuildsTree string `toml:"toggle_guilds_tree"`
|
||||
ToggleChannelsTree string `toml:"toggle_channels_tree"`
|
||||
ToggleMessagesPanel string `toml:"toggle_messages_panel"`
|
||||
ToggleMessageInput string `toml:"toggle_message_input"`
|
||||
|
||||
OpenMessageActionsList string `toml:"open_message_actions_list"`
|
||||
OpenExternalEditor string `toml:"open_external_editor"`
|
||||
|
||||
SelectPreviousMessage string `toml:"select_previous_message"`
|
||||
SelectNextMessage string `toml:"select_next_message"`
|
||||
SelectFirstMessage string `toml:"select_first_message"`
|
||||
SelectLastMessage string `toml:"select_last_message"`
|
||||
}
|
||||
|
||||
type ThemeConfig struct {
|
||||
Background string `toml:"background"`
|
||||
Border string `toml:"border"`
|
||||
Title string `toml:"title"`
|
||||
}
|
||||
//go:embed config.lua
|
||||
var LuaConfig []byte
|
||||
|
||||
// Config initializes a new Lua state, loads a configuration file, and defines essential micellaneous fields.
|
||||
type Config struct {
|
||||
Mouse bool `toml:"mouse"`
|
||||
Timestamps bool `toml:"timestamps"`
|
||||
MessagesLimit uint `toml:"messages_limit"`
|
||||
Timezone string `toml:"timezone"`
|
||||
TimeFormat string `toml:"time_format"`
|
||||
AttachmentDownloadsDir string `toml:"attachment_downloads_dir"`
|
||||
Identify IdentifyConfig `toml:"identify"`
|
||||
Theme ThemeConfig `toml:"theme"`
|
||||
Keys KeysConfig `toml:"keys"`
|
||||
// Path is the path of the configuration file. Its value is the configuration directory until Load() is called.
|
||||
Path string
|
||||
State *lua.LState
|
||||
}
|
||||
|
||||
func New() *Config {
|
||||
func NewConfig(path string) *Config {
|
||||
return &Config{
|
||||
Mouse: true,
|
||||
Timestamps: false,
|
||||
MessagesLimit: 50,
|
||||
Timezone: "Local",
|
||||
TimeFormat: time.Stamp,
|
||||
AttachmentDownloadsDir: UserDownloadsDir(),
|
||||
Identify: IdentifyConfig{
|
||||
UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
|
||||
Browser: "Chrome",
|
||||
BrowserVersion: "104.0.5112.102",
|
||||
Os: "Linux",
|
||||
},
|
||||
Theme: ThemeConfig{
|
||||
Background: "black",
|
||||
Border: "white",
|
||||
Title: "white",
|
||||
},
|
||||
Keys: KeysConfig{
|
||||
ToggleGuildsTree: "Rune[g]",
|
||||
ToggleChannelsTree: "Rune[c]",
|
||||
ToggleMessagesPanel: "Rune[m]",
|
||||
ToggleMessageInput: "Rune[i]",
|
||||
|
||||
OpenMessageActionsList: "Rune[a]",
|
||||
OpenExternalEditor: "Ctrl+E",
|
||||
|
||||
SelectPreviousMessage: "Up",
|
||||
SelectNextMessage: "Down",
|
||||
SelectFirstMessage: "Home",
|
||||
SelectLastMessage: "End",
|
||||
},
|
||||
Path: path,
|
||||
State: lua.NewState(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) Load(path string) error {
|
||||
func (c *Config) Load() error {
|
||||
// Create directories that do not exist and are mentioned in the path recursively.
|
||||
err := os.MkdirAll(filepath.Dir(path), os.ModePerm)
|
||||
err := os.MkdirAll(c.Path, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the configuration file does not exist already, create a new file; otherwise, open the existing file with read-write flag.
|
||||
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, os.ModePerm)
|
||||
c.Path = filepath.Join(c.Path, "config.lua")
|
||||
// Open the existing configuration file with read-only flag.
|
||||
f, err := os.Open(c.Path)
|
||||
// If the configuration file does not exist, create a new configuration file with the read-write flag.
|
||||
if os.IsNotExist(err) {
|
||||
f, err = os.Create(c.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.Write(LuaConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return f.Sync()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the file is empty (the size of the file is zero), write the default configuration to the file.
|
||||
if fi.Size() == 0 {
|
||||
return toml.NewEncoder(f).Encode(c)
|
||||
}
|
||||
|
||||
_, err = toml.NewDecoder(f).Decode(&c)
|
||||
return err
|
||||
LuaConfig = b
|
||||
return nil
|
||||
}
|
||||
|
||||
func DefaultPath() string {
|
||||
path, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
func (c *Config) KeyLua(s *lua.LState) int {
|
||||
keyTable := s.NewTable()
|
||||
keyTable.RawSetString("name", s.Get(1))
|
||||
keyTable.RawSetString("description", s.Get(2))
|
||||
keyTable.RawSetString("action", s.Get(3))
|
||||
|
||||
path += "/discordo/config.toml"
|
||||
return path
|
||||
}
|
||||
|
||||
func UserDownloadsDir() string {
|
||||
// We try to set the download folder location to the default Downloads folder
|
||||
var dlloc string
|
||||
if runtime.GOOS == "windows" {
|
||||
h, _ := os.UserHomeDir()
|
||||
dlloc = h + "\\Downloads"
|
||||
} else if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
|
||||
h, _ := os.UserHomeDir()
|
||||
dlloc = h + "/Downloads"
|
||||
} else {
|
||||
dlloc = os.TempDir() // Very lame fallback, I know
|
||||
}
|
||||
|
||||
return dlloc
|
||||
s.Push(keyTable) // Push the result
|
||||
return 1 // Number of results
|
||||
}
|
||||
|
|
|
|||
127
config/config.lua
Normal file
127
config/config.lua
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
local string = require "string"
|
||||
|
||||
-- Whether the mouse is usable or not.
|
||||
mouse = true
|
||||
|
||||
-- The maximum number of messages to fetch and display on the messages panel.
|
||||
-- Its value must not be lesser than 1 and greater than 100.
|
||||
messagesLimit = 50
|
||||
|
||||
-- Whether to display the timestamp of the message beside the displayed message or not.
|
||||
timestamps = false
|
||||
|
||||
-- The timezone of the timestamps.
|
||||
-- Learn more: https://pkg.go.dev/time#LoadLocation
|
||||
timezone = "Local"
|
||||
|
||||
-- A textual representation of the time value formatted according to the layout defined by its value.
|
||||
-- Learn more: https://pkg.go.dev/time#Layout
|
||||
timeFormat = "3:04PM"
|
||||
|
||||
browser = "Chrome"
|
||||
browserVersion = "104.0.5112.102"
|
||||
oss = "Linux"
|
||||
|
||||
-- Identify properties are connection properties that are dispatched in the IDENTIFY gateway event to trigger the initial handshake with the gateway.
|
||||
-- Learn more: https://discord.com/developers/docs/topics/gateway#identify
|
||||
identifyProperties = {
|
||||
userAgent = string.format(
|
||||
"Mozilla/5.0 (X11; %s x86_64) AppleWebKit/537.36 (KHTML, like Gecko) %s/%s Safari/537.36",
|
||||
oss,
|
||||
browser,
|
||||
browserVersion
|
||||
),
|
||||
browser = browser,
|
||||
browserVersion = browserVersion,
|
||||
os = oss
|
||||
}
|
||||
|
||||
-- Keybindings
|
||||
keys = {
|
||||
application = {
|
||||
key(
|
||||
"Rune[g]",
|
||||
"Focus the guilds tree widget.",
|
||||
function(core, event)
|
||||
core.Application:SetFocus(core.GuildsTree)
|
||||
end
|
||||
),
|
||||
key(
|
||||
"Rune[c]",
|
||||
"Focus the channels tree widget.",
|
||||
function(core, event)
|
||||
core.Application:SetFocus(core.ChannelsTree)
|
||||
end
|
||||
),
|
||||
key(
|
||||
"Rune[m]",
|
||||
"Focus the messages panel widget.",
|
||||
function(core, event)
|
||||
core.Application:SetFocus(core.MessagesPanel)
|
||||
end
|
||||
),
|
||||
key(
|
||||
"Rune[i]",
|
||||
"Focus the message input widget.",
|
||||
function(core, event)
|
||||
core.Application:SetFocus(core.MessageInput)
|
||||
end
|
||||
)
|
||||
},
|
||||
messagesPanel = {
|
||||
key(
|
||||
"Rune[a]",
|
||||
"Open the message actions list widget.",
|
||||
function(core, event)
|
||||
return openMessageActionsList()
|
||||
end
|
||||
),
|
||||
key(
|
||||
"Up",
|
||||
"Select the previous message.",
|
||||
function(core, event)
|
||||
return selectPreviousMessage()
|
||||
end
|
||||
),
|
||||
key(
|
||||
"Down",
|
||||
"Select the next message.",
|
||||
function(core, event)
|
||||
return selectNextMessage()
|
||||
end
|
||||
),
|
||||
key(
|
||||
"Home",
|
||||
"Select the first message.",
|
||||
function(core, event)
|
||||
return selectFirstMessage()
|
||||
end
|
||||
),
|
||||
key(
|
||||
"End",
|
||||
"Select the last message.",
|
||||
function(core, event)
|
||||
return selectLastMessage()
|
||||
end
|
||||
)
|
||||
},
|
||||
messageInput = {
|
||||
key(
|
||||
"Ctrl+E",
|
||||
"Open the external editor.",
|
||||
function()
|
||||
return openExternalEditor()
|
||||
end
|
||||
),
|
||||
key(
|
||||
"Ctrl+V",
|
||||
"Paste the clipboard content.",
|
||||
function()
|
||||
return pasteClipboardContent()
|
||||
end
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
-- Theme
|
||||
theme = {background = "#1F2430", border = "white", title = "white"}
|
||||
7
go.mod
7
go.mod
|
|
@ -3,14 +3,15 @@ module github.com/ayntgl/discordo
|
|||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.0
|
||||
github.com/alecthomas/kong v0.6.1
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/diamondburned/arikawa/v3 v3.0.0
|
||||
github.com/diamondburned/arikawa/v3 v3.1.0
|
||||
github.com/gdamore/tcell/v2 v2.5.3
|
||||
github.com/rivo/tview v0.0.0-20220812085834-0e6b21a48e96
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64
|
||||
github.com/zalando/go-keyring v0.2.1
|
||||
layeh.com/gopher-luar v1.0.10
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -24,7 +25,7 @@ require (
|
|||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.3.4 // indirect
|
||||
golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24 // indirect
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
|
||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
|
||||
|
|
|
|||
17
go.sum
17
go.sum
|
|
@ -1,5 +1,3 @@
|
|||
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
|
||||
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/alecthomas/kong v0.6.1 h1:1kNhcFepkR+HmasQpbiKDLylIL8yh5B5y1zPp5bJimA=
|
||||
github.com/alecthomas/kong v0.6.1/go.mod h1:JfHWDzLmbh/puW6I3V7uWenoh56YNVONW+w8eKeUr9I=
|
||||
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
|
||||
|
|
@ -8,6 +6,9 @@ github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVK
|
|||
github.com/alessio/shellescape v1.4.1/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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
|
||||
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
|
||||
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
|
||||
|
|
@ -16,6 +17,8 @@ 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.0.0 h1:VbdX1DtrBLE752IJftZHInVy6v8I3T8vhN9rKGvO6AY=
|
||||
github.com/diamondburned/arikawa/v3 v3.0.0/go.mod h1:5jBSNnp82Z/EhsKa6Wk9FsOqSxfVkNZDTDBPOj47LpY=
|
||||
github.com/diamondburned/arikawa/v3 v3.1.0 h1:M7ZyjoCM4o1+rzai2NMXjOSukSrPdP6OxV68WKvKZ6E=
|
||||
github.com/diamondburned/arikawa/v3 v3.1.0/go.mod h1:5jBSNnp82Z/EhsKa6Wk9FsOqSxfVkNZDTDBPOj47LpY=
|
||||
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/tcell/v2 v2.5.3 h1:b9XQrT6QGbgI7JvZOJXFNczOQeIYbo8BfeSMzt2sAV0=
|
||||
|
|
@ -49,17 +52,21 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
|
|||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
||||
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ=
|
||||
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/zalando/go-keyring v0.2.1 h1:MBRN/Z8H4U5wEKXiD67YbDAr5cj/DOStmSga70/2qKc=
|
||||
github.com/zalando/go-keyring v0.2.1/go.mod h1:g63M2PPn0w5vjmEbwAX3ib5I+41zdm4esSETOn9Y6Dw=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211001092434-39dca1131b70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24 h1:TyKJRhyo17yWxOMCTHKWrc5rddHORMlnZ/j57umaUd8=
|
||||
golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
|
||||
|
|
@ -78,3 +85,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
layeh.com/gopher-luar v1.0.10 h1:55b0mpBhN9XSshEd2Nz6WsbYXctyBT35azk4POQNSXo=
|
||||
layeh.com/gopher-luar v1.0.10/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk=
|
||||
|
|
|
|||
74
main.go
74
main.go
|
|
@ -2,12 +2,14 @@ package main
|
|||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/ayntgl/discordo/config"
|
||||
"github.com/ayntgl/discordo/ui"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
|
|
@ -18,7 +20,7 @@ const (
|
|||
|
||||
var cli struct {
|
||||
Token string `help:"The authentication token."`
|
||||
Config string `help:"The path of the configuration file." type:"path"`
|
||||
Config string `help:"The path to the configuration directory." type:"path"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
@ -31,30 +33,28 @@ func main() {
|
|||
|
||||
// Defaults
|
||||
if cli.Config == "" {
|
||||
cli.Config = config.DefaultPath()
|
||||
path, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
cli.Config = filepath.Join(path, name)
|
||||
}
|
||||
|
||||
if cli.Token == "" {
|
||||
cli.Token, _ = keyring.Get(name, "token")
|
||||
}
|
||||
|
||||
c := config.New()
|
||||
err := c.Load(cli.Config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
app := ui.NewApp(cli.Token, c)
|
||||
c := ui.NewCore(cli.Config)
|
||||
if cli.Token != "" {
|
||||
err := app.Connect()
|
||||
err := c.Run(cli.Token)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
app.DrawMainFlex()
|
||||
|
||||
app.SetRoot(app.MainFlex, true)
|
||||
app.SetFocus(app.GuildsTree)
|
||||
c.DrawMainFlex()
|
||||
c.Application.SetRoot(c.MainFlex, true)
|
||||
c.Application.SetFocus(c.GuildsTree)
|
||||
} else {
|
||||
loginForm := ui.NewLoginForm(false)
|
||||
loginForm.AddButton("Login", func() {
|
||||
|
|
@ -65,21 +65,21 @@ func main() {
|
|||
}
|
||||
|
||||
// Login using the email and password only
|
||||
lr, err := app.State.Login(email, password)
|
||||
lr, err := c.State.Login(email, password)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if lr.Token != "" && !lr.MFA {
|
||||
app.State.Token = lr.Token
|
||||
err = app.Connect()
|
||||
err = c.Run(lr.Token)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
app.DrawMainFlex()
|
||||
app.SetRoot(app.MainFlex, true)
|
||||
app.SetFocus(app.GuildsTree)
|
||||
c.DrawMainFlex()
|
||||
c.Application.SetRoot(c.MainFlex, true)
|
||||
c.Application.SetFocus(c.GuildsTree)
|
||||
|
||||
go keyring.Set(name, "token", lr.Token)
|
||||
} else {
|
||||
// The account has MFA enabled, reattempt login with MFA code and ticket.
|
||||
|
|
@ -90,29 +90,28 @@ func main() {
|
|||
return
|
||||
}
|
||||
|
||||
lr, err = app.State.TOTP(code, lr.Ticket)
|
||||
lr, err = c.State.TOTP(code, lr.Ticket)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
app.State.Token = lr.Token
|
||||
err = app.Connect()
|
||||
err = c.Run(lr.Token)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
app.DrawMainFlex()
|
||||
app.SetRoot(app.MainFlex, true)
|
||||
app.SetFocus(app.GuildsTree)
|
||||
c.DrawMainFlex()
|
||||
c.Application.SetRoot(c.MainFlex, true)
|
||||
c.Application.SetFocus(c.GuildsTree)
|
||||
|
||||
go keyring.Set(name, "token", lr.Token)
|
||||
})
|
||||
|
||||
app.SetRoot(mfaLoginForm, true)
|
||||
c.Application.SetRoot(mfaLoginForm, true)
|
||||
}
|
||||
})
|
||||
|
||||
app.SetRoot(loginForm, true)
|
||||
c.Application.SetRoot(loginForm, true)
|
||||
}
|
||||
|
||||
tview.Borders.TopLeftFocus = tview.Borders.TopLeft
|
||||
|
|
@ -128,11 +127,20 @@ func main() {
|
|||
tview.Borders.Horizontal = 0
|
||||
tview.Borders.Vertical = 0
|
||||
|
||||
tview.Styles.PrimitiveBackgroundColor = tcell.GetColor(app.Config.Theme.Background)
|
||||
tview.Styles.BorderColor = tcell.GetColor(app.Config.Theme.Border)
|
||||
tview.Styles.TitleColor = tcell.GetColor(app.Config.Theme.Title)
|
||||
themeTable, ok := c.Config.State.GetGlobal("theme").(*lua.LTable)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
err = app.Run()
|
||||
background := themeTable.RawGetString("background")
|
||||
border := themeTable.RawGetString("border")
|
||||
title := themeTable.RawGetString("title")
|
||||
|
||||
tview.Styles.PrimitiveBackgroundColor = tcell.GetColor(lua.LVAsString(background))
|
||||
tview.Styles.BorderColor = tcell.GetColor(lua.LVAsString(border))
|
||||
tview.Styles.TitleColor = tcell.GetColor(lua.LVAsString(title))
|
||||
|
||||
err := c.Application.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
205
ui/app.go
205
ui/app.go
|
|
@ -1,205 +0,0 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/ayntgl/discordo/config"
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
*tview.Application
|
||||
MainFlex *tview.Flex
|
||||
GuildsTree *GuildsTree
|
||||
ChannelsTree *ChannelsTree
|
||||
MessagesPanel *MessagesPanel
|
||||
MessageInput *MessageInput
|
||||
|
||||
Config *config.Config
|
||||
State *state.State
|
||||
}
|
||||
|
||||
func NewApp(token string, c *config.Config) *App {
|
||||
app := &App{
|
||||
Application: tview.NewApplication(),
|
||||
MainFlex: tview.NewFlex(),
|
||||
Config: c,
|
||||
|
||||
State: state.NewWithIdentifier(gateway.NewIdentifier(gateway.IdentifyCommand{
|
||||
Token: token,
|
||||
Intents: nil,
|
||||
Properties: gateway.IdentifyProperties{
|
||||
Browser: c.Identify.Browser,
|
||||
BrowserUserAgent: c.Identify.UserAgent,
|
||||
BrowserVersion: c.Identify.BrowserVersion,
|
||||
OS: c.Identify.Os,
|
||||
},
|
||||
// The official client sets the compress field as false.
|
||||
Compress: false,
|
||||
})),
|
||||
}
|
||||
|
||||
app.GuildsTree = NewGuildsTree(app)
|
||||
app.ChannelsTree = NewChannelsTree(app)
|
||||
app.MessagesPanel = NewMessagesPanel(app)
|
||||
app.MessageInput = NewMessageInput(app)
|
||||
app.EnableMouse(app.Config.Mouse)
|
||||
app.MainFlex.SetInputCapture(app.onInputCapture)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func (app *App) Connect() error {
|
||||
// For user accounts, all of the guilds, the user is in, are dispatched in the READY gateway event.
|
||||
// Whereas, for bot accounts, the guilds are dispatched discretely in the GUILD_CREATE gateway events.
|
||||
if !strings.HasPrefix(app.State.Token, "Bot") {
|
||||
api.UserAgent = app.Config.Identify.UserAgent
|
||||
app.State.AddHandler(app.onStateReady)
|
||||
}
|
||||
|
||||
app.State.AddHandler(app.onStateGuildCreate)
|
||||
app.State.AddHandler(app.onStateGuildDelete)
|
||||
app.State.AddHandler(app.onStateMessageCreate)
|
||||
return app.State.Open(context.Background())
|
||||
}
|
||||
|
||||
func (app *App) onInputCapture(e *tcell.EventKey) *tcell.EventKey {
|
||||
if app.MessageInput.HasFocus() {
|
||||
return e
|
||||
}
|
||||
|
||||
if app.MainFlex.GetItemCount() != 0 {
|
||||
switch e.Name() {
|
||||
case app.Config.Keys.ToggleGuildsTree:
|
||||
app.SetFocus(app.GuildsTree)
|
||||
return nil
|
||||
case app.Config.Keys.ToggleChannelsTree:
|
||||
app.SetFocus(app.ChannelsTree)
|
||||
return nil
|
||||
case app.Config.Keys.ToggleMessagesPanel:
|
||||
app.SetFocus(app.MessagesPanel)
|
||||
return nil
|
||||
case app.Config.Keys.ToggleMessageInput:
|
||||
app.SetFocus(app.MessageInput)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func (app *App) DrawMainFlex() {
|
||||
leftFlex := tview.NewFlex().
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(app.GuildsTree, 10, 1, false).
|
||||
AddItem(app.ChannelsTree, 0, 1, false)
|
||||
rightFlex := tview.NewFlex().
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(app.MessagesPanel, 0, 1, false).
|
||||
AddItem(app.MessageInput, 3, 1, false)
|
||||
app.MainFlex.
|
||||
AddItem(leftFlex, 0, 1, false).
|
||||
AddItem(rightFlex, 0, 4, false)
|
||||
}
|
||||
|
||||
func (app *App) onStateReady(r *gateway.ReadyEvent) {
|
||||
rootNode := app.GuildsTree.GetRoot()
|
||||
for _, gf := range r.UserSettings.GuildFolders {
|
||||
if gf.ID == 0 {
|
||||
for _, gID := range gf.GuildIDs {
|
||||
g, err := app.State.Cabinet.Guild(gID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
guildNode := tview.NewTreeNode(g.Name)
|
||||
guildNode.SetReference(g.ID)
|
||||
rootNode.AddChild(guildNode)
|
||||
}
|
||||
} else {
|
||||
var b strings.Builder
|
||||
|
||||
if gf.Color != discord.NullColor {
|
||||
b.WriteByte('[')
|
||||
b.WriteString(gf.Color.String())
|
||||
b.WriteByte(']')
|
||||
} else {
|
||||
b.WriteString("[#ED4245]")
|
||||
}
|
||||
|
||||
if gf.Name != "" {
|
||||
b.WriteString(gf.Name)
|
||||
} else {
|
||||
b.WriteString("Folder")
|
||||
}
|
||||
|
||||
b.WriteString("[-]")
|
||||
|
||||
folderNode := tview.NewTreeNode(b.String())
|
||||
rootNode.AddChild(folderNode)
|
||||
|
||||
for _, gID := range gf.GuildIDs {
|
||||
g, err := app.State.Cabinet.Guild(gID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
guildNode := tview.NewTreeNode(g.Name)
|
||||
guildNode.SetReference(g.ID)
|
||||
folderNode.AddChild(guildNode)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
app.GuildsTree.SetCurrentNode(rootNode)
|
||||
app.SetFocus(app.GuildsTree)
|
||||
}
|
||||
|
||||
func (app *App) onStateGuildCreate(g *gateway.GuildCreateEvent) {
|
||||
guildNode := tview.NewTreeNode(g.Name)
|
||||
guildNode.SetReference(g.ID)
|
||||
|
||||
rootNode := app.GuildsTree.GetRoot()
|
||||
rootNode.AddChild(guildNode)
|
||||
|
||||
app.Draw()
|
||||
}
|
||||
|
||||
func (app *App) onStateGuildDelete(g *gateway.GuildDeleteEvent) {
|
||||
rootNode := app.GuildsTree.GetRoot()
|
||||
var parentNode *tview.TreeNode
|
||||
rootNode.Walk(func(node, _ *tview.TreeNode) bool {
|
||||
if node.GetReference() == g.ID {
|
||||
parentNode = node
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if parentNode != nil {
|
||||
rootNode.RemoveChild(parentNode)
|
||||
}
|
||||
|
||||
app.Draw()
|
||||
}
|
||||
|
||||
func (app *App) onStateMessageCreate(m *gateway.MessageCreateEvent) {
|
||||
if app.ChannelsTree.SelectedChannel != nil && app.ChannelsTree.SelectedChannel.ID == m.ChannelID {
|
||||
_, err := app.MessagesPanel.Write(buildMessage(app, m.Message))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(app.MessagesPanel.GetHighlights()) == 0 {
|
||||
app.MessagesPanel.ScrollToEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,10 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func buildMessage(app *App, m discord.Message) []byte {
|
||||
func buildMessage(c *Core, m discord.Message) []byte {
|
||||
var b strings.Builder
|
||||
|
||||
switch m.Type {
|
||||
|
|
@ -20,25 +21,30 @@ func buildMessage(app *App, m discord.Message) []byte {
|
|||
b.WriteString(m.ID.String())
|
||||
b.WriteString("\"]")
|
||||
// Build the message associated with crosspost, channel follow add, pin, or a reply.
|
||||
buildReferencedMessage(&b, m.ReferencedMessage, app.State.Ready().User.ID)
|
||||
buildReferencedMessage(&b, m.ReferencedMessage, c.State.Ready().User.ID)
|
||||
|
||||
if app.Config.Timestamps {
|
||||
loc, err := time.LoadLocation(app.Config.Timezone)
|
||||
timestamps := c.Config.State.GetGlobal("timestamps")
|
||||
|
||||
if lua.LVAsBool(timestamps) {
|
||||
timezone := c.Config.State.GetGlobal("timezone")
|
||||
loc, err := time.LoadLocation(lua.LVAsString(timezone))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
timeFormat := c.Config.State.GetGlobal("timeFormat")
|
||||
|
||||
b.WriteString("[::d]")
|
||||
b.WriteString(m.Timestamp.Time().In(loc).Format(app.Config.TimeFormat))
|
||||
b.WriteString(m.Timestamp.Time().In(loc).Format(lua.LVAsString(timeFormat)))
|
||||
b.WriteString("[::-]")
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
|
||||
// Build the author of this message.
|
||||
buildAuthor(&b, m.Author, app.State.Ready().User.ID)
|
||||
buildAuthor(&b, m.Author, c.State.Ready().User.ID)
|
||||
|
||||
// Build the contents of the message.
|
||||
buildContent(&b, m, app.State.Ready().User.ID)
|
||||
buildContent(&b, m, c.State.Ready().User.ID)
|
||||
|
||||
if m.EditedTimestamp.IsValid() {
|
||||
b.WriteString(" [::d](edited)[::-]")
|
||||
|
|
|
|||
|
|
@ -5,19 +5,20 @@ import (
|
|||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/rivo/tview"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
type ChannelsTree struct {
|
||||
*tview.TreeView
|
||||
app *App
|
||||
|
||||
SelectedChannel *discord.Channel
|
||||
|
||||
core *Core
|
||||
}
|
||||
|
||||
func NewChannelsTree(app *App) *ChannelsTree {
|
||||
func NewChannelsTree(c *Core) *ChannelsTree {
|
||||
ct := &ChannelsTree{
|
||||
TreeView: tview.NewTreeView(),
|
||||
app: app,
|
||||
core: c,
|
||||
}
|
||||
|
||||
ct.SetRoot(tview.NewTreeNode(""))
|
||||
|
|
@ -34,15 +35,15 @@ func NewChannelsTree(app *App) *ChannelsTree {
|
|||
|
||||
func (ct *ChannelsTree) onSelected(node *tview.TreeNode) {
|
||||
ct.SelectedChannel = nil
|
||||
ct.app.MessagesPanel.SelectedMessage = -1
|
||||
ct.app.MessagesPanel.
|
||||
ct.core.MessagesPanel.SelectedMessage = -1
|
||||
ct.core.MessagesPanel.
|
||||
Highlight().
|
||||
Clear().
|
||||
SetTitle("")
|
||||
ct.app.MessageInput.SetText("")
|
||||
ct.core.MessageInput.SetText("")
|
||||
|
||||
ref := node.GetReference()
|
||||
c, err := ct.app.State.Cabinet.Channel(ref.(discord.ChannelID))
|
||||
c, err := ct.core.State.Cabinet.Channel(ref.(discord.ChannelID))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -54,29 +55,30 @@ func (ct *ChannelsTree) onSelected(node *tview.TreeNode) {
|
|||
}
|
||||
|
||||
ct.SelectedChannel = c
|
||||
ct.app.SetFocus(ct.app.MessageInput)
|
||||
ct.core.Application.SetFocus(ct.core.MessageInput)
|
||||
|
||||
title := channelToString(*c)
|
||||
if c.Topic != "" {
|
||||
title += " - " + parseMarkdown(c.Topic)
|
||||
}
|
||||
ct.app.MessagesPanel.SetTitle(title)
|
||||
ct.core.MessagesPanel.SetTitle(title)
|
||||
|
||||
go func() {
|
||||
messagesLimit := ct.core.Config.State.GetGlobal("messagesLimit")
|
||||
// The returned slice will be sorted from latest to oldest.
|
||||
ms, err := ct.app.State.Messages(c.ID, ct.app.Config.MessagesLimit)
|
||||
ms, err := ct.core.State.Messages(c.ID, uint(lua.LVAsNumber(messagesLimit)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i := len(ms) - 1; i >= 0; i-- {
|
||||
_, err = ct.app.MessagesPanel.Write(buildMessage(ct.app, ms[i]))
|
||||
_, err = ct.core.MessagesPanel.Write(buildMessage(ct.core, ms[i]))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ct.app.MessagesPanel.ScrollToEnd()
|
||||
ct.core.MessagesPanel.ScrollToEnd()
|
||||
}()
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +90,7 @@ func (ct *ChannelsTree) createChannelNode(c discord.Channel) *tview.TreeNode {
|
|||
}
|
||||
|
||||
func (ct *ChannelsTree) createPrivateChannelNodes(rootNode *tview.TreeNode) {
|
||||
cs, err := ct.app.State.Cabinet.PrivateChannels()
|
||||
cs, err := ct.core.State.Cabinet.PrivateChannels()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -103,7 +105,7 @@ func (ct *ChannelsTree) createPrivateChannelNodes(rootNode *tview.TreeNode) {
|
|||
}
|
||||
|
||||
func (ct *ChannelsTree) createGuildChannelNodes(rootNode *tview.TreeNode, gID discord.GuildID) {
|
||||
cs, err := ct.app.State.Cabinet.Channels(gID)
|
||||
cs, err := ct.core.State.Cabinet.Channels(gID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
261
ui/core.go
Normal file
261
ui/core.go
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/ayntgl/discordo/config"
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
luar "layeh.com/gopher-luar"
|
||||
)
|
||||
|
||||
// Core is responsible for the following:
|
||||
// - Initialization of the application, UI elements, configuration, and state.
|
||||
// - Configuration of the application and state when Run is called.
|
||||
// - Management of the application and state.
|
||||
type Core struct {
|
||||
Application *tview.Application
|
||||
MainFlex *tview.Flex
|
||||
GuildsTree *GuildsTree
|
||||
ChannelsTree *ChannelsTree
|
||||
MessagesPanel *MessagesPanel
|
||||
MessageInput *MessageInput
|
||||
|
||||
Config *config.Config
|
||||
State *state.State
|
||||
}
|
||||
|
||||
func NewCore(path string) *Core {
|
||||
c := &Core{
|
||||
Application: tview.NewApplication(),
|
||||
MainFlex: tview.NewFlex(),
|
||||
|
||||
Config: config.NewConfig(path),
|
||||
}
|
||||
|
||||
c.MainFlex.SetInputCapture(c.onInputCapture)
|
||||
c.GuildsTree = NewGuildsTree(c)
|
||||
c.ChannelsTree = NewChannelsTree(c)
|
||||
c.MessagesPanel = NewMessagesPanel(c)
|
||||
c.MessageInput = NewMessageInput(c)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Core) Run(token string) error {
|
||||
err := c.Config.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.register()
|
||||
err = c.Config.State.DoString(string(config.LuaConfig))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Application.EnableMouse(lua.LVAsBool(c.Config.State.GetGlobal("mouse")))
|
||||
|
||||
identifyProperties, ok := c.Config.State.GetGlobal("identifyProperties").(*lua.LTable)
|
||||
if !ok {
|
||||
identifyProperties = c.Config.State.NewTable()
|
||||
}
|
||||
|
||||
userAgent := lua.LVAsString(identifyProperties.RawGetString("userAgent"))
|
||||
|
||||
c.State = state.NewWithIdentifier(gateway.NewIdentifier(gateway.IdentifyCommand{
|
||||
Token: token,
|
||||
Intents: nil,
|
||||
Properties: gateway.IdentifyProperties{
|
||||
Browser: lua.LVAsString(identifyProperties.RawGetString("browser")),
|
||||
BrowserVersion: lua.LVAsString(identifyProperties.RawGetString("browserVersion")),
|
||||
BrowserUserAgent: userAgent,
|
||||
OS: lua.LVAsString(identifyProperties.RawGetString("os")),
|
||||
},
|
||||
// The official client sets the compress field as false.
|
||||
Compress: false,
|
||||
}))
|
||||
|
||||
// For user accounts, all of the guilds, the user is in, are dispatched in the READY gateway event.
|
||||
// Whereas, for bot accounts, the guilds are dispatched discretely in the GUILD_CREATE gateway events.
|
||||
if !strings.HasPrefix(c.State.Token, "Bot") {
|
||||
api.UserAgent = userAgent
|
||||
c.State.AddHandler(c.onStateReady)
|
||||
}
|
||||
|
||||
c.State.AddHandler(c.onStateGuildCreate)
|
||||
c.State.AddHandler(c.onStateGuildDelete)
|
||||
c.State.AddHandler(c.onStateMessageCreate)
|
||||
return c.State.Open(context.Background())
|
||||
}
|
||||
|
||||
func (c *Core) register() {
|
||||
c.Config.State.SetGlobal("key", c.Config.State.NewFunction(c.Config.KeyLua))
|
||||
// Messages panel
|
||||
c.Config.State.SetGlobal("openMessageActionsList", c.Config.State.NewFunction(c.MessagesPanel.openMessageActionsListLua))
|
||||
c.Config.State.SetGlobal("selectPreviousMessage", c.Config.State.NewFunction(c.MessagesPanel.selectPreviousMessageLua))
|
||||
c.Config.State.SetGlobal("selectNextMessage", c.Config.State.NewFunction(c.MessagesPanel.selectNextMessageLua))
|
||||
c.Config.State.SetGlobal("selectFirstMessage", c.Config.State.NewFunction(c.MessagesPanel.selectFirstMessageLua))
|
||||
c.Config.State.SetGlobal("selectLastMessage", c.Config.State.NewFunction(c.MessagesPanel.selectLastMessageLua))
|
||||
// Message input
|
||||
c.Config.State.SetGlobal("openExternalEditor", c.Config.State.NewFunction(c.MessageInput.openExternalEditorLua))
|
||||
c.Config.State.SetGlobal("pasteClipboardContent", c.Config.State.NewFunction(c.MessageInput.pasteClipboardContentLua))
|
||||
}
|
||||
|
||||
func (c *Core) DrawMainFlex() {
|
||||
leftFlex := tview.NewFlex().
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(c.GuildsTree, 10, 1, false).
|
||||
AddItem(c.ChannelsTree, 0, 1, false)
|
||||
rightFlex := tview.NewFlex().
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(c.MessagesPanel, 0, 1, false).
|
||||
AddItem(c.MessageInput, 3, 1, false)
|
||||
c.MainFlex.
|
||||
AddItem(leftFlex, 0, 1, false).
|
||||
AddItem(rightFlex, 0, 4, false)
|
||||
}
|
||||
|
||||
func (c *Core) onInputCapture(e *tcell.EventKey) *tcell.EventKey {
|
||||
if c.MessageInput.HasFocus() {
|
||||
return e
|
||||
}
|
||||
// If the main flex is nil, that is, it is not initialized yet, then the login form is currently focused.
|
||||
if c.MainFlex == nil {
|
||||
return e
|
||||
}
|
||||
|
||||
keysTable, ok := c.Config.State.GetGlobal("keys").(*lua.LTable)
|
||||
if !ok {
|
||||
return e
|
||||
}
|
||||
|
||||
applicationTable, ok := keysTable.RawGetString("application").(*lua.LTable)
|
||||
if !ok {
|
||||
return e
|
||||
}
|
||||
|
||||
var fn lua.LValue
|
||||
applicationTable.ForEach(func(k, v lua.LValue) {
|
||||
keyTable := v.(*lua.LTable)
|
||||
if e.Name() == lua.LVAsString(keyTable.RawGetString("name")) {
|
||||
fn = keyTable.RawGetString("action")
|
||||
}
|
||||
})
|
||||
|
||||
c.Config.State.CallByParam(lua.P{
|
||||
Fn: fn,
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
}, luar.New(c.Config.State, c), luar.New(c.Config.State, e))
|
||||
// Returned value
|
||||
ret, ok := c.Config.State.Get(-1).(*lua.LUserData)
|
||||
if !ok {
|
||||
return e
|
||||
}
|
||||
|
||||
// Remove returned value
|
||||
c.Config.State.Pop(1)
|
||||
return ret.Value.(*tcell.EventKey)
|
||||
}
|
||||
|
||||
func (c *Core) onStateReady(r *gateway.ReadyEvent) {
|
||||
rootNode := c.GuildsTree.GetRoot()
|
||||
for _, gf := range r.UserSettings.GuildFolders {
|
||||
if gf.ID == 0 {
|
||||
for _, gID := range gf.GuildIDs {
|
||||
g, err := c.State.Cabinet.Guild(gID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
guildNode := tview.NewTreeNode(g.Name)
|
||||
guildNode.SetReference(g.ID)
|
||||
rootNode.AddChild(guildNode)
|
||||
}
|
||||
} else {
|
||||
var b strings.Builder
|
||||
|
||||
if gf.Color != discord.NullColor {
|
||||
b.WriteByte('[')
|
||||
b.WriteString(gf.Color.String())
|
||||
b.WriteByte(']')
|
||||
} else {
|
||||
b.WriteString("[#ED4245]")
|
||||
}
|
||||
|
||||
if gf.Name != "" {
|
||||
b.WriteString(gf.Name)
|
||||
} else {
|
||||
b.WriteString("Folder")
|
||||
}
|
||||
|
||||
b.WriteString("[-]")
|
||||
|
||||
folderNode := tview.NewTreeNode(b.String())
|
||||
rootNode.AddChild(folderNode)
|
||||
|
||||
for _, gID := range gf.GuildIDs {
|
||||
g, err := c.State.Cabinet.Guild(gID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
guildNode := tview.NewTreeNode(g.Name)
|
||||
guildNode.SetReference(g.ID)
|
||||
folderNode.AddChild(guildNode)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
c.GuildsTree.SetCurrentNode(rootNode)
|
||||
c.Application.SetFocus(c.GuildsTree)
|
||||
}
|
||||
|
||||
func (c *Core) onStateGuildCreate(g *gateway.GuildCreateEvent) {
|
||||
guildNode := tview.NewTreeNode(g.Name)
|
||||
guildNode.SetReference(g.ID)
|
||||
|
||||
rootNode := c.GuildsTree.GetRoot()
|
||||
rootNode.AddChild(guildNode)
|
||||
|
||||
c.Application.Draw()
|
||||
}
|
||||
|
||||
func (c *Core) onStateGuildDelete(g *gateway.GuildDeleteEvent) {
|
||||
rootNode := c.GuildsTree.GetRoot()
|
||||
var parentNode *tview.TreeNode
|
||||
rootNode.Walk(func(node, _ *tview.TreeNode) bool {
|
||||
if node.GetReference() == g.ID {
|
||||
parentNode = node
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if parentNode != nil {
|
||||
rootNode.RemoveChild(parentNode)
|
||||
}
|
||||
|
||||
c.Application.Draw()
|
||||
}
|
||||
|
||||
func (c *Core) onStateMessageCreate(m *gateway.MessageCreateEvent) {
|
||||
if c.ChannelsTree.SelectedChannel != nil && c.ChannelsTree.SelectedChannel.ID == m.ChannelID {
|
||||
_, err := c.MessagesPanel.Write(buildMessage(c, m.Message))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(c.MessagesPanel.GetHighlights()) == 0 {
|
||||
c.MessagesPanel.ScrollToEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,13 +7,13 @@ import (
|
|||
|
||||
type GuildsTree struct {
|
||||
*tview.TreeView
|
||||
app *App
|
||||
core *Core
|
||||
}
|
||||
|
||||
func NewGuildsTree(app *App) *GuildsTree {
|
||||
func NewGuildsTree(c *Core) *GuildsTree {
|
||||
gt := &GuildsTree{
|
||||
TreeView: tview.NewTreeView(),
|
||||
app: app,
|
||||
core: c,
|
||||
}
|
||||
|
||||
rootNode := tview.NewTreeNode("")
|
||||
|
|
@ -32,15 +32,15 @@ func NewGuildsTree(app *App) *GuildsTree {
|
|||
}
|
||||
|
||||
func (gt *GuildsTree) onSelected(node *tview.TreeNode) {
|
||||
gt.app.ChannelsTree.SelectedChannel = nil
|
||||
gt.app.MessagesPanel.SelectedMessage = -1
|
||||
rootNode := gt.app.ChannelsTree.GetRoot()
|
||||
gt.core.ChannelsTree.SelectedChannel = nil
|
||||
gt.core.MessagesPanel.SelectedMessage = -1
|
||||
rootNode := gt.core.ChannelsTree.GetRoot()
|
||||
rootNode.ClearChildren()
|
||||
gt.app.MessagesPanel.
|
||||
gt.core.MessagesPanel.
|
||||
Highlight().
|
||||
Clear().
|
||||
SetTitle("")
|
||||
gt.app.MessageInput.SetText("")
|
||||
gt.core.MessageInput.SetText("")
|
||||
|
||||
// If the selected node has children (guild folder), expand the selected node if it is collapsed, otherwise collapse.
|
||||
if len(node.GetChildren()) != 0 {
|
||||
|
|
@ -51,11 +51,11 @@ func (gt *GuildsTree) onSelected(node *tview.TreeNode) {
|
|||
ref := node.GetReference()
|
||||
// If the reference of the selected node is nil, it must be the direct messages node.
|
||||
if ref == nil {
|
||||
gt.app.ChannelsTree.createPrivateChannelNodes(rootNode)
|
||||
gt.core.ChannelsTree.createPrivateChannelNodes(rootNode)
|
||||
} else { // Guild
|
||||
gt.app.ChannelsTree.createGuildChannelNodes(rootNode, ref.(discord.GuildID))
|
||||
gt.core.ChannelsTree.createGuildChannelNodes(rootNode, ref.(discord.GuildID))
|
||||
}
|
||||
|
||||
gt.app.ChannelsTree.SetCurrentNode(rootNode)
|
||||
gt.app.SetFocus(gt.app.ChannelsTree)
|
||||
gt.core.ChannelsTree.SetCurrentNode(rootNode)
|
||||
gt.core.Application.SetFocus(gt.core.ChannelsTree)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,26 +17,25 @@ var linkRegex = regexp.MustCompile("https?://.+")
|
|||
|
||||
type MessageActionsList struct {
|
||||
*tview.List
|
||||
app *App
|
||||
core *Core
|
||||
message *discord.Message
|
||||
}
|
||||
|
||||
func NewMessageActionsList(app *App, m *discord.Message) *MessageActionsList {
|
||||
func NewMessageActionsList(c *Core, m *discord.Message) *MessageActionsList {
|
||||
mal := &MessageActionsList{
|
||||
List: tview.NewList(),
|
||||
app: app,
|
||||
core: c,
|
||||
message: m,
|
||||
}
|
||||
|
||||
mal.ShowSecondaryText(false)
|
||||
mal.SetDoneFunc(func() {
|
||||
app.
|
||||
SetRoot(app.MainFlex, true).
|
||||
SetFocus(app.MessagesPanel)
|
||||
c.Application.SetRoot(c.MainFlex, true)
|
||||
c.Application.SetFocus(c.MessagesPanel)
|
||||
})
|
||||
|
||||
// If the client user has the `SEND_MESSAGES` permission, add "Reply" and "Mention Reply" actions.
|
||||
if hasPermission(app.State, app.ChannelsTree.SelectedChannel.ID, discord.PermissionSendMessages) {
|
||||
if hasPermission(c.State, c.ChannelsTree.SelectedChannel.ID, discord.PermissionSendMessages) {
|
||||
mal.AddItem("Reply", "", 'r', mal.replyAction)
|
||||
mal.AddItem("Mention Reply", "", 'R', mal.mentionReplyAction)
|
||||
}
|
||||
|
|
@ -54,8 +53,8 @@ func NewMessageActionsList(app *App, m *discord.Message) *MessageActionsList {
|
|||
go open.Run(l)
|
||||
}
|
||||
|
||||
app.SetRoot(app.MainFlex, true)
|
||||
app.SetFocus(app.MessagesPanel)
|
||||
c.Application.SetRoot(c.MainFlex, true)
|
||||
c.Application.SetFocus(c.MessagesPanel)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +65,7 @@ func NewMessageActionsList(app *App, m *discord.Message) *MessageActionsList {
|
|||
}
|
||||
|
||||
// If the client user has the `MANAGE_MESSAGES` permission, add a new action to delete the message.
|
||||
if hasPermission(app.State, app.ChannelsTree.SelectedChannel.ID, discord.PermissionManageMessages) {
|
||||
if hasPermission(c.State, c.ChannelsTree.SelectedChannel.ID, discord.PermissionManageMessages) {
|
||||
mal.AddItem("Delete", "", 'd', mal.deleteAction)
|
||||
}
|
||||
|
||||
|
|
@ -82,35 +81,32 @@ func NewMessageActionsList(app *App, m *discord.Message) *MessageActionsList {
|
|||
}
|
||||
|
||||
func (mal *MessageActionsList) replyAction() {
|
||||
mal.app.MessageInput.SetTitle("Replying to " + mal.message.Author.Tag())
|
||||
mal.core.MessageInput.SetTitle("Replying to " + mal.message.Author.Tag())
|
||||
|
||||
mal.app.
|
||||
SetRoot(mal.app.MainFlex, true).
|
||||
SetFocus(mal.app.MessageInput)
|
||||
mal.core.Application.SetRoot(mal.core.MainFlex, true)
|
||||
mal.core.Application.SetFocus(mal.core.MessageInput)
|
||||
}
|
||||
|
||||
func (mal *MessageActionsList) mentionReplyAction() {
|
||||
mal.app.MessageInput.SetTitle("[@] Replying to " + mal.message.Author.Tag())
|
||||
mal.core.MessageInput.SetTitle("[@] Replying to " + mal.message.Author.Tag())
|
||||
|
||||
mal.app.
|
||||
SetRoot(mal.app.MainFlex, true).
|
||||
SetFocus(mal.app.MessageInput)
|
||||
mal.core.Application.SetRoot(mal.core.MainFlex, true)
|
||||
mal.core.Application.SetFocus(mal.core.MessageInput)
|
||||
}
|
||||
|
||||
func (mal *MessageActionsList) selectReplyAction() {
|
||||
ms, err := mal.app.State.Cabinet.Messages(mal.message.ChannelID)
|
||||
ms, err := mal.core.State.Cabinet.Messages(mal.message.ChannelID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mal.app.MessagesPanel.SelectedMessage, _ = findMessageByID(ms, mal.message.ReferencedMessage.ID)
|
||||
mal.app.MessagesPanel.
|
||||
mal.core.MessagesPanel.SelectedMessage, _ = findMessageByID(ms, mal.message.ReferencedMessage.ID)
|
||||
mal.core.MessagesPanel.
|
||||
Highlight(mal.message.ReferencedMessage.ID.String()).
|
||||
ScrollToHighlight()
|
||||
|
||||
mal.app.
|
||||
SetRoot(mal.app.MainFlex, true).
|
||||
SetFocus(mal.app.MessagesPanel)
|
||||
mal.core.Application.SetRoot(mal.core.MainFlex, true)
|
||||
mal.core.Application.SetFocus(mal.core.MessagesPanel)
|
||||
}
|
||||
|
||||
func (mal *MessageActionsList) openAttachmentAction() {
|
||||
|
|
@ -136,14 +132,19 @@ func (mal *MessageActionsList) openAttachmentAction() {
|
|||
go open.Run(f.Name())
|
||||
}
|
||||
|
||||
mal.app.
|
||||
SetRoot(mal.app.MainFlex, true).
|
||||
SetFocus(mal.app.MessagesPanel)
|
||||
mal.core.Application.SetRoot(mal.core.MainFlex, true)
|
||||
mal.core.Application.SetFocus(mal.core.MessagesPanel)
|
||||
}
|
||||
|
||||
func (mal *MessageActionsList) downloadAttachmentAction() {
|
||||
for _, a := range mal.message.Attachments {
|
||||
f, err := os.Create(filepath.Join(mal.app.Config.AttachmentDownloadsDir, a.Filename))
|
||||
path, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
path = os.TempDir()
|
||||
}
|
||||
|
||||
path = filepath.Join(path, "Downloads", a.Filename)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -162,40 +163,38 @@ func (mal *MessageActionsList) downloadAttachmentAction() {
|
|||
f.Write(d)
|
||||
}
|
||||
|
||||
mal.app.
|
||||
SetRoot(mal.app.MainFlex, true).
|
||||
SetFocus(mal.app.MessagesPanel)
|
||||
mal.core.Application.SetRoot(mal.core.MainFlex, true)
|
||||
mal.core.Application.SetFocus(mal.core.MessagesPanel)
|
||||
}
|
||||
|
||||
func (mal *MessageActionsList) deleteAction() {
|
||||
mal.app.MessagesPanel.Clear()
|
||||
mal.core.MessagesPanel.Clear()
|
||||
|
||||
err := mal.app.State.MessageRemove(mal.message.ChannelID, mal.message.ID)
|
||||
err := mal.core.State.MessageRemove(mal.message.ChannelID, mal.message.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = mal.app.State.DeleteMessage(mal.message.ChannelID, mal.message.ID, "Unknown")
|
||||
err = mal.core.State.DeleteMessage(mal.message.ChannelID, mal.message.ID, "Unknown")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// The returned slice will be sorted from latest to oldest.
|
||||
ms, err := mal.app.State.Cabinet.Messages(mal.message.ChannelID)
|
||||
ms, err := mal.core.State.Cabinet.Messages(mal.message.ChannelID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i := len(ms) - 1; i >= 0; i-- {
|
||||
_, err = mal.app.MessagesPanel.Write(buildMessage(mal.app, ms[i]))
|
||||
_, err = mal.core.MessagesPanel.Write(buildMessage(mal.core, ms[i]))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
mal.app.
|
||||
SetRoot(mal.app.MainFlex, true).
|
||||
SetFocus(mal.app.MessagesPanel)
|
||||
mal.core.Application.SetRoot(mal.core.MainFlex, true)
|
||||
mal.core.Application.SetFocus(mal.core.MessagesPanel)
|
||||
}
|
||||
|
||||
func (mal *MessageActionsList) copyContentAction() {
|
||||
|
|
@ -204,8 +203,8 @@ func (mal *MessageActionsList) copyContentAction() {
|
|||
return
|
||||
}
|
||||
|
||||
mal.app.SetRoot(mal.app.MainFlex, true)
|
||||
mal.app.SetFocus(mal.app.MessagesPanel)
|
||||
mal.core.Application.SetRoot(mal.core.MainFlex, true)
|
||||
mal.core.Application.SetFocus(mal.core.MessagesPanel)
|
||||
}
|
||||
|
||||
func (mal *MessageActionsList) copyIDAction() {
|
||||
|
|
@ -214,6 +213,6 @@ func (mal *MessageActionsList) copyIDAction() {
|
|||
return
|
||||
}
|
||||
|
||||
mal.app.SetRoot(mal.app.MainFlex, true)
|
||||
mal.app.SetFocus(mal.app.MessagesPanel)
|
||||
mal.core.Application.SetRoot(mal.core.MainFlex, true)
|
||||
mal.core.Application.SetFocus(mal.core.MessagesPanel)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,17 +12,19 @@ import (
|
|||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
luar "layeh.com/gopher-luar"
|
||||
)
|
||||
|
||||
type MessageInput struct {
|
||||
*tview.InputField
|
||||
app *App
|
||||
core *Core
|
||||
}
|
||||
|
||||
func NewMessageInput(app *App) *MessageInput {
|
||||
func NewMessageInput(c *Core) *MessageInput {
|
||||
mi := &MessageInput{
|
||||
InputField: tview.NewInputField(),
|
||||
app: app,
|
||||
core: c,
|
||||
}
|
||||
|
||||
mi.SetFieldBackgroundColor(tview.Styles.PrimitiveBackgroundColor)
|
||||
|
|
@ -38,29 +40,65 @@ func NewMessageInput(app *App) *MessageInput {
|
|||
}
|
||||
|
||||
func (mi *MessageInput) onInputCapture(e *tcell.EventKey) *tcell.EventKey {
|
||||
keysTable, ok := mi.core.Config.State.GetGlobal("keys").(*lua.LTable)
|
||||
if !ok {
|
||||
return e
|
||||
}
|
||||
|
||||
messageInputTable, ok := keysTable.RawGetString("messageInput").(*lua.LTable)
|
||||
if !ok {
|
||||
return e
|
||||
}
|
||||
|
||||
var fn lua.LValue
|
||||
messageInputTable.ForEach(func(k, v lua.LValue) {
|
||||
keyTable := v.(*lua.LTable)
|
||||
if e.Name() == lua.LVAsString(keyTable.RawGetString("name")) {
|
||||
fn = keyTable.RawGetString("action")
|
||||
}
|
||||
})
|
||||
|
||||
if fn != nil {
|
||||
mi.core.Config.State.CallByParam(lua.P{
|
||||
Fn: fn,
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
}, luar.New(mi.core.Config.State, mi.core), luar.New(mi.core.Config.State, e))
|
||||
// Returned value
|
||||
ret, ok := mi.core.Config.State.Get(-1).(*lua.LUserData)
|
||||
if !ok {
|
||||
return e
|
||||
}
|
||||
|
||||
// Remove returned value
|
||||
mi.core.Config.State.Pop(1)
|
||||
|
||||
ev, ok := ret.Value.(*tcell.EventKey)
|
||||
if ok {
|
||||
return ev
|
||||
}
|
||||
}
|
||||
|
||||
// Defaults
|
||||
switch e.Name() {
|
||||
case "Enter":
|
||||
return mi.sendMessage()
|
||||
case "Ctrl+V":
|
||||
return mi.pasteFromClipboard()
|
||||
case "Esc":
|
||||
mi.
|
||||
SetText("").
|
||||
SetTitle("")
|
||||
mi.app.SetFocus(mi.app.MainFlex)
|
||||
mi.core.Application.SetFocus(mi.core.MainFlex)
|
||||
|
||||
mi.app.MessagesPanel.SelectedMessage = -1
|
||||
mi.app.MessagesPanel.Highlight()
|
||||
mi.core.MessagesPanel.SelectedMessage = -1
|
||||
mi.core.MessagesPanel.Highlight()
|
||||
return nil
|
||||
case mi.app.Config.Keys.OpenExternalEditor:
|
||||
return mi.openExternalEditor()
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func (mi *MessageInput) sendMessage() *tcell.EventKey {
|
||||
if mi.app.ChannelsTree.SelectedChannel == nil {
|
||||
if mi.core.ChannelsTree.SelectedChannel == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -69,13 +107,14 @@ func (mi *MessageInput) sendMessage() *tcell.EventKey {
|
|||
return nil
|
||||
}
|
||||
|
||||
ms, err := mi.app.State.Messages(mi.app.ChannelsTree.SelectedChannel.ID, mi.app.Config.MessagesLimit)
|
||||
messagesLimit := mi.core.Config.State.GetGlobal("messagesLimit")
|
||||
ms, err := mi.core.State.Messages(mi.core.ChannelsTree.SelectedChannel.ID, uint(lua.LVAsNumber(messagesLimit)))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(mi.app.MessagesPanel.GetHighlights()) != 0 {
|
||||
mID, err := discord.ParseSnowflake(mi.app.MessagesPanel.GetHighlights()[0])
|
||||
if len(mi.core.MessagesPanel.GetHighlights()) != 0 {
|
||||
mID, err := discord.ParseSnowflake(mi.core.MessagesPanel.GetHighlights()[0])
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -92,36 +131,36 @@ func (mi *MessageInput) sendMessage() *tcell.EventKey {
|
|||
d.AllowedMentions.RepliedUser = option.True
|
||||
}
|
||||
|
||||
go mi.app.State.SendMessageComplex(m.ChannelID, d)
|
||||
go mi.core.State.SendMessageComplex(m.ChannelID, d)
|
||||
|
||||
mi.app.MessagesPanel.SelectedMessage = -1
|
||||
mi.app.MessagesPanel.Highlight()
|
||||
mi.core.MessagesPanel.SelectedMessage = -1
|
||||
mi.core.MessagesPanel.Highlight()
|
||||
|
||||
mi.SetTitle("")
|
||||
} else {
|
||||
go mi.app.State.SendMessage(mi.app.ChannelsTree.SelectedChannel.ID, t)
|
||||
go mi.core.State.SendMessage(mi.core.ChannelsTree.SelectedChannel.ID, t)
|
||||
}
|
||||
|
||||
mi.SetText("")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mi *MessageInput) pasteFromClipboard() *tcell.EventKey {
|
||||
func (mi *MessageInput) pasteClipboardContentLua(s *lua.LState) int {
|
||||
text, _ := clipboard.ReadAll()
|
||||
text = mi.GetText() + text
|
||||
mi.SetText(text)
|
||||
return nil
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
||||
func (mi *MessageInput) openExternalEditor() *tcell.EventKey {
|
||||
func (mi *MessageInput) openExternalEditorLua(s *lua.LState) int {
|
||||
e := os.Getenv("EDITOR")
|
||||
if e == "" {
|
||||
return nil
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
||||
f, err := os.CreateTemp(os.TempDir(), "discordo-*.md")
|
||||
if err != nil {
|
||||
return nil
|
||||
return returnNilLua(s)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
|
|
@ -129,7 +168,7 @@ func (mi *MessageInput) openExternalEditor() *tcell.EventKey {
|
|||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
|
||||
mi.app.Suspend(func() {
|
||||
mi.core.Application.Suspend(func() {
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return
|
||||
|
|
@ -138,9 +177,9 @@ func (mi *MessageInput) openExternalEditor() *tcell.EventKey {
|
|||
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
||||
mi.SetText(string(b))
|
||||
return nil
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,21 +4,24 @@ import (
|
|||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
luar "layeh.com/gopher-luar"
|
||||
)
|
||||
|
||||
type MessagesPanel struct {
|
||||
*tview.TextView
|
||||
app *App
|
||||
// The index of the currently selected message.
|
||||
// The index of the currently selected message. A negative index indicates that there is no currently selected message.
|
||||
SelectedMessage int
|
||||
|
||||
core *Core
|
||||
}
|
||||
|
||||
func NewMessagesPanel(app *App) *MessagesPanel {
|
||||
func NewMessagesPanel(c *Core) *MessagesPanel {
|
||||
mp := &MessagesPanel{
|
||||
TextView: tview.NewTextView(),
|
||||
app: app,
|
||||
// Negative index indicates that there is no currently selected message.
|
||||
TextView: tview.NewTextView(),
|
||||
SelectedMessage: -1,
|
||||
|
||||
core: c,
|
||||
}
|
||||
|
||||
mp.SetDynamicColors(true)
|
||||
|
|
@ -26,7 +29,7 @@ func NewMessagesPanel(app *App) *MessagesPanel {
|
|||
mp.SetWordWrap(true)
|
||||
mp.SetInputCapture(mp.onInputCapture)
|
||||
mp.SetChangedFunc(func() {
|
||||
mp.app.Draw()
|
||||
mp.core.Application.Draw()
|
||||
})
|
||||
|
||||
mp.SetTitle("Messages")
|
||||
|
|
@ -38,30 +41,60 @@ func NewMessagesPanel(app *App) *MessagesPanel {
|
|||
}
|
||||
|
||||
func (mp *MessagesPanel) onInputCapture(e *tcell.EventKey) *tcell.EventKey {
|
||||
if mp.app.ChannelsTree.SelectedChannel == nil {
|
||||
if mp.core.ChannelsTree.SelectedChannel == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Messages should return messages ordered from latest to earliest.
|
||||
ms, err := mp.app.State.Cabinet.Messages(mp.app.ChannelsTree.SelectedChannel.ID)
|
||||
ms, err := mp.core.State.Cabinet.Messages(mp.core.ChannelsTree.SelectedChannel.ID)
|
||||
if err != nil || len(ms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
keysTable, ok := mp.core.Config.State.GetGlobal("keys").(*lua.LTable)
|
||||
if !ok {
|
||||
return e
|
||||
}
|
||||
|
||||
messagesPanel, ok := keysTable.RawGetString("messagesPanel").(*lua.LTable)
|
||||
if !ok {
|
||||
return e
|
||||
}
|
||||
|
||||
var fn lua.LValue
|
||||
messagesPanel.ForEach(func(k, v lua.LValue) {
|
||||
keyTable := v.(*lua.LTable)
|
||||
if e.Name() == lua.LVAsString(keyTable.RawGetString("name")) {
|
||||
fn = keyTable.RawGetString("action")
|
||||
}
|
||||
})
|
||||
|
||||
if fn != nil {
|
||||
mp.core.Config.State.CallByParam(lua.P{
|
||||
Fn: fn,
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
}, luar.New(mp.core.Config.State, mp.core), luar.New(mp.core.Config.State, e))
|
||||
// Returned value
|
||||
ret, ok := mp.core.Config.State.Get(-1).(*lua.LUserData)
|
||||
if !ok {
|
||||
return e
|
||||
}
|
||||
|
||||
// Remove returned value
|
||||
mp.core.Config.State.Pop(1)
|
||||
|
||||
ev, ok := ret.Value.(*tcell.EventKey)
|
||||
if ok {
|
||||
return ev
|
||||
}
|
||||
}
|
||||
|
||||
// Defaults
|
||||
switch e.Name() {
|
||||
case mp.app.Config.Keys.SelectPreviousMessage:
|
||||
return mp.selectPreviousMessage(ms)
|
||||
case mp.app.Config.Keys.SelectNextMessage:
|
||||
return mp.selectNextMessage(ms)
|
||||
case mp.app.Config.Keys.SelectFirstMessage:
|
||||
return mp.selectFirstMessage(ms)
|
||||
case mp.app.Config.Keys.SelectLastMessage:
|
||||
return mp.selectLastMessage(ms)
|
||||
case mp.app.Config.Keys.OpenMessageActionsList:
|
||||
return mp.openMessageActionsList(ms)
|
||||
case "Esc":
|
||||
mp.SelectedMessage = -1
|
||||
mp.app.SetFocus(mp.app.MainFlex)
|
||||
mp.core.Application.SetFocus(mp.core.MainFlex)
|
||||
mp.
|
||||
Clear().
|
||||
Highlight().
|
||||
|
|
@ -72,7 +105,13 @@ func (mp *MessagesPanel) onInputCapture(e *tcell.EventKey) *tcell.EventKey {
|
|||
return e
|
||||
}
|
||||
|
||||
func (mp *MessagesPanel) selectPreviousMessage(ms []discord.Message) *tcell.EventKey {
|
||||
func (mp *MessagesPanel) selectPreviousMessageLua(s *lua.LState) int {
|
||||
// Messages should return messages ordered from latest to earliest.
|
||||
ms, err := mp.core.State.Cabinet.Messages(mp.core.ChannelsTree.SelectedChannel.ID)
|
||||
if err != nil || len(ms) == 0 {
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
||||
// If there are no highlighted regions, select the latest (last) message in the messages panel.
|
||||
if len(mp.GetHighlights()) == 0 {
|
||||
mp.SelectedMessage = 0
|
||||
|
|
@ -85,13 +124,18 @@ func (mp *MessagesPanel) selectPreviousMessage(ms []discord.Message) *tcell.Even
|
|||
}
|
||||
}
|
||||
|
||||
mp.
|
||||
Highlight(ms[mp.SelectedMessage].ID.String()).
|
||||
ScrollToHighlight()
|
||||
return nil
|
||||
mp.Highlight(ms[mp.SelectedMessage].ID.String())
|
||||
mp.ScrollToHighlight()
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
||||
func (mp *MessagesPanel) selectNextMessage(ms []discord.Message) *tcell.EventKey {
|
||||
func (mp *MessagesPanel) selectNextMessageLua(s *lua.LState) int {
|
||||
// Messages should return messages ordered from latest to earliest.
|
||||
ms, err := mp.core.State.Cabinet.Messages(mp.core.ChannelsTree.SelectedChannel.ID)
|
||||
if err != nil || len(ms) == 0 {
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
||||
// If there are no highlighted regions, select the latest (last) message in the messages panel.
|
||||
if len(mp.GetHighlights()) == 0 {
|
||||
mp.SelectedMessage = 0
|
||||
|
|
@ -107,42 +151,60 @@ func (mp *MessagesPanel) selectNextMessage(ms []discord.Message) *tcell.EventKey
|
|||
mp.
|
||||
Highlight(ms[mp.SelectedMessage].ID.String()).
|
||||
ScrollToHighlight()
|
||||
return nil
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
||||
func (mp *MessagesPanel) selectFirstMessage(ms []discord.Message) *tcell.EventKey {
|
||||
func (mp *MessagesPanel) selectFirstMessageLua(s *lua.LState) int {
|
||||
// Messages should return messages ordered from latest to earliest.
|
||||
ms, err := mp.core.State.Cabinet.Messages(mp.core.ChannelsTree.SelectedChannel.ID)
|
||||
if err != nil || len(ms) == 0 {
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
||||
mp.SelectedMessage = len(ms) - 1
|
||||
mp.
|
||||
Highlight(ms[mp.SelectedMessage].ID.String()).
|
||||
ScrollToHighlight()
|
||||
return nil
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
||||
func (mp *MessagesPanel) selectLastMessage(ms []discord.Message) *tcell.EventKey {
|
||||
func (mp *MessagesPanel) selectLastMessageLua(s *lua.LState) int {
|
||||
// Messages should return messages ordered from latest to earliest.
|
||||
ms, err := mp.core.State.Cabinet.Messages(mp.core.ChannelsTree.SelectedChannel.ID)
|
||||
if err != nil || len(ms) == 0 {
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
||||
mp.SelectedMessage = 0
|
||||
mp.
|
||||
Highlight(ms[mp.SelectedMessage].ID.String()).
|
||||
ScrollToHighlight()
|
||||
return nil
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
||||
func (mp *MessagesPanel) openMessageActionsList(ms []discord.Message) *tcell.EventKey {
|
||||
func (mp *MessagesPanel) openMessageActionsListLua(s *lua.LState) int {
|
||||
// Messages should return messages ordered from latest to earliest.
|
||||
ms, err := mp.core.State.Cabinet.Messages(mp.core.ChannelsTree.SelectedChannel.ID)
|
||||
if err != nil || len(ms) == 0 {
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
||||
hs := mp.GetHighlights()
|
||||
if len(hs) == 0 {
|
||||
return nil
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
||||
mID, err := discord.ParseSnowflake(hs[0])
|
||||
if err != nil {
|
||||
return nil
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
||||
_, m := findMessageByID(ms, discord.MessageID(mID))
|
||||
if m == nil {
|
||||
return nil
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
||||
actionsList := NewMessageActionsList(mp.app, m)
|
||||
mp.app.SetRoot(actionsList, true)
|
||||
return nil
|
||||
actionsList := NewMessageActionsList(mp.core, m)
|
||||
mp.core.Application.SetRoot(actionsList, true)
|
||||
return returnNilLua(s)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -66,3 +67,8 @@ func hasPermission(s *state.State, cID discord.ChannelID, p discord.Permissions)
|
|||
|
||||
return perm&p == p
|
||||
}
|
||||
|
||||
func returnNilLua(s *lua.LState) int {
|
||||
s.Push(lua.LNil) // Push the result
|
||||
return 1 // Number of results
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue