Przeglądaj źródła

refactor(ui/chat): adapt UI handlers to unified HandleEvent API

ayn2op 2 miesięcy temu
rodzic
commit
aa2e1e2497

+ 6 - 1
go.mod

@@ -2,11 +2,16 @@ module github.com/ayn2op/discordo
 
 go 1.26.0
 
+// replace (
+// 	github.com/ayn2op/tview => ../tview
+// 	github.com/diamondburned/arikawa/v3 => ../arikawa
+// )
+
 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-20260301025618-69fc2518f451
+	github.com/ayn2op/tview v0.0.0-20260302045935-716714ee7442
 	github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb
 	github.com/diamondburned/arikawa/v3 v3.6.1-0.20260226015332-783a3e8e8e86
 	github.com/diamondburned/ningen/v3 v3.0.1-0.20260226220604-93f1e60c3cdb

+ 2 - 2
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-20260301025618-69fc2518f451 h1:DfZZiNSwERJNZ9f33hsxWIFFiAgGY2T+Fi7unZ6Ka7A=
-github.com/ayn2op/tview v0.0.0-20260301025618-69fc2518f451/go.mod h1:lZ8RdOegQWBQafTOasGE7Ps1/Ymy4jmXoPt5vz2QsS0=
+github.com/ayn2op/tview v0.0.0-20260302045935-716714ee7442 h1:5CN5nE/5s7zbIp51hiH1CY6hpDgvzfvh4CF38FXJCGk=
+github.com/ayn2op/tview v0.0.0-20260302045935-716714ee7442/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=

+ 31 - 28
internal/ui/chat/guilds_tree.go

@@ -393,36 +393,39 @@ func (gt *guildsTree) collapseParentNode(node *tview.TreeNode) {
 		})
 }
 
-func (gt *guildsTree) InputHandler(event *tcell.EventKey) tview.Command {
-	consume := tview.ConsumeEventCommand{}
-	consumeRedraw := tview.BatchCommand{tview.RedrawCommand{}, tview.ConsumeEventCommand{}}
-	handler := gt.TreeView.InputHandler
-
-	switch {
-	case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.CollapseParentNode.Keybind):
-		gt.collapseParentNode(gt.GetCurrentNode())
-		return consumeRedraw
-	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):
-		return handler(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone))
-	case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Down.Keybind):
-		return handler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone))
-	case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Top.Keybind):
-		gt.Move(gt.GetRowCount() * -1)
-		return consumeRedraw
-	case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Bottom.Keybind):
-		gt.Move(gt.GetRowCount())
-		return consumeRedraw
-	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):
-		gt.yankID()
+func (gt *guildsTree) HandleEvent(event tcell.Event) tview.Command {
+	switch event := event.(type) {
+	case *tview.KeyEvent:
+		consume := tview.ConsumeEventCommand{}
+		consumeRedraw := tview.BatchCommand{tview.RedrawCommand{}, tview.ConsumeEventCommand{}}
+		handler := gt.TreeView.HandleEvent
+
+		switch {
+		case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.CollapseParentNode.Keybind):
+			gt.collapseParentNode(gt.GetCurrentNode())
+			return consumeRedraw
+		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):
+			return handler(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone))
+		case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Down.Keybind):
+			return handler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone))
+		case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Top.Keybind):
+			gt.Move(gt.GetRowCount() * -1)
+			return consumeRedraw
+		case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Bottom.Keybind):
+			gt.Move(gt.GetRowCount())
+			return consumeRedraw
+		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):
+			gt.yankID()
+			return consume
+		}
+		// Do not fall through to TreeView defaults for unmatched keys.
 		return consume
 	}
-
-	// Do not fall through to TreeView defaults for unmatched keys.
-	return consume
+	return gt.TreeView.HandleEvent(event)
 }
 
 func (gt *guildsTree) yankID() {

+ 85 - 67
internal/ui/chat/message_input.go

@@ -92,81 +92,99 @@ func (mi *messageInput) stopTypingTimer() {
 	}
 }
 
