ui.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  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. guildsTreeView *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(guildsTreeView)
  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 newGuildsTreeView() *tview.TreeView {
  37. w := tview.NewTreeView()
  38. w.
  39. SetSelectedFunc(onGuildsTreeViewSelected).
  40. SetTopLevel(1).
  41. SetRoot(tview.NewTreeNode("")).
  42. SetBorder(true).
  43. SetBorderPadding(0, 0, 1, 0).
  44. SetTitle("Guilds").
  45. SetTitleAlign(tview.AlignLeft)
  46. return w
  47. }
  48. func onGuildsTreeViewSelected(n *tview.TreeNode) {
  49. selectedChannel = nil
  50. selectedMessage = nil
  51. messagesTextView.
  52. Clear().
  53. SetTitle("")
  54. messageInputField.SetText("")
  55. // Unhighlight the already-highlighted regions.
  56. messagesTextView.Highlight()
  57. switch n.GetLevel() {
  58. case 1:
  59. if len(n.GetChildren()) != 0 {
  60. n.SetExpanded(!n.IsExpanded())
  61. return
  62. }
  63. n.ClearChildren()
  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. }
  99. }
  100. }
  101. func newTextChannelTreeNode(c *discordgo.Channel) *tview.TreeNode {
  102. n := tview.NewTreeNode("[::d]#" + c.Name + "[::-]").
  103. SetReference(c.ID)
  104. return n
  105. }
  106. func createTopLevelChannelsTreeNodes(
  107. n *tview.TreeNode,
  108. cs []*discordgo.Channel,
  109. ) {
  110. for _, c := range cs {
  111. if (c.Type == discordgo.ChannelTypeGuildText || c.Type == discordgo.ChannelTypeGuildNews) &&
  112. (c.ParentID == "") {
  113. if p, err := session.State.UserChannelPermissions(session.State.User.ID, c.ID); err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
  114. continue
  115. }
  116. cn := newTextChannelTreeNode(c)
  117. n.AddChild(cn)
  118. continue
  119. }
  120. }
  121. }
  122. func createCategoryChannelsTreeNodes(
  123. n *tview.TreeNode,
  124. cs []*discordgo.Channel,
  125. ) {
  126. CategoryLoop:
  127. for _, c := range cs {
  128. if c.Type == discordgo.ChannelTypeGuildCategory {
  129. if p, err := session.State.UserChannelPermissions(session.State.User.ID, c.ID); err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
  130. continue
  131. }
  132. for _, child := range cs {
  133. if child.ParentID == c.ID {
  134. cn := tview.NewTreeNode(c.Name).
  135. SetReference(c.ID)
  136. n.AddChild(cn)
  137. continue CategoryLoop
  138. }
  139. }
  140. cn := tview.NewTreeNode(c.Name).
  141. SetReference(c.ID)
  142. n.AddChild(cn)
  143. }
  144. }
  145. }
  146. func createSecondLevelChannelsTreeNodes(cs []*discordgo.Channel) {
  147. for _, c := range cs {
  148. if (c.Type == discordgo.ChannelTypeGuildText || c.Type == discordgo.ChannelTypeGuildNews) &&
  149. (c.ParentID != "") {
  150. if p, err := session.State.UserChannelPermissions(session.State.User.ID, c.ID); err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
  151. continue
  152. }
  153. if pn := getTreeNodeByReference(c.ParentID); pn != nil {
  154. cn := newTextChannelTreeNode(c)
  155. pn.AddChild(cn)
  156. }
  157. }
  158. }
  159. }
  160. func getTreeNodeByReference(r interface{}) (mn *tview.TreeNode) {
  161. guildsTreeView.GetRoot().Walk(func(n, _ *tview.TreeNode) bool {
  162. if n.GetReference() == r {
  163. mn = n
  164. return false
  165. }
  166. return true
  167. })
  168. return
  169. }
  170. func newMessagesTextView() *tview.TextView {
  171. w := tview.NewTextView()
  172. w.
  173. SetRegions(true).
  174. SetDynamicColors(true).
  175. SetWordWrap(true).
  176. ScrollToEnd().
  177. SetChangedFunc(func() {
  178. app.Draw()
  179. }).
  180. SetInputCapture(onMessagesTextViewInputCapture).
  181. SetBorder(true).
  182. SetBorderPadding(0, 0, 1, 0).
  183. SetTitleAlign(tview.AlignLeft)
  184. return w
  185. }
  186. func findByMessageID(ms []*discordgo.Message, mID string) (int, *discordgo.Message) {
  187. for i, m := range ms {
  188. if mID == m.ID {
  189. return i, m
  190. }
  191. }
  192. return -1, nil
  193. }
  194. func onMessagesTextViewInputCapture(e *tcell.EventKey) *tcell.EventKey {
  195. if selectedChannel == nil {
  196. return nil
  197. }
  198. switch e.Name() {
  199. case conf.Keybindings.MessagesTextViewSelectPrevious:
  200. ms := selectedChannel.Messages
  201. if len(ms) == 0 {
  202. return nil
  203. }
  204. hs := messagesTextView.GetHighlights()
  205. if len(hs) == 0 {
  206. messagesTextView.
  207. Highlight(ms[len(ms)-1].ID).
  208. ScrollToHighlight()
  209. } else {
  210. idx, _ := findByMessageID(ms, hs[0])
  211. if idx == -1 || idx == 0 {
  212. return nil
  213. }
  214. messagesTextView.
  215. Highlight(ms[idx-1].ID).
  216. ScrollToHighlight()
  217. }
  218. return nil
  219. case conf.Keybindings.MessagesTextViewSelectNext:
  220. ms := selectedChannel.Messages
  221. if len(ms) == 0 {
  222. return nil
  223. }
  224. hs := messagesTextView.GetHighlights()
  225. if len(hs) == 0 {
  226. messagesTextView.
  227. Highlight(ms[len(ms)-1].ID).
  228. ScrollToHighlight()
  229. } else {
  230. idx, _ := findByMessageID(ms, hs[0])
  231. if idx == -1 || idx == len(ms)-1 {
  232. return nil
  233. }
  234. messagesTextView.
  235. Highlight(ms[idx+1].ID).
  236. ScrollToHighlight()
  237. }
  238. return nil
  239. case conf.Keybindings.MessagesTextViewSelectFirst:
  240. ms := selectedChannel.Messages
  241. if len(ms) == 0 {
  242. return nil
  243. }
  244. messagesTextView.
  245. Highlight(ms[0].ID).
  246. ScrollToHighlight()
  247. case conf.Keybindings.MessagesTextViewSelectLast:
  248. ms := selectedChannel.Messages
  249. if len(ms) == 0 {
  250. return nil
  251. }
  252. messagesTextView.
  253. Highlight(ms[len(ms)-1].ID).
  254. ScrollToHighlight()
  255. case conf.Keybindings.MessagesTextViewReplySelected:
  256. ms := selectedChannel.Messages
  257. if len(ms) == 0 {
  258. return nil
  259. }
  260. hs := messagesTextView.GetHighlights()
  261. if len(hs) == 0 {
  262. return nil
  263. }
  264. _, selectedMessage = findByMessageID(ms, hs[0])
  265. messageInputField.SetTitle(
  266. "Replying to " + selectedMessage.Author.Username,
  267. )
  268. app.SetFocus(messageInputField)
  269. case conf.Keybindings.MessagesTextViewMentionReplySelected:
  270. ms := selectedChannel.Messages
  271. if len(ms) == 0 {
  272. return nil
  273. }
  274. hs := messagesTextView.GetHighlights()
  275. if len(hs) == 0 {
  276. return nil
  277. }
  278. _, selectedMessage = findByMessageID(ms, hs[0])
  279. messageInputField.SetTitle("[@] Repling to " + selectedMessage.Author.Username)
  280. app.SetFocus(messageInputField)
  281. }
  282. return e
  283. }
  284. func newMessageInputField() *tview.InputField {
  285. w := tview.NewInputField()
  286. w.
  287. SetPlaceholder("Message...").
  288. SetPlaceholderTextColor(tcell.ColorWhite).
  289. SetFieldBackgroundColor(tview.Styles.PrimitiveBackgroundColor).
  290. SetInputCapture(onMessageInputFieldInputCapture).
  291. SetBorder(true).
  292. SetBorderPadding(0, 0, 1, 0).
  293. SetTitleAlign(tview.AlignLeft)
  294. return w
  295. }
  296. func onMessageInputFieldInputCapture(e *tcell.EventKey) *tcell.EventKey {
  297. // If the "Alt" modifier key is pressed, do not handle the event.
  298. if e.Modifiers() == tcell.ModAlt {
  299. return nil
  300. }
  301. switch e.Key() {
  302. case tcell.KeyEnter:
  303. if selectedChannel == nil {
  304. return nil
  305. }
  306. t := strings.TrimSpace(messageInputField.GetText())
  307. if t == "" {
  308. return nil
  309. }
  310. if selectedMessage != nil {
  311. d := &discordgo.MessageSend{
  312. Content: t,
  313. Reference: selectedMessage.Reference(),
  314. AllowedMentions: &discordgo.MessageAllowedMentions{RepliedUser: false},
  315. }
  316. if strings.HasPrefix(messageInputField.GetTitle(), "[@]") {
  317. d.AllowedMentions.RepliedUser = true
  318. } else {
  319. d.AllowedMentions.RepliedUser = false
  320. }
  321. go session.ChannelMessageSendComplex(selectedMessage.ChannelID, d)
  322. messageInputField.SetTitle("")
  323. selectedMessage = nil
  324. } else {
  325. go session.ChannelMessageSend(selectedChannel.ID, t)
  326. }
  327. messageInputField.SetText("")
  328. case tcell.KeyCtrlV:
  329. text, _ := clipboard.ReadAll()
  330. text = messageInputField.GetText() + text
  331. messageInputField.SetText(text)
  332. case tcell.KeyEscape:
  333. messageInputField.SetTitle("")
  334. selectedMessage = nil
  335. }
  336. return e
  337. }
  338. func newLoginForm(onLoginFormLoginButtonSelected func(), mfa bool) *tview.Form {
  339. w := tview.NewForm()
  340. w.
  341. AddButton("Login", onLoginFormLoginButtonSelected).
  342. SetButtonsAlign(tview.AlignCenter).
  343. SetBorder(true).
  344. SetBorderPadding(0, 0, 1, 0)
  345. if mfa {
  346. w.AddPasswordField("Code", "", 0, 0, nil)
  347. } else {
  348. w.
  349. AddInputField("Email", "", 0, nil, nil).
  350. AddPasswordField("Password", "", 0, 0, nil)
  351. }
  352. return w
  353. }