feat(frame): add focus management fields, Focused(), Focus(), WithKeyMap()

This commit is contained in:
Claude 2026-02-22 21:07:32 +00:00
parent 02e8343ee5
commit acfbc2aaee
No known key found for this signature in database
GPG key ID: AF404715446AEB41
2 changed files with 86 additions and 0 deletions

View file

@ -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() {

View file

@ -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 {