-func (mi *messageInput) InputHandler(event *tcell.EventKey) tview.Command {
-	consume := tview.ConsumeEventCommand{}
-	consumeRedraw := tview.BatchCommand{tview.RedrawCommand{}, tview.ConsumeEventCommand{}}
-	handler := mi.TextArea.InputHandler
-
-	switch {
-	case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Paste.Keybind):
-		mi.paste()
-		return handler(tcell.NewEventKey(tcell.KeyCtrlV, "", tcell.ModNone))
-	case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Send.Keybind):
-		if mi.chatView.GetVisible(mentionsListLayerName) {
-			mi.tabComplete()
-		} else {
-			mi.send()
-		}
-		return consumeRedraw
-	case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.OpenEditor.Keybind):
-		var cmd tview.Command
-		mi.stopTabCompletion(func(next tview.Command) { cmd = tview.AppendCommand(cmd, next) })
-		mi.editor()
-		return tview.AppendCommand(cmd, consumeRedraw)
-	case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.OpenFilePicker.Keybind):
-		var cmd tview.Command
-		mi.stopTabCompletion(func(next tview.Command) { cmd = tview.AppendCommand(cmd, next) })
-		mi.openFilePicker()
-		return tview.AppendCommand(cmd, consumeRedraw)
-	case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Cancel.Keybind):
-		var cmd tview.Command
-		if mi.chatView.GetVisible(mentionsListLayerName) {
-			mi.stopTabCompletion(func(next tview.Command) { cmd = tview.AppendCommand(cmd, next) })
-		} else {
-			mi.reset()
+func (mi *messageInput) HandleEvent(event tcell.Event) tview.Command {
+	switch event := event.(type) {
+	case *tview.KeyEvent:
+		consume := tview.ConsumeEventCommand{}
+		consumeRedraw := tview.BatchCommand{tview.RedrawCommand{}, tview.ConsumeEventCommand{}}
+		handler := mi.TextArea.HandleEvent
+		switch {
+		case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Paste.Keybind):
+			mi.paste()
+			return handler(tcell.NewEventKey(tcell.KeyCtrlV, "", tcell.ModNone))
+		case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Send.Keybind):
+			if mi.chatView.GetVisible(mentionsListLayerName) {
+				mi.tabComplete()
+			} else {
+				mi.send()
+			}
+			return consumeRedraw
+		case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.OpenEditor.Keybind):
+			var cmds tview.BatchCommand
+			mi.stopTabCompletion(func(next tview.Command) {
+				if next != nil {
+					cmds = append(cmds, next)
+				}
+			})
+			mi.editor()
+			cmds = append(cmds, consumeRedraw)
+			return cmds
+		case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.OpenFilePicker.Keybind):
+			var cmds tview.BatchCommand
+			mi.stopTabCompletion(func(next tview.Command) {
+				if next != nil {
+					cmds = append(cmds, next)
+				}
+			})
+			mi.openFilePicker()
+			cmds = append(cmds, consumeRedraw)
+			return cmds
+		case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Cancel.Keybind):
+			var cmds tview.BatchCommand
+			if mi.chatView.GetVisible(mentionsListLayerName) {
+				mi.stopTabCompletion(func(next tview.Command) {
+					if next != nil {
+						cmds = append(cmds, next)
+					}
+				})
+			} else {
+				mi.reset()
+			}
+			cmds = append(cmds, consumeRedraw)
+			return cmds
+		case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.TabComplete.Keybind):
+			go mi.chatView.app.QueueUpdateDraw(func() { mi.tabComplete() })
+			return consume
+		case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Undo.Keybind):
+			return handler(tcell.NewEventKey(tcell.KeyCtrlZ, "", tcell.ModNone))
 		}
