ui.go 9.2 KB

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