refactor(ui): remove redraw commands and inline command events
This commit is contained in:
parent
abdb0748a2
commit
2d79e45f6f
15 changed files with 215 additions and 329 deletions
14
go.mod
14
go.mod
|
|
@ -11,9 +11,9 @@ require (
|
|||
github.com/alecthomas/chroma/v2 v2.23.1
|
||||
github.com/andybalholm/brotli v1.2.0
|
||||
github.com/ayn2op/clipboard v0.0.0-20260308203959-c5ad7df3fc97
|
||||
github.com/ayn2op/tview v0.0.0-20260311012550-9b46cad522bf
|
||||
github.com/ayn2op/tview v0.0.0-20260314084311-e6fda4e5838b
|
||||
github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb
|
||||
github.com/diamondburned/arikawa/v3 v3.6.1-0.20260309010533-e61165a61b64
|
||||
github.com/diamondburned/arikawa/v3 v3.6.1-0.20260311205148-176ad9b9440f
|
||||
github.com/diamondburned/ningen/v3 v3.0.1-0.20260306213430-5a08d3a709b4
|
||||
github.com/gdamore/tcell/v3 v3.1.2
|
||||
github.com/gen2brain/beeep v0.11.2
|
||||
|
|
@ -53,11 +53,11 @@ require (
|
|||
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
|
||||
github.com/twmb/murmur3 v1.1.8 // indirect
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||
golang.org/x/exp/shiny v0.0.0-20260218203240-3dfff04db8fa // indirect
|
||||
golang.org/x/image v0.36.0 // indirect
|
||||
golang.org/x/mobile v0.0.0-20260217195705-b56b3793a9c4 // indirect
|
||||
golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90 // indirect
|
||||
golang.org/x/image v0.37.0 // indirect
|
||||
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/term v0.40.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/term v0.41.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
)
|
||||
|
|
|
|||
28
go.sum
28
go.sum
|
|
@ -16,8 +16,8 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo
|
|||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/ayn2op/clipboard v0.0.0-20260308203959-c5ad7df3fc97 h1:WujETUV+v0DEJyZgjeLzQvihWyL80c0Tg4qf0dDo+Io=
|
||||
github.com/ayn2op/clipboard v0.0.0-20260308203959-c5ad7df3fc97/go.mod h1:3kFnpNCa3dF6WryzOMCDao7PfZ7DTCh+pievlfuwV80=
|
||||
github.com/ayn2op/tview v0.0.0-20260311012550-9b46cad522bf h1:ay4OtKYsFGvK4Ng1pmZ/n8MA1CWzZO80lDlrDiCpdag=
|
||||
github.com/ayn2op/tview v0.0.0-20260311012550-9b46cad522bf/go.mod h1:lZ8RdOegQWBQafTOasGE7Ps1/Ymy4jmXoPt5vz2QsS0=
|
||||
github.com/ayn2op/tview v0.0.0-20260314084311-e6fda4e5838b h1:ZGDcIf5Vrk+/Tl3GMaxuXNQek8oT/FW27nn5xdkXx5A=
|
||||
github.com/ayn2op/tview v0.0.0-20260314084311-e6fda4e5838b/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=
|
||||
|
|
@ -27,8 +27,8 @@ github.com/dchest/jsmin v1.0.0 h1:Y2hWXmGZiRxtl+VcTksyucgTlYxnhPzTozCwx9gy9zI=
|
|||
github.com/dchest/jsmin v1.0.0/go.mod h1:AVBIund7Mr7lKXT70hKT2YgL3XEXUaUk5iw9DZ8b0Uc=
|
||||
github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb h1:6S+TKObz6+Io2c8IOkcbK4Sz7nj6RpEVU7TkvmsZZcw=
|
||||
github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb/go.mod h1:wf3nKtOnQqCp7kp9xB7hHnNlZ6m3NoiOxjrB9hFRq4Y=
|
||||
github.com/diamondburned/arikawa/v3 v3.6.1-0.20260309010533-e61165a61b64 h1:ZdzbwItbGTvOdNfoCH7QIbRiBPDufsq8AwfJUaM4mHM=
|
||||
github.com/diamondburned/arikawa/v3 v3.6.1-0.20260309010533-e61165a61b64/go.mod h1:TpV2GvCJIYSwAXUEAx4sutPcftyAbVidiFlG6l7K0go=
|
||||
github.com/diamondburned/arikawa/v3 v3.6.1-0.20260311205148-176ad9b9440f h1:IQWwTKdWCWl0MiD2ln0LSTiO9FMOrp4FlbOfKgaF8/A=
|
||||
github.com/diamondburned/arikawa/v3 v3.6.1-0.20260311205148-176ad9b9440f/go.mod h1:TpV2GvCJIYSwAXUEAx4sutPcftyAbVidiFlG6l7K0go=
|
||||
github.com/diamondburned/ningen/v3 v3.0.1-0.20260306213430-5a08d3a709b4 h1:m1WyrOUuFE4BuIcQzPeRmmLQmJMBNElT/tfx/s2+AoE=
|
||||
github.com/diamondburned/ningen/v3 v3.0.1-0.20260306213430-5a08d3a709b4/go.mod h1:JSqSCBN5MAI9yjAO6tZlSayXM9KWx+8q3EhhlkCuscM=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
|
|
@ -117,12 +117,12 @@ go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH
|
|||
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp/shiny v0.0.0-20260218203240-3dfff04db8fa h1:+7e7RPzOw2fG8DBbddatlOmHGNCg+VlA2Ar0yVMw7sM=
|
||||
golang.org/x/exp/shiny v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:zxsA7NyDTOUjcveVwAMFI/YIErWwayTW/4RGqB/RzKk=
|
||||
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
||||
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
|
||||
golang.org/x/mobile v0.0.0-20260217195705-b56b3793a9c4 h1:uT3oYo9M38vJa7JpT4kCie2lJwOpoUrx7FvV0H7kXSc=
|
||||
golang.org/x/mobile v0.0.0-20260217195705-b56b3793a9c4/go.mod h1:4OGHIUSBiIqyFAQDaX1tpY0BVnO20DvNDeATBu8aeFQ=
|
||||
golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90 h1:kyPrwnEYXdME284bE7xgS9BPxhG7MCa5hw1/TpaTJVs=
|
||||
golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:jqkJFnLVkS8zgKKY4+MOPCZtuZGw3hONUjhapUSwZ8c=
|
||||
golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
|
||||
golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
|
||||
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60 h1:MOzyaj0wu2xneBkzkg9LHNYjDBB4W5vP043A2SYQRPA=
|
||||
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60/go.mod h1:th6VJvzjMbrYF8SduQY5rpD0HG0GleGxjadkqSxFs3k=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
|
@ -144,15 +144,15 @@ golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
|
|
|||
|
|
@ -9,14 +9,12 @@ import (
|
|||
|
||||
type LogoutEvent struct{ tcell.EventTime }
|
||||
|
||||
func newLogoutEvent() *LogoutEvent {
|
||||
event := &LogoutEvent{}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
func (v *Model) logout() tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event { return newLogoutEvent() })
|
||||
return func() tcell.Event {
|
||||
event := &LogoutEvent{}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
type QuitEvent struct{ tcell.EventTime }
|
||||
|
|
@ -28,11 +26,24 @@ func NewQuitEvent() *QuitEvent {
|
|||
}
|
||||
|
||||
func (v *Model) closeState() tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
if err := v.CloseState(); err != nil {
|
||||
slog.Error("failed to close the session", "err", err)
|
||||
return tcell.NewEventError(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type closeLayerEvent struct {
|
||||
tcell.EventTime
|
||||
name string
|
||||
}
|
||||
|
||||
func closeLayer(name string) tview.Command {
|
||||
return func() tcell.Event {
|
||||
event := &closeLayerEvent{name: name}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -397,13 +397,12 @@ func (gt *guildsTree) collapseParentNode(node *tview.TreeNode) {
|
|||
func (gt *guildsTree) HandleEvent(event tcell.Event) tview.Command {
|
||||
switch event := event.(type) {
|
||||
case *tview.KeyEvent:
|
||||
redraw := tview.RedrawCommand{}
|
||||
handler := gt.TreeView.HandleEvent
|
||||
|
||||
switch {
|
||||
case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.CollapseParentNode.Keybind):
|
||||
gt.collapseParentNode(gt.GetCurrentNode())
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.MoveToParentNode.Keybind):
|
||||
return handler(tcell.NewEventKey(tcell.KeyRune, "K", tcell.ModNone))
|
||||
case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Up.Keybind):
|
||||
|
|
@ -412,10 +411,10 @@ func (gt *guildsTree) HandleEvent(event tcell.Event) tview.Command {
|
|||
return handler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone))
|
||||
case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Top.Keybind):
|
||||
gt.Move(gt.GetRowCount() * -1)
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Bottom.Keybind):
|
||||
gt.Move(gt.GetRowCount())
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.SelectCurrent.Keybind):
|
||||
return handler(tcell.NewEventKey(tcell.KeyEnter, "", tcell.ModNone))
|
||||
case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.YankID.Keybind):
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import (
|
|||
"github.com/ayn2op/tview"
|
||||
"github.com/ayn2op/tview/help"
|
||||
"github.com/ayn2op/tview/keybind"
|
||||
"github.com/ayn2op/tview/layers"
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
|
|
@ -107,7 +106,6 @@ func (mi *messageInput) stopTypingTimer() {
|
|||
func (mi *messageInput) HandleEvent(event tcell.Event) tview.Command {
|
||||
switch event := event.(type) {
|
||||
case *tview.KeyEvent:
|
||||
redraw := tview.RedrawCommand{}
|
||||
handler := mi.TextArea.HandleEvent
|
||||
switch {
|
||||
case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Paste.Keybind):
|
||||
|
|
@ -119,29 +117,27 @@ func (mi *messageInput) HandleEvent(event tcell.Event) tview.Command {
|
|||
} else {
|
||||
mi.send()
|
||||
}
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.OpenEditor.Keybind):
|
||||
var cmds tview.BatchCommand
|
||||
var cmds []tview.Command
|
||||
mi.stopTabCompletion(func(next tview.Command) {
|
||||
if next != nil {
|
||||
cmds = append(cmds, next)
|
||||
}
|
||||
})
|
||||
mi.editor()
|
||||
cmds = append(cmds, redraw)
|
||||
return cmds
|
||||
return tview.Batch(cmds...)
|
||||
case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.OpenFilePicker.Keybind):
|
||||
var cmds tview.BatchCommand
|
||||
var cmds []tview.Command
|
||||
mi.stopTabCompletion(func(next tview.Command) {
|
||||
if next != nil {
|
||||
cmds = append(cmds, next)
|
||||
}
|
||||
})
|
||||
mi.openFilePicker()
|
||||
cmds = append(cmds, redraw)
|
||||
return cmds
|
||||
return tview.Batch(cmds...)
|
||||
case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Cancel.Keybind):
|
||||
var cmds tview.BatchCommand
|
||||
var cmds []tview.Command
|
||||
if mi.chat.GetVisible(mentionsListLayerName) {
|
||||
mi.stopTabCompletion(func(next tview.Command) {
|
||||
if next != nil {
|
||||
|
|
@ -151,11 +147,10 @@ func (mi *messageInput) HandleEvent(event tcell.Event) tview.Command {
|
|||
} else {
|
||||
mi.reset()
|
||||
}
|
||||
cmds = append(cmds, redraw)
|
||||
return cmds
|
||||
return tview.Batch(cmds...)
|
||||
case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.TabComplete.Keybind):
|
||||
go mi.chat.app.QueueUpdateDraw(func() { mi.tabComplete() })
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Undo.Keybind):
|
||||
return handler(tcell.NewEventKey(tcell.KeyCtrlZ, "", tcell.ModNone))
|
||||
}
|
||||
|
|
@ -177,16 +172,16 @@ func (mi *messageInput) HandleEvent(event tcell.Event) tview.Command {
|
|||
switch {
|
||||
case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Up.Keybind):
|
||||
mi.mentionsList.HandleEvent(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone))
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Down.Keybind):
|
||||
mi.mentionsList.HandleEvent(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone))
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Top.Keybind):
|
||||
mi.mentionsList.HandleEvent(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone))
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Bottom.Keybind):
|
||||
mi.mentionsList.HandleEvent(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone))
|
||||
return redraw
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -631,8 +626,8 @@ func (mi *messageInput) stopTabCompletion(emit func(tview.Command)) {
|
|||
if mi.cfg.AutocompleteLimit > 0 {
|
||||
mi.mentionsList.clear()
|
||||
if emit != nil {
|
||||
emit(layers.CloseLayerCommand{Name: mentionsListLayerName})
|
||||
emit(tview.SetFocusCommand{Target: mi})
|
||||
emit(closeLayer(mentionsListLayerName))
|
||||
emit(tview.SetFocus(mi))
|
||||
} else {
|
||||
mi.removeMentionsList()
|
||||
mi.chat.app.SetFocus(mi)
|
||||
|
|
|
|||
|
|
@ -840,38 +840,37 @@ func (ml *messagesList) selectedMessage() (*discord.Message, error) {
|
|||
func (ml *messagesList) HandleEvent(event tcell.Event) tview.Command {
|
||||
switch event := event.(type) {
|
||||
case *tview.KeyEvent:
|
||||
redraw := tview.RedrawCommand{}
|
||||
switch {
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollUp.Keybind):
|
||||
ml.ScrollUp()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollDown.Keybind):
|
||||
ml.ScrollDown()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollTop.Keybind):
|
||||
ml.ScrollToStart()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollBottom.Keybind):
|
||||
ml.ScrollToEnd()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Cancel.Keybind):
|
||||
ml.clearSelection()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectUp.Keybind):
|
||||
ml.selectUp()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectDown.Keybind):
|
||||
ml.selectDown()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectTop.Keybind):
|
||||
ml.selectTop()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectBottom.Keybind):
|
||||
ml.selectBottom()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectReply.Keybind):
|
||||
ml.selectReply()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.YankID.Keybind):
|
||||
ml.yankID()
|
||||
return nil
|
||||
|
|
@ -883,22 +882,22 @@ func (ml *messagesList) HandleEvent(event tcell.Event) tview.Command {
|
|||
return nil
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Open.Keybind):
|
||||
ml.open()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Reply.Keybind):
|
||||
ml.reply(false)
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ReplyMention.Keybind):
|
||||
ml.reply(true)
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Edit.Keybind):
|
||||
ml.edit()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Delete.Keybind):
|
||||
ml.delete()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.DeleteConfirm.Keybind):
|
||||
ml.confirmDelete()
|
||||
return redraw
|
||||
return nil
|
||||
}
|
||||
// Do not fall through to List defaults for unmatched keys.
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -203,18 +203,18 @@ func (v *Model) focusNext() {
|
|||
func (v *Model) HandleEvent(event tcell.Event) tview.Command {
|
||||
switch event := event.(type) {
|
||||
case *tview.InitEvent:
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
if err := v.OpenState(v.token); err != nil {
|
||||
slog.Error("failed to open chat state", "err", err)
|
||||
return tcell.NewEventError(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
case *QuitEvent:
|
||||
return tview.BatchCommand{
|
||||
return tview.Batch(
|
||||
v.closeState(),
|
||||
tview.Quit(),
|
||||
}
|
||||
)
|
||||
case *tview.ModalDoneEvent:
|
||||
if v.HasLayer(confirmModalLayerName) {
|
||||
v.RemoveLayer(confirmModalLayerName)
|
||||
|
|
@ -227,88 +227,43 @@ func (v *Model) HandleEvent(event tcell.Event) tview.Command {
|
|||
if onDone != nil {
|
||||
onDone(event.ButtonLabel)
|
||||
}
|
||||
return tview.RedrawCommand{}
|
||||
return nil
|
||||
}
|
||||
case *tview.KeyEvent:
|
||||
redraw := tview.RedrawCommand{}
|
||||
switch {
|
||||
case keybind.Matches(event, v.cfg.Keybinds.FocusGuildsTree.Keybind):
|
||||
v.messageInput.removeMentionsList()
|
||||
v.focusGuildsTree()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, v.cfg.Keybinds.FocusMessagesList.Keybind):
|
||||
v.messageInput.removeMentionsList()
|
||||
v.app.SetFocus(v.messagesList)
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, v.cfg.Keybinds.FocusMessageInput.Keybind):
|
||||
v.focusMessageInput()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, v.cfg.Keybinds.FocusPrevious.Keybind):
|
||||
v.focusPrevious()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, v.cfg.Keybinds.FocusNext.Keybind):
|
||||
v.focusNext()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, v.cfg.Keybinds.Logout.Keybind):
|
||||
return tview.BatchCommand{v.closeState(), v.logout()}
|
||||
return tview.Batch(v.closeState(), v.logout())
|
||||
case keybind.Matches(event, v.cfg.Keybinds.ToggleGuildsTree.Keybind):
|
||||
v.toggleGuildsTree()
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, v.cfg.Keybinds.ToggleChannelsPicker.Keybind):
|
||||
v.togglePicker()
|
||||
return redraw
|
||||
return nil
|
||||
}
|
||||
case *closeLayerEvent:
|
||||
if v.HasLayer(event.name) {
|
||||
v.HideLayer(event.name)
|
||||
}
|
||||
}
|
||||
cmd := v.Layers.HandleEvent(event)
|
||||
return v.consumeLayerCommands(cmd)
|
||||
}
|
||||
|
||||
func (v *Model) consumeLayerCommands(command tview.Command) tview.Command {
|
||||
if command == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var commands []tview.Command
|
||||
switch c := command.(type) {
|
||||
case tview.BatchCommand:
|
||||
commands = c
|
||||
default:
|
||||
commands = []tview.Command{c}
|
||||
}
|
||||
|
||||
remaining := make([]tview.Command, 0, len(commands))
|
||||
for _, cmd := range commands {
|
||||
switch c := cmd.(type) {
|
||||
case layers.OpenLayerCommand:
|
||||
if v.HasLayer(c.Name) {
|
||||
v.ShowLayer(c.Name).SendToFront(c.Name)
|
||||
}
|
||||
continue
|
||||
case layers.CloseLayerCommand:
|
||||
if v.HasLayer(c.Name) {
|
||||
v.HideLayer(c.Name)
|
||||
}
|
||||
continue
|
||||
case layers.ToggleLayerCommand:
|
||||
if v.HasLayer(c.Name) {
|
||||
if v.GetVisible(c.Name) {
|
||||
v.HideLayer(c.Name)
|
||||
} else {
|
||||
v.ShowLayer(c.Name).SendToFront(c.Name)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
remaining = append(remaining, cmd)
|
||||
}
|
||||
|
||||
if len(remaining) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(remaining) == 1 {
|
||||
return remaining[0]
|
||||
}
|
||||
return tview.BatchCommand(remaining)
|
||||
return v.Layers.HandleEvent(event)
|
||||
}
|
||||
|
||||
func (v *Model) showConfirmModal(prompt string, buttons []string, onDone func(label string)) {
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ import (
|
|||
)
|
||||
|
||||
func setClipboard(content string) tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
if err := clipboard.Write(clipboard.FmtText, []byte(content)); err != nil {
|
||||
slog.Error("failed to copy error message", "err", err)
|
||||
return tcell.NewEventError(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,14 +22,13 @@ const (
|
|||
type Model struct {
|
||||
*layers.Layers
|
||||
tabs *tabs.Model
|
||||
app *tview.Application
|
||||
|
||||
cfg *config.Config
|
||||
errorModalText string
|
||||
}
|
||||
|
||||
func NewModel(app *tview.Application, cfg *config.Config) *Model {
|
||||
tabs := tabs.NewModel([]tabs.Tab{token.NewModel(), qr.NewModel(app)})
|
||||
func NewModel(cfg *config.Config) *Model {
|
||||
tabs := tabs.NewModel([]tabs.Tab{token.NewModel(), qr.NewModel()})
|
||||
|
||||
l := layers.New()
|
||||
ui.ConfigureBox(l.Box, &cfg.Theme)
|
||||
|
|
@ -38,9 +37,7 @@ func NewModel(app *tview.Application, cfg *config.Config) *Model {
|
|||
return &Model{
|
||||
Layers: l,
|
||||
tabs: tabs,
|
||||
app: app,
|
||||
|
||||
cfg: cfg,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,10 +45,9 @@ func (m *Model) HandleEvent(event tcell.Event) tview.Command {
|
|||
switch event := event.(type) {
|
||||
case *tcell.EventError:
|
||||
if m.HasLayer(errorLayerName) {
|
||||
return tview.RedrawCommand{}
|
||||
return nil
|
||||
}
|
||||
m.onError(event)
|
||||
return tview.RedrawCommand{}
|
||||
return m.showErrorDialog(event)
|
||||
case *tview.ModalDoneEvent:
|
||||
if !m.HasLayer(errorLayerName) {
|
||||
return nil
|
||||
|
|
@ -61,12 +57,12 @@ func (m *Model) HandleEvent(event tcell.Event) tview.Command {
|
|||
}
|
||||
m.RemoveLayer(errorLayerName)
|
||||
m.errorModalText = ""
|
||||
return tview.RedrawCommand{}
|
||||
return nil
|
||||
}
|
||||
return m.Layers.HandleEvent(event)
|
||||
}
|
||||
|
||||
func (m *Model) onError(err error) {
|
||||
func (m *Model) showErrorDialog(err error) tview.Command {
|
||||
slog.Error("failed to login", "err", err)
|
||||
|
||||
message := err.Error()
|
||||
|
|
@ -99,6 +95,5 @@ func (m *Model) onError(err error) {
|
|||
layers.WithOverlay(),
|
||||
).
|
||||
SendToFront(errorLayerName)
|
||||
modal.SetFocus(0)
|
||||
m.app.SetFocus(modal)
|
||||
return tview.SetFocus(modal)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,12 +24,6 @@ type TokenEvent struct {
|
|||
Token string
|
||||
}
|
||||
|
||||
func newTokenEvent(token string) *TokenEvent {
|
||||
event := &TokenEvent{Token: token}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
const remoteAuthGatewayURL = "wss://remote-auth-gateway.discord.gg/?v=2"
|
||||
|
||||
type connCreateEvent struct {
|
||||
|
|
@ -37,41 +31,33 @@ type connCreateEvent struct {
|
|||
conn *websocket.Conn
|
||||
}
|
||||
|
||||
func newConnCreateEvent(conn *websocket.Conn) *connCreateEvent {
|
||||
event := &connCreateEvent{conn: conn}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
type connCloseEvent struct{ tcell.EventTime }
|
||||
|
||||
func newConnCloseEvent() *connCloseEvent {
|
||||
event := &connCloseEvent{}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
func (m *Model) connect() tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
headers := http.Headers()
|
||||
headers.Set("User-Agent", http.BrowserUserAgent)
|
||||
conn, _, err := websocket.DefaultDialer.Dial(remoteAuthGatewayURL, headers)
|
||||
if err != nil {
|
||||
return tcell.NewEventError(err)
|
||||
}
|
||||
return newConnCreateEvent(conn)
|
||||
})
|
||||
event := &connCreateEvent{conn: conn}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) close() tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
if m.conn != nil {
|
||||
if err := m.conn.Close(); err != nil {
|
||||
return tcell.NewEventError(err)
|
||||
}
|
||||
}
|
||||
return newConnCloseEvent()
|
||||
})
|
||||
event := &connCloseEvent{}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
type helloEvent struct {
|
||||
|
|
@ -80,66 +66,30 @@ type helloEvent struct {
|
|||
timeoutMS int
|
||||
}
|
||||
|
||||
func newHelloEvent(heartbeatInterval, timeoutMS int) *helloEvent {
|
||||
event := &helloEvent{heartbeatInterval: heartbeatInterval, timeoutMS: timeoutMS}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
type nonceProofEvent struct {
|
||||
tcell.EventTime
|
||||
encryptedNonce string
|
||||
}
|
||||
|
||||
func newNonceProofEvent(encryptedNonce string) *nonceProofEvent {
|
||||
event := &nonceProofEvent{encryptedNonce: encryptedNonce}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
type pendingRemoteInitEvent struct {
|
||||
tcell.EventTime
|
||||
fingerprint string
|
||||
}
|
||||
|
||||
func newPendingRemoteInitEvent(fingerprint string) *pendingRemoteInitEvent {
|
||||
event := &pendingRemoteInitEvent{fingerprint: fingerprint}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
type pendingTicketEvent struct {
|
||||
tcell.EventTime
|
||||
encryptedUserPayload string
|
||||
}
|
||||
|
||||
func newPendingTicketEvent(encryptedUserPayload string) *pendingTicketEvent {
|
||||
event := &pendingTicketEvent{encryptedUserPayload: encryptedUserPayload}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
type pendingLoginEvent struct {
|
||||
tcell.EventTime
|
||||
ticket string
|
||||
}
|
||||
|
||||
func newPendingLoginEvent(ticket string) *pendingLoginEvent {
|
||||
event := &pendingLoginEvent{ticket: ticket}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
type cancelEvent struct{ tcell.EventTime }
|
||||
|
||||
func newCancelEvent() *cancelEvent {
|
||||
event := &cancelEvent{}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
func (m *Model) listen() tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
if m.conn == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -165,7 +115,9 @@ func (m *Model) listen() tview.Command {
|
|||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return tcell.NewEventError(err)
|
||||
}
|
||||
return newHelloEvent(payload.HeartbeatInterval, payload.TimeoutMS)
|
||||
event := &helloEvent{heartbeatInterval: payload.HeartbeatInterval, timeoutMS: payload.TimeoutMS}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
case "nonce_proof":
|
||||
var payload struct {
|
||||
EncryptedNonce string `json:"encrypted_nonce"`
|
||||
|
|
@ -173,7 +125,9 @@ func (m *Model) listen() tview.Command {
|
|||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return tcell.NewEventError(err)
|
||||
}
|
||||
return newNonceProofEvent(payload.EncryptedNonce)
|
||||
event := &nonceProofEvent{encryptedNonce: payload.EncryptedNonce}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
case "pending_remote_init":
|
||||
var payload struct {
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
|
|
@ -181,7 +135,9 @@ func (m *Model) listen() tview.Command {
|
|||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return tcell.NewEventError(err)
|
||||
}
|
||||
return newPendingRemoteInitEvent(payload.Fingerprint)
|
||||
event := &pendingRemoteInitEvent{fingerprint: payload.Fingerprint}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
case "pending_ticket":
|
||||
var payload struct {
|
||||
EncryptedUserPayload string `json:"encrypted_user_payload"`
|
||||
|
|
@ -189,9 +145,13 @@ func (m *Model) listen() tview.Command {
|
|||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return tcell.NewEventError(err)
|
||||
}
|
||||
return newPendingTicketEvent(payload.EncryptedUserPayload)
|
||||
event := &pendingTicketEvent{encryptedUserPayload: payload.EncryptedUserPayload}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
case "cancel":
|
||||
return newCancelEvent()
|
||||
event := &cancelEvent{}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
case "pending_login":
|
||||
var payload struct {
|
||||
Ticket string `json:"ticket"`
|
||||
|
|
@ -199,30 +159,28 @@ func (m *Model) listen() tview.Command {
|
|||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return tcell.NewEventError(err)
|
||||
}
|
||||
return newPendingLoginEvent(payload.Ticket)
|
||||
event := &pendingLoginEvent{ticket: payload.Ticket}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type heartbeatTickEvent struct{ tcell.EventTime }
|
||||
|
||||
func newHeartbeatTickEvent() *heartbeatTickEvent {
|
||||
event := &heartbeatTickEvent{}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
func (m *Model) heartbeat() tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
time.Sleep(m.heartbeatInterval)
|
||||
return newHeartbeatTickEvent()
|
||||
})
|
||||
event := &heartbeatTickEvent{}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) sendHeartbeat() tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
if m.conn == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -233,7 +191,7 @@ func (m *Model) sendHeartbeat() tview.Command {
|
|||
return tcell.NewEventError(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type privateKeyEvent struct {
|
||||
|
|
@ -241,24 +199,20 @@ type privateKeyEvent struct {
|
|||
privateKey *rsa.PrivateKey
|
||||
}
|
||||
|
||||
func newPrivateKeyEvent(privateKey *rsa.PrivateKey) *privateKeyEvent {
|
||||
event := &privateKeyEvent{privateKey: privateKey}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
func (m *Model) generatePrivateKey() tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return tcell.NewEventError(err)
|
||||
}
|
||||
return newPrivateKeyEvent(privateKey)
|
||||
})
|
||||
event := &privateKeyEvent{privateKey: privateKey}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) sendInit() tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
if m.privateKey == nil {
|
||||
return tcell.NewEventError(errors.New("missing private key"))
|
||||
}
|
||||
|
|
@ -275,11 +229,11 @@ func (m *Model) sendInit() tview.Command {
|
|||
return tcell.NewEventError(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) sendNonceProof(encryptedNonce string) tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
decodedNonce, err := base64.StdEncoding.DecodeString(encryptedNonce)
|
||||
if err != nil {
|
||||
return tcell.NewEventError(err)
|
||||
|
|
@ -299,7 +253,7 @@ func (m *Model) sendNonceProof(encryptedNonce string) tview.Command {
|
|||
return tcell.NewEventError(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type qrCodeEvent struct {
|
||||
|
|
@ -307,22 +261,18 @@ type qrCodeEvent struct {
|
|||
qrCode *qrcode.QRCode
|
||||
}
|
||||
|
||||
func newQRCodeEvent(qrCode *qrcode.QRCode) *qrCodeEvent {
|
||||
event := &qrCodeEvent{qrCode: qrCode}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
func (m *Model) generateQRCode(fingerprint string) tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
content := "https://discord.com/ra/" + fingerprint
|
||||
qrCode, err := qrcode.New(content, qrcode.Low)
|
||||
if err != nil {
|
||||
return tcell.NewEventError(err)
|
||||
}
|
||||
qrCode.DisableBorder = true
|
||||
return newQRCodeEvent(qrCode)
|
||||
})
|
||||
event := &qrCodeEvent{qrCode: qrCode}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
type userEvent struct {
|
||||
|
|
@ -331,14 +281,8 @@ type userEvent struct {
|
|||
username string
|
||||
}
|
||||
|
||||
func newUserEvent(discriminator, username string) *userEvent {
|
||||
event := &userEvent{discriminator: discriminator, username: username}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
func (m *Model) decryptUserPayload(encryptedPayload string) tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
decodedPayload, err := base64.StdEncoding.DecodeString(encryptedPayload)
|
||||
if err != nil {
|
||||
return tcell.NewEventError(err)
|
||||
|
|
@ -354,12 +298,14 @@ func (m *Model) decryptUserPayload(encryptedPayload string) tview.Command {
|
|||
return tcell.NewEventError(errors.New("invalid user payload"))
|
||||
}
|
||||
|
||||
return newUserEvent(parts[1], parts[3])
|
||||
})
|
||||
event := &userEvent{discriminator: parts[1], username: parts[3]}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) exchangeTicket(ticket string) tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
headers := http.Headers()
|
||||
headers.Set("Referer", "https://discord.com/login")
|
||||
if m.fingerprint != "" {
|
||||
|
|
@ -383,6 +329,8 @@ func (m *Model) exchangeTicket(ticket string) tview.Command {
|
|||
if err != nil {
|
||||
return tcell.NewEventError(err)
|
||||
}
|
||||
return newTokenEvent(string(decryptedToken))
|
||||
})
|
||||
event := &TokenEvent{Token: string(decryptedToken)}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import (
|
|||
|
||||
type Model struct {
|
||||
*tview.TextView
|
||||
app *tview.Application
|
||||
|
||||
conn *websocket.Conn
|
||||
heartbeatInterval time.Duration
|
||||
|
|
@ -26,18 +25,14 @@ type Model struct {
|
|||
msg string
|
||||
}
|
||||
|
||||
func NewModel(app *tview.Application) *Model {
|
||||
func NewModel() *Model {
|
||||
m := &Model{
|
||||
TextView: tview.NewTextView(),
|
||||
app: app,
|
||||
}
|
||||
m.
|
||||
SetScrollable(true).
|
||||
SetWrap(false).
|
||||
SetTextAlign(tview.AlignmentCenter).
|
||||
SetChangedFunc(func() {
|
||||
m.app.QueueUpdateDraw(func() {})
|
||||
})
|
||||
SetTextAlign(tview.AlignmentCenter)
|
||||
|
||||
m.msg = "Press Ctrl+N to open QR login"
|
||||
return m
|
||||
|
|
@ -57,7 +52,7 @@ func (m *Model) HandleEvent(event tcell.Event) tview.Command {
|
|||
case *tview.KeyEvent:
|
||||
if event.Key() == tcell.KeyEsc {
|
||||
m.msg = "Canceled"
|
||||
return tview.BatchCommand{m.close(), tview.RedrawCommand{}}
|
||||
return tview.Batch(m.close(), nil)
|
||||
}
|
||||
return m.TextView.HandleEvent(event)
|
||||
|
||||
|
|
@ -71,21 +66,21 @@ func (m *Model) HandleEvent(event tcell.Event) tview.Command {
|
|||
|
||||
case *helloEvent:
|
||||
m.heartbeatInterval = time.Duration(event.heartbeatInterval) * time.Millisecond
|
||||
return tview.BatchCommand{m.listen(), m.heartbeat(), m.generatePrivateKey()}
|
||||
return tview.Batch(m.listen(), m.heartbeat(), m.generatePrivateKey())
|
||||
case *privateKeyEvent:
|
||||
m.privateKey = event.privateKey
|
||||
return tview.BatchCommand{m.listen(), m.sendInit()}
|
||||
return tview.Batch(m.listen(), m.sendInit())
|
||||
case *nonceProofEvent:
|
||||
return tview.BatchCommand{m.listen(), m.sendNonceProof(event.encryptedNonce)}
|
||||
return tview.Batch(m.listen(), m.sendNonceProof(event.encryptedNonce))
|
||||
case *pendingRemoteInitEvent:
|
||||
m.fingerprint = event.fingerprint
|
||||
return tview.BatchCommand{m.listen(), m.generateQRCode(event.fingerprint)}
|
||||
return tview.Batch(m.listen(), m.generateQRCode(event.fingerprint))
|
||||
case *qrCodeEvent:
|
||||
m.qrCode = event.qrCode
|
||||
m.msg = "Scan this with the Discord mobile app to log in instantly."
|
||||
return m.listen()
|
||||
case *pendingTicketEvent:
|
||||
return tview.BatchCommand{m.listen(), m.decryptUserPayload(event.encryptedUserPayload)}
|
||||
return tview.Batch(m.listen(), m.decryptUserPayload(event.encryptedUserPayload))
|
||||
case *userEvent:
|
||||
name := event.username
|
||||
if event.discriminator != "0" {
|
||||
|
|
@ -95,7 +90,7 @@ func (m *Model) HandleEvent(event tcell.Event) tview.Command {
|
|||
return m.listen()
|
||||
case *pendingLoginEvent:
|
||||
m.msg = "Authenticating..."
|
||||
return tview.BatchCommand{m.close(), m.exchangeTicket(event.ticket)}
|
||||
return tview.Batch(m.close(), m.exchangeTicket(event.ticket))
|
||||
case *cancelEvent:
|
||||
m.msg = "Login canceled on mobile"
|
||||
return m.close()
|
||||
|
|
@ -104,11 +99,11 @@ func (m *Model) HandleEvent(event tcell.Event) tview.Command {
|
|||
if m.conn == nil {
|
||||
return nil
|
||||
}
|
||||
return tview.BatchCommand{m.heartbeat(), m.sendHeartbeat()}
|
||||
return tview.Batch(m.heartbeat(), m.sendHeartbeat())
|
||||
|
||||
case *tcell.EventError:
|
||||
m.msg = event.Error()
|
||||
return tview.BatchCommand{m.close(), event}
|
||||
return tview.Batch(m.close(), tview.Command(func() tcell.Event { return event }))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -10,14 +10,10 @@ type TokenEvent struct {
|
|||
Token string
|
||||
}
|
||||
|
||||
func newTokenEvent(token string) *TokenEvent {
|
||||
event := &TokenEvent{Token: token}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
func tokenCommand(token string) tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return newTokenEvent(token)
|
||||
})
|
||||
return func() tcell.Event {
|
||||
event := &TokenEvent{Token: token}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,63 +14,57 @@ type tokenEvent struct {
|
|||
token string
|
||||
}
|
||||
|
||||
func newTokenEvent(token string) *tokenEvent {
|
||||
event := &tokenEvent{token: token}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
func tokenCommand(token string) tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return newTokenEvent(token)
|
||||
})
|
||||
return func() tcell.Event {
|
||||
event := &tokenEvent{token: token}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
type loginEvent struct{ tcell.EventTime }
|
||||
|
||||
func newLoginEvent() *loginEvent {
|
||||
event := &loginEvent{}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
|
||||
func getToken() tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
token, err := keyring.GetToken()
|
||||
if err != nil {
|
||||
slog.Info("failed to retrieve token from keyring", "err", err)
|
||||
return newLoginEvent()
|
||||
event := &loginEvent{}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
return newTokenEvent(token)
|
||||
})
|
||||
event := &tokenEvent{token: token}
|
||||
event.SetEventNow()
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
func setToken(token string) tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
if err := keyring.SetToken(token); err != nil {
|
||||
slog.Error("failed to set token to keyring", "err", err)
|
||||
return tcell.NewEventError(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func deleteToken() tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
if err := keyring.DeleteToken(); err != nil {
|
||||
slog.Error("failed to delete token from keyring", "err", err)
|
||||
return tcell.NewEventError(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func initClipboard() tview.Command {
|
||||
return tview.EventCommand(func() tcell.Event {
|
||||
return func() tcell.Event {
|
||||
if err := clipboard.Init(); err != nil {
|
||||
slog.Error("failed to init clipboard", "err", err)
|
||||
return tcell.NewEventError(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,15 +53,15 @@ func NewModel(cfg *config.Config, app *tview.Application) *Model {
|
|||
}
|
||||
|
||||
func (m *Model) showLogin() tview.Command {
|
||||
m.inner = login.NewModel(m.app, m.cfg)
|
||||
m.inner = login.NewModel(m.cfg)
|
||||
m.buildLayout()
|
||||
return tview.BatchCommand{m.inner.HandleEvent(tview.NewInitEvent()), tview.SetFocusCommand{Target: m}}
|
||||
return tview.Batch(m.inner.HandleEvent(tview.NewInitEvent()), tview.SetFocus(m))
|
||||
}
|
||||
|
||||
func (m *Model) showChat(token string) tview.Command {
|
||||
m.inner = chat.NewView(m.app, m.cfg, token)
|
||||
m.buildLayout()
|
||||
return tview.BatchCommand{m.inner.HandleEvent(tview.NewInitEvent()), tview.SetFocusCommand{Target: m}}
|
||||
return tview.Batch(m.inner.HandleEvent(tview.NewInitEvent()), tview.SetFocus(m))
|
||||
}
|
||||
|
||||
func (m *Model) buildLayout() {
|
||||
|
|
@ -88,11 +88,11 @@ func (m *Model) HandleEvent(event tcell.Event) tview.Command {
|
|||
} else {
|
||||
cmd = getToken()
|
||||
}
|
||||
return tview.BatchCommand{
|
||||
return tview.Batch(
|
||||
tview.SetTitle(consts.Name),
|
||||
initClipboard(),
|
||||
cmd,
|
||||
}
|
||||
)
|
||||
|
||||
case *loginEvent:
|
||||
return m.showLogin()
|
||||
|
|
@ -100,22 +100,22 @@ func (m *Model) HandleEvent(event tcell.Event) tview.Command {
|
|||
return m.showChat(event.token)
|
||||
|
||||
case *token.TokenEvent:
|
||||
return tview.BatchCommand{m.showChat(event.Token), setToken(event.Token)}
|
||||
return tview.Batch(m.showChat(event.Token), setToken(event.Token))
|
||||
case *qr.TokenEvent:
|
||||
return tview.BatchCommand{m.showChat(event.Token), setToken(event.Token)}
|
||||
return tview.Batch(m.showChat(event.Token), setToken(event.Token))
|
||||
|
||||
case *chat.LogoutEvent:
|
||||
return tview.BatchCommand{
|
||||
return tview.Batch(
|
||||
m.showLogin(),
|
||||
deleteToken(),
|
||||
}
|
||||
)
|
||||
|
||||
case *tview.KeyEvent:
|
||||
switch {
|
||||
case keybind.Matches(event, m.cfg.Keybinds.ToggleHelp.Keybind):
|
||||
m.help.SetShowAll(!m.help.ShowAll())
|
||||
m.updateHelpHeight()
|
||||
return tview.RedrawCommand{}
|
||||
return nil
|
||||
case keybind.Matches(event, m.cfg.Keybinds.Suspend.Keybind):
|
||||
m.suspend()
|
||||
return nil
|
||||
|
|
@ -124,7 +124,7 @@ func (m *Model) HandleEvent(event tcell.Event) tview.Command {
|
|||
if m.inner != nil {
|
||||
innerCmd = m.inner.HandleEvent(chat.NewQuitEvent())
|
||||
}
|
||||
return tview.BatchCommand{innerCmd, tview.Quit()}
|
||||
return tview.Batch(innerCmd, tview.Quit())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -151,29 +151,28 @@ func (p *Picker) onInputChanged(text string) {
|
|||
func (p *Picker) HandleEvent(event tcell.Event) tview.Command {
|
||||
switch event := event.(type) {
|
||||
case *tview.KeyEvent:
|
||||
redraw := tview.RedrawCommand{}
|
||||
if p.keyMap != nil {
|
||||
switch {
|
||||
case keybind.Matches(event, p.keyMap.Up):
|
||||
p.list.HandleEvent(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone))
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, p.keyMap.Down):
|
||||
p.list.HandleEvent(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone))
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, p.keyMap.Top):
|
||||
p.list.HandleEvent(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone))
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, p.keyMap.Bottom):
|
||||
p.list.HandleEvent(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone))
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, p.keyMap.Select):
|
||||
p.onListSelected(p.list.Cursor())
|
||||
return redraw
|
||||
return nil
|
||||
case keybind.Matches(event, p.keyMap.Cancel):
|
||||
if p.onCancel != nil {
|
||||
p.onCancel()
|
||||
}
|
||||
return redraw
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue