1package ansi
2
3import (
4 "bytes"
5 "strconv"
6)
7
8// drawState carries the per-Draw terminal state model: the cursor position we
9// last put the terminal at (when it is known with certainty) and the SGR pen
10// currently active. It lives for exactly one Draw call: between Draws the
11// terminal may be moved by other writers, so cursor tracking never persists
12// (this also trivially satisfies "invalidate on Reset"). The pen starts as
13// the default style because every Draw that emits output ends with \x1b[0m,
14// which is this package's output contract.
15type drawState struct {
16 curKnown bool
17 curRow int
18 curCol int
19 pen Style
20}
21
22func newDrawState() drawState {
23 return drawState{pen: DefaultStyle()}
24}
25
26func (st *drawState) setCursor(row, col int) {
27 st.curKnown = true
28 st.curRow = row
29 st.curCol = col
30}
31
32// invalidateCursor marks the tracked position as uncertain. It is called
33// whenever an emission leaves the cursor in a terminal-dependent state, most
34// importantly after writing into the last column: terminals differ on
35// wrap-pending behavior, so we never model it and fall back to absolute CUP.
36func (st *drawState) invalidateCursor() {
37 st.curKnown = false
38}
39
40// Cursor-motion candidates considered by planMove. Absolute CUP is always
41// correct; the others are only valid relative to a certain tracked position.
42const (
43 moveCUP = 'H' // \x1b[<r>;<c>H
44 moveCR = '\r' // carriage return: same row, column 0
45 moveCUU = 'A' // \x1b[<n>A: up, same column
46 moveCUD = 'B' // \x1b[<n>B: down, same column
47 moveCUF = 'C' // \x1b[<n>C: forward, same row
48 moveCUB = 'D' // \x1b[<n>D: backward, same row
49)
50
51// planMove picks the cheapest correct sequence to move from the tracked
52// position to (row, col). "Cheapest" is decided by comparing the actual
53// encoded byte lengths of every applicable candidate (no fixed thresholds):
54// CUP costs 4 + digits(row+1) + digits(col+1); CR costs 1; CUU/CUD/CUF/CUB
55// cost 3 for a delta of 1 (count omitted) and 3 + digits(n) otherwise.
56// Relative candidates are considered only when the tracked position is
57// certain; ties go to absolute CUP, the safest form.
58func (st *drawState) planMove(row, col int) (op byte, n, cost int) {
59 op, n, cost = moveCUP, 0, 4+decimalLen(row+1)+decimalLen(col+1)
60 if !st.curKnown {
61 return op, n, cost
62 }
63 consider := func(candOp byte, candN, candCost int) {
64 if candCost < cost {
65 op, n, cost = candOp, candN, candCost
66 }
67 }
68 if row == st.curRow {
69 if col == 0 {
70 consider(moveCR, 0, 1)
71 }
72 if dc := col - st.curCol; dc > 0 {
73 consider(moveCUF, dc, csiRelLen(dc))
74 } else if dc < 0 {
75 consider(moveCUB, -dc, csiRelLen(-dc))
76 }
77 } else if col == st.curCol {
78 if dr := row - st.curRow; dr > 0 {
79 consider(moveCUD, dr, csiRelLen(dr))
80 } else {
81 consider(moveCUU, -dr, csiRelLen(-dr))
82 }
83 }
84 return op, n, cost
85}
86
87// moveTo emits the cheapest correct cursor-positioning sequence for (row, col)
88// and updates tracking. Moving to the already-tracked position emits nothing.
89func (st *drawState) moveTo(out *bytes.Buffer, row, col int) {
90 if st.curKnown && st.curRow == row && st.curCol == col {
91 return
92 }
93 op, n, _ := st.planMove(row, col)
94 switch op {
95 case moveCUP:
96 writeCursor(out, row, col)
97 case moveCR:
98 out.WriteByte('\r')
99 default:
100 writeCSIRel(out, n, op)
101 }
102 st.setCursor(row, col)
103}
104
105// moveCost returns the encoded length moveTo would emit for (row, col).
106func (st *drawState) moveCost(row, col int) int {
107 if st.curKnown && st.curRow == row && st.curCol == col {
108 return 0
109 }
110 _, _, cost := st.planMove(row, col)
111 return cost
112}
113
114// setPen emits an SGR sequence if style differs from the current pen.
115func (st *drawState) setPen(out *bytes.Buffer, style Style) {
116 if style.Equal(st.pen) {
117 return
118 }
119 writeStyle(out, style)
120 st.pen = style
121}
122
123// isBlank reports whether a cell renders as a default-style space. This is
124// the differ's notion of blank: rune ' ' or 0, default style, and not the
125// continuation half of a wide pair.
126func isBlank(c Cell) bool {
127 return !c.Continuation && (c.Rune == ' ' || c.Rune == 0) && c.Style.Equal(DefaultStyle())
128}
129
130// Native-clear encoded lengths: EL "\x1b[K" and EL2 "\x1b[2K".
131const (
132 elLen = 3
133 el2Len = 4
134)
135
136// emitSpan positions the cursor and emits the cells of logical row y in
137// [x, x+width), already clamped by the caller. It applies the three output
138// minimizations:
139//
140// - cursor motion: moveTo picks the cheapest sequence, and after emission
141// the cursor is tracked at x+width (continuation cells count as columns:
142// the terminal advances two columns per wide rune). A span that touches
143// the last column invalidates tracking instead — wrap-pending state is
144// terminal-dependent and never modeled.
145// - SGR: the pen persists across spans (st is Draw-scoped); a style change
146// is emitted only when a cell's style differs from the pen.
147// - native clears: a trailing run of blank cells that reaches the frame's
148// right edge is replaced by EL when EL is not longer than the spaces it
149// replaces (ties prefer EL: it keeps the cursor position certain). An
150// entirely blank full-width line may instead use a vertical-only move
151// plus 2K when that encoding is cheaper. Both clears require the default
152// pen (BCE: terminals fill cleared cells with the pen background).
153func (r *Renderer) emitSpan(out *bytes.Buffer, frame Frame, y, x, width int, st *drawState) {
154 end := x + width
155 // Trailing blank suffix is EL-eligible only when the span reaches the
156 // frame's right edge (EL clears to the end of the line, nothing less).
157 t := end
158 if end == frame.Width {
159 for t > x && isBlank(frame.At(t-1, y)) {
160 t--
161 }
162 }
163 useEL := end == frame.Width && end-t >= elLen
164 if !useEL {
165 t = end
166 }
167
168 if useEL && t == x && x == 0 && width == frame.Width {
169 // Entire line blank: compare "move to col 0 + EL" against
170 // "vertical-only move + 2K" (2K clears the whole line from any
171 // column, so a cheap same-column CUD/CUU can be reused).
172 elCost := st.moveCost(y, 0) + elLen
173 if st.curKnown && st.moveCost(y, st.curCol)+el2Len < elCost {
174 st.setPen(out, DefaultStyle())
175 st.moveTo(out, y, st.curCol)
176 out.WriteString("\x1b[2K")
177 return
178 }
179 }
180
181 st.moveTo(out, y, x)
182 for col := x; col < t; col++ {
183 cell := frame.At(col, y)
184 // Continuation cells are the right half of a wide rune. Emit nothing:
185 // the terminal already advanced two columns for the left cell's rune.
186 if cell.Continuation {
187 continue
188 }
189 // Pen check kept inline (not via setPen): this is the hottest loop in
190 // the renderer and the method call costs measurably on full redraws.
191 if !cell.Style.Equal(st.pen) {
192 writeStyle(out, cell.Style)
193 st.pen = cell.Style
194 }
195 if cell.Rune == 0 {
196 out.WriteByte(' ')
197 } else {
198 out.WriteRune(cell.Rune)
199 }
200 }
201 if useEL {
202 st.setPen(out, DefaultStyle())
203 out.WriteString("\x1b[K")
204 // EL does not move the cursor: it stays at the start of the blanks.
205 st.setCursor(y, t)
206 } else if end >= frame.Width {
207 st.invalidateCursor()
208 } else {
209 st.setCursor(y, end)
210 }
211 // A span that splits a wide pair breaks the "terminal advance == span
212 // width" assumption tracking relies on: a leading orphan continuation
213 // (head outside the span) advances one column less, and a span ending on
214 // a wide head whose continuation lies outside advances one more. Both are
215 // malformed inputs the VT layer should never produce, but tracking must
216 // stay certain, so drop it. (t is never a continuation on the EL path —
217 // blanks are not continuations — so this check is exact for both paths.)
218 if frame.At(x, y).Continuation || (t < frame.Width && frame.At(t, y).Continuation) {
219 st.invalidateCursor()
220 }
221}
222
223// writeCSIRel emits a relative cursor move "\x1b[<n><letter>", omitting the
224// count when n == 1 as the CSI default allows.
225func writeCSIRel(out *bytes.Buffer, n int, letter byte) {
226 out.WriteString("\x1b[")
227 if n != 1 {
228 var b [16]byte
229 out.Write(strconv.AppendInt(b[:0], int64(n), 10))
230 }
231 out.WriteByte(letter)
232}
233
234// csiRelLen is the encoded length of writeCSIRel's output.
235func csiRelLen(n int) int {
236 if n == 1 {
237 return 3
238 }
239 return 3 + decimalLen(n)
240}
241
242// decimalLen is the number of decimal digits of a non-negative int.
243func decimalLen(n int) int {
244 l := 1
245 for n >= 10 {
246 n /= 10
247 l++
248 }
249 return l
250}