ui.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  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. mainTreeView *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(mainTreeView)
  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 newMainTreeView() *tview.TreeView {
  37. w := tview.NewTreeView()
  38. w.
  39. SetSelectedFunc(onMainTreeViewSelected).
  40. SetTopLevel(1).
  41. SetRoot(tview.NewTreeNode("")).
  42. SetBorder(true).
  43. SetBorderPadding(0, 0, 1, 0)
  44. return w
  45. }
  46. func onMainTreeViewSelected(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 := "#" + c.Name
  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]#" + c.Name + "[::-]").
  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. mainTreeView.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 findByMessageID(ms []*discordgo.Message, mID string) (int, *discordgo.Message) {
  194. for i, m := range ms {
  195. if mID == m.ID {
  196. return i, m
  197. }
  198. }
  199. return -1, nil
  200. }
  201. func onMessagesTextViewInputCapture(e *tcell.EventKey) *tcell.EventKey {
  202. if selectedChannel == nil {
  203. return nil
  204. }
  205. switch e.Name() {
  206. case conf.Keybindings.MessagesTextViewSelectPrevious:
  207. ms := selectedChannel.Messages
  208. if len(ms) == 0 {
  209. return nil
  210. }
  211. hs := messagesTextView.GetHighlights()
  212. if len(hs) == 0 {
  213. messagesTextView.
  214. Highlight(ms[len(ms)-1].ID).
  215. ScrollToHighlight()
  216. } else {
  217. idx, _ := findByMessageID(ms, hs[0])
  218. if idx == -1 || idx == 0 {
  219. return nil
  220. }
  221. messagesTextView.
  222. Highlight(ms[idx-1].ID).
  223. ScrollToHighlight()
  224. }
  225. return nil
  226. case conf.Keybindings.MessagesTextViewSelectNext:
  227. ms := selectedChannel.Messages
  228. if len(ms) == 0 {
  229. return nil
  230. }
  231. hs := messagesTextView.GetHighlights()
  232. if len(hs) == 0 {
  233. messagesTextView.
  234. Highlight(ms[len(ms)-1].ID).
  235. ScrollToHighlight()
  236. } else {
  237. idx, _ := findByMessageID(ms, hs[0])
  238. if idx == -1 || idx == len(ms)-1 {
  239. return nil
  240. }
  241. messagesTextView.
  242. Highlight(ms[idx+1].ID).
  243. ScrollToHighlight()
  244. }
  245. return nil
  246. case conf.Keybindings.MessagesTextViewSelectFirst:
  247. ms := selectedChannel.Messages
  248. if len(ms) == 0 {
  249. return nil
  250. }
  251. messagesTextView.
  252. Highlight(ms[0].ID).
  253. ScrollToHighlight()
  254. case conf.Keybindings.MessagesTextViewSelectLast:
  255. ms := selectedChannel.Messages
  256. if len(ms) == 0 {
  257. return nil
  258. }
  259. messagesTextView.
  260. Highlight(ms[len(ms)-1].ID).
  261. ScrollToHighlight()
  262. case conf.Keybindings.MessagesTextViewReplySelected:
  263. ms := selectedChannel.Messages
  264. if len(ms) == 0 {
  265. return nil
  266. }
  267. hs := messagesTextView.GetHighlights()
  268. if len(hs) == 0 {
  269. return nil
  270. }
  271. _, selectedMessage = findByMessageID(ms, hs[0])
  272. messageInputField.SetTitle(
  273. "Replying to " + selectedMessage.Author.Username,
  274. )
  275. app.SetFocus(messageInputField)
  276. case conf.Keybindings.MessagesTextViewMentionReplySelected:
  277. ms := selectedChannel.Messages
  278. if len(ms) == 0 {
  279. return nil
  280. }
  281. hs := messagesTextView.GetHighlights()
  282. if len(hs) == 0 {
  283. return nil
  284. }
  285. _, selectedMessage = findByMessageID(ms, hs[0])
  286. messageInputField.SetTitle("[@] Repling to " + selectedMessage.Author.Username)
  287. app.SetFocus(messageInputField)
  288. }
  289. return e
  290. }
  291. func newMessageInputField() *tview.InputField {
  292. w := tview.NewInputField()
  293. w.
  294. SetPlaceholder("Message...").
  295. SetPlaceholderTextColor(tcell.ColorWhite).
  296. SetFieldBackgroundColor(tview.Styles.PrimitiveBackgroundColor).
  297. SetInputCapture(onMessageInputFieldInputCapture).
  298. SetBorder(true).
  299. SetBorderPadding(0, 0, 1, 0).
  300. SetTitleAlign(tview.AlignLeft)
  301. return w
  302. }
  303. func onMessageInputFieldInputCapture(e *tcell.EventKey) *tcell.EventKey {
  304. // If the "Alt" modifier key is pressed, do not handle the event.
  305. if e.Modifiers() == tcell.ModAlt {
  306. return nil
  307. }
  308. switch e.Key() {
  309. case tcell.KeyEnter:
  310. if selectedChannel == nil {
  311. return nil
  312. }
  313. t := strings.TrimSpace(messageInputField.GetText())
  314. if t == "" {
  315. return nil
  316. }
  317. if selectedMessage != nil {
  318. d := &discordgo.MessageSend{
  319. Content: t,
  320. Reference: selectedMessage.Reference(),
  321. AllowedMentions: &discordgo.MessageAllowedMentions{RepliedUser: false},
  322. }
  323. if strings.HasPrefix(messageInputField.GetTitle(), "[@]") {
  324. d.AllowedMentions.RepliedUser = true
  325. } else {
  326. d.AllowedMentions.RepliedUser = false
  327. }
  328. go session.ChannelMessageSendComplex(selectedMessage.ChannelID, d)
  329. messageInputField.SetTitle("")
  330. selectedMessage = nil
  331. } else {
  332. go session.ChannelMessageSend(selectedChannel.ID, t)
  333. }
  334. messageInputField.SetText("")
  335. case tcell.KeyCtrlV:
  336. text, _ := clipboard.ReadAll()
  337. text = messageInputField.GetText() + text
  338. messageInputField.SetText(text)
  339. case tcell.KeyEscape:
  340. messageInputField.SetTitle("")
  341. selectedMessage = nil
  342. }
  343. return e
  344. }
  345. func newLoginForm(onLoginFormLoginButtonSelected func(), mfa bool) *tview.Form {
  346. w := tview.NewForm()
  347. w.
  348. AddButton("Login", onLoginFormLoginButtonSelected).
  349. SetButtonsAlign(tview.AlignCenter).
  350. SetBorder(true).
  351. SetBorderPadding(0, 0, 1, 0)
  352. if mfa {
  353. w.AddPasswordField("Code", "", 0, 0, nil)
  354. } else {
  355. w.
  356. AddInputField("Email", "", 0, nil, nil).
  357. AddPasswordField("Password", "", 0, 0, nil)
  358. }
  359. return w
  360. }