From 63fef0b13eb99fb8d8c40b41aff187a14a3b45f6 Mon Sep 17 00:00:00 2001 From: ayn2op Date: Tue, 24 Feb 2026 20:10:17 -0500 Subject: [PATCH] build(deps): upgrade tview to latest --- cmd/root.go | 4 +- go.mod | 2 +- go.sum | 4 +- internal/app/app.go | 117 ------------ internal/app/suspend_default.go | 5 - internal/ui/chat/attachments_picker.go | 101 +++++++++++ internal/ui/chat/guilds_tree.go | 13 +- internal/ui/chat/message_input.go | 20 ++- internal/ui/chat/messages_list.go | 122 +++---------- internal/ui/chat/view.go | 11 +- internal/ui/login/qr.go | 23 +-- internal/ui/root/suspend_default.go | 5 + internal/{app => ui/root}/suspend_unix.go | 6 +- internal/ui/root/view.go | 209 ++++++++++++++++++++++ pkg/picker/picker.go | 24 ++- 15 files changed, 406 insertions(+), 260 deletions(-) delete mode 100644 internal/app/app.go delete mode 100644 internal/app/suspend_default.go create mode 100644 internal/ui/chat/attachments_picker.go create mode 100644 internal/ui/root/suspend_default.go rename internal/{app => ui/root}/suspend_unix.go (75%) create mode 100644 internal/ui/root/view.go diff --git a/cmd/root.go b/cmd/root.go index 242253b..bd9a2c4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,9 +5,9 @@ import ( "fmt" "log/slog" - "github.com/ayn2op/discordo/internal/app" "github.com/ayn2op/discordo/internal/config" "github.com/ayn2op/discordo/internal/logger" + "github.com/ayn2op/discordo/internal/ui/root" "github.com/diamondburned/arikawa/v3/utils/ws" ) @@ -45,5 +45,5 @@ func Run() error { return fmt.Errorf("failed to load config: %w", err) } - return app.New(cfg).Run() + return root.NewView(cfg).Run() } diff --git a/go.mod b/go.mod index 98fe4dc..0313a93 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/BurntSushi/toml v1.6.0 github.com/alecthomas/chroma/v2 v2.23.1 github.com/andybalholm/brotli v1.2.0 - github.com/ayn2op/tview v0.0.0-20260223211434-6859308d597f + github.com/ayn2op/tview v0.0.0-20260225005318-47ca92c515c0 github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb github.com/diamondburned/arikawa/v3 v3.6.1-0.20260221051847-b81b70d1a5cb github.com/diamondburned/ningen/v3 v3.0.1-0.20250920191746-98fbd92e134d diff --git a/go.sum b/go.sum index 5392dd1..ee12add 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/ayn2op/tview v0.0.0-20260223211434-6859308d597f h1:ZxDODg1E/RPoTWlXhnhZOKITe1ebBM0Cpy+GiGaa+xk= -github.com/ayn2op/tview v0.0.0-20260223211434-6859308d597f/go.mod h1:lZ8RdOegQWBQafTOasGE7Ps1/Ymy4jmXoPt5vz2QsS0= +github.com/ayn2op/tview v0.0.0-20260225005318-47ca92c515c0 h1:aWW3YTB4CKSrHlzg542uT7gIo812+VvNYIOogu5GAAs= +github.com/ayn2op/tview v0.0.0-20260225005318-47ca92c515c0/go.mod h1:lZ8RdOegQWBQafTOasGE7Ps1/Ymy4jmXoPt5vz2QsS0= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/app/app.go b/internal/app/app.go deleted file mode 100644 index a726181..0000000 --- a/internal/app/app.go +++ /dev/null @@ -1,117 +0,0 @@ -package app - -import ( - "fmt" - "log/slog" - "os" - - "github.com/ayn2op/discordo/internal/clipboard" - "github.com/ayn2op/discordo/internal/config" - "github.com/ayn2op/discordo/internal/consts" - "github.com/ayn2op/discordo/internal/keyring" - "github.com/ayn2op/discordo/internal/ui/chat" - "github.com/ayn2op/discordo/internal/ui/login" - "github.com/ayn2op/tview" - "github.com/ayn2op/tview/keybind" - "github.com/gdamore/tcell/v3" -) - -type App struct { - inner *tview.Application - chatView *chat.View - cfg *config.Config -} - -func New(cfg *config.Config) *App { - tview.Styles = tview.Theme{} - app := &App{ - inner: tview.NewApplication(), - cfg: cfg, - } - - if err := clipboard.Init(); err != nil { - slog.Error("failed to init clipboard", "err", err) - } - - app.inner.SetInputCapture(app.onInputCapture) - return app -} - -func (a *App) Run() error { - token := os.Getenv("DISCORDO_TOKEN") - if token == "" { - t, err := keyring.GetToken() - if err != nil { - slog.Info("failed to retrieve token from keyring", "err", err) - } - token = t - } - - screen, err := tcell.NewScreen() - if err != nil { - return fmt.Errorf("failed to create screen: %w", err) - } - - if err := screen.Init(); err != nil { - return fmt.Errorf("failed to init screen: %w", err) - } - - if a.cfg.Mouse { - screen.EnableMouse() - } - - screen.SetTitle(consts.Name) - screen.EnablePaste() - screen.EnableFocus() - a.inner.SetScreen(screen) - - if token == "" { - loginForm := login.NewForm(a.inner, a.cfg, func(token string) { - if err := a.showChatView(token); err != nil { - slog.Error("failed to show chat view", "err", err) - } - }) - a.inner.SetRoot(loginForm) - } else { - if err := a.showChatView(token); err != nil { - return err - } - } - - return a.inner.Run() -} - -func (a *App) showChatView(token string) error { - a.chatView = chat.NewView(a.inner, a.cfg, a.quit) - if err := a.chatView.OpenState(token); err != nil { - return err - } - a.inner.SetRoot(a.chatView) - return nil -} - -func (a *App) quit() { - if a.chatView != nil { - if err := a.chatView.CloseState(); err != nil { - slog.Error("failed to close the session", "err", err) - } - } - - a.inner.Stop() -} - -func (a *App) onInputCapture(event *tcell.EventKey) *tcell.EventKey { - switch { - case keybind.Matches(event, a.cfg.Keybinds.Suspend.Keybind): - a.suspend() - return nil - case keybind.Matches(event, a.cfg.Keybinds.Quit.Keybind): - a.quit() - return nil - case keybind.Matches(event, keybind.NewKeybind(keybind.WithKeys("ctrl+c"))): - // https://github.com/ayn2op/tview/blob/a64fc48d7654432f71922c8b908280cdb525805c/application.go#L153 - return tcell.NewEventKey(tcell.KeyCtrlC, "", tcell.ModNone) - } - - return event -} diff --git a/internal/app/suspend_default.go b/internal/app/suspend_default.go deleted file mode 100644 index c020fba..0000000 --- a/internal/app/suspend_default.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build !unix - -package app - -func (a *App) suspend() {} diff --git a/internal/ui/chat/attachments_picker.go b/internal/ui/chat/attachments_picker.go new file mode 100644 index 0000000..59dbea0 --- /dev/null +++ b/internal/ui/chat/attachments_picker.go @@ -0,0 +1,101 @@ +package chat + +import ( + "github.com/ayn2op/discordo/internal/config" + "github.com/ayn2op/discordo/internal/ui" + "github.com/ayn2op/discordo/pkg/picker" + "github.com/ayn2op/tview" + "github.com/ayn2op/tview/help" + "github.com/ayn2op/tview/keybind" +) + +type attachmentItem struct { + label string + open func() +} + +type attachmentsPicker struct { + *picker.Picker + cfg *config.Config + chatView *View + items []attachmentItem +} + +var _ help.KeyMap = (*attachmentsPicker)(nil) + +func newAttachmentsPicker(cfg *config.Config, chatView *View) *attachmentsPicker { + ap := &attachmentsPicker{ + Picker: picker.New(), + cfg: cfg, + chatView: chatView, + } + ap.Box = ui.ConfigureBox(tview.NewBox(), &cfg.Theme) + ap. + SetBlurFunc(nil). + SetFocusFunc(nil). + SetBorderSet(cfg.Theme.Border.ActiveSet.BorderSet). + SetBorderStyle(cfg.Theme.Border.ActiveStyle.Style). + SetTitleStyle(cfg.Theme.Title.ActiveStyle.Style). + SetFooterStyle(cfg.Theme.Footer.ActiveStyle.Style) + + ap.SetTitle("Attachments") + ap.SetSelectedFunc(ap.onSelected) + ap.SetCancelFunc(ap.close) + ap.SetKeyMap(&picker.KeyMap{ + Cancel: cfg.Keybinds.Picker.Cancel.Keybind, + Up: cfg.Keybinds.Picker.Up.Keybind, + Down: cfg.Keybinds.Picker.Down.Keybind, + Top: cfg.Keybinds.Picker.Top.Keybind, + Bottom: cfg.Keybinds.Picker.Bottom.Keybind, + Select: cfg.Keybinds.Picker.Select.Keybind, + }) + ap.SetScrollBarVisibility(cfg.Theme.ScrollBar.Visibility.ScrollBarVisibility) + ap.SetScrollBar(tview.NewScrollBar(). + SetTrackStyle(cfg.Theme.ScrollBar.TrackStyle.Style). + SetThumbStyle(cfg.Theme.ScrollBar.ThumbStyle.Style). + SetGlyphSet(cfg.Theme.ScrollBar.GlyphSet.GlyphSet)) + return ap +} + +func (ap *attachmentsPicker) SetItems(items []attachmentItem) { + ap.items = items + ap.ClearItems() + for i, item := range items { + ap.AddItem(picker.Item{ + Text: item.label, + FilterText: item.label, + Reference: i, + }) + } + ap.Update() +} + +func (ap *attachmentsPicker) onSelected(item picker.Item) { + index, ok := item.Reference.(int) + if !ok { + return + } + if index < 0 || index >= len(ap.items) { + return + } + ap.items[index].open() + ap.close() +} + +func (ap *attachmentsPicker) close() { + ap.chatView.RemoveLayer(attachmentsListLayerName) + ap.chatView.app.SetFocus(ap.chatView.messagesList) +} + +func (ap *attachmentsPicker) ShortHelp() []keybind.Keybind { + cfg := ap.cfg.Keybinds.Picker + return []keybind.Keybind{cfg.Up.Keybind, cfg.Down.Keybind, cfg.Select.Keybind, cfg.Cancel.Keybind} +} + +func (ap *attachmentsPicker) FullHelp() [][]keybind.Keybind { + cfg := ap.cfg.Keybinds.Picker + return [][]keybind.Keybind{ + {cfg.Up.Keybind, cfg.Down.Keybind, cfg.Top.Keybind, cfg.Bottom.Keybind}, + {cfg.Select.Keybind, cfg.Cancel.Keybind}, + } +} diff --git a/internal/ui/chat/guilds_tree.go b/internal/ui/chat/guilds_tree.go index 53e7c27..169473d 100644 --- a/internal/ui/chat/guilds_tree.go +++ b/internal/ui/chat/guilds_tree.go @@ -55,8 +55,7 @@ func newGuildsTree(cfg *config.Config, chatView *View) *guildsTree { SetGraphics(cfg.Theme.GuildsTree.Graphics). SetGraphicsColor(tcell.GetColor(cfg.Theme.GuildsTree.GraphicsColor)). SetSelectedFunc(gt.onSelected). - SetTitle("Guilds"). - SetInputCapture(gt.onInputCapture) + SetTitle("Guilds") return gt } @@ -375,7 +374,7 @@ func (gt *guildsTree) collapseParentNode(node *tview.TreeNode) { }) } -func (gt *guildsTree) onInputCapture(event *tcell.EventKey) *tcell.EventKey { +func (gt *guildsTree) handleInput(event *tcell.EventKey) *tcell.EventKey { switch { case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.CollapseParentNode.Keybind): gt.collapseParentNode(gt.GetCurrentNode()) @@ -404,6 +403,14 @@ func (gt *guildsTree) onInputCapture(event *tcell.EventKey) *tcell.EventKey { return nil } +func (gt *guildsTree) InputHandler(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + event = gt.handleInput(event) + if event == nil { + return + } + gt.TreeView.InputHandler(event, setFocus) +} + func (gt *guildsTree) yankID() { node := gt.GetCurrentNode() if node == nil { diff --git a/internal/ui/chat/message_input.go b/internal/ui/chat/message_input.go index 02f62b7..1a333fd 100644 --- a/internal/ui/chat/message_input.go +++ b/internal/ui/chat/message_input.go @@ -66,7 +66,6 @@ func newMessageInput(cfg *config.Config, chatView *View) *messageInput { mentionsList: newMentionsList(cfg), } mi.Box = ui.ConfigureBox(mi.Box, &cfg.Theme) - mi.SetInputCapture(mi.onInputCapture) mi. SetPlaceholder(tview.NewLine(tview.NewSegment("Select a channel to start chatting", tcell.StyleDefault.Dim(true)))). SetClipboard( @@ -93,7 +92,7 @@ func (mi *messageInput) stopTypingTimer() { } } -func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey { +func (mi *messageInput) handleInput(event *tcell.EventKey) *tcell.EventKey { switch { case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Paste.Keybind): mi.paste() @@ -144,19 +143,18 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey { if mi.cfg.AutocompleteLimit > 0 { if mi.chatView.GetVisible(mentionsListLayerName) { - handler := mi.mentionsList.InputHandler() switch { case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Up.Keybind): - handler(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone), nil) + mi.mentionsList.InputHandler(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone), nil) return nil case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Down.Keybind): - handler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone), nil) + mi.mentionsList.InputHandler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone), nil) return nil case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Top.Keybind): - handler(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone), nil) + mi.mentionsList.InputHandler(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone), nil) return nil case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Bottom.Keybind): - handler(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone), nil) + mi.mentionsList.InputHandler(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone), nil) return nil } } @@ -167,6 +165,14 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey { return event } +func (mi *messageInput) InputHandler(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + event = mi.handleInput(event) + if event == nil { + return + } + mi.TextArea.InputHandler(event, setFocus) +} + func (mi *messageInput) paste() { if data := clipboard.Read(clipboard.FmtImage); data != nil { name := "clipboard.png" diff --git a/internal/ui/chat/messages_list.go b/internal/ui/chat/messages_list.go index 7b9f4c0..1d2cb62 100644 --- a/internal/ui/chat/messages_list.go +++ b/internal/ui/chat/messages_list.go @@ -52,6 +52,8 @@ type messagesList struct { // itemByID caches unselected message TextViews. itemByID map[discord.MessageID]*tview.TextView + attachmentsPicker *attachmentsPicker + fetchingMembers struct { mu sync.Mutex value bool @@ -83,6 +85,7 @@ func newMessagesList(cfg *config.Config, chatView *View) *messagesList { renderer: markdown.NewRenderer(cfg), itemByID: make(map[discord.MessageID]*tview.TextView), } + ml.attachmentsPicker = newAttachmentsPicker(cfg, chatView) ml.Box = ui.ConfigureBox(ml.Box, &cfg.Theme) ml.SetTitle("Messages") @@ -94,7 +97,6 @@ func newMessagesList(cfg *config.Config, chatView *View) *messagesList { SetTrackStyle(cfg.Theme.ScrollBar.TrackStyle.Style). SetThumbStyle(cfg.Theme.ScrollBar.ThumbStyle.Style). SetGlyphSet(cfg.Theme.ScrollBar.GlyphSet.GlyphSet)) - ml.SetInputCapture(ml.onInputCapture) return ml } @@ -526,7 +528,7 @@ func (ml *messagesList) selectedMessage() (*discord.Message, error) { return &ml.messages[cursor], nil } -func (ml *messagesList) onInputCapture(event *tcell.EventKey) *tcell.EventKey { +func (ml *messagesList) handleInput(event *tcell.EventKey) *tcell.EventKey { switch { case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollUp.Keybind): ml.ScrollUp() @@ -592,6 +594,14 @@ func (ml *messagesList) onInputCapture(event *tcell.EventKey) *tcell.EventKey { return event } +func (ml *messagesList) InputHandler(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + event = ml.handleInput(event) + if event == nil { + return + } + ml.List.InputHandler(event, setFocus) +} + func (ml *messagesList) selectUp() { messages := ml.messages if len(messages) == 0 { @@ -788,19 +798,8 @@ func extractURLs(content string) []string { } func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord.Attachment) { - type attachmentAction struct { - label string - shortcut rune - open func() - } - - closeList := func() { - ml.chatView.RemoveLayer(attachmentsListLayerName) - ml.chatView.app.SetFocus(ml) - } - - var actions []attachmentAction - for i, a := range attachments { + var items []attachmentItem + for _, a := range attachments { attachment := a action := func() { if strings.HasPrefix(attachment.ContentType, "image/") { @@ -809,103 +808,30 @@ func (ml *messagesList) showAttachmentsList(urls []string, attachments []discord go ml.openURL(attachment.URL) } } - actions = append(actions, attachmentAction{ - label: attachment.Filename, - shortcut: rune('a' + i), - open: action, + items = append(items, attachmentItem{ + label: attachment.Filename, + open: action, }) } - for i, u := range urls { + for _, u := range urls { url := u - actions = append(actions, attachmentAction{ - label: url, - shortcut: rune('1' + i), - open: func() { go ml.openURL(url) }, + items = append(items, attachmentItem{ + label: url, + open: func() { go ml.openURL(url) }, }) } - - normalItems := make([]*tview.TextView, len(actions)) - selectedItems := make([]*tview.TextView, len(actions)) - for i, action := range actions { - normalItems[i] = tview.NewTextView(). - SetScrollable(false). - SetWrap(false). - SetWordWrap(false). - SetLines([]tview.Line{{{Text: action.label, Style: tcell.StyleDefault}}}) - selectedItems[i] = tview.NewTextView(). - SetScrollable(false). - SetWrap(false). - SetWordWrap(false). - SetLines([]tview.Line{{{Text: action.label, Style: tcell.StyleDefault.Reverse(true)}}}) - } - - list := tview.NewList(). - SetSnapToItems(true). - SetBuilder(func(index int, cursor int) tview.ListItem { - if index < 0 || index >= len(actions) { - return nil - } - if index == cursor { - return selectedItems[index] - } - return normalItems[index] - }) - list.Box = ui.ConfigureBox(list.Box, &ml.cfg.Theme) - if len(actions) > 0 { - list.SetCursor(0) - } - list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch { - case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectUp.Keybind): - return tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone) - case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectDown.Keybind): - return tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone) - case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectTop.Keybind): - return tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone) - case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectBottom.Keybind): - return tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone) - case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Cancel.Keybind): - closeList() - return nil - } - - if event.Key() == tcell.KeyEnter || event.Key() == tcell.KeyRune && event.Str() == " " { - index := list.Cursor() - if index >= 0 && index < len(actions) { - actions[index].open() - closeList() - } - return nil - } - - if event.Key() == tcell.KeyRune { - key := event.Str() - if key == "" { - return event - } - ch := []rune(key)[0] - for index, action := range actions { - if action.shortcut == ch { - list.SetCursor(index) - actions[index].open() - closeList() - return nil - } - } - } - - return event - }) + ml.attachmentsPicker.SetItems(items) ml.chatView. AddLayer( - ui.Centered(list, 0, 0), + ui.Centered(ml.attachmentsPicker, 0, 0), layers.WithName(attachmentsListLayerName), layers.WithResize(true), layers.WithVisible(true), layers.WithOverlay(), ). SendToFront(attachmentsListLayerName) + ml.chatView.app.SetFocus(ml.attachmentsPicker) } func (ml *messagesList) openAttachment(attachment discord.Attachment) { diff --git a/internal/ui/chat/view.go b/internal/ui/chat/view.go index 691355d..4df7b2b 100644 --- a/internal/ui/chat/view.go +++ b/internal/ui/chat/view.go @@ -91,7 +91,6 @@ func NewView(app *tview.Application, cfg *config.Config, onLogout func()) *View v.help.SetBorderPadding(0, 0, cfg.Help.Padding[0], cfg.Help.Padding[1]) v.SetBackgroundLayerStyle(v.cfg.Theme.Dialog.BackgroundStyle.Style) - v.SetInputCapture(v.onInputCapture) v.buildLayout() return v } @@ -222,7 +221,7 @@ func (v *View) focusNext() { } } -func (v *View) onInputCapture(event *tcell.EventKey) *tcell.EventKey { +func (v *View) handleInput(event *tcell.EventKey) *tcell.EventKey { switch { case keybind.Matches(event, v.cfg.Keybinds.ToggleHelp.Keybind): v.help.SetShowAll(!v.help.ShowAll()) @@ -267,6 +266,14 @@ func (v *View) onInputCapture(event *tcell.EventKey) *tcell.EventKey { return event } +func (v *View) InputHandler(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + event = v.handleInput(event) + if event == nil { + return + } + v.Layers.InputHandler(event, setFocus) +} + func (v *View) updateHelpHeight() { height := 1 if v.help.ShowAll() { diff --git a/internal/ui/login/qr.go b/internal/ui/login/qr.go index 2323d63..c024bbc 100644 --- a/internal/ui/login/qr.go +++ b/internal/ui/login/qr.go @@ -58,21 +58,22 @@ func newQRLogin(app *tview.Application, cfg *config.Config, done func(token stri SetChangedFunc(func() { q.app.QueueUpdateDraw(func() {}) }). - SetTitle("Login with QR"). - SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey { - if ev.Key() == tcell.KeyEsc { - q.stop() - if q.done != nil { - q.done("", nil) - } - return nil - } - return ev - }) + SetTitle("Login with QR") return q } +func (q *qrLogin) InputHandler(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + if event.Key() == tcell.KeyEsc { + q.stop() + if q.done != nil { + q.done("", nil) + } + return + } + q.TextView.InputHandler(event, setFocus) +} + func (q *qrLogin) start() { ctx, cancel := context.WithCancel(context.Background()) q.cancel = cancel diff --git a/internal/ui/root/suspend_default.go b/internal/ui/root/suspend_default.go new file mode 100644 index 0000000..cf8286c --- /dev/null +++ b/internal/ui/root/suspend_default.go @@ -0,0 +1,5 @@ +//go:build !unix + +package root + +func (v *View) suspend() {} diff --git a/internal/app/suspend_unix.go b/internal/ui/root/suspend_unix.go similarity index 75% rename from internal/app/suspend_unix.go rename to internal/ui/root/suspend_unix.go index baff2d6..61921bb 100644 --- a/internal/app/suspend_unix.go +++ b/internal/ui/root/suspend_unix.go @@ -1,6 +1,6 @@ //go:build unix -package app +package root import ( "os" @@ -8,8 +8,8 @@ import ( "syscall" ) -func (a *App) suspend() { - a.inner.Suspend(func() { +func (v *View) suspend() { + v.app.Suspend(func() { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGCONT) defer signal.Stop(c) diff --git a/internal/ui/root/view.go b/internal/ui/root/view.go new file mode 100644 index 0000000..0e83289 --- /dev/null +++ b/internal/ui/root/view.go @@ -0,0 +1,209 @@ +package root + +import ( + "fmt" + "log/slog" + "os" + + "github.com/ayn2op/discordo/internal/clipboard" + "github.com/ayn2op/discordo/internal/config" + "github.com/ayn2op/discordo/internal/consts" + "github.com/ayn2op/discordo/internal/keyring" + "github.com/ayn2op/discordo/internal/ui/chat" + "github.com/ayn2op/discordo/internal/ui/login" + "github.com/ayn2op/tview" + "github.com/ayn2op/tview/keybind" + "github.com/gdamore/tcell/v3" +) + +type View struct { + *tview.Box + app *tview.Application + inner tview.Primitive + chat *chat.View + + cfg *config.Config +} + +func NewView(cfg *config.Config) *View { + tview.Styles = tview.Theme{} + v := &View{ + Box: tview.NewBox(), + app: tview.NewApplication(), + cfg: cfg, + } + + if err := clipboard.Init(); err != nil { + slog.Error("failed to init clipboard", "err", err) + } + + return v +} + +func (v *View) Run() error { + token := os.Getenv("DISCORDO_TOKEN") + if token == "" { + t, err := keyring.GetToken() + if err != nil { + slog.Info("failed to retrieve token from keyring", "err", err) + } + token = t + } + + screen, err := tcell.NewScreen() + if err != nil { + return fmt.Errorf("failed to create screen: %w", err) + } + + if err := screen.Init(); err != nil { + return fmt.Errorf("failed to init screen: %w", err) + } + + if v.cfg.Mouse { + screen.EnableMouse() + } + + screen.SetTitle(consts.Name) + screen.EnablePaste() + screen.EnableFocus() + v.app.SetScreen(screen) + v.app.SetRoot(v) + + if token == "" { + v.showLoginView() + } else { + if err := v.showChatView(token); err != nil { + return err + } + } + + v.app.SetFocus(v) + err = v.app.Run() + v.closeChatViewState() + return err +} + +func (v *View) showLoginView() { + loginForm := login.NewForm(v.app, v.cfg, func(token string) { + if err := v.showChatView(token); err != nil { + slog.Error("failed to show chat view", "err", err) + } + }) + v.inner = loginForm + v.MarkDirty() + v.app.SetFocus(v) +} + +func (v *View) showChatView(token string) error { + v.chat = chat.NewView(v.app, v.cfg, v.showLoginView) + if err := v.chat.OpenState(token); err != nil { + return err + } + v.inner = v.chat + v.MarkDirty() + v.app.SetFocus(v) + return nil +} + +func (v *View) quit() { + v.closeChatViewState() + v.app.Stop() +} + +func (v *View) closeChatViewState() { + if v.chat != nil { + if err := v.chat.CloseState(); err != nil { + slog.Error("failed to close the session", "err", err) + } + v.chat = nil + } +} + +func (v *View) Draw(screen tcell.Screen) { + if v.inner == nil { + return + } + x, y, width, height := v.GetRect() + v.inner.SetRect(x, y, width, height) + v.inner.Draw(screen) +} + +func (v *View) InputHandler(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + switch { + case keybind.Matches(event, v.cfg.Keybinds.Suspend.Keybind): + v.suspend() + return + case keybind.Matches(event, v.cfg.Keybinds.Quit.Keybind): + v.quit() + return + } + + if v.inner != nil { + v.inner.InputHandler(event, setFocus) + } +} + +func (v *View) Focus(delegate func(p tview.Primitive)) { + if v.inner != nil { + delegate(v.inner) + return + } + v.Box.Focus(delegate) +} + +func (v *View) HasFocus() bool { + if v.inner != nil && v.inner.HasFocus() { + return true + } + return v.Box.HasFocus() +} + +func (v *View) Blur() { + if v.inner != nil { + v.inner.Blur() + } + v.Box.Blur() +} + +func (v *View) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { + return v.WrapMouseHandler(func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { + if v.inner == nil { + return false, nil + } + handler := v.inner.MouseHandler() + if handler == nil { + return false, nil + } + return handler(action, event, setFocus) + }) +} + +func (v *View) PasteHandler() func(text string, setFocus func(p tview.Primitive)) { + return v.WrapPasteHandler(func(text string, setFocus func(p tview.Primitive)) { + if v.inner == nil { + return + } + handler := v.inner.PasteHandler() + if handler == nil { + return + } + handler(text, setFocus) + }) +} + +func (v *View) IsDirty() bool { + if v.Box.IsDirty() { + return true + } + if v.inner == nil { + return false + } + return v.inner.IsDirty() +} + +func (v *View) MarkClean() { + v.Box.MarkClean() + if v.inner != nil { + v.inner.MarkClean() + } +} diff --git a/pkg/picker/picker.go b/pkg/picker/picker.go index 4726b7e..f4ab511 100644 --- a/pkg/picker/picker.go +++ b/pkg/picker/picker.go @@ -43,8 +43,7 @@ func New() *Picker { SetLabel("> "). SetBorders(tview.BordersBottom). SetBorderSet(borderSet). - SetBorderStyle(tcell.StyleDefault.Dim(true)). - SetInputCapture(p.onInputCapture) + SetBorderStyle(tcell.StyleDefault.Dim(true)) p. SetDirection(tview.FlexRow). @@ -149,24 +148,23 @@ func (p *Picker) onInputChanged(text string) { p.setFilteredItems(fuzzied) } -func (p *Picker) onInputCapture(event *tcell.EventKey) *tcell.EventKey { +func (p *Picker) handleInput(event *tcell.EventKey) *tcell.EventKey { if p.keyMap == nil { - return nil + return event } - handler := p.list.InputHandler() switch { case keybind.Matches(event, p.keyMap.Up): - handler(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone), nil) + p.list.InputHandler(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone), nil) return nil case keybind.Matches(event, p.keyMap.Down): - handler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone), nil) + p.list.InputHandler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone), nil) return nil case keybind.Matches(event, p.keyMap.Top): - handler(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone), nil) + p.list.InputHandler(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone), nil) return nil case keybind.Matches(event, p.keyMap.Bottom): - handler(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone), nil) + p.list.InputHandler(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone), nil) case keybind.Matches(event, p.keyMap.Select): p.onListSelected(p.list.Cursor()) return nil @@ -180,3 +178,11 @@ func (p *Picker) onInputCapture(event *tcell.EventKey) *tcell.EventKey { return event } + +func (p *Picker) InputHandler(event *tcell.EventKey, setFocus func(p2 tview.Primitive)) { + event = p.handleInput(event) + if event == nil { + return + } + p.Flex.InputHandler(event, setFocus) +}