-		return tview.AppendCommand(cmd, consumeRedraw)
-	case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.TabComplete.Keybind):
-		go mi.chatView.app.QueueUpdateDraw(func() { mi.tabComplete() })
-		return consume
-	case keybind.Matches(event, mi.cfg.Keybinds.MessageInput.Undo.Keybind):
-		return handler(tcell.NewEventKey(tcell.KeyCtrlZ, "", tcell.ModNone))
-	}
-
-	if mi.cfg.TypingIndicator.Send && mi.typingTimer == nil {
-		mi.typingTimer = time.AfterFunc(typingDuration, func() {
-			mi.typingTimerMu.Lock()
-			mi.typingTimer = nil
-			mi.typingTimerMu.Unlock()
-		})
 
-		if selectedChannel := mi.chatView.SelectedChannel(); selectedChannel != nil {
-			go mi.chatView.state.Typing(selectedChannel.ID)
+		if mi.cfg.TypingIndicator.Send && mi.typingTimer == nil {
+			mi.typingTimer = time.AfterFunc(typingDuration, func() {
+				mi.typingTimerMu.Lock()
+				mi.typingTimer = nil
+				mi.typingTimerMu.Unlock()
+			})
+
+			if selectedChannel := mi.chatView.SelectedChannel(); selectedChannel != nil {
+				go mi.chatView.state.Typing(selectedChannel.ID)
+			}
 		}
-	}
 
-	if mi.cfg.AutocompleteLimit > 0 {
-		if mi.chatView.GetVisible(mentionsListLayerName) {
-			switch {
-			case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Up.Keybind):
-				mi.mentionsList.InputHandler(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone))
-				return consumeRedraw
-			case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Down.Keybind):
-				mi.mentionsList.InputHandler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone))
-				return consumeRedraw
-			case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Top.Keybind):
-				mi.mentionsList.InputHandler(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone))
-				return consumeRedraw
-			case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Bottom.Keybind):
-				mi.mentionsList.InputHandler(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone))
-				return consumeRedraw
+		if mi.cfg.AutocompleteLimit > 0 {
+			if mi.chatView.GetVisible(mentionsListLayerName) {
+				switch {
+				case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Up.Keybind):
+					mi.mentionsList.HandleEvent(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone))
+					return consumeRedraw
+				case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Down.Keybind):
+					mi.mentionsList.HandleEvent(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone))
+					return consumeRedraw
+				case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Top.Keybind):
+					mi.mentionsList.HandleEvent(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone))
+					return consumeRedraw
+				case keybind.Matches(event, mi.cfg.Keybinds.MentionsList.Bottom.Keybind):
+					mi.mentionsList.HandleEvent(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone))
+					return consumeRedraw
+				}
 			}
+
+			go mi.chatView.app.QueueUpdateDraw(func() { mi.tabSuggestion() })
 		}
 
-		go mi.chatView.app.QueueUpdateDraw(func() { mi.tabSuggestion() })
+		return handler(event)
 	}
-
-	return handler(event)
+	return mi.TextArea.HandleEvent(event)
 }
 
 func (mi *messageInput) paste() {

+ 68 - 64
internal/ui/chat/messages_list.go

@@ -525,72 +525,76 @@ func (ml *messagesList) selectedMessage() (*discord.Message, error) {
 	return &ml.messages[cursor], nil
 }
 
-func (ml *messagesList) InputHandler(event *tcell.EventKey) tview.Command {
-	consume := tview.ConsumeEventCommand{}
-	consumeRedraw := tview.BatchCommand{tview.RedrawCommand{}, tview.ConsumeEventCommand{}}
+func (ml *messagesList) HandleEvent(event tcell.Event) tview.Command {
+	switch event := event.(type) {
+	case *tview.KeyEvent:
+		consume := tview.ConsumeEventCommand{}
+		consumeRedraw := tview.BatchCommand{tview.RedrawCommand{}, tview.ConsumeEventCommand{}}
+
+		switch {
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollUp.Keybind):
+			ml.ScrollUp()
+			return consumeRedraw
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollDown.Keybind):
+			ml.ScrollDown()
+			return consumeRedraw
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollTop.Keybind):
+			ml.ScrollToStart()
+			return consumeRedraw
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollBottom.Keybind):
+			ml.ScrollToEnd()
+			return consumeRedraw
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Cancel.Keybind):
+			ml.clearSelection()
+			return consumeRedraw
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectUp.Keybind):
+			ml.selectUp()
+			return consumeRedraw
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectDown.Keybind):
+			ml.selectDown()
+			return consumeRedraw
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectTop.Keybind):
+			ml.selectTop()
+			return consumeRedraw
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectBottom.Keybind):
+			ml.selectBottom()
+			return consumeRedraw
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectReply.Keybind):
+			ml.selectReply()
+			return consumeRedraw
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.YankID.Keybind):
+			ml.yankID()
+			return consume
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.YankContent.Keybind):
+			ml.yankContent()
+			return consume
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.YankURL.Keybind):
+			ml.yankURL()
+			return consume
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Open.Keybind):
+			ml.open()
+			return consume
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Reply.Keybind):
+			ml.reply(false)
+			return consumeRedraw
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ReplyMention.Keybind):
+			ml.reply(true)
+			return consumeRedraw
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Edit.Keybind):
+			ml.edit()
+			return consumeRedraw
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Delete.Keybind):
+			ml.delete()
+			return consumeRedraw
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.DeleteConfirm.Keybind):
+			ml.confirmDelete()
+			return consumeRedraw
+		}
 
