From acfbc2aaee7fb4abcb86546e816dde9d39b7258a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Feb 2026 21:07:32 +0000 Subject: [PATCH] feat(frame): add focus management fields, Focused(), Focus(), WithKeyMap() --- pkg/cli/frame.go | 48 +++++++++++++++++++++++++++++++++++++++++++ pkg/cli/frame_test.go | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/pkg/cli/frame.go b/pkg/cli/frame.go index a3aaff04..dc13cfd2 100644 --- a/pkg/cli/frame.go +++ b/pkg/cli/frame.go @@ -8,6 +8,7 @@ import ( "sync" "time" + tea "github.com/charmbracelet/bubbletea" "golang.org/x/term" ) @@ -40,6 +41,13 @@ type Frame struct { out io.Writer done chan struct{} mu sync.Mutex + + // Focus management (bubbletea upgrade) + focused Region + keyMap KeyMap + width int + height int + program *tea.Program } // NewFrame creates a new Frame with the given HLCRF variant string. @@ -53,6 +61,10 @@ func NewFrame(variant string) *Frame { models: make(map[Region]Model), out: os.Stdout, done: make(chan struct{}), + focused: RegionContent, + keyMap: DefaultKeyMap(), + width: 80, + height: 24, } } @@ -110,6 +122,42 @@ func (f *Frame) Stop() { } } +// WithKeyMap sets custom key bindings for Frame navigation. +func (f *Frame) WithKeyMap(km KeyMap) *Frame { + f.keyMap = km + return f +} + +// Focused returns the currently focused region. +func (f *Frame) Focused() Region { + f.mu.Lock() + defer f.mu.Unlock() + return f.focused +} + +// Focus sets focus to a specific region. +// Ignores the request if the region is not in this Frame's variant. +func (f *Frame) Focus(r Region) { + f.mu.Lock() + defer f.mu.Unlock() + if _, exists := f.layout.regions[r]; exists { + f.focused = r + } +} + +// buildFocusRing returns the ordered list of regions in this Frame's variant. +// Order follows HLCRF convention. +func (f *Frame) buildFocusRing() []Region { + order := []Region{RegionHeader, RegionLeft, RegionContent, RegionRight, RegionFooter} + var ring []Region + for _, r := range order { + if _, exists := f.layout.regions[r]; exists { + ring = append(ring, r) + } + } + return ring +} + // Run renders the frame and blocks. In TTY mode, it live-refreshes at ~12fps. // In non-TTY mode, it renders once and returns immediately. func (f *Frame) Run() { diff --git a/pkg/cli/frame_test.go b/pkg/cli/frame_test.go index 902cc12b..3173ab4e 100644 --- a/pkg/cli/frame_test.go +++ b/pkg/cli/frame_test.go @@ -261,6 +261,44 @@ func TestKeyMap_Good(t *testing.T) { }) } +func TestFrameFocus_Good(t *testing.T) { + t.Run("default focus is Content", func(t *testing.T) { + f := NewFrame("HCF") + assert.Equal(t, RegionContent, f.Focused()) + }) + + t.Run("Focus sets focused region", func(t *testing.T) { + f := NewFrame("HCF") + f.Focus(RegionHeader) + assert.Equal(t, RegionHeader, f.Focused()) + }) + + t.Run("Focus ignores invalid region", func(t *testing.T) { + f := NewFrame("HCF") + f.Focus(RegionLeft) // Left not in "HCF" + assert.Equal(t, RegionContent, f.Focused()) // unchanged + }) + + t.Run("WithKeyMap returns frame for chaining", func(t *testing.T) { + km := DefaultKeyMap() + km.Quit = tea.KeyCtrlQ + f := NewFrame("HCF").WithKeyMap(km) + assert.Equal(t, tea.KeyCtrlQ, f.keyMap.Quit) + }) + + t.Run("focusRing builds from variant", func(t *testing.T) { + f := NewFrame("HLCRF") + ring := f.buildFocusRing() + assert.Equal(t, []Region{RegionHeader, RegionLeft, RegionContent, RegionRight, RegionFooter}, ring) + }) + + t.Run("focusRing respects variant order", func(t *testing.T) { + f := NewFrame("HCF") + ring := f.buildFocusRing() + assert.Equal(t, []Region{RegionHeader, RegionContent, RegionFooter}, ring) + }) +} + // indexOf returns the position of substr in s, or -1 if not found. func indexOf(s, substr string) int { for i := range len(s) - len(substr) + 1 {