Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
6.2 kB · 212 lines
at acp-stdio
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213bubbletea v2.0.10 with demand-driven painting.
upstream paints from a time.Ticker at the program's fps for as long as theprogram runs, so an idle TUI still wakes 60 times a second to flush a framethat did not change. here the frame writer sleeps until render() orexecute() asks for a frame, then paints at most once per frame interval;the fps cap is unchanged while frames keep coming. a frame taller than thescreen keeps only the rows on screen, instead of a slice that holds thetallest frame ever drawn alive.
a command run with Exec hands its result to the model as the next message,before any input read once the terminal is back. upstream sends it from agoroutine, so a key typed as an editor closes could reach the model first andact on the state from before the edit.
to bump bubbletea: copy the new release over third_party/bubbletea andapply this patch (patch -p1 -d third_party/bubbletea < third_party/bubbletea.patch).
--- a/cursed_renderer.go+++ b/cursed_renderer.go@@ -345,9 +345,13 @@ content.Draw(s.cellbuf, s.cellbuf.Bounds()) // If the frame height is greater than the screen height, we drop the- // lines from the top of the buffer.+ // lines from the top of the buffer. The kept lines are copied: a slice of+ // the tall frame would keep every line of it alive, so the buffer would+ // hold the tallest frame it ever drew for as long as the program runs. if frameHeight := frameArea.Dy(); frameHeight > s.height {- s.cellbuf.Lines = s.cellbuf.Lines[frameHeight-s.height:]+ kept := make([]uv.Line, s.height)+ copy(kept, s.cellbuf.Lines[frameHeight-s.height:])+ s.cellbuf.Lines = kept } // Alt screen mode.--- a/tea.go+++ b/tea.go@@ -539,8 +539,9 @@ // modes keeps track of terminal modes that have been enabled or disabled. ignoreSignals uint32 - // ticker is the ticker that will be used to write to the renderer.- ticker *time.Ticker+ // frames wakes the frame writer when there is something to paint. It+ // replaces upstream's always-on ticker, so an idle program never wakes.+ frames chan struct{} // once is used to stop the renderer. once sync.Once@@ -606,6 +607,7 @@ msgs: make(chan Msg), errs: make(chan error, 1), rendererDone: make(chan struct{}),+ frames: make(chan struct{}, 1), } // Apply all options to the program.@@ -895,6 +897,16 @@ if p.renderer != nil { p.renderer.render(model.View()) // send view to renderer }+ p.requestFrame()+}++// requestFrame asks the frame writer to paint. A pending request already+// covers this one.+func (p *Program) requestFrame() {+ select {+ case p.frames <- struct{}{}:+ default:+ } } func (p *Program) execSequenceMsg(msg sequenceMsg) {@@ -1224,6 +1236,7 @@ p.mu.Lock() _, _ = p.outputBuf.WriteString(seq) p.mu.Unlock()+ p.requestFrame() } // executeQuery writes a terminal query to the program output. Queries expect a@@ -1414,16 +1427,10 @@ } } -// startRenderer starts the renderer.+// startRenderer starts the renderer. It paints when asked to, at most once+// per frame, instead of on a fixed-rate ticker. func (p *Program) startRenderer() { framerate := time.Second / time.Duration(p.fps)- if p.ticker == nil {- p.ticker = time.NewTicker(framerate)- } else {- // If the ticker already exists, it has been stopped and we need to- // reset it.- p.ticker.Reset(framerate)- } // Since the renderer can be restarted after a stop, we need to reset // the done channel and its corresponding sync.Once.@@ -1431,17 +1438,28 @@ // Start the renderer. p.renderer.start()+ p.requestFrame() // paint whatever was rendered while stopped go func() {+ pace := time.NewTimer(framerate)+ pace.Stop()+ var last time.Time for { select { case <-p.rendererDone:- p.ticker.Stop() return-- case <-p.ticker.C:- _ = p.flush()- _ = p.renderer.flush(false)+ case <-p.frames:+ }+ if wait := framerate - time.Since(last); wait > 0 {+ pace.Reset(wait)+ select {+ case <-p.rendererDone:+ return+ case <-pace.C:+ } }+ _ = p.flush()+ _ = p.renderer.flush(false)+ last = time.Now() } }() }--- a/exec.go+++ b/exec.go@@ -98,14 +98,20 @@ } } -// exec runs an ExecCommand and delivers the results to the program as a Msg.-func (p *Program) exec(c ExecCommand, fn ExecCallback) {+// exec runs an ExecCommand and returns its callback's Msg, or nil without a+// callback. The event loop handles that Msg before any input read after the+// terminal comes back: sent through the queue instead, a key typed right after+// the command could reach the model ahead of the command's result.+func (p *Program) exec(c ExecCommand, fn ExecCallback) Msg {+ result := func(err error) Msg {+ if fn == nil {+ return nil+ }+ return fn(err)+ } if err := p.releaseTerminal(false); err != nil { // If we can't release input, abort.- if fn != nil {- go p.Send(fn(err))- }- return+ return result(err) } c.SetStdin(p.input)@@ -115,15 +121,9 @@ // Execute system command. if err := c.Run(); err != nil { _ = p.RestoreTerminal() // also try to restore the terminal.- if fn != nil {- go p.Send(fn(err))- }- return+ return result(err) } // Have the program re-capture input.- err := p.RestoreTerminal()- if fn != nil {- go p.Send(fn(err))- }+ return result(p.RestoreTerminal()) }--- a/tea.go+++ b/tea.go@@ -762,6 +762,15 @@ case msg := <-p.msgs: msg = p.translateInputEvent(msg) + // A command that takes the terminal runs here (NB: this blocks),+ // and its result is handled as this message, so input read once+ // the terminal is back cannot overtake it.+ if exec, ok := msg.(execMsg); ok {+ if result := p.exec(exec.cmd, exec.fn); result != nil {+ msg = result+ }+ }+ // Filter messages. if p.filter != nil { msg = p.filter(model, msg)@@ -837,10 +846,6 @@ case cursorColorMsg: p.executeQuery(ansi.RequestCursorColor)-- case execMsg:- // NB: this blocks.- p.exec(msg.cmd, msg.fn) case terminalVersion: p.executeQuery(ansi.RequestNameVersion)