From 9e98b3d2f3316c96a18d4d949e319448d048be3a Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 25 Aug 2026 09:17:00 +0200 Subject: [PATCH] Let an editable view opt into receiving printable keys as keybindings Printable keys are withheld from keybindings while the user is typing in a field, so that they end up as text. Decide that from the field that has the focus rather than from the view a binding happens to be registered for: a field can be embedded in another view, and that view's keys must be withheld too, or its bindings would swallow the characters. That makes it worth honouring KeybindOnEdit, which has been documented but ignored ever since it was introduced. A field that sets it sees printable keys offered to the keybindings first, and still gets them if no binding handles them, which is what lets a view keep its keys until the field has something to type into. Co-authored-by: Claude Opus 5 (1M context) --- pkg/gocui/gui.go | 8 +++--- pkg/gocui/parent_view_test.go | 46 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/pkg/gocui/gui.go b/pkg/gocui/gui.go index 15c454c88..1bc184625 100644 --- a/pkg/gocui/gui.go +++ b/pkg/gocui/gui.go @@ -2091,13 +2091,15 @@ func (g *Gui) isSuspended() bool { return g.suspended } -// matchView returns if the keybinding matches the current view (and the view's context) +// matchView returns if the keybinding matches the given view (and the view's context) func (g *Gui) matchView(v *View, kb *keybinding) bool { - // if the user is typing in a field, ignore char keys if v == nil { return false } - if v.Editable && kb.key.IsPrintable() { + // If the user is typing in a field, printable keys are theirs to type, so no + // keybinding gets a look at them: not the field's own, and not those of the + // view it is embedded in either. + if field := g.currentView; field != nil && field.Editable && !field.KeybindOnEdit && kb.key.IsPrintable() { return false } if kb.viewName != v.name { diff --git a/pkg/gocui/parent_view_test.go b/pkg/gocui/parent_view_test.go index 993eb08c1..510acdbbb 100644 --- a/pkg/gocui/parent_view_test.go +++ b/pkg/gocui/parent_view_test.go @@ -56,6 +56,52 @@ func TestFirstMatchingKeybindingOfParentViewWins(t *testing.T) { assert.Equal(t, []string{"first"}, pressed) } +func TestPrintableKeysGoToTheFieldBeingTypedIn(t *testing.T) { + for _, test := range []struct { + name string + keybindOnEdit bool + declineKeybinding bool + expectedPresses int + expectedEdits int + }{ + {name: "the field gets the key", expectedEdits: 1}, + {name: "the parent view gets the key", keybindOnEdit: true, expectedPresses: 1}, + { + name: "the field gets the key the parent view declined", + keybindOnEdit: true, + declineKeybinding: true, + expectedPresses: 1, + expectedEdits: 1, + }, + } { + t.Run(test.name, func(t *testing.T) { + g := newTestGui(t) + parent, child := setupParentAndChildView(t, g) + child.Editable = true + child.KeybindOnEdit = test.keybindOnEdit + + edits := 0 + child.Editor = EditorFunc(func(*View, Key) bool { + edits++ + return true + }) + presses := 0 + g.SetKeybinding(parent.Name(), NewKeyRune('j'), func(*Gui, *View) error { + presses++ + if test.declineKeybinding { + return ErrKeybindingNotHandled + } + return nil + }) + + assert.NoError(t, g.onKey(&GocuiEvent{Type: eventKey, Key: NewKeyRune('j')})) + + assert.Equal(t, test.expectedPresses, presses) + assert.Equal(t, test.expectedEdits, edits) + }) + } +} + func TestUnhandledKeybindingOfParentViewFallsThroughToEditor(t *testing.T) { g := newTestGui(t) parent, child := setupParentAndChildView(t, g)