on main help sign in
fix(ui): improve key handling and keybindings for chat/editor
1bb5aa8Ayman Bagabas
internal/ui/model/keys.go+35−15
@@ -18,21 +18,25 @@ type KeyMap struct {
18 }
18 }
19
19
20 Chat struct {
20 Chat struct {
21− NewSession key.Binding
21+ NewSession key.Binding
22− AddAttachment key.Binding
22+ AddAttachment key.Binding
23− Cancel key.Binding
23+ Cancel key.Binding
24− Tab key.Binding
24+ Tab key.Binding
25− Details key.Binding
25+ Details key.Binding
26− Down key.Binding
26+ Down key.Binding
27+ Up key.Binding
27− Up key.Binding
28+ UpDown key.Binding
28− DownOneItem key.Binding
29+ DownOneItem key.Binding
29− UpOneItem key.Binding
30+ UpOneItem key.Binding
31+ UpDownOneItem key.Binding
30− PageDown key.Binding
32+ PageDown key.Binding
31− PageUp key.Binding
33+ PageUp key.Binding
32− HalfPageDown key.Binding
34+ HalfPageDown key.Binding
33− HalfPageUp key.Binding
35+ HalfPageUp key.Binding
34− Home key.Binding
36+ Home key.Binding
37+ End key.Binding
35− End key.Binding
38+ Copy key.Binding
39+ ClearHighlight key.Binding
36 }
40 }
37
41
38 Initialize struct {
42 Initialize struct {
@@ -153,6 +157,10 @@ func DefaultKeyMap() KeyMap {
153 key.WithKeys("up", "ctrl+k", "ctrl+p", "k"),
157 key.WithKeys("up", "ctrl+k", "ctrl+p", "k"),
154 key.WithHelp("↑", "up"),
158 key.WithHelp("↑", "up"),
155 )
159 )
160+ km.Chat.UpDown = key.NewBinding(
161+ key.WithKeys("up", "down"),
162+ key.WithHelp("↑↓", "scroll"),
163+ )
156 km.Chat.UpOneItem = key.NewBinding(
164 km.Chat.UpOneItem = key.NewBinding(
157 key.WithKeys("shift+up", "K"),
165 key.WithKeys("shift+up", "K"),
158 key.WithHelp("shift+↑", "up one item"),
166 key.WithHelp("shift+↑", "up one item"),
@@ -161,6 +169,10 @@ func DefaultKeyMap() KeyMap {
161 key.WithKeys("shift+down", "J"),
169 key.WithKeys("shift+down", "J"),
162 key.WithHelp("shift+↓", "down one item"),
170 key.WithHelp("shift+↓", "down one item"),
163 )
171 )
172+ km.Chat.UpDownOneItem = key.NewBinding(
173+ key.WithKeys("shift+up", "shift+down"),
174+ key.WithHelp("shift+↑↓", "scroll one item"),
175+ )
164 km.Chat.HalfPageDown = key.NewBinding(
176 km.Chat.HalfPageDown = key.NewBinding(
165 key.WithKeys("d"),
177 key.WithKeys("d"),
166 key.WithHelp("d", "half page down"),
178 key.WithHelp("d", "half page down"),
@@ -185,6 +197,14 @@ func DefaultKeyMap() KeyMap {
185 key.WithKeys("G", "end"),
197 key.WithKeys("G", "end"),
186 key.WithHelp("G", "end"),
198 key.WithHelp("G", "end"),
187 )
199 )
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+ )
188
208
189 km.Initialize.Yes = key.NewBinding(
209 km.Initialize.Yes = key.NewBinding(
190 key.WithKeys("y", "Y"),
210 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) {
313 }
313 }
314 }
314 }
315 case tea.KeyPressMsg:
315 case tea.KeyPressMsg:
316+ if cmd := m.handleKeyPressMsg(msg); cmd != nil {
316− cmds = append(cmds, m.handleKeyPressMsg(msg)...)
317+ cmds = append(cmds, cmd)
318+ }
317 case tea.PasteMsg:
319 case tea.PasteMsg:
318 if cmd := m.handlePasteMsg(msg); cmd != nil {
320 if cmd := m.handlePasteMsg(msg); cmd != nil {
319 cmds = append(cmds, cmd)
321 cmds = append(cmds, cmd)
@@ -341,7 +343,9 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
341 return m, tea.Batch(cmds...)
343 return m, tea.Batch(cmds...)
342}
344}
343
345
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+
345 handleQuitKeys := func(msg tea.KeyPressMsg) bool {
349 handleQuitKeys := func(msg tea.KeyPressMsg) bool {
346 switch {
350 switch {
347 case key.Matches(msg, m.keyMap.Quit):
351 case key.Matches(msg, m.keyMap.Quit):
@@ -369,6 +373,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
369 return true
373 return true
370 case key.Matches(msg, m.keyMap.Models):
374 case key.Matches(msg, m.keyMap.Models):
371 // TODO: Implement me
375 // TODO: Implement me
376+ return true
372 case key.Matches(msg, m.keyMap.Sessions):
377 case key.Matches(msg, m.keyMap.Sessions):
373 if m.dialog.ContainsDialog(dialog.SessionsID) {
378 if m.dialog.ContainsDialog(dialog.SessionsID) {
374 // Bring to front
379 // Bring to front
@@ -385,12 +390,12 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
385 if m.dialog.HasDialogs() {
390 if m.dialog.HasDialogs() {
386 // Always handle quit keys first
391 // Always handle quit keys first
387 if handleQuitKeys(msg) {
392 if handleQuitKeys(msg) {
388− return cmds
393+ return tea.Batch(cmds...)
389 }
394 }
390
395
391 msg := m.dialog.Update(msg)
396 msg := m.dialog.Update(msg)
392 if msg == nil {
397 if msg == nil {
393− return cmds
398+ return tea.Batch(cmds...)
394 }
399 }
395
400
396 switch msg := msg.(type) {
401 switch msg := msg.(type) {
@@ -424,18 +429,21 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
424 cmds = append(cmds, tea.Quit)
429 cmds = append(cmds, tea.Quit)
425 }
430 }
426
431
427− return cmds
432+ return tea.Batch(cmds...)
428 }
433 }
429
434
430 switch m.state {
435 switch m.state {
431 case uiConfigure:
436 case uiConfigure:
432− return cmds
437+ return tea.Batch(cmds...)
433 case uiInitialize:
438 case uiInitialize:
434− return append(cmds, m.updateInitializeView(msg)...)
439+ cmds = append(cmds, m.updateInitializeView(msg)...)
440+ return tea.Batch(cmds...)
435 case uiChat, uiLanding, uiChatCompact:
441 case uiChat, uiLanding, uiChatCompact:
436 switch m.focus {
442 switch m.focus {
437 case uiFocusEditor:
443 case uiFocusEditor:
438 switch {
444 switch {
445+ case key.Matches(msg, m.keyMap.Editor.SendMessage):
446+ // TODO: Implement me
439 case key.Matches(msg, m.keyMap.Tab):
447 case key.Matches(msg, m.keyMap.Tab):
440 m.focus = uiFocusMain
448 m.focus = uiFocusMain
441 m.textarea.Blur()
449 m.textarea.Blur()
@@ -447,6 +455,8 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
447 break
455 break
448 }
456 }
449 cmds = append(cmds, m.openEditor(m.textarea.Value()))
457 cmds = append(cmds, m.openEditor(m.textarea.Value()))
458+ case key.Matches(msg, m.keyMap.Editor.Newline):
459+ m.textarea.InsertRune('\n')
450 default:
460 default:
451 if handleGlobalKeys(msg) {
461 if handleGlobalKeys(msg) {
452 // Handle global keys first before passing to textarea.
462 // Handle global keys first before passing to textarea.
@@ -509,12 +519,12 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
509 handleGlobalKeys(msg)
519 handleGlobalKeys(msg)
510 }
520 }
511
521
512− return cmds
522+ return tea.Batch(cmds...)
513}
523}
514
524
515// Draw implements [tea.Layer] and draws the UI model.
525// Draw implements [tea.Layer] and draws the UI model.
516func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) {
526func (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())
518
528
519 if m.layout != layout {
529 if m.layout != layout {
520 m.layout = layout
530 m.layout = layout
@@ -665,46 +675,58 @@ func (m *UI) View() tea.View {
665func (m *UI) ShortHelp() []key.Binding {
675func (m *UI) ShortHelp() []key.Binding {
666 var binds []key.Binding
676 var binds []key.Binding
667 k := &m.keyMap
677 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+ }
668
683
669 switch m.state {
684 switch m.state {
670 case uiInitialize:
685 case uiInitialize:
671 binds = append(binds, k.Quit)
686 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+ }
672 default:
714 default:
673 // TODO: other states
715 // TODO: other states
674 // if m.session == nil {
716 // if m.session == nil {
675 // no session selected
717 // no session selected
676 binds = append(binds,
718 binds = append(binds,
677− k.Commands,
719+ commands,
678 k.Models,
720 k.Models,
679 k.Editor.Newline,
721 k.Editor.Newline,
680− k.Quit,
681− k.Help,
682 )
722 )
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− // }
706 }
723 }
707
724
725+ binds = append(binds,
726+ k.Quit,
727+ k.Help,
728+ )
729+
708 return binds
730 return binds
709}
731}
710
732
@@ -714,6 +736,12 @@ func (m *UI) FullHelp() [][]key.Binding {
714 k := &m.keyMap
736 k := &m.keyMap
715 help := k.Help
737 help := k.Help
716 help.SetHelp("ctrl+g", "less")
738 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+ }
717
745
718 switch m.state {
746 switch m.state {
719 case uiInitialize:
747 case uiInitialize:
@@ -721,12 +749,72 @@ func (m *UI) FullHelp() [][]key.Binding {
721 []key.Binding{
749 []key.Binding{
722 k.Quit,
750 k.Quit,
723 })
751 })
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+ }
724 default:
812 default:
725 if m.session == nil {
813 if m.session == nil {
726 // no session selected
814 // no session selected
727 binds = append(binds,
815 binds = append(binds,
728 []key.Binding{
816 []key.Binding{
729− k.Commands,
817+ commands,
730 k.Models,
818 k.Models,
731 k.Sessions,
819 k.Sessions,
732 },
820 },
@@ -741,23 +829,21 @@ func (m *UI) FullHelp() [][]key.Binding {
741 },
829 },
742 )
830 )
743 }
831 }
744− // else {
745− // we have a session
746− // }
747 }
832 }
748
833
749− // switch m.state {
750− // case uiChat:
751− // case uiEdit:
752− // binds = append(binds, m.ShortHelp())
834+ binds = append(binds,
835+ []key.Binding{
836+ help,
837+ k.Quit,
753− // }
838+ },
839+ )
754
840
755 return binds
841 return binds
756}
842}
757
843
758// updateLayoutAndSize updates the layout and sizes of UI components.
844// updateLayoutAndSize updates the layout and sizes of UI components.
759func (m *UI) updateLayoutAndSize() {
845func (m *UI) updateLayoutAndSize() {
760− m.layout = generateLayout(m, m.width, m.height)
846+ m.layout = m.generateLayout(m.width, m.height)
761 m.updateSize()
847 m.updateSize()
762}
848}
763
849
@@ -786,7 +872,7 @@ func (m *UI) updateSize() {
786
872
787// generateLayout calculates the layout rectangles for all UI components based
873// generateLayout calculates the layout rectangles for all UI components based
788// on the current UI state and terminal dimensions.
874// 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 {
790 // The screen area we're working with
876 // The screen area we're working with
791 area := image.Rect(0, 0, w, h)
877 area := image.Rect(0, 0, w, h)
792
878