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 kind := DamageScrollUp
126 if scroll.Down {
127 kind = DamageScrollDown
128 }
129 emitScroll(buf, Damage{Kind: kind, X: 0, Y: scroll.Y, Width: columns, Height: scroll.Height, Count: scroll.Count})
130 }
131 for _, span := range plan.Spans {
132 r.emitSpan(buf, frame, span.Y, span.X, span.Width, &st)
133 }
134 buf.WriteString("\x1b[0m")
135 }
136 if r.caps.SynchronizedOutput {
137 buf.WriteString(SyncEndCSI)
138 }
139 prepared.data = copyBytes(buf)
140 return prepared, nil
141}
142
143func (r *Renderer) Draw(frame CellSource, damage []Damage) ([]byte, error) {
144 prepared, err := r.Prepare(frame, damage, false)
145 if err != nil {
146 return nil, err
147 }
148 prepared.Commit()
149 return prepared.Bytes(), nil
150}
151
152func (r *Renderer) committedFrame() Frame {
153 return r.committed
154}
155
156func (r *Renderer) setCommittedFrame(frame Frame) {
157 r.width = frame.Width
158 r.height = frame.Height
159 r.hasCommitted = true
160 r.committed = frame
161}
162
163// copyBytes copies the buffer contents into a fresh byte slice and is used
164// to return output that is independent of the pooled scratch buffer.
165func copyBytes(buf *bytes.Buffer) []byte {
166 b := buf.Bytes()
167 out := make([]byte, len(b))
168 copy(out, b)
169 return out
170}
171
172func putBuffer(buf *bytes.Buffer) {
173 if buf.Cap() > maxPooledBufferCap {
174 return
175 }
176 bufferPool.Put(buf)
177}
178
179func needsFull(damage []Damage) bool {
180 if len(damage) == 0 {
181 return false
182 }
183 for _, d := range damage {
184 if d.Kind == DamageFullRedraw {
185 return true
186 }
187 }
188 return false
189}
190
191func hasScrollDamage(damage []Damage) bool {
192 for _, d := range damage {
193 if d.Kind == DamageScrollUp || d.Kind == DamageScrollDown {
194 return true
195 }
196 }
197 return false
198}
199
200func (r *Renderer) writeFull(out *bytes.Buffer, frame CellSource, st *drawState) {
201 columns := frame.Columns()
202 for y := range frame.Rows() {
203 r.emitSpan(out, frame, y, 0, columns, st)
204 }
205 out.WriteString("\x1b[0m")
206}
207
208func (r *Renderer) emitDamageSpans(out *bytes.Buffer, frame CellSource, spans []Span, st *drawState) {
209 for _, span := range spans {
210 r.emitSpan(out, frame, span.Y, span.X, span.Width, st)
211 }
212 out.WriteString("\x1b[0m")
213}
214func clampRect(frame CellSource, x, y, width, height int) (int, int, int, int, bool) {
215 x, width, okX := clampRange(x, width, frame.Columns())
216 y, height, okY := clampRange(y, height, frame.Rows())
217 if !okX || !okY {
218 return 0, 0, 0, 0, false
219 }
220 return x, y, width, height, true
221}
222
223// clampRange intersects [pos, pos+size) with [0, limit) without evaluating an
224// overflowing endpoint from untrusted damage coordinates.
225func clampRange(pos, size, limit int) (int, int, bool) {
226 if size <= 0 || limit <= 0 || pos >= limit {
227 return 0, 0, false
228 }
229 if pos < 0 {
230 // -size is safe because size is positive. If pos is at or before that
231 // point, the rectangle ends at or before zero.
232 if pos <= -size {
233 return 0, 0, false
234 }
235 end := pos + size // pos > -size proves this addition cannot overflow.
236 if end > limit {
237 end = limit
238 }
239 return 0, end, true
240 }
241
242 available := limit - pos
243 return pos, min(size, available), true
244}
245
246// writeCursor emits a cursor-positioning CSI sequence without fmt.Fprintf
247// allocations. It uses a stack-allocated buffer for integer formatting.
248func writeCursor(out *bytes.Buffer, y, x int) {
249 out.WriteString("\x1b[")
250 var b [16]byte
251 n := strconv.AppendInt(b[:0], int64(y+1), 10)
252 out.Write(n)
253 out.WriteByte(';')
254 n = strconv.AppendInt(b[:0], int64(x+1), 10)
255 out.Write(n)
256 out.WriteByte('H')
257}
258
259func sameDamage(a, b Damage) bool {
260 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
261}