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 shadow []Cell
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.shadow = nil
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 p.candidate.Commit(&committed)
67 p.renderer.setCommittedFrame(committed)
68 })
69}
70
71// Prepare plans and encodes a transactional draw. The renderer advances only
72// when the returned draw is committed. Keep at most one prepared draw
73// outstanding; commit or discard it before calling Prepare again.
74func (r *Renderer) Prepare(frame Frame, damage []Damage, reset bool) (PreparedDraw, error) {
75 var candidate DeltaCandidate
76 var err error
77 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) {
78 if err = frame.Validate(); err == nil {
79 plan := planSingleDamage(frame, damage[0])
80 candidate = DeltaCandidate{Plan: plan}
81 if plan.Snapshot || plan.Scroll.Height != 0 || len(plan.Spans) != 0 {
82 candidate.frame = frame.Clone()
83 }
84 }
85 } else {
86 candidate, err = PlanDelta(frame, damage, r.committedFrame(), reset || len(r.shadow) == 0)
87 }
88 if err != nil {
89 return PreparedDraw{}, err
90 }
91 prepared := PreparedDraw{renderer: r, candidate: candidate, commitOnce: new(sync.Once)}
92 plan := candidate.Plan
93 if !plan.Snapshot && plan.Scroll.Height == 0 && len(plan.Spans) == 0 {
94 return prepared, nil
95 }
96
97 buf, ok := bufferPool.Get().(*bytes.Buffer)
98 if !ok {
99 buf = new(bytes.Buffer)
100 }
101 buf.Reset()
102 defer putBuffer(buf)
103 if r.caps.SynchronizedOutput {
104 buf.WriteString(SyncStartCSI)
105 }
106
107 st := newDrawStateForProfile(r.colorProfile)
108 if plan.Snapshot {
109 if len(plan.Spans) > 0 {
110 r.emitDamageSpans(buf, frame, plan.Spans, &st)
111 } else {
112 r.writeFull(buf, frame, &st)
113 }
114 } else {
115 if plan.Scroll.Height != 0 {
116 scroll := plan.Scroll
117 emitScrollUp(buf, Damage{Kind: DamageScrollUp, X: 0, Y: scroll.Y, Width: frame.Width, Height: scroll.Height, Count: scroll.Count})
118 }
119 for _, span := range plan.Spans {
120 r.emitSpan(buf, frame, span.Y, span.X, span.Width, &st)
121 }
122 buf.WriteString("\x1b[0m")
123 }
124 if r.caps.SynchronizedOutput {
125 buf.WriteString(SyncEndCSI)
126 }
127 prepared.data = copyBytes(buf)
128 return prepared, nil
129}
130
131func (r *Renderer) Draw(frame Frame, damage []Damage) ([]byte, error) {
132 prepared, err := r.Prepare(frame, damage, false)
133 if err != nil {
134 return nil, err
135 }
136 prepared.Commit()
137 return prepared.Bytes(), nil
138}
139
140func (r *Renderer) committedFrame() Frame {
141 return r.committed
142}
143
144func (r *Renderer) setCommittedFrame(frame Frame) {
145 r.width = frame.Width
146 r.height = frame.Height
147 r.shadow = frame.Cells
148 r.committed = frame
149}
150
151// copyBytes copies the buffer contents into a fresh byte slice and is used
152// to return output that is independent of the pooled scratch buffer.
153func copyBytes(buf *bytes.Buffer) []byte {
154 b := buf.Bytes()
155 out := make([]byte, len(b))
156 copy(out, b)
157 return out
158}
159
160func putBuffer(buf *bytes.Buffer) {
161 if buf.Cap() > maxPooledBufferCap {
162 return
163 }
164 bufferPool.Put(buf)
165}
166
167func needsFull(damage []Damage) bool {
168 if len(damage) == 0 {
169 return false
170 }
171 for _, d := range damage {
172 if d.Kind == DamageFullRedraw {
173 return true
174 }
175 }
176 return false
177}
178
179func hasScrollDamage(damage []Damage) bool {
180 for _, d := range damage {
181 if d.Kind == DamageScrollUp {
182 return true
183 }
184 }
185 return false
186}
187
188func (r *Renderer) writeFull(out *bytes.Buffer, frame Frame, st *drawState) {
189 for y := range frame.Height {
190 r.emitSpan(out, frame, y, 0, frame.Width, st)
191 }
192 out.WriteString("\x1b[0m")
193}
194
195func (r *Renderer) emitDamageSpans(out *bytes.Buffer, frame Frame, spans []Span, st *drawState) {
196 for _, span := range spans {
197 r.emitSpan(out, frame, span.Y, span.X, span.Width, st)
198 }
199 out.WriteString("\x1b[0m")
200}
201func clampRect(frame Frame, x, y, width, height int) (int, int, int, int, bool) {
202 x, width, okX := clampRange(x, width, frame.Width)
203 y, height, okY := clampRange(y, height, frame.Height)
204 if !okX || !okY {
205 return 0, 0, 0, 0, false
206 }
207 return x, y, width, height, true
208}
209
210// clampRange intersects [pos, pos+size) with [0, limit) without evaluating an
211// overflowing endpoint from untrusted damage coordinates.
212func clampRange(pos, size, limit int) (int, int, bool) {
213 if size <= 0 || limit <= 0 || pos >= limit {
214 return 0, 0, false
215 }
216 if pos < 0 {
217 // -size is safe because size is positive. If pos is at or before that
218 // point, the rectangle ends at or before zero.
219 if pos <= -size {
220 return 0, 0, false
221 }
222 end := pos + size // pos > -size proves this addition cannot overflow.
223 if end > limit {
224 end = limit
225 }
226 return 0, end, true
227 }
228
229 available := limit - pos
230 return pos, min(size, available), true
231}
232
233// writeCursor emits a cursor-positioning CSI sequence without fmt.Fprintf
234// allocations. It uses a stack-allocated buffer for integer formatting.
235func writeCursor(out *bytes.Buffer, y, x int) {
236 out.WriteString("\x1b[")
237 var b [16]byte
238 n := strconv.AppendInt(b[:0], int64(y+1), 10)
239 out.Write(n)
240 out.WriteByte(';')
241 n = strconv.AppendInt(b[:0], int64(x+1), 10)
242 out.Write(n)
243 out.WriteByte('H')
244}
245
246func sameDamage(a, b Damage) bool {
247 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
248}