core.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. package ui
  2. import (
  3. "context"
  4. "strings"
  5. "github.com/ayntgl/discordo/config"
  6. "github.com/diamondburned/arikawa/v3/api"
  7. "github.com/diamondburned/arikawa/v3/discord"
  8. "github.com/diamondburned/arikawa/v3/gateway"
  9. "github.com/diamondburned/arikawa/v3/state"
  10. "github.com/gdamore/tcell/v2"
  11. "github.com/rivo/tview"
  12. lua "github.com/yuin/gopher-lua"
  13. luar "layeh.com/gopher-luar"
  14. )
  15. type focused int
  16. const (
  17. guildsTree focused = iota
  18. channelsTree
  19. messagesPanel
  20. messageInput
  21. )
  22. // Core is responsible for the following:
  23. // - Initialization of the application, UI elements, configuration, and state.
  24. // - Configuration of the application and state when Run is called.
  25. // - Management of the application and state.
  26. type Core struct {
  27. Application *tview.Application
  28. MainFlex *tview.Flex
  29. GuildsTree *GuildsTree
  30. ChannelsTree *ChannelsTree
  31. MessagesPanel *MessagesPanel
  32. MessageInput *MessageInput
  33. Config *config.Config
  34. State *state.State
  35. focused focused
  36. }
  37. func NewCore(cfg *config.Config) *Core {
  38. c := &Core{
  39. Application: tview.NewApplication(),
  40. MainFlex: tview.NewFlex(),
  41. Config: cfg,
  42. }
  43. c.Application.SetInputCapture(c.onInputCapture)
  44. c.GuildsTree = NewGuildsTree(c)
  45. c.ChannelsTree = NewChannelsTree(c)
  46. c.MessagesPanel = NewMessagesPanel(c)
  47. c.MessageInput = NewMessageInput(c)
  48. return c
  49. }
  50. func (c *Core) Run(token string) error {
  51. c.register()
  52. err := c.Config.State.DoString(string(config.LuaConfig))
  53. if err != nil {
  54. return err
  55. }
  56. themeTable, ok := c.Config.State.GetGlobal("theme").(*lua.LTable)
  57. if !ok {
  58. themeTable = c.Config.State.NewTable()
  59. }
  60. backgroundColor := tcell.GetColor(lua.LVAsString(themeTable.RawGetString("background")))
  61. borderColor := tcell.GetColor(lua.LVAsString(themeTable.RawGetString("border")))
  62. titleColor := tcell.GetColor(lua.LVAsString(themeTable.RawGetString("title")))
  63. c.GuildsTree.SetBackgroundColor(backgroundColor)
  64. c.GuildsTree.SetBorderColor(borderColor)
  65. c.GuildsTree.SetTitleColor(titleColor)
  66. c.ChannelsTree.SetBackgroundColor(backgroundColor)
  67. c.ChannelsTree.SetBorderColor(borderColor)
  68. c.ChannelsTree.SetTitleColor(titleColor)
  69. c.MessagesPanel.SetBackgroundColor(backgroundColor)
  70. c.MessagesPanel.SetBorderColor(borderColor)
  71. c.MessagesPanel.SetTitleColor(titleColor)
  72. c.MessageInput.SetBackgroundColor(backgroundColor)
  73. c.MessageInput.SetBorderColor(borderColor)
  74. c.MessageInput.SetTitleColor(titleColor)
  75. c.MessageInput.SetPlaceholderStyle(tcell.StyleDefault.Background(backgroundColor))
  76. c.Application.SetBeforeDrawFunc(func(s tcell.Screen) bool {
  77. if backgroundColor == 0 {
  78. s.Clear()
  79. }
  80. return false
  81. })
  82. c.Application.EnableMouse(lua.LVAsBool(c.Config.State.GetGlobal("mouse")))
  83. identifyProperties, ok := c.Config.State.GetGlobal("identifyProperties").(*lua.LTable)
  84. if !ok {
  85. identifyProperties = c.Config.State.NewTable()
  86. }
  87. userAgent := lua.LVAsString(identifyProperties.RawGetString("userAgent"))
  88. c.State = state.NewWithIdentifier(gateway.NewIdentifier(gateway.IdentifyCommand{
  89. Token: token,
  90. Intents: nil,
  91. Properties: gateway.IdentifyProperties{
  92. Browser: lua.LVAsString(identifyProperties.RawGetString("browser")),
  93. BrowserVersion: lua.LVAsString(identifyProperties.RawGetString("browserVersion")),
  94. BrowserUserAgent: userAgent,
  95. OS: lua.LVAsString(identifyProperties.RawGetString("os")),
  96. },
  97. // The official client sets the compress field as false.
  98. Compress: false,
  99. }))
  100. // For user accounts, all of the guilds, the user is in, are dispatched in the READY gateway event. Whereas, the guilds are dispatched discretely in the GUILD_CREATE gateway events for bot accounts.
  101. if !strings.HasPrefix(c.State.Token, "Bot") {
  102. api.UserAgent = userAgent
  103. c.State.AddHandler(c.onStateReady)
  104. } else {
  105. c.State.AddIntents(gateway.IntentGuilds | gateway.IntentGuildMessages)
  106. }
  107. c.State.AddHandler(c.onStateGuildCreate)
  108. c.State.AddHandler(c.onStateGuildDelete)
  109. c.State.AddHandler(c.onStateMessageCreate)
  110. return c.State.Open(context.Background())
  111. }
  112. func (c *Core) register() {
  113. c.Config.State.SetGlobal("key", c.Config.State.NewFunction(c.Config.KeyLua))
  114. // Messages panel
  115. c.Config.State.SetGlobal("openMessageActionsList", c.Config.State.NewFunction(c.MessagesPanel.openMessageActionsListLua))
  116. c.Config.State.SetGlobal("selectPreviousMessage", c.Config.State.NewFunction(c.MessagesPanel.selectPreviousMessageLua))
  117. c.Config.State.SetGlobal("selectNextMessage", c.Config.State.NewFunction(c.MessagesPanel.selectNextMessageLua))
  118. c.Config.State.SetGlobal("selectFirstMessage", c.Config.State.NewFunction(c.MessagesPanel.selectFirstMessageLua))
  119. c.Config.State.SetGlobal("selectLastMessage", c.Config.State.NewFunction(c.MessagesPanel.selectLastMessageLua))
  120. // Message input
  121. c.Config.State.SetGlobal("openExternalEditor", c.Config.State.NewFunction(c.MessageInput.openExternalEditorLua))
  122. c.Config.State.SetGlobal("pasteClipboardContent", c.Config.State.NewFunction(c.MessageInput.pasteClipboardContentLua))
  123. }
  124. func (c *Core) DrawMainFlex() {
  125. leftFlex := tview.NewFlex().
  126. SetDirection(tview.FlexRow).
  127. AddItem(c.GuildsTree, 10, 1, false).
  128. AddItem(c.ChannelsTree, 0, 1, false)
  129. rightFlex := tview.NewFlex().
  130. SetDirection(tview.FlexRow).
  131. AddItem(c.MessagesPanel, 0, 1, false).
  132. AddItem(c.MessageInput, 3, 1, false)
  133. c.MainFlex.
  134. AddItem(leftFlex, 0, 1, false).
  135. AddItem(rightFlex, 0, 4, false)
  136. }
  137. func (c *Core) onInputCapture(e *tcell.EventKey) *tcell.EventKey {
  138. // If the main flex is nil, that is, it is not initialized yet, then the login form is currently focused.
  139. if c.MainFlex == nil {
  140. return e
  141. }
  142. keysTable, ok := c.Config.State.GetGlobal("keys").(*lua.LTable)
  143. if !ok {
  144. keysTable = c.Config.State.NewTable()
  145. }
  146. applicationTable, ok := keysTable.RawGetString("application").(*lua.LTable)
  147. if !ok {
  148. applicationTable = c.Config.State.NewTable()
  149. }
  150. var fn lua.LValue
  151. applicationTable.ForEach(func(k, v lua.LValue) {
  152. keyTable := v.(*lua.LTable)
  153. if e.Name() == lua.LVAsString(keyTable.RawGetString("name")) {
  154. fn = keyTable.RawGetString("action")
  155. }
  156. })
  157. if fn != nil {
  158. c.Config.State.CallByParam(lua.P{
  159. Fn: fn,
  160. NRet: 1,
  161. Protect: true,
  162. }, luar.New(c.Config.State, c), luar.New(c.Config.State, e))
  163. // Returned value
  164. ret, ok := c.Config.State.Get(-1).(*lua.LUserData)
  165. if !ok {
  166. return e
  167. }
  168. // Remove returned value
  169. c.Config.State.Pop(1)
  170. ev, ok := ret.Value.(*tcell.EventKey)
  171. if ok {
  172. return ev
  173. }
  174. }
  175. // Default
  176. switch e.Key() {
  177. case tcell.KeyEsc:
  178. c.focused = 0
  179. case tcell.KeyBacktab:
  180. // If the currently focused widget is the guilds tree widget (first), then focus the message input widget (last)
  181. if c.focused == 0 {
  182. c.focused = messageInput
  183. } else {
  184. c.focused--
  185. }
  186. c.setFocus()
  187. case tcell.KeyTab:
  188. // If the currently focused widget is the message input widget (last), then focus the guilds tree widget (first)
  189. if c.focused == messageInput {
  190. c.focused = guildsTree
  191. } else {
  192. c.focused++
  193. }
  194. c.setFocus()
  195. }
  196. return e
  197. }
  198. func (c *Core) setFocus() {
  199. var p tview.Primitive
  200. switch c.focused {
  201. case guildsTree:
  202. p = c.GuildsTree
  203. case channelsTree:
  204. p = c.ChannelsTree
  205. case messagesPanel:
  206. p = c.MessagesPanel
  207. case messageInput:
  208. p = c.MessageInput
  209. }
  210. c.Application.SetFocus(p)
  211. }
  212. func (c *Core) onStateReady(r *gateway.ReadyEvent) {
  213. rootNode := c.GuildsTree.GetRoot()
  214. for _, gf := range r.UserSettings.GuildFolders {
  215. if gf.ID == 0 {
  216. for _, gID := range gf.GuildIDs {
  217. g, err := c.State.Cabinet.Guild(gID)
  218. if err != nil {
  219. return
  220. }
  221. guildNode := tview.NewTreeNode(g.Name)
  222. guildNode.SetReference(g.ID)
  223. rootNode.AddChild(guildNode)
  224. }
  225. } else {
  226. var b strings.Builder
  227. if gf.Color != discord.NullColor {
  228. b.WriteByte('[')
  229. b.WriteString(gf.Color.String())
  230. b.WriteByte(']')
  231. } else {
  232. b.WriteString("[#ED4245]")
  233. }
  234. if gf.Name != "" {
  235. b.WriteString(gf.Name)
  236. } else {
  237. b.WriteString("Folder")
  238. }
  239. b.WriteString("[-]")
  240. folderNode := tview.NewTreeNode(b.String())
  241. rootNode.AddChild(folderNode)
  242. for _, gID := range gf.GuildIDs {
  243. g, err := c.State.Cabinet.Guild(gID)
  244. if err != nil {
  245. return
  246. }
  247. guildNode := tview.NewTreeNode(g.Name)
  248. guildNode.SetReference(g.ID)
  249. folderNode.AddChild(guildNode)
  250. }
  251. }
  252. }
  253. c.GuildsTree.SetCurrentNode(rootNode)
  254. c.Application.SetFocus(c.GuildsTree)
  255. }
  256. func (c *Core) onStateGuildCreate(g *gateway.GuildCreateEvent) {
  257. guildNode := tview.NewTreeNode(g.Name)
  258. guildNode.SetReference(g.ID)
  259. rootNode := c.GuildsTree.GetRoot()
  260. rootNode.AddChild(guildNode)
  261. c.GuildsTree.SetCurrentNode(rootNode)
  262. c.Application.SetFocus(c.GuildsTree)
  263. c.Application.Draw()
  264. }
  265. func (c *Core) onStateGuildDelete(g *gateway.GuildDeleteEvent) {
  266. rootNode := c.GuildsTree.GetRoot()
  267. var parentNode *tview.TreeNode
  268. rootNode.Walk(func(node, _ *tview.TreeNode) bool {
  269. if node.GetReference() == g.ID {
  270. parentNode = node
  271. return false
  272. }
  273. return true
  274. })
  275. if parentNode != nil {
  276. rootNode.RemoveChild(parentNode)
  277. }
  278. c.Application.Draw()
  279. }
  280. func (c *Core) onStateMessageCreate(m *gateway.MessageCreateEvent) {
  281. if c.ChannelsTree.SelectedChannel != nil && c.ChannelsTree.SelectedChannel.ID == m.ChannelID {
  282. _, err := c.MessagesPanel.Write(buildMessage(c, m.Message))
  283. if err != nil {
  284. return
  285. }
  286. if len(c.MessagesPanel.GetHighlights()) == 0 {
  287. c.MessagesPanel.ScrollToEnd()
  288. }
  289. }
  290. }