ui.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. package main
  2. import (
  3. "strings"
  4. "github.com/atotto/clipboard"
  5. "github.com/ayntgl/discordgo"
  6. "github.com/gdamore/tcell/v2"
  7. "github.com/rivo/tview"
  8. )
  9. var (
  10. app *tview.Application
  11. loginForm *tview.Form
  12. channelsTree *tview.TreeView
  13. messagesTextView *tview.TextView
  14. messageInputField *tview.InputField
  15. mainFlex *tview.Flex
  16. )
  17. func newApplication() *tview.Application {
  18. a := tview.NewApplication()
  19. a.
  20. EnableMouse(conf.Mouse).
  21. SetInputCapture(onAppInputCapture)
  22. return a
  23. }
  24. func onAppInputCapture(e *tcell.EventKey) *tcell.EventKey {
  25. switch e.Name() {
  26. case conf.Keybindings.GuildsTreeViewFocus:
  27. app.SetFocus(channelsTree)
  28. case conf.Keybindings.MessagesTextViewFocus:
  29. app.SetFocus(messagesTextView)
  30. case conf.Keybindings.MessageInputFieldFocus:
  31. app.SetFocus(messageInputField)
  32. }
  33. return e
  34. }
  35. func newChannelsTree() *tview.TreeView {
  36. channelsTree := tview.NewTreeView()
  37. channelsTree.
  38. SetSelectedFunc(onChannelsTreeSelected).
  39. SetTopLevel(1).
  40. SetRoot(tview.NewTreeNode("")).
  41. SetBorder(true).
  42. SetBorderPadding(0, 0, 1, 0)
  43. return channelsTree
  44. }
  45. func onChannelsTreeSelected(n *tview.TreeNode) {
  46. selectedChannel = nil
  47. selectedMessage = nil
  48. messagesTextView.
  49. Clear().
  50. SetTitle("")
  51. messageInputField.SetText("")
  52. // Unhighlight the already-highlighted regions.
  53. messagesTextView.Highlight()
  54. if len(n.GetChildren()) != 0 {
  55. n.SetExpanded(!n.IsExpanded())
  56. } else {
  57. cID := n.GetReference().(string)
  58. c, err := session.State.Channel(cID)
  59. if err != nil {
  60. return
  61. }
  62. selectedChannel = c
  63. app.SetFocus(messageInputField)
  64. switch c.Type {
  65. case discordgo.ChannelTypeGuildText, discordgo.ChannelTypeGuildNews:
  66. title := generateChannelRepr(c)
  67. if c.Topic != "" {
  68. title += " - " + c.Topic
  69. }
  70. messagesTextView.SetTitle(title)
  71. case discordgo.ChannelTypeDM, discordgo.ChannelTypeGroupDM:
  72. messagesTextView.SetTitle(generateChannelRepr(c))
  73. }
  74. messagesTextView.Clear()
  75. go renderMessages(c.ID)
  76. }
  77. }
  78. func newTextChannelTreeNode(c *discordgo.Channel) *tview.TreeNode {
  79. n := tview.NewTreeNode("[::d]" + generateChannelRepr(c) + "[::-]").
  80. SetReference(c.ID)
  81. return n
  82. }
  83. func createTopLevelChannelsTreeNodes(
  84. n *tview.TreeNode,
  85. cs []*discordgo.Channel,
  86. ) {
  87. for _, c := range cs {
  88. if (c.Type == discordgo.ChannelTypeGuildText || c.Type == discordgo.ChannelTypeGuildNews) &&
  89. (c.ParentID == "") {
  90. if p, err := session.State.UserChannelPermissions(session.State.User.ID, c.ID); err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
  91. continue
  92. }
  93. cn := newTextChannelTreeNode(c)
  94. n.AddChild(cn)
  95. continue
  96. }
  97. }
  98. }
  99. func createCategoryChannelsTreeNodes(
  100. n *tview.TreeNode,
  101. cs []*discordgo.Channel,
  102. ) {
  103. CategoryLoop:
  104. for _, c := range cs {
  105. if c.Type == discordgo.ChannelTypeGuildCategory {
  106. if p, err := session.State.UserChannelPermissions(session.State.User.ID, c.ID); err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
  107. continue
  108. }
  109. for _, child := range cs {
  110. if child.ParentID == c.ID {
  111. cn := tview.NewTreeNode(c.Name).
  112. SetReference(c.ID)
  113. n.AddChild(cn)
  114. continue CategoryLoop
  115. }
  116. }
  117. cn := tview.NewTreeNode(c.Name).
  118. SetReference(c.ID)
  119. n.AddChild(cn)
  120. }
  121. }
  122. }
  123. func createSecondLevelChannelsTreeNodes(cs []*discordgo.Channel) {
  124. for _, c := range cs {
  125. if (c.Type == discordgo.ChannelTypeGuildText || c.Type == discordgo.ChannelTypeGuildNews) &&
  126. (c.ParentID != "") {
  127. if p, err := session.State.UserChannelPermissions(session.State.User.ID, c.ID); err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
  128. continue
  129. }
  130. if pn := getTreeNodeByReference(c.ParentID); pn != nil {
  131. cn := newTextChannelTreeNode(c)
  132. pn.AddChild(cn)
  133. }
  134. }
  135. }
  136. }
  137. func getTreeNodeByReference(r interface{}) (mn *tview.TreeNode) {
  138. channelsTree.GetRoot().Walk(func(n, _ *tview.TreeNode) bool {
  139. if n.GetReference() == r {
  140. mn = n
  141. return false
  142. }
  143. return true
  144. })
  145. return
  146. }
  147. func newMessagesTextView() *tview.TextView {
  148. w := tview.NewTextView()
  149. w.
  150. SetRegions(true).
  151. SetDynamicColors(true).
  152. SetWordWrap(true).
  153. ScrollToEnd().
  154. SetChangedFunc(func() {
  155. app.Draw()
  156. }).
  157. SetInputCapture(onMessagesTextViewInputCapture).
  158. SetBorder(true).
  159. SetBorderPadding(0, 0, 1, 0).
  160. SetTitleAlign(tview.AlignLeft)
  161. return w
  162. }
  163. func onMessagesTextViewInputCapture(e *tcell.EventKey) *tcell.EventKey {
  164. if selectedChannel == nil {
  165. return nil
  166. }
  167. switch e.Name() {
  168. case conf.Keybindings.MessagesTextViewSelectPrevious:
  169. ms := selectedChannel.Messages
  170. if len(ms) == 0 {
  171. return nil
  172. }
  173. hs := messagesTextView.GetHighlights()
  174. if len(hs) == 0 {
  175. messagesTextView.
  176. Highlight(ms[len(ms)-1].ID).
  177. ScrollToHighlight()
  178. } else {
  179. idx, _ := findByMessageID(ms, hs[0])
  180. if idx == -1 || idx == 0 {
  181. return nil
  182. }
  183. messagesTextView.
  184. Highlight(ms[idx-1].ID).
  185. ScrollToHighlight()
  186. }
  187. return nil
  188. case conf.Keybindings.MessagesTextViewSelectNext:
  189. ms := selectedChannel.Messages
  190. if len(ms) == 0 {
  191. return nil
  192. }
  193. hs := messagesTextView.GetHighlights()
  194. if len(hs) == 0 {
  195. messagesTextView.
  196. Highlight(ms[len(ms)-1].ID).
  197. ScrollToHighlight()
  198. } else {
  199. idx, _ := findByMessageID(ms, hs[0])
  200. if idx == -1 || idx == len(ms)-1 {
  201. return nil
  202. }
  203. messagesTextView.
  204. Highlight(ms[idx+1].ID).
  205. ScrollToHighlight()
  206. }
  207. return nil
  208. case conf.Keybindings.MessagesTextViewSelectFirst:
  209. ms := selectedChannel.Messages
  210. if len(ms) == 0 {
  211. return nil
  212. }
  213. messagesTextView.
  214. Highlight(ms[0].ID).
  215. ScrollToHighlight()
  216. case conf.Keybindings.MessagesTextViewSelectLast:
  217. ms := selectedChannel.Messages
  218. if len(ms) == 0 {
  219. return nil
  220. }
  221. messagesTextView.
  222. Highlight(ms[len(ms)-1].ID).
  223. ScrollToHighlight()
  224. case conf.Keybindings.MessagesTextViewReplySelected:
  225. ms := selectedChannel.Messages
  226. if len(ms) == 0 {
  227. return nil
  228. }
  229. hs := messagesTextView.GetHighlights()
  230. if len(hs) == 0 {
  231. return nil
  232. }
  233. _, selectedMessage = findByMessageID(ms, hs[0])
  234. messageInputField.SetTitle(
  235. "Replying to " + selectedMessage.Author.Username,
  236. )
  237. app.SetFocus(messageInputField)
  238. case conf.Keybindings.MessagesTextViewMentionReplySelected:
  239. ms := selectedChannel.Messages
  240. if len(ms) == 0 {
  241. return nil
  242. }
  243. hs := messagesTextView.GetHighlights()
  244. if len(hs) == 0 {
  245. return nil
  246. }
  247. _, selectedMessage = findByMessageID(ms, hs[0])
  248. messageInputField.SetTitle("[@] Repling to " + selectedMessage.Author.Username)
  249. app.SetFocus(messageInputField)
  250. }
  251. return e
  252. }
  253. func newMessageInputField() *tview.InputField {
  254. w := tview.NewInputField()
  255. w.
  256. SetPlaceholder("Message...").
  257. SetPlaceholderTextColor(tcell.ColorWhite).
  258. SetFieldBackgroundColor(tview.Styles.PrimitiveBackgroundColor).
  259. SetInputCapture(onMessageInputFieldInputCapture).
  260. SetBorder(true).
  261. SetBorderPadding(0, 0, 1, 0).
  262. SetTitleAlign(tview.AlignLeft)
  263. return w
  264. }
  265. func onMessageInputFieldInputCapture(e *tcell.EventKey) *tcell.EventKey {
  266. // If the "Alt" modifier key is pressed, do not handle the event.
  267. if e.Modifiers() == tcell.ModAlt {
  268. return nil
  269. }
  270. switch e.Key() {
  271. case tcell.KeyEnter:
  272. if selectedChannel == nil {
  273. return nil
  274. }
  275. t := strings.TrimSpace(messageInputField.GetText())
  276. if t == "" {
  277. return nil
  278. }
  279. if selectedMessage != nil {
  280. d := &discordgo.MessageSend{
  281. Content: t,
  282. Reference: selectedMessage.Reference(),
  283. AllowedMentions: &discordgo.MessageAllowedMentions{RepliedUser: false},
  284. }
  285. if strings.HasPrefix(messageInputField.GetTitle(), "[@]") {
  286. d.AllowedMentions.RepliedUser = true
  287. } else {
  288. d.AllowedMentions.RepliedUser = false
  289. }
  290. go session.ChannelMessageSendComplex(selectedMessage.ChannelID, d)
  291. messageInputField.SetTitle("")
  292. selectedMessage = nil
  293. } else {
  294. go session.ChannelMessageSend(selectedChannel.ID, t)
  295. }
  296. messageInputField.SetText("")
  297. case tcell.KeyCtrlV:
  298. text, _ := clipboard.ReadAll()
  299. text = messageInputField.GetText() + text
  300. messageInputField.SetText(text)
  301. case tcell.KeyEscape:
  302. messageInputField.SetTitle("")
  303. selectedMessage = nil
  304. }
  305. return e
  306. }
  307. func newLoginForm(onLoginFormLoginButtonSelected func(), mfa bool) *tview.Form {
  308. w := tview.NewForm()
  309. w.
  310. AddButton("Login", onLoginFormLoginButtonSelected).
  311. SetButtonsAlign(tview.AlignCenter).
  312. SetBorder(true).
  313. SetBorderPadding(0, 0, 1, 0)
  314. if mfa {
  315. w.AddPasswordField("Code", "", 0, 0, nil)
  316. } else {
  317. w.
  318. AddInputField("Email", "", 0, nil, nil).
  319. AddPasswordField("Password", "", 0, 0, nil)
  320. }
  321. return w
  322. }