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