feat(frame): replace raw ANSI runLive with tea.Program

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-02-22 21:16:48 +00:00
parent 331bcd564d
commit 1c6e910251
No known key found for this signature in database
GPG key ID: AF404715446AEB41
2 changed files with 34 additions and 51 deletions

View file

@ -116,6 +116,10 @@ func (f *Frame) Back() bool {
// Stop signals the Frame to exit its Run loop.
func (f *Frame) Stop() {
if f.program != nil {
f.program.Quit()
return
}
select {
case <-f.done:
default:
@ -123,6 +127,14 @@ func (f *Frame) Stop() {
}
}
// Send injects a message into the Frame's tea.Program.
// Safe to call before Run() (message is discarded).
func (f *Frame) Send(msg tea.Msg) {
if f.program != nil {
f.program.Send(msg)
}
}
// WithKeyMap sets custom key bindings for Frame navigation.
func (f *Frame) WithKeyMap(km KeyMap) *Frame {
f.keyMap = km
@ -482,60 +494,18 @@ func (f *Frame) regionSize(r Region, totalW, totalH int) (int, int) {
}
func (f *Frame) runLive() {
// Enter alt-screen.
fmt.Fprint(f.out, "\033[?1049h")
// Hide cursor.
fmt.Fprint(f.out, "\033[?25l")
defer func() {
// Show cursor.
fmt.Fprint(f.out, "\033[?25h")
// Leave alt-screen.
fmt.Fprint(f.out, "\033[?1049l")
}()
ticker := time.NewTicker(80 * time.Millisecond)
defer ticker.Stop()
for {
f.renderFrame()
select {
case <-f.done:
return
case <-ticker.C:
}
opts := []tea.ProgramOption{
tea.WithAltScreen(),
}
if f.out != os.Stdout {
opts = append(opts, tea.WithOutput(f.out))
}
}
func (f *Frame) renderFrame() {
f.mu.Lock()
defer f.mu.Unlock()
p := tea.NewProgram(f, opts...)
f.program = p
w, h := f.termSize()
// Move to top-left.
fmt.Fprint(f.out, "\033[H")
// Clear screen.
fmt.Fprint(f.out, "\033[2J")
order := []Region{RegionHeader, RegionLeft, RegionContent, RegionRight, RegionFooter}
for _, r := range order {
if _, exists := f.layout.regions[r]; !exists {
continue
}
m, ok := f.models[r]
if !ok {
continue
}
rw, rh := f.regionSize(r, w, h)
view := m.View(rw, rh)
if view != "" {
fmt.Fprint(f.out, view)
if !strings.HasSuffix(view, "\n") {
fmt.Fprintln(f.out)
}
}
if _, err := p.Run(); err != nil {
Error(err.Error())
}
}

View file

@ -426,6 +426,19 @@ func TestFrameTeaModel_Good(t *testing.T) {
})
}
func TestFrameSend_Good(t *testing.T) {
t.Run("Send is safe before Run", func(t *testing.T) {
f := NewFrame("C")
f.out = &bytes.Buffer{}
f.Content(StaticModel("x"))
// Should not panic when program is nil
assert.NotPanics(t, func() {
f.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'x'}})
})
})
}
// 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 {