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}
28
29// PreparedDraw owns encoded output and its transactional delta until Commit.
30// It is returned by value so ordinary prepared draws do not allocate.
31type PreparedDraw struct {
32 renderer *Renderer
33 candidate DeltaCandidate
34 data []byte
35 commitOnce *sync.Once
36}
37
38func New(caps Capabilities) *Renderer {
39 return NewWithColorProfile(caps, ColorProfileTrueColor)
40}
41
42// NewWithColorProfile constructs a renderer for a target color profile.
43func NewWithColorProfile(caps Capabilities, profile ColorProfile) *Renderer {
44 return &Renderer{caps: caps, colorProfile: profile}
45}
46
47func (r *Renderer) Reset() {
48 r.width = 0
49 r.height = 0
50 r.hasCommitted = false
51 r.committed = Frame{}
52}
53
54// Bytes returns the prepared ANSI output. The returned bytes remain valid after
55// another draw.
56func (p PreparedDraw) Bytes() []byte { return p.data }
57
58// Commit applies the prepared delta exactly once. Discarding it leaves the
59// renderer's committed state unchanged.
60func (p *PreparedDraw) Commit() {
61 if p == nil || p.renderer == nil || p.commitOnce == nil {
62 return
63 }
64 p.commitOnce.Do(func() {
65 committed := p.renderer.committedFrame()
66 if p.candidate.Plan.Snapshot {
67 // PreparedDraw privately owns this snapshot. Its shared Once makes
68 // transfer safe even when the caller retains copies of the draw.
69 committed = p.candidate.frame
70 } else {
71 p.candidate.Commit(&committed)
72 }
73 p.renderer.setCommittedFrame(committed)
74 })
75}
76
77// Prepare plans and encodes a transactional draw. The renderer advances only
78// when the returned draw is committed. Keep at most one prepared draw
79// outstanding; commit or discard it before calling Prepare again.
80func (r *Renderer) Prepare(frame CellSource, damage []Damage, reset bool) (PreparedDraw, error) {
81 var candidate DeltaCandidate
82 var err error
83 if err = validateCellSource(frame); err != nil {
84 return PreparedDraw{}, err
85 }
86 columns, rows := frame.Columns(), frame.Rows()
87 if !reset && r.hasCommitted && r.width == columns && r.height == rows && len(damage) == 1 && (damage[0].Kind == DamageText || damage[0].Kind == DamageClear) {
88 plan := planSingleDamage(frame, damage[0])
89 candidate = DeltaCandidate{Plan: plan}
90 if plan.Snapshot || plan.Scroll.Height != 0 || len(plan.Spans) != 0 {
91 candidate.frame = cloneCellSource(frame)
92 }
93 } else {
94 candidate, err = PlanDelta(frame, damage, r.committedFrame(), reset || !r.hasCommitted)
95 }
96 if err != nil {
97 return PreparedDraw{}, err
98 }
99 prepared := PreparedDraw{renderer: r, candidate: candidate, commitOnce: new(sync.Once)}
100 plan := candidate.Plan
101 if !plan.Snapshot && plan.Scroll.Height == 0 && len(plan.Spans) == 0 {
102 return prepared, nil
103 }
104
105 buf, ok := bufferPool.Get().(*bytes.Buffer)
106 if !ok {
107 buf = new(bytes.Buffer)
108 }
109 buf.Reset()
110 defer putBuffer(buf)
111 if r.caps.SynchronizedOutput {
112 buf.WriteString(SyncStartCSI)
113 }
114
115 st := newDrawStateForProfile(r.colorProfile)
116 if plan.Snapshot {
117 if len(plan.Spans) > 0 {
118 r.emitDamageSpans(buf, frame, plan.Spans, &st)
119 } else {
120 r.writeFull(buf, frame, &st)
121 }
122 } else {
123 if plan.Scroll.Height != 0 {
124 scroll := plan.Scroll
125 emitScrollUp(buf, Damage{Kind: DamageScrollUp, X: 0, Y: scroll.Y, Width: columns, Height: scroll.Height, Count: scroll.Count})
126 }
127 for _, span := range plan.Spans {
128 r.emitSpan(buf, frame, span.Y, span.X, span.Width, &st)
129 }
130 buf.WriteString("\x1b[0m")
131 }
132 if r.caps.SynchronizedOutput {
133 buf.WriteString(SyncEndCSI)
134 }
135 prepared.data = copyBytes(buf)
136 return prepared, nil
137}
138
139func (r *Renderer) Draw(frame CellSource, damage []Damage) ([]byte, error) {
140 prepared, err := r.Prepare(frame, damage, false)
141 if err != nil {
142 return nil, err
143 }
144 prepared.Commit()
145 return prepared.Bytes(), nil
146}
147
148func (r *Renderer) committedFrame() Frame {
149 return r.committed
150}
151
152func (r *Renderer) setCommittedFrame(frame Frame) {
153 r.width = frame.Width
154 r.height = frame.Height
155 r.hasCommitted = true
156 r.committed = frame
157}
158
159// copyBytes copies the buffer contents into a fresh byte slice and is used
160// to return output that is independent of the pooled scratch buffer.
161func copyBytes(buf *bytes.Buffer) []byte {
162 b := buf.Bytes()
163 out := make([]byte, len(b))
164 copy(out, b)
165 return out
166}
167
168func putBuffer(buf *bytes.Buffer) {
169 if buf.Cap() > maxPooledBufferCap {
170 return
171 }
172 bufferPool.Put(buf)
173}
174
175func needsFull(damage []Damage) bool {
176 if len(damage) == 0 {
177 return false
178 }
179 for _, d := range damage {
180 if d.Kind == DamageFullRedraw {
181 return true
182 }
183 }
184 return false
185}
186
187func hasScrollDamage(damage []Damage) bool {
188 for _, d := range damage {
189 if d.Kind == DamageScrollUp {
190 return true
191 }
192 }
193 return false
194}
195
196func (r *Renderer) writeFull(out *bytes.Buffer, frame CellSource, st *drawState) {
197 columns := frame.Columns()
198 for y := range frame.Rows() {
199 r.emitSpan(out, frame, y, 0, columns, st)
200 }
201 out.WriteString("\x1b[0m")
202}
203
204func (r *Renderer) emitDamageSpans(out *bytes.Buffer, frame CellSource, spans []Span, st *drawState) {
205 for _, span := range spans {
206 r.emitSpan(out, frame, span.Y, span.X, span.Width, st)
207 }
208 out.WriteString("\x1b[0m")
209}
210func clampRect(frame CellSource, x, y, width, height int) (int, int, int, int, bool) {
211 x, width, okX := clampRange(x, width, frame.Columns())
212 y, height, okY := clampRange(y, height, frame.Rows())
213 if !okX || !okY {
214 return 0, 0, 0, 0, false
215 }
216 return x, y, width, height, true
217}
218
219// clampRange intersects [pos, pos+size) with [0, limit) without evaluating an
220// overflowing endpoint from untrusted damage coordinates.
221func clampRange(pos, size, limit int) (int, int, bool) {
222 if size <= 0 || limit <= 0 || pos >= limit {
223 return 0, 0, false
224 }
225 if pos < 0 {
226 // -size is safe because size is positive. If pos is at or before that
227 // point, the rectangle ends at or before zero.
228 if pos <= -size {
229 return 0, 0, false
230 }
231 end := pos + size // pos > -size proves this addition cannot overflow.
232 if end > limit {
233 end = limit
234 }
235 return 0, end, true
236 }
237
238 available := limit - pos
239 return pos, min(size, available), true
240}
241
242// writeCursor emits a cursor-positioning CSI sequence without fmt.Fprintf
243// allocations. It uses a stack-allocated buffer for integer formatting.
244func writeCursor(out *bytes.Buffer, y, x int) {
245 out.WriteString("\x1b[")
246 var b [16]byte
247 n := strconv.AppendInt(b[:0], int64(y+1), 10)
248 out.Write(n)
249 out.WriteByte(';')
250 n = strconv.AppendInt(b[:0], int64(x+1), 10)
251 out.Write(n)
252 out.WriteByte('H')
253}
254
255func sameDamage(a, b Damage) bool {
256 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
257}