Преглед изворни кода

refactor(ui): remove redraw commands and inline command events

ayn2op пре 1 месец
родитељ
комит
2d79e45f6f

+ 7 - 7
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
 )

+ 14 - 14
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=

+ 20 - 9
internal/ui/chat/events.go

@@ -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
+	}
 }

+ 3 - 4
internal/ui/chat/guilds_tree.go

@@ -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):

+ 14 - 19
internal/ui/chat/message_input.go

@@ -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)

+ 16 - 17
internal/ui/chat/messages_list.go

@@ -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

+ 17 - 62
internal/ui/chat/model.go

@@ -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
 		}
-	}
-	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
+	case *closeLayerEvent:
+		if v.HasLayer(event.name) {
+			v.HideLayer(event.name)
 		}
-		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)) {

+ 2 - 2
internal/ui/login/events.go

@@ -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
-	})
+	}
 }

+ 8 - 13
internal/ui/login/model.go

@@ -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)
 }

+ 61 - 113
internal/ui/login/qr/events.go

@@ -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
+	}
 }

+ 11 - 16
internal/ui/login/qr/model.go

@@ -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

+ 5 - 9
internal/ui/login/token/events.go

@@ -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
+	}
 }

+ 19 - 25
internal/ui/root/events.go

@@ -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
-	})
+	}
 }

+ 11 - 11
internal/ui/root/model.go

@@ -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())
 		}
 	}
 

+ 6 - 7
pkg/picker/picker.go

@@ -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
 			}
 		}