ui.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  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.ChannelsTree.Focus:
  27. app.SetFocus(channelsTree)
  28. case conf.Keybindings.MessagesTextView.Focus:
  29. app.SetFocus(messagesTextView)
  30. case conf.Keybindings.MessageInputField.Focus:
  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 || n.GetText() == "Direct Messages" {
  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. if strings.HasPrefix(n.GetText(), "[::b]") {
  75. n.SetText("[::d]" + generateChannelRepr(c) + "[::-]")
  76. }
  77. messagesTextView.Clear()
  78. go func() {
  79. ms, err := session.ChannelMessages(cID, conf.GetMessagesLimit, "", "", "")
  80. if err != nil {
  81. return
  82. }
  83. for i := len(ms) - 1; i >= 0; i-- {
  84. selectedChannel.Messages = append(selectedChannel.Messages, ms[i])
  85. renderMessage(ms[i])
  86. }
  87. if len(ms) != 0 && isUnread(c) {
  88. session.ChannelMessageAck(c.ID, c.LastMessageID, "")
  89. }
  90. }()
  91. }
  92. }
  93. func createTopLevelChannelsTreeNodes(
  94. n *tview.TreeNode,
  95. cs []*discordgo.Channel,
  96. ) {
  97. for _, c := range cs {
  98. if (c.Type == discordgo.ChannelTypeGuildText || c.Type == discordgo.ChannelTypeGuildNews) &&
  99. (c.ParentID == "") {
  100. p, err := session.State.UserChannelPermissions(session.State.User.ID, c.ID)
  101. if err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
  102. continue
  103. }
  104. var tag string
  105. if isUnread(c) {
  106. tag = "[::b]"
  107. } else {
  108. tag = "[::d]"
  109. }
  110. cn := tview.NewTreeNode(tag + generateChannelRepr(c) + "[::-]").
  111. SetReference(c.ID)
  112. n.AddChild(cn)
  113. continue
  114. }
  115. }
  116. }
  117. func createCategoryChannelsTreeNodes(
  118. n *tview.TreeNode,
  119. cs []*discordgo.Channel,
  120. ) {
  121. CategoryLoop:
  122. for _, c := range cs {
  123. if c.Type == discordgo.ChannelTypeGuildCategory {
  124. p, err := session.State.UserChannelPermissions(session.State.User.ID, c.ID)
  125. if err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
  126. continue
  127. }
  128. for _, child := range cs {
  129. if child.ParentID == c.ID {
  130. cn := tview.NewTreeNode(c.Name).
  131. SetReference(c.ID)
  132. n.AddChild(cn)
  133. continue CategoryLoop
  134. }
  135. }
  136. cn := tview.NewTreeNode(c.Name).
  137. SetReference(c.ID)
  138. n.AddChild(cn)
  139. }
  140. }
  141. }
  142. func createSecondLevelChannelsTreeNodes(cs []*discordgo.Channel) {
  143. for _, c := range cs {
  144. if (c.Type == discordgo.ChannelTypeGuildText || c.Type == discordgo.ChannelTypeGuildNews) &&
  145. (c.ParentID != "") {
  146. p, err := session.State.UserChannelPermissions(session.State.User.ID, c.ID)
  147. if err != nil || p&discordgo.PermissionViewChannel != discordgo.PermissionViewChannel {
  148. continue
  149. }
  150. var tag string
  151. if isUnread(c) {
  152. tag = "[::b]"
  153. } else {
  154. tag = "[::d]"
  155. }
  156. pn := getTreeNodeByReference(c.ParentID)
  157. if pn != nil {
  158. cn := tview.NewTreeNode(tag + generateChannelRepr(c) + "[::-]").
  159. SetReference(c.ID)
  160. pn.AddChild(cn)
  161. }
  162. }
  163. }
  164. }
  165. func getTreeNodeByReference(r interface{}) (mn *tview.TreeNode) {
  166. channelsTree.GetRoot().Walk(func(n, _ *tview.TreeNode) bool {
  167. if n.GetReference() == r {
  168. mn = n
  169. return false
  170. }
  171. return true
  172. })
  173. return
  174. }
  175. func newMessagesTextView() *tview.TextView {
  176. w := tview.NewTextView()
  177. w.
  178. SetRegions(true).
  179. SetDynamicColors(true).
  180. SetWordWrap(true).
  181. ScrollToEnd().
  182. SetChangedFunc(func() {
  183. app.Draw()
  184. }).
  185. SetInputCapture(onMessagesTextViewInputCapture).
  186. SetBorder(true).
  187. SetBorderPadding(0, 0, 1, 0).
  188. SetTitleAlign(tview.AlignLeft)
  189. return w
  190. }
  191. func onMessagesTextViewInputCapture(e *tcell.EventKey) *tcell.EventKey {
  192. if selectedChannel == nil {
  193. return nil
  194. }
  195. switch e.Name() {
  196. case conf.Keybindings.MessagesTextView.SelectPrevious:
  197. ms := selectedChannel.Messages
  198. if len(ms) == 0 {
  199. return nil
  200. }
  201. hs := messagesTextView.GetHighlights()
  202. if len(hs) == 0 {
  203. messagesTextView.
  204. Highlight(ms[len(ms)-1].ID).
  205. ScrollToHighlight()
  206. } else {
  207. idx, _ := findByMessageID(ms, hs[0])
  208. if idx == -1 || idx == 0 {
  209. return nil
  210. }
  211. messagesTextView.
  212. Highlight(ms[idx-1].ID).
  213. ScrollToHighlight()
  214. }
  215. return nil
  216. case conf.Keybindings.MessagesTextView.SelectNext:
  217. ms := selectedChannel.Messages
  218. if len(ms) == 0 {
  219. return nil
  220. }
  221. hs := messagesTextView.GetHighlights()
  222. if len(hs) == 0 {
  223. messagesTextView.
  224. Highlight(ms[len(ms)-1].ID).
  225. ScrollToHighlight()
  226. } else {
  227. idx, _ := findByMessageID(ms, hs[0])
  228. if idx == -1 || idx == len(ms)-1 {
  229. return nil
  230. }
  231. messagesTextView.
  232. Highlight(ms[idx+1].ID).
  233. ScrollToHighlight()
  234. }
  235. return nil
  236. case conf.Keybindings.MessagesTextView.SelectFirst:
  237. ms := selectedChannel.Messages
  238. if len(ms) == 0 {
  239. return nil
  240. }
  241. messagesTextView.
  242. Highlight(ms[0].ID).
  243. ScrollToHighlight()
  244. case conf.Keybindings.MessagesTextView.SelectLast:
  245. ms := selectedChannel.Messages
  246. if len(ms) == 0 {
  247. return nil
  248. }
  249. messagesTextView.
  250. Highlight(ms[len(ms)-1].ID).
  251. ScrollToHighlight()
  252. case conf.Keybindings.MessagesTextView.Reply:
  253. ms := selectedChannel.Messages
  254. if len(ms) == 0 {
  255. return nil
  256. }
  257. hs := messagesTextView.GetHighlights()
  258. if len(hs) == 0 {
  259. return nil
  260. }
  261. _, selectedMessage = findByMessageID(ms, hs[0])
  262. messageInputField.SetTitle(
  263. "Replying to " + selectedMessage.Author.Username,
  264. )
  265. app.SetFocus(messageInputField)
  266. case conf.Keybindings.MessagesTextView.ReplyMention:
  267. ms := selectedChannel.Messages
  268. if len(ms) == 0 {
  269. return nil
  270. }
  271. hs := messagesTextView.GetHighlights()
  272. if len(hs) == 0 {
  273. return nil
  274. }
  275. _, selectedMessage = findByMessageID(ms, hs[0])
  276. messageInputField.SetTitle("[@] Repling to " + selectedMessage.Author.Username)
  277. app.SetFocus(messageInputField)
  278. }
  279. return e
  280. }
  281. func newMessageInputField() *tview.InputField {
  282. w := tview.NewInputField()
  283. w.
  284. SetPlaceholder("Message...").
  285. SetPlaceholderTextColor(tcell.ColorWhite).
  286. SetFieldBackgroundColor(tview.Styles.PrimitiveBackgroundColor).
  287. SetInputCapture(onMessageInputFieldInputCapture).
  288. SetBorder(true).
  289. SetBorderPadding(0, 0, 1, 0).
  290. SetTitleAlign(tview.AlignLeft)
  291. return w
  292. }
  293. func onMessageInputFieldInputCapture(e *tcell.EventKey) *tcell.EventKey {
  294. // If the "Alt" modifier key is pressed, do not handle the event.
  295. if e.Modifiers() == tcell.ModAlt {
  296. return nil
  297. }
  298. switch e.Key() {
  299. case tcell.KeyEnter:
  300. if selectedChannel == nil {
  301. return nil
  302. }
  303. t := strings.TrimSpace(messageInputField.GetText())
  304. if t == "" {
  305. return nil
  306. }
  307. if selectedMessage != nil {
  308. d := &discordgo.MessageSend{
  309. Content: t,
  310. Reference: selectedMessage.Reference(),
  311. AllowedMentions: &discordgo.MessageAllowedMentions{RepliedUser: false},
  312. }
  313. if strings.HasPrefix(messageInputField.GetTitle(), "[@]") {
  314. d.AllowedMentions.RepliedUser = true
  315. } else {
  316. d.AllowedMentions.RepliedUser = false
  317. }
  318. go session.ChannelMessageSendComplex(selectedMessage.ChannelID, d)
  319. messageInputField.SetTitle("")
  320. selectedMessage = nil
  321. } else {
  322. go session.ChannelMessageSend(selectedChannel.ID, t)
  323. }
  324. messageInputField.SetText("")
  325. case tcell.KeyCtrlV:
  326. text, _ := clipboard.ReadAll()
  327. text = messageInputField.GetText() + text
  328. messageInputField.SetText(text)
  329. case tcell.KeyEscape:
  330. messageInputField.SetTitle("")
  331. selectedMessage = nil
  332. }
  333. return e
  334. }
  335. func newLoginForm(onLoginFormLoginButtonSelected func(), mfa bool) *tview.Form {
  336. w := tview.NewForm()
  337. w.
  338. AddButton("Login", onLoginFormLoginButtonSelected).
  339. SetButtonsAlign(tview.AlignCenter).
  340. SetBorder(true).
  341. SetBorderPadding(0, 0, 1, 0)
  342. if mfa {
  343. w.AddPasswordField("Code", "", 0, 0, nil)
  344. } else {
  345. w.
  346. AddInputField("Email", "", 0, nil, nil).
  347. AddPasswordField("Password", "", 0, 0, nil)
  348. }
  349. return w
  350. }