1package ansi
2
3import (
4 "bytes"
5 "strconv"
6 "sync"
7)
8
9const maxPooledBufferCap = 1 << 20
10
11var bufferPool = sync.Pool{
12 New: func() any { return new(bytes.Buffer) },
13}
14
15// Capabilities describes the fixed output features of a Renderer target.
16type Capabilities struct {
17 SynchronizedOutput bool
18}
19
20type Renderer struct {
21 caps Capabilities
22 colorProfile ColorProfile
23 width int
24 height int
25 hasCommitted bool
26 committed Frame
27 // scratch receives the snapshot of the frame being prepared. It never
28 // aliases committed or caller storage: a snapshot commit swaps the two so
29 // both buffers are reused across draws without allocating.
30 scratch Frame
31 // generation identifies the latest prepared draw that owns scratch.
32 generation uint64
33}
34
35// PreparedDraw owns encoded output and its transactional delta until Commit.
36// It is returned by value so ordinary prepared draws do not allocate.
37type PreparedDraw struct {
38 renderer *Renderer
39 candidate DeltaCandidate
40 data []byte
41 generation uint64
42 commitOnce *sync.Once
43}
44
45func New(caps Capabilities) *Renderer {
46 return NewWithColorProfile(caps, ColorProfileTrueColor)
47}
48
49// NewWithColorProfile constructs a renderer for a target color profile.
50func NewWithColorProfile(caps Capabilities, profile ColorProfile) *Renderer {
51 return &Renderer{caps: caps, colorProfile: profile}
52}
53
54// Reset forgets the committed shadow and invalidates every outstanding
55// prepared draw. The private frame buffers are kept for reuse.
56func (r *Renderer) Reset() {
57 r.width = 0
58 r.height = 0
59 r.hasCommitted = false
60 r.generation++
61}
62
63// Bytes returns the prepared ANSI output. The returned bytes remain valid after
64// another draw.
65func (p PreparedDraw) Bytes() []byte { return p.data }
66
67// Commit applies the prepared delta exactly once. Discarding it leaves the
68// renderer's committed state unchanged.
69//
70// Committing a draw that a later Prepare has superseded does not apply it:
71// the renderer instead drops its committed state, so the next Prepare
72// re-emits a full snapshot instead of diffing against a shadow that may no
73// longer match the terminal. Draws without output are unaffected.
74func (p *PreparedDraw) Commit() {
75 if p == nil || p.renderer == nil || p.commitOnce == nil {
76 return
77 }
78 p.commitOnce.Do(func() {
79 r := p.renderer
80 plan := p.candidate.Plan
81 if !plan.Snapshot && plan.Scroll.Height == 0 && len(plan.Spans) == 0 {
82 return
83 }
84 if p.generation != r.generation {
85 // A later Prepare or Reset reused the scratch frame this draw's
86 // snapshot lives in, so it cannot be applied. Its bytes may
87 // nevertheless have reached the terminal, which would then differ
88 // from the committed shadow in unknown ways: forget the shadow and
89 // invalidate every outstanding draw, so even a delta prepared
90 // after this one cannot re-establish a shadow on commit and the
91 // next Prepare emits a full snapshot.
92 r.hasCommitted = false
93 r.generation++
94 return
95 }
96 if !plan.Snapshot && !r.hasCommitted {
97 // Unreachable for current draws: Prepare plans a snapshot whenever
98 // no shadow exists, and every path that drops the shadow also
99 // advances the generation. Keep the shadow dropped defensively.
100 return
101 }
102 if plan.Snapshot {
103 // The snapshot lives in the renderer's scratch buffer; promote it
104 // and recycle the previous committed buffer as the next scratch.
105 r.committed, r.scratch = p.candidate.frame, r.committed
106 } else {
107 p.candidate.Commit(&r.committed)
108 }
109 r.width = r.committed.Width
110 r.height = r.committed.Height
111 r.hasCommitted = true
112 })
113}
114
115// Prepare plans and encodes a transactional draw. The renderer advances only
116// when the returned draw is committed. Keep at most one prepared draw
117// outstanding; commit or discard it before calling Prepare again. A draw
118// superseded by a later Prepare is not applied: committing it forces the next
119// Prepare to emit a full snapshot.
120//
121// The renderer copies frame into reusable private storage, so the caller may
122// keep mutating frame between draws.
123func (r *Renderer) Prepare(frame CellSource, damage []Damage, reset bool) (PreparedDraw, error) {
124 var candidate DeltaCandidate
125 var err error
126 if err = validateCellSource(frame); err != nil {
127 return PreparedDraw{}, err
128 }
129 // Any earlier prepared draw loses its claim on the scratch snapshot, even
130 // if this Prepare fails after partially rewriting it.
131 r.generation++
132 columns, rows := frame.Columns(), frame.Rows()
133 if !reset && r.hasCommitted && r.width == columns && r.height == rows && len(damage) == 1 && (damage[0].Kind == DamageText || damage[0].Kind == DamageClear) {
134 plan := planSingleDamage(frame, damage[0])
135 candidate = newDeltaCandidate(frame, plan, &r.scratch)
136 } else {
137 candidate, err = planDelta(frame, damage, r.committed, reset || !r.hasCommitted, &r.scratch)
138 }
139 if err != nil {
140 return PreparedDraw{}, err
141 }
142 prepared := PreparedDraw{renderer: r, candidate: candidate, generation: r.generation, commitOnce: new(sync.Once)}
143 plan := candidate.Plan
144 if !plan.Snapshot && plan.Scroll.Height == 0 && len(plan.Spans) == 0 {
145 return prepared, nil
146 }
147
148 buf, ok := bufferPool.Get().(*bytes.Buffer)
149 if !ok {
150 buf = new(bytes.Buffer)
151 }
152 buf.Reset()
153 defer putBuffer(buf)
154 if r.caps.SynchronizedOutput {
155 buf.WriteString(SyncStartCSI)
156 }
157
158 st := newDrawStateForProfile(r.colorProfile)
159 if plan.Snapshot {
160 if len(plan.Spans) > 0 {
161 r.emitDamageSpans(buf, frame, plan.Spans, &st)
162 } else {
163 r.writeFull(buf, frame, &st)
164 }
165 } else {
166 if plan.Scroll.Height != 0 {
167 scroll := plan.Scroll
168 kind := DamageScrollUp
169 if scroll.Down {
170 kind = DamageScrollDown
171 }
172 emitScroll(buf, Damage{Kind: kind, X: 0, Y: scroll.Y, Width: columns, Height: scroll.Height, Count: scroll.Count})
173 }
174 for _, span := range plan.Spans {
175 r.emitSpan(buf, frame, span.Y, span.X, span.Width, &st)
176 }
177 buf.WriteString("\x1b[0m")
178 }
179 if r.caps.SynchronizedOutput {
180 buf.WriteString(SyncEndCSI)
181 }
182 prepared.data = copyBytes(buf)
183 return prepared, nil
184}
185
186func (r *Renderer) Draw(frame CellSource, damage []Damage) ([]byte, error) {
187 prepared, err := r.Prepare(frame, damage, false)
188 if err != nil {
189 return nil, err
190 }
191 prepared.Commit()
192 return prepared.Bytes(), nil
193}
194
195// copyBytes copies the buffer contents into a fresh byte slice and is used
196// to return output that is independent of the pooled scratch buffer.
197func copyBytes(buf *bytes.Buffer) []byte {
198 b := buf.Bytes()
199 out := make([]byte, len(b))
200 copy(out, b)
201 return out
202}
203
204func putBuffer(buf *bytes.Buffer) {
205 if buf.Cap() > maxPooledBufferCap {
206 return
207 }
208 bufferPool.Put(buf)
209}
210
211func needsFull(damage []Damage) bool {
212 if len(damage) == 0 {
213 return false
214 }
215 for _, d := range damage {
216 if d.Kind == DamageFullRedraw {
217 return true
218 }
219 }
220 return false
221}
222
223func hasScrollDamage(damage []Damage) bool {
224 for _, d := range damage {
225 if d.Kind == DamageScrollUp || d.Kind == DamageScrollDown {
226 return true
227 }
228 }
229 return false
230}
231
232func (r *Renderer) writeFull(out *bytes.Buffer, frame CellSource, st *drawState) {
233 columns := frame.Columns()
234 for y := range frame.Rows() {
235 r.emitSpan(out, frame, y, 0, columns, st)
236 }
237 out.WriteString("\x1b[0m")
238}
239
240func (r *Renderer) emitDamageSpans(out *bytes.Buffer, frame CellSource, spans []Span, st *drawState) {
241 for _, span := range spans {
242 r.emitSpan(out, frame, span.Y, span.X, span.Width, st)
243 }
244 out.WriteString("\x1b[0m")
245}
246func clampRect(frame CellSource, x, y, width, height int) (int, int, int, int, bool) {
247 x, width, okX := clampRange(x, width, frame.Columns())
248 y, height, okY := clampRange(y, height, frame.Rows())
249 if !okX || !okY {
250 return 0, 0, 0, 0, false
251 }
252 return x, y, width, height, true
253}
254
255// clampRange intersects [pos, pos+size) with [0, limit) without evaluating an
256// overflowing endpoint from untrusted damage coordinates.
257func clampRange(pos, size, limit int) (int, int, bool) {
258 if size <= 0 || limit <= 0 || pos >= limit {
259 return 0, 0, false
260 }
261 if pos < 0 {
262 // -size is safe because size is positive. If pos is at or before that
263 // point, the rectangle ends at or before zero.
264 if pos <= -size {
265 return 0, 0, false
266 }
267 end := pos + size // pos > -size proves this addition cannot overflow.
268 if end > limit {
269 end = limit
270 }
271 return 0, end, true
272 }
273
274 available := limit - pos
275 return pos, min(size, available), true
276}
277
278// writeCursor emits a cursor-positioning CSI sequence without fmt.Fprintf
279// allocations. It uses a stack-allocated buffer for integer formatting.
280func writeCursor(out *bytes.Buffer, y, x int) {
281 out.WriteString("\x1b[")
282 var b [16]byte
283 n := strconv.AppendInt(b[:0], int64(y+1), 10)
284 out.Write(n)
285 out.WriteByte(';')
286 n = strconv.AppendInt(b[:0], int64(x+1), 10)
287 out.Write(n)
288 out.WriteByte('H')
289}
290
291func sameDamage(a, b Damage) bool {
292 return a.Kind == b.Kind && a.X == b.X && a.Y == b.Y && a.Width == b.Width && a.Height == b.Height && a.Count == b.Count
293}