From 28c5f5748ce68dac246cfb21bf720a68a07a42a9 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 25 Aug 2026 09:16:12 +0200 Subject: [PATCH] Give a parent view's keybindings the same precedence as a view's own When a key matches several bindings of the same view, the first one wins; when it matches several of the view's parent, the last one did. Co-authored-by: Claude Opus 5 (1M context) --- pkg/gocui/gui.go | 2 +- pkg/gocui/parent_view_test.go | 76 +++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 pkg/gocui/parent_view_test.go diff --git a/pkg/gocui/gui.go b/pkg/gocui/gui.go index ba4c7e74e..15c454c88 100644 --- a/pkg/gocui/gui.go +++ b/pkg/gocui/gui.go @@ -1979,7 +1979,7 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error { matchingParentViewKb = nil break } - if v != nil && g.matchView(v.ParentView, kb) { + if matchingParentViewKb == nil && v != nil && g.matchView(v.ParentView, kb) { matchingParentViewKb = kb } if globalKb == nil && kb.viewName == "" { diff --git a/pkg/gocui/parent_view_test.go b/pkg/gocui/parent_view_test.go new file mode 100644 index 000000000..993eb08c1 --- /dev/null +++ b/pkg/gocui/parent_view_test.go @@ -0,0 +1,76 @@ +package gocui + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// A view and its parent view, with the child holding the focus. +func setupParentAndChildView(t *testing.T, g *Gui) (*View, *View) { + t.Helper() + + parent, _ := g.SetView("parent", 0, 0, 20, 10, 0) + child, _ := g.SetView("child", 0, 10, 20, 12, 0) + child.ParentView = parent + _, err := g.SetCurrentView(child.Name()) + assert.NoError(t, err) + + return parent, child +} + +func TestKeybindingOfParentViewIsUsedWhenChildHasNone(t *testing.T) { + g := newTestGui(t) + parent, child := setupParentAndChildView(t, g) + + pressed := []string{} + g.SetKeybinding(parent.Name(), NewKeyName(KeyArrowDown), func(*Gui, *View) error { + pressed = append(pressed, "parent") + return nil + }) + g.SetKeybinding(child.Name(), NewKeyName(KeyEnter), func(*Gui, *View) error { + pressed = append(pressed, "child") + return nil + }) + + assert.NoError(t, g.onKey(&GocuiEvent{Type: eventKey, Key: NewKeyName(KeyArrowDown)})) + assert.NoError(t, g.onKey(&GocuiEvent{Type: eventKey, Key: NewKeyName(KeyEnter)})) + + assert.Equal(t, []string{"parent", "child"}, pressed) +} + +func TestFirstMatchingKeybindingOfParentViewWins(t *testing.T) { + g := newTestGui(t) + parent, _ := setupParentAndChildView(t, g) + + pressed := []string{} + for _, name := range []string{"first", "second"} { + g.SetKeybinding(parent.Name(), NewKeyName(KeyArrowDown), func(*Gui, *View) error { + pressed = append(pressed, name) + return nil + }) + } + + assert.NoError(t, g.onKey(&GocuiEvent{Type: eventKey, Key: NewKeyName(KeyArrowDown)})) + + assert.Equal(t, []string{"first"}, pressed) +} + +func TestUnhandledKeybindingOfParentViewFallsThroughToEditor(t *testing.T) { + g := newTestGui(t) + parent, child := setupParentAndChildView(t, g) + + edited := []Key{} + child.Editable = true + child.Editor = EditorFunc(func(_ *View, key Key) bool { + edited = append(edited, key) + return true + }) + g.SetKeybinding(parent.Name(), NewKeyName(KeyArrowDown), func(*Gui, *View) error { + return ErrKeybindingNotHandled + }) + + assert.NoError(t, g.onKey(&GocuiEvent{Type: eventKey, Key: NewKeyName(KeyArrowDown)})) + + assert.Equal(t, []Key{NewKeyName(KeyArrowDown)}, edited) +}