Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui albedo.sh
Something went wrong. Try again.
3.9 kB · 131 lines
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132bubbletea 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.
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() } }() }