-	switch {
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollUp.Keybind):
-		ml.ScrollUp()
-		return consumeRedraw
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollDown.Keybind):
-		ml.ScrollDown()
-		return consumeRedraw
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollTop.Keybind):
-		ml.ScrollToStart()
-		return consumeRedraw
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ScrollBottom.Keybind):
-		ml.ScrollToEnd()
-		return consumeRedraw
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Cancel.Keybind):
-		ml.clearSelection()
-		return consumeRedraw
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectUp.Keybind):
-		ml.selectUp()
-		return consumeRedraw
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectDown.Keybind):
-		ml.selectDown()
-		return consumeRedraw
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectTop.Keybind):
-		ml.selectTop()
-		return consumeRedraw
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectBottom.Keybind):
-		ml.selectBottom()
-		return consumeRedraw
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectReply.Keybind):
-		ml.selectReply()
-		return consumeRedraw
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.YankID.Keybind):
-		ml.yankID()
-		return consume
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.YankContent.Keybind):
-		ml.yankContent()
+		// Do not fall through to List defaults for unmatched keys.
 		return consume
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.YankURL.Keybind):
-		ml.yankURL()
-		return consume
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Open.Keybind):
-		ml.open()
-		return consume
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Reply.Keybind):
-		ml.reply(false)
-		return consumeRedraw
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ReplyMention.Keybind):
-		ml.reply(true)
-		return consumeRedraw
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Edit.Keybind):
-		ml.edit()
-		return consumeRedraw
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Delete.Keybind):
-		ml.delete()
-		return consumeRedraw
-	case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.DeleteConfirm.Keybind):
-		ml.confirmDelete()
-		return consumeRedraw
-	}
-
-	// Do not fall through to List defaults for unmatched keys.
-	return consume
+	}
+	return ml.List.HandleEvent(event)
 }
 
 func (ml *messagesList) selectUp() {

+ 45 - 40
internal/ui/chat/view.go

@@ -222,48 +222,53 @@ func (v *View) focusNext() {
 	}
 }
 
-func (v *View) InputHandler(event *tcell.EventKey) tview.Command {
-	consume := tview.BatchCommand{tview.RedrawCommand{}, tview.ConsumeEventCommand{}}
-
-	switch {
-	case keybind.Matches(event, v.cfg.Keybinds.ToggleHelp.Keybind):
-		v.help.SetShowAll(!v.help.ShowAll())
-		v.updateHelpHeight()
-		return consume
-	case keybind.Matches(event, v.cfg.Keybinds.FocusGuildsTree.Keybind):
-		v.messageInput.removeMentionsList()
-		v.focusGuildsTree()
-		return consume
-	case keybind.Matches(event, v.cfg.Keybinds.FocusMessagesList.Keybind):
-		v.messageInput.removeMentionsList()
-		v.app.SetFocus(v.messagesList)
-		return consume
-	case keybind.Matches(event, v.cfg.Keybinds.FocusMessageInput.Keybind):
-		v.focusMessageInput()
-		return consume
-	case keybind.Matches(event, v.cfg.Keybinds.FocusPrevious.Keybind):
-		v.focusPrevious()
-		return consume
-	case keybind.Matches(event, v.cfg.Keybinds.FocusNext.Keybind):
-		v.focusNext()
-		return consume
-	case keybind.Matches(event, v.cfg.Keybinds.Logout.Keybind):
-		if v.onLogout != nil {
-			v.onLogout()
-		}
-		if err := keyring.DeleteToken(); err != nil {
-			slog.Error("failed to delete token from keyring", "err", err)
+func (v *View) HandleEvent(event tcell.Event) tview.Command {
+	switch event := event.(type) {
+	case *tview.KeyEvent:
+		consume := tview.BatchCommand{tview.RedrawCommand{}, tview.ConsumeEventCommand{}}
+
+		switch {
+		case keybind.Matches(event, v.cfg.Keybinds.ToggleHelp.Keybind):
+			v.help.SetShowAll(!v.help.ShowAll())
+			v.updateHelpHeight()
+			return consume
+		case keybind.Matches(event, v.cfg.Keybinds.FocusGuildsTree.Keybind):
+			v.messageInput.removeMentionsList()
+			v.focusGuildsTree()
+			return consume
+		case keybind.Matches(event, v.cfg.Keybinds.FocusMessagesList.Keybind):
+			v.messageInput.removeMentionsList()
+			v.app.SetFocus(v.messagesList)
+			return consume
+		case keybind.Matches(event, v.cfg.Keybinds.FocusMessageInput.Keybind):
+			v.focusMessageInput()
+			return consume
+		case keybind.Matches(event, v.cfg.Keybinds.FocusPrevious.Keybind):
+			v.focusPrevious()
+			return consume
+		case keybind.Matches(event, v.cfg.Keybinds.FocusNext.Keybind):
+			v.focusNext()
+			return consume
+		case keybind.Matches(event, v.cfg.Keybinds.Logout.Keybind):
+			if v.onLogout != nil {
+				v.onLogout()
+			}
+			if err := keyring.DeleteToken(); err != nil {
+				slog.Error("failed to delete token from keyring", "err", err)
+			}
+			return consume
+		case keybind.Matches(event, v.cfg.Keybinds.ToggleGuildsTree.Keybind):
+			v.toggleGuildsTree()
+			return consume
+		case keybind.Matches(event, v.cfg.Keybinds.ToggleChannelsPicker.Keybind):
+			v.togglePicker()
+			return consume
 		}
-		return consume
-	case keybind.Matches(event, v.cfg.Keybinds.ToggleGuildsTree.Keybind):
-		v.toggleGuildsTree()
-		return consume
-	case keybind.Matches(event, v.cfg.Keybinds.ToggleChannelsPicker.Keybind):
-		v.togglePicker()
-		return consume
-	}
 
-	cmd := v.Layers.InputHandler(event)
+		cmd := v.Layers.HandleEvent(event)
+		return v.consumeLayerCommands(cmd)
+	}
+	cmd := v.Layers.HandleEvent(event)
 	return v.consumeLayerCommands(cmd)
 }
 

+ 11 - 7
internal/ui/login/qr.go

@@ -63,15 +63,19 @@ func newQRLogin(app *tview.Application, cfg *config.Config, done func(token stri
 	return q
 }
 
-func (q *qrLogin) InputHandler(event *tcell.EventKey) tview.Command {
-	if event.Key() == tcell.KeyEsc {
-		q.stop()
-		if q.done != nil {
-			q.done("", nil)
+func (q *qrLogin) HandleEvent(event tcell.Event) tview.Command {
+	switch event := event.(type) {
+	case *tview.KeyEvent:
+		if event.Key() == tcell.KeyEsc {
+			q.stop()
+			if q.done != nil {
+				q.done("", nil)
+			}
+			return tview.BatchCommand{tview.RedrawCommand{}, tview.ConsumeEventCommand{}}
 		}
-		return tview.BatchCommand{tview.RedrawCommand{}, tview.ConsumeEventCommand{}}
+		return q.TextView.HandleEvent(event)
 	}
-	return q.TextView.InputHandler(event)
+	return q.TextView.HandleEvent(event)
 }
 
 func (q *qrLogin) start() {

+ 18 - 25
internal/ui/root/view.go

@@ -123,18 +123,25 @@ func (v *View) Draw(screen tcell.Screen) {
 	v.inner.Draw(screen)
 }
 
-func (v *View) InputHandler(event *tcell.EventKey) tview.Command {
-	switch {
-	case keybind.Matches(event, v.cfg.Keybinds.Suspend.Keybind):
-		v.suspend()
-		return tview.ConsumeEventCommand{}
-	case keybind.Matches(event, v.cfg.Keybinds.Quit.Keybind):
-		v.closeChatViewState()
-		return tview.QuitCommand{}
-	}
+func (v *View) HandleEvent(event tcell.Event) tview.Command {
+	switch event := event.(type) {
+	case *tview.KeyEvent:
+		switch {
+		case keybind.Matches(event, v.cfg.Keybinds.Suspend.Keybind):
+			v.suspend()
+			return tview.ConsumeEventCommand{}
+		case keybind.Matches(event, v.cfg.Keybinds.Quit.Keybind):
+			v.closeChatViewState()
+			return tview.QuitCommand{}
+		}
 
-	if v.inner != nil {
-		return v.inner.InputHandler(event)
+		if v.inner != nil {
+			return v.inner.HandleEvent(event)
+		}
+	case *tview.MouseEvent, *tview.PasteEvent:
+		if v.inner != nil {
+			return v.inner.HandleEvent(event)
+		}
 	}
 	return nil
 }
@@ -160,17 +167,3 @@ func (v *View) Blur() {
 	}
 	v.Box.Blur()
 }
-
-func (v *View) MouseHandler(action tview.MouseAction, event *tcell.EventMouse) (tview.Primitive, tview.Command) {
-	if v.inner == nil {
-		return nil, nil
-	}
-	return v.inner.MouseHandler(action, event)
-}
-
-func (v *View) PasteHandler(text string) tview.Command {
-	if v.inner == nil {
-		return nil
-	}
-	return v.inner.PasteHandler(text)
-}

+ 30 - 26
pkg/picker/picker.go

@@ -148,33 +148,37 @@ func (p *Picker) onInputChanged(text string) {
 	p.setFilteredItems(fuzzied)
 }
 
-func (p *Picker) InputHandler(event *tcell.EventKey) tview.Command {
-	consume := tview.BatchCommand{tview.RedrawCommand{}, tview.ConsumeEventCommand{}}
-
-	if p.keyMap != nil {
-		switch {
-		case keybind.Matches(event, p.keyMap.Up):
-			p.list.InputHandler(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone))
-			return consume
-		case keybind.Matches(event, p.keyMap.Down):
-			p.list.InputHandler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone))
-			return consume
-		case keybind.Matches(event, p.keyMap.Top):
-			p.list.InputHandler(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone))
-			return consume
-		case keybind.Matches(event, p.keyMap.Bottom):
-			p.list.InputHandler(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone))
-			return consume
-		case keybind.Matches(event, p.keyMap.Select):
-			p.onListSelected(p.list.Cursor())
-			return consume
-		case keybind.Matches(event, p.keyMap.Cancel):
-			if p.onCancel != nil {
-				p.onCancel()
+func (p *Picker) HandleEvent(event tcell.Event) tview.Command {
+	switch event := event.(type) {
+	case *tview.KeyEvent:
+		consume := tview.BatchCommand{tview.RedrawCommand{}, tview.ConsumeEventCommand{}}
+
+		if p.keyMap != nil {
+			switch {
+			case keybind.Matches(event, p.keyMap.Up):
+				p.list.HandleEvent(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone))
+				return consume
+			case keybind.Matches(event, p.keyMap.Down):
+				p.list.HandleEvent(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone))
+				return consume
+			case keybind.Matches(event, p.keyMap.Top):
+				p.list.HandleEvent(tcell.NewEventKey(tcell.KeyHome, "", tcell.ModNone))
+				return consume
+			case keybind.Matches(event, p.keyMap.Bottom):
+				p.list.HandleEvent(tcell.NewEventKey(tcell.KeyEnd, "", tcell.ModNone))
+				return consume
+			case keybind.Matches(event, p.keyMap.Select):
+				p.onListSelected(p.list.Cursor())
+				return consume
+			case keybind.Matches(event, p.keyMap.Cancel):
+				if p.onCancel != nil {
+					p.onCancel()
+				}
+				return consume
 			}
-			return consume
 		}
-	}
 
-	return p.Flex.InputHandler(event)
+		return p.Flex.HandleEvent(event)
+	}
+	return p.Flex.HandleEvent(event)
 }