interactive_render.go RawHistoryFolder
1package modes
2
3import (
4 "sync"
5
6 "github.com/bnema/zut/packages/tui"
7)
8
9// renderRequest is deliberately a signal, not a frame. The render owner
10// reads the newest synchronized interactive state when it wakes, so a burst
11// replaces the one pending request instead of queueing stale snapshots.
12type renderRequest struct {
13 clear bool
14 invalidate bool
15 theme *tui.Theme
16}
17
18type latestFrameScheduler struct {
19 wake chan struct{}
20 done chan struct{}
21
22 mu sync.Mutex
23 clear bool
24 invalidate bool
25 theme *tui.Theme
26 stopped bool
27}
28
29func newLatestFrameScheduler() *latestFrameScheduler {
30 return &latestFrameScheduler{
31 wake: make(chan struct{}, 1),
32 done: make(chan struct{}),
33 }
34}
35
36func (s *latestFrameScheduler) request(clear, invalidate bool) bool {
37 s.mu.Lock()
38 defer s.mu.Unlock()
39 if s.stopped {
40 return false
41 }
42 s.clear = s.clear || clear
43 s.invalidate = s.invalidate || invalidate
44 select {
45 case s.wake <- struct{}{}:
46 default:
47 }
48 return true
49}
50
51func (s *latestFrameScheduler) requestTheme(theme tui.Theme) bool {
52 s.mu.Lock()
53 defer s.mu.Unlock()
54 if s.stopped {
55 return false
56 }
57 // Theme changes repaint only the visible frame. Clear() emits 3J and
58 // destroys scrollback, which is never an acceptable side effect of a live
59 // terminal appearance update.
60 s.invalidate = true
61 s.theme = &theme
62 select {
63 case s.wake <- struct{}{}:
64 default:
65 }
66 return true
67}
68
69func (s *latestFrameScheduler) next() (renderRequest, bool) {
70 <-s.wake
71 s.mu.Lock()
72 defer s.mu.Unlock()
73 if s.stopped {
74 return renderRequest{}, false
75 }
76 req := renderRequest{clear: s.clear, invalidate: s.invalidate, theme: s.theme}
77 s.clear = false
78 s.invalidate = false
79 s.theme = nil
80 return req, true
81}
82
83func (s *latestFrameScheduler) run(render func(renderRequest)) {
84 defer close(s.done)
85 for {
86 req, ok := s.next()
87 if !ok {
88 return
89 }
90 render(req)
91 }
92}
93
94// stop waits for the in-flight render callback to finish. Callers must not
95// invoke it while holding i.mu: the callback may need to acquire i.mu.
96func (s *latestFrameScheduler) stop() {
97 s.mu.Lock()
98 if !s.stopped {
99 s.stopped = true
100 close(s.wake)
101 }
102 s.mu.Unlock()
103 <-s.done
104}
105
106func (i *Interactive) requestRendererClear() {
107 if scheduler := i.renderScheduler.Load(); scheduler != nil {
108 scheduler.request(true, false)
109 return
110 }
111 if i.rend != nil {
112 i.rend.Clear()
113 }
114}
115
116func (i *Interactive) requestRendererInvalidate() {
117 if scheduler := i.renderScheduler.Load(); scheduler != nil {
118 scheduler.request(false, true)
119 return
120 }
121 if i.rend != nil {
122 i.rend.Invalidate()
123 }
124}
125
126func (i *Interactive) requestRendererTheme(theme tui.Theme) {
127 if scheduler := i.renderScheduler.Load(); scheduler != nil {
128 scheduler.requestTheme(theme)
129 return
130 }
131 if i.rend != nil {
132 i.rend.SetTheme(theme)
133 }
134}
135
136func (i *Interactive) invalidate() {
137 i.renderRevision.Add(1)
138 // Keep ordinary state changes on the main-loop throttle. The scheduler
139 // owns terminal output, but scheduling it here would bypass
140 // redrawMinInterval and paint every intermediate tool-event frame.
141 select {
142 case i.dirty <- struct{}{}:
143 default:
144 }
145}