on main help sign in
fix(ui): improve key handling and keybindings for chat/editor
commit1bb5aa8e0784f68313c07d54188a083b5d009121 authorAyman Bagabas <ayman.bagabas@gmail.com> dateTue 16 Dec 2025, 15:59 (1 year ago) parent7b267bc
2 files changed+167−61
internal/ui/model/keys.go+35−15 internal/ui/model/ui.go+132−46
internal/ui/model/keys.go+35−15
@@ -18,21 +18,25 @@ type KeyMap struct {
1818 }
1919
2020 Chat struct {
21− NewSession key.Binding
22− AddAttachment key.Binding
23− Cancel key.Binding
24− Tab key.Binding
25− Details key.Binding
26− Down key.Binding
27− Up key.Binding
28− DownOneItem key.Binding
29− UpOneItem key.Binding
30− PageDown key.Binding
31− PageUp key.Binding
32− HalfPageDown key.Binding
33− HalfPageUp key.Binding
34− Home key.Binding
35− End key.Binding
21+ NewSession key.Binding
22+ AddAttachment key.Binding
23+ Cancel key.Binding
24+ Tab key.Binding
25+ Details key.Binding
26+ Down key.Binding
27+ Up key.Binding
28+ UpDown key.Binding
29+ DownOneItem key.Binding
30+ UpOneItem key.Binding
31+ UpDownOneItem key.Binding
32+ PageDown key.Binding
33+ PageUp key.Binding
34+ HalfPageDown key.Binding
35+ HalfPageUp key.Binding
36+ Home key.Binding
37+ End key.Binding
38+ Copy key.Binding
39+ ClearHighlight key.Binding
3640 }
3741
3842 Initialize struct {
@@ -153,6 +157,10 @@ func DefaultKeyMap() KeyMap {
153157 key.WithKeys("up", "ctrl+k", "ctrl+p", "k"),
154158 key.WithHelp("↑", "up"),
155159 )
160+ km.Chat.UpDown = key.NewBinding(
161+ key.WithKeys("up", "down"),
162+ key.WithHelp("↑↓", "scroll"),
163+ )
156164 km.Chat.UpOneItem = key.NewBinding(
157165 key.WithKeys("shift+up", "K"),
158166 key.WithHelp("shift+↑", "up one item"),
@@ -161,6 +169,10 @@ func DefaultKeyMap() KeyMap {
161169 key.WithKeys("shift+down", "J"),
162170 key.WithHelp("shift+↓", "down one item"),
163171 )
172+ km.Chat.UpDownOneItem = key.NewBinding(
173+ key.WithKeys("shift+up", "shift+down"),
174+ key.WithHelp("shift+↑↓", "scroll one item"),
175+ )
164176 km.Chat.HalfPageDown = key.NewBinding(
165177 key.WithKeys("d"),
166178 key.WithHelp("d", "half page down"),
@@ -185,6 +197,14 @@ func DefaultKeyMap() KeyMap {
185197 key.WithKeys("G", "end"),
186198 key.WithHelp("G", "end"),
187199 )
200+ km.Chat.Copy = key.NewBinding(
201+ key.WithKeys("c", "y", "C", "Y"),
202+ key.WithHelp("c/y", "copy"),
203+ )
204+ km.Chat.ClearHighlight = key.NewBinding(
205+ key.WithKeys("esc", "alt+esc"),
206+ key.WithHelp("esc", "clear selection"),
207+ )
188208
189209 km.Initialize.Yes = key.NewBinding(
190210 key.WithKeys("y", "Y"),
internal/ui/model/ui.go+132−46
@@ -313,7 +313,9 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
313313 }
314314 }
315315 case tea.KeyPressMsg:
316− cmds = append(cmds, m.handleKeyPressMsg(msg)...)
316+ if cmd := m.handleKeyPressMsg(msg); cmd != nil {
317+ cmds = append(cmds, cmd)
318+ }
317319 case tea.PasteMsg:
318320 if cmd := m.handlePasteMsg(msg); cmd != nil {
319321 cmds = append(cmds, cmd)
@@ -341,7 +343,9 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
341343 return m, tea.Batch(cmds...)
342344}
343345
344−func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
346+func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
347+ var cmds []tea.Cmd
348+
345349 handleQuitKeys := func(msg tea.KeyPressMsg) bool {
346350 switch {
347351 case key.Matches(msg, m.keyMap.Quit):
@@ -369,6 +373,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
369373 return true
370374 case key.Matches(msg, m.keyMap.Models):
371375 // TODO: Implement me
376+ return true
372377 case key.Matches(msg, m.keyMap.Sessions):
373378 if m.dialog.ContainsDialog(dialog.SessionsID) {
374379 // Bring to front
@@ -385,12 +390,12 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
385390 if m.dialog.HasDialogs() {
386391 // Always handle quit keys first
387392 if handleQuitKeys(msg) {
388− return cmds
393+ return tea.Batch(cmds...)
389394 }
390395
391396 msg := m.dialog.Update(msg)
392397 if msg == nil {
393− return cmds
398+ return tea.Batch(cmds...)
394399 }
395400
396401 switch msg := msg.(type) {
@@ -424,18 +429,21 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
424429 cmds = append(cmds, tea.Quit)
425430 }
426431
427− return cmds
432+ return tea.Batch(cmds...)
428433 }
429434
430435 switch m.state {
431436 case uiConfigure:
432− return cmds
437+ return tea.Batch(cmds...)
433438 case uiInitialize:
434− return append(cmds, m.updateInitializeView(msg)...)
439+ cmds = append(cmds, m.updateInitializeView(msg)...)
440+ return tea.Batch(cmds...)
435441 case uiChat, uiLanding, uiChatCompact:
436442 switch m.focus {
437443 case uiFocusEditor:
438444 switch {
445+ case key.Matches(msg, m.keyMap.Editor.SendMessage):
446+ // TODO: Implement me
439447 case key.Matches(msg, m.keyMap.Tab):
440448 m.focus = uiFocusMain
441449 m.textarea.Blur()
@@ -447,6 +455,8 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
447455 break
448456 }
449457 cmds = append(cmds, m.openEditor(m.textarea.Value()))
458+ case key.Matches(msg, m.keyMap.Editor.Newline):
459+ m.textarea.InsertRune('\n')
450460 default:
451461 if handleGlobalKeys(msg) {
452462 // Handle global keys first before passing to textarea.
@@ -509,12 +519,12 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
509519 handleGlobalKeys(msg)
510520 }
511521
512− return cmds
522+ return tea.Batch(cmds...)
513523}
514524
515525// Draw implements [tea.Layer] and draws the UI model.
516526func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) {
517− layout := generateLayout(m, area.Dx(), area.Dy())
527+ layout := m.generateLayout(area.Dx(), area.Dy())
518528
519529 if m.layout != layout {
520530 m.layout = layout
@@ -665,46 +675,58 @@ func (m *UI) View() tea.View {
665675func (m *UI) ShortHelp() []key.Binding {
666676 var binds []key.Binding
667677 k := &m.keyMap
678+ tab := k.Tab
679+ commands := k.Commands
680+ if m.focus == uiFocusEditor && m.textarea.LineCount() == 0 {
681+ commands.SetHelp("/ or ctrl+p", "commands")
682+ }
668683
669684 switch m.state {
670685 case uiInitialize:
671686 binds = append(binds, k.Quit)
687+ case uiChat:
688+ if m.focus == uiFocusEditor {
689+ tab.SetHelp("tab", "focus chat")
690+ } else {
691+ tab.SetHelp("tab", "focus editor")
692+ }
693+
694+ binds = append(binds,
695+ tab,
696+ commands,
697+ k.Models,
698+ )
699+
700+ switch m.focus {
701+ case uiFocusEditor:
702+ binds = append(binds,
703+ k.Editor.Newline,
704+ )
705+ case uiFocusMain:
706+ binds = append(binds,
707+ k.Chat.UpDown,
708+ k.Chat.UpDownOneItem,
709+ k.Chat.PageUp,
710+ k.Chat.PageDown,
711+ k.Chat.Copy,
712+ )
713+ }
672714 default:
673715 // TODO: other states
674716 // if m.session == nil {
675717 // no session selected
676718 binds = append(binds,
677− k.Commands,
719+ commands,
678720 k.Models,
679721 k.Editor.Newline,
680− k.Quit,
681− k.Help,
682722 )
683− // }
684− // else {
685− // we have a session
686− // }
687−
688− // switch m.state {
689− // case uiChat:
690− // case uiEdit:
691− // binds = append(binds,
692− // k.Editor.AddFile,
693− // k.Editor.SendMessage,
694− // k.Editor.OpenEditor,
695− // k.Editor.Newline,
696− // )
697− //
698− // if len(m.attachments) > 0 {
699− // binds = append(binds,
700− // k.Editor.AttachmentDeleteMode,
701− // k.Editor.DeleteAllAttachments,
702− // k.Editor.Escape,
703− // )
704− // }
705− // }
706723 }
707724
725+ binds = append(binds,
726+ k.Quit,
727+ k.Help,
728+ )
729+
708730 return binds
709731}
710732
@@ -714,6 +736,12 @@ func (m *UI) FullHelp() [][]key.Binding {
714736 k := &m.keyMap
715737 help := k.Help
716738 help.SetHelp("ctrl+g", "less")
739+ hasAttachments := false // TODO: implement attachments
740+ hasSession := m.session != nil && m.session.ID != ""
741+ commands := k.Commands
742+ if m.focus == uiFocusEditor && m.textarea.LineCount() == 0 {
743+ commands.SetHelp("/ or ctrl+p", "commands")
744+ }
717745
718746 switch m.state {
719747 case uiInitialize:
@@ -721,12 +749,72 @@ func (m *UI) FullHelp() [][]key.Binding {
721749 []key.Binding{
722750 k.Quit,
723751 })
752+ case uiChat:
753+ mainBinds := []key.Binding{}
754+ tab := k.Tab
755+ if m.focus == uiFocusEditor {
756+ tab.SetHelp("tab", "focus chat")
757+ } else {
758+ tab.SetHelp("tab", "focus editor")
759+ }
760+
761+ mainBinds = append(mainBinds,
762+ tab,
763+ commands,
764+ k.Models,
765+ k.Sessions,
766+ )
767+ if hasSession {
768+ mainBinds = append(mainBinds, k.Chat.NewSession)
769+ }
770+
771+ binds = append(binds, mainBinds)
772+
773+ switch m.focus {
774+ case uiFocusEditor:
775+ binds = append(binds,
776+ []key.Binding{
777+ k.Editor.Newline,
778+ k.Editor.AddImage,
779+ k.Editor.MentionFile,
780+ k.Editor.OpenEditor,
781+ },
782+ )
783+ if hasAttachments {
784+ binds = append(binds,
785+ []key.Binding{
786+ k.Editor.AttachmentDeleteMode,
787+ k.Editor.DeleteAllAttachments,
788+ k.Editor.Escape,
789+ },
790+ )
791+ }
792+ case uiFocusMain:
793+ binds = append(binds,
794+ []key.Binding{
795+ k.Chat.UpDown,
796+ k.Chat.UpDownOneItem,
797+ k.Chat.PageUp,
798+ k.Chat.PageDown,
799+ },
800+ []key.Binding{
801+ k.Chat.HalfPageUp,
802+ k.Chat.HalfPageDown,
803+ k.Chat.Home,
804+ k.Chat.End,
805+ },
806+ []key.Binding{
807+ k.Chat.Copy,
808+ k.Chat.ClearHighlight,
809+ },
810+ )
811+ }
724812 default:
725813 if m.session == nil {
726814 // no session selected
727815 binds = append(binds,
728816 []key.Binding{
729− k.Commands,
817+ commands,
730818 k.Models,
731819 k.Sessions,
732820 },
@@ -741,23 +829,21 @@ func (m *UI) FullHelp() [][]key.Binding {
741829 },
742830 )
743831 }
744− // else {
745− // we have a session
746− // }
747832 }
748833
749− // switch m.state {
750− // case uiChat:
751− // case uiEdit:
752− // binds = append(binds, m.ShortHelp())
753− // }
834+ binds = append(binds,
835+ []key.Binding{
836+ help,
837+ k.Quit,
838+ },
839+ )
754840
755841 return binds
756842}
757843
758844// updateLayoutAndSize updates the layout and sizes of UI components.
759845func (m *UI) updateLayoutAndSize() {
760− m.layout = generateLayout(m, m.width, m.height)
846+ m.layout = m.generateLayout(m.width, m.height)
761847 m.updateSize()
762848}
763849
@@ -786,7 +872,7 @@ func (m *UI) updateSize() {
786872
787873// generateLayout calculates the layout rectangles for all UI components based
788874// on the current UI state and terminal dimensions.
789−func generateLayout(m *UI, w, h int) layout {
875+func (m *UI) generateLayout(w, h int) layout {
790876 // The screen area we're working with
791877 area := image.Rect(0, 0, w, h)
792878