build(deps): upgrade tview to latest
This commit is contained in:
parent
06ee7fdad1
commit
63fef0b13e
15 changed files with 406 additions and 260 deletions
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
2
go.mod
2
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
|
||||
|
|
|
|||
4
go.sum
4
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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
//go:build !unix
|
||||
|
||||
package app
|
||||
|
||||
func (a *App) suspend() {}
|
||||
101
internal/ui/chat/attachments_picker.go
Normal file
101
internal/ui/chat/attachments_picker.go
Normal file
|
|
@ -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},
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
5
internal/ui/root/suspend_default.go
Normal file
5
internal/ui/root/suspend_default.go
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
//go:build !unix
|
||||
|
||||
package root
|
||||
|
||||
func (v *View) suspend() {}
|
||||
|
|
@ -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)
|
||||
209
internal/ui/root/view.go
Normal file
209
internal/ui/root/view.go
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue