core.go 7.9 KB

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