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
15type Capabilities struct {
16 SynchronizedOutput bool
17}
18
19type Renderer struct {
20 caps Capabilities
21 width int
22 height int
23 shadow []Cell
24 committed Frame
25}
26
27// PreparedDraw owns encoded output and its transactional delta until Commit.
28// It is returned by value so ordinary prepared draws do not allocate.
29type PreparedDraw struct {
30 renderer *Renderer
31 candidate DeltaCandidate
32 data []byte
33 commitOnce *sync.Once
34}
35
36func New(caps Capabilities) *Renderer { return &Renderer{caps: caps} }
37
38func (r *Renderer) Reset() {
39 r.width = 0
40 r.height = 0
41 r.shadow = nil
42 r.committed = Frame{}
43}
44
45// Bytes returns the prepared ANSI output. The returned bytes remain valid after
46// another draw.
47func (p PreparedDraw) Bytes() []byte { return p.data }
48
49// Commit applies the prepared delta exactly once. Discarding it leaves the
50// renderer's committed state unchanged.
51func (p *PreparedDraw) Commit() {
52 if p == nil || p.renderer == nil || p.commitOnce == nil {
53 return
54 }
55 p.commitOnce.Do(func() {
56 committed := p.renderer.committedFrame()
57 p.candidate.Commit(&committed)
58 p.renderer.setCommittedFrame(committed)
59 })
60}
61
62// Prepare plans and encodes a transactional draw. The renderer advances only
63// when the returned draw is committed. Keep at most one prepared draw
64// outstanding; commit or discard it before calling Prepare again.
65func (r *Renderer) Prepare(frame Frame, damage []Damage, reset bool) (PreparedDraw, error) {
66 var candidate DeltaCandidate
67 var err error
68 if !reset && len(r.shadow) != 0 && r.width == frame.Width && r.height == frame.Height && len(damage) == 1 && (damage[0].Kind == DamageText || damage[0].Kind == DamageClear) {
69 if err = frame.Validate(); err == nil {
70 plan := planSingleDamage(frame, damage[0])
71 candidate = DeltaCandidate{Plan: plan}
72 if plan.Snapshot || plan.Scroll.Height != 0 || len(plan.Spans) != 0 {
73 candidate.frame = frame.Clone()
74 }
75 }
76 } else {
77 candidate, err = PlanDelta(frame, damage, r.committedFrame(), reset || len(r.shadow) == 0)
78 }
79 if err != nil {
80 return PreparedDraw{}, err
81 }
82 prepared := PreparedDraw{renderer: r, candidate: candidate, commitOnce: new(sync.Once)}
83 plan := candidate.Plan
84 if !plan.Snapshot && plan.Scroll.Height == 0 && len(plan.Spans) == 0 {
85 return prepared, nil
86 }
87
88 buf, ok := bufferPool.Get().(*bytes.Buffer)
89 if !ok {
90 buf = new(bytes.Buffer)
91 }
92 buf.Reset()
93 defer putBuffer(buf)
94 if r.caps.SynchronizedOutput {
95 buf.WriteString(SyncStartCSI)
96 }
97
98 st := newDrawState()
99 if plan.Snapshot {
100 if len(plan.Spans) > 0 {
101 r.emitDamageSpans(buf, frame, plan.Spans, &st)
102 } else {
103 r.writeFull(buf, frame, &st)
104 }
105 } else {
106 if plan.Scroll.Height != 0 {
107 scroll := plan.Scroll
108 emitScrollUp(buf, Damage{Kind: DamageScrollUp, X: 0, Y: scroll.Y, Width: frame.Width, Height: scroll.Height, Count: scroll.Count})
109 }
110 for _, span := range plan.Spans {
111 r.emitSpan(buf, frame, span.Y, span.X, span.Width, &st)
112 }
113 buf.WriteString("\x1b[0m")
114 }
115 if r.caps.SynchronizedOutput {
116 buf.WriteString(SyncEndCSI)
117 }
118 prepared.data = copyBytes(buf)
119 return prepared, nil
120}
121
122func (r *Renderer) Draw(frame Frame, damage []Damage) ([]byte, error) {
123 prepared, err := r.Prepare(frame, damage, false)
124 if err != nil {
125 return nil, err
126 }
127 prepared.Commit()
128 return prepared.Bytes(), nil
129}
130
131func (r *Renderer) committedFrame() Frame {
132 return r.committed
133}
134
135func (r *Renderer) setCommittedFrame(frame Frame) {
136 r.width = frame.Width
137 r.height = frame.Height
138 r.shadow = frame.Cells
139 r.committed = frame
140}
141
142// copyBytes copies the buffer contents into a fresh byte slice and is used
143// to return output that is independent of the pooled scratch buffer.
144func copyBytes(buf *bytes.Buffer) []byte {
145 b := buf.Bytes()
146 out := make([]byte, len(b))
147 copy(out, b)
148 return out
149}
150
151func putBuffer(buf *bytes.Buffer) {
152 if buf.Cap() > maxPooledBufferCap {
153 return
154 }
155 bufferPool.Put(buf)
156}
157
158func needsFull(damage []Damage) bool {
159 if len(damage) == 0 {
160 return false
161 }
162 for _, d := range damage {
163 if d.Kind == DamageFullRedraw {
164 return true
165 }
166 }
167 return false
168}
169
170func hasScrollDamage(damage []Damage) bool {
171 for _, d := range damage {
172 if d.Kind == DamageScrollUp {
173 return true
174 }
175 }
176 return false
177}
178
179func (r *Renderer) writeFull(out *bytes.Buffer, frame Frame, st *drawState) {
180 for y := range frame.Height {
181 r.emitSpan(out, frame, y, 0, frame.Width, st)
182 }
183 out.WriteString("\x1b[0m")
184}
185
186func (r *Renderer) emitDamageSpans(out *bytes.Buffer, frame Frame, spans []Span, st *drawState) {
187 for _, span := range spans {
188 r.emitSpan(out, frame, span.Y, span.X, span.Width, st)
189 }
190 out.WriteString("\x1b[0m")
191}
192func clampRect(frame Frame, x, y, width, height int) (int, int, int, int, bool) {
193 x, width, okX := clampRange(x, width, frame.Width)
194 y, height, okY := clampRange(y, height, frame.Height)
195 if !okX || !okY {
196 return 0, 0, 0, 0, false
197 }
198 return x, y, width, height, true
199}
200
201// clampRange intersects [pos, pos+size) with [0, limit) without evaluating an
202// overflowing endpoint from untrusted damage coordinates.
203func clampRange(pos, size, limit int) (int, int, bool) {
204 if size <= 0 || limit <= 0 || pos >= limit {
205 return 0, 0, false
206 }
207 if pos < 0 {
208 // -size is safe because size is positive. If pos is at or before that
209 // point, the rectangle ends at or before zero.
210 if pos <= -size {
211 return 0, 0, false
212 }
213 end := pos + size // pos > -size proves this addition cannot overflow.
214 if end > limit {
215 end = limit
216 }
217 return 0, end, true
218 }
219
220 available := limit - pos
221 return pos, min(size, available), true
222}
223
224// writeCursor emits a cursor-positioning CSI sequence without fmt.Fprintf
225// allocations. It uses a stack-allocated buffer for integer formatting.
226func writeCursor(out *bytes.Buffer, y, x int) {
227 out.WriteString("\x1b[")
228 var b [16]byte
229 n := strconv.AppendInt(b[:0], int64(y+1), 10)
230 out.Write(n)
231 out.WriteByte(';')
232 n = strconv.AppendInt(b[:0], int64(x+1), 10)
233 out.Write(n)
234 out.WriteByte('H')
235}
236
237// writeStyle emits SGR style parameters without fmt.Fprintf allocations.
238func writeStyle(out *bytes.Buffer, style Style) {
239 out.WriteString("\x1b[0")
240 if style.Bold {
241 out.WriteString(";1")
242 }
243 if style.Attrs&AttrDim != 0 {
244 out.WriteString(";2")
245 }
246 if style.Italic {
247 out.WriteString(";3")
248 }
249 if style.Attrs&AttrUnderline != 0 {
250 switch style.UnderlineStyle {
251 case UnderlineDouble:
252 out.WriteString(";21")
253 case UnderlineCurly:
254 out.WriteString(";4:3")
255 case UnderlineDotted:
256 out.WriteString(";4:4")
257 case UnderlineDashed:
258 out.WriteString(";4:5")
259 default:
260 out.WriteString(";4")
261 }
262 }
263 if style.Attrs&AttrBlink != 0 {
264 out.WriteString(";5")
265 }
266 if style.Inverse {
267 out.WriteString(";7")
268 }
269 if style.Attrs&AttrStrikethrough != 0 {
270 out.WriteString(";9")
271 }
272 var b [16]byte
273 if style.HasForegroundRGB {
274 out.WriteString(";38;2;")
275 writeRGB(out, &b, style.ForegroundRGB)
276 } else if style.Foreground >= 0 {
277 out.WriteString(";38;5;")
278 n := strconv.AppendInt(b[:0], int64(style.Foreground), 10)
279 out.Write(n)
280 }
281 if style.HasBackgroundRGB {
282 out.WriteString(";48;2;")
283 writeRGB(out, &b, style.BackgroundRGB)
284 } else if style.Background >= 0 {
285 out.WriteString(";48;5;")
286 n := strconv.AppendInt(b[:0], int64(style.Background), 10)
287 out.Write(n)
288 }
289 if style.HasUnderlineColorRGB {
290 out.WriteString(";58;2;")
291 writeRGB(out, &b, style.UnderlineColorRGB)
292 } else if style.HasUnderlineColor {
293 out.WriteString(";58;5;")
294 n := strconv.AppendInt(b[:0], int64(style.UnderlineColor), 10)
295 out.Write(n)
296 }
297 out.WriteByte('m')
298}
299
300func writeRGB(out *bytes.Buffer, b *[16]byte, rgb RGB) {
301 n := strconv.AppendInt(b[:0], int64(rgb.R), 10)
302 out.Write(n)
303 out.WriteByte(';')
304 n = strconv.AppendInt(b[:0], int64(rgb.G), 10)
305 out.Write(n)
306 out.WriteByte(';')
307 n = strconv.AppendInt(b[:0], int64(rgb.B), 10)
308 out.Write(n)
309}
310
311func sameDamage(a, b Damage) bool {
312 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
313}