1package vt
2
3import renderer "github.com/bnema/vev-vt/core"
4
5func (s *Screen) putRune(r rune) {
6 if r < 0x20 || (r >= 0x7f && r <= 0x9f) {
7 // REP only repeats a graphic character that directly precedes it.
8 s.lastPrinted = 0
9 }
10 switch r {
11 case '\a':
12 if s.OnBell != nil {
13 s.OnBell()
14 }
15 return
16 case 0x0e:
17 s.shiftOut = true
18 return
19 case 0x0f:
20 s.shiftOut = false
21 return
22 case 0x00, 0x7f:
23 return
24 case '\r':
25 s.Col = 0
26 case '\n', '\v', '\f':
27 if s.frame.Width > 0 && s.Col >= s.frame.Width {
28 s.Col = s.frame.Width - 1
29 }
30 s.buffer.hard(s.Row)
31 s.index()
32 case '\b':
33 if s.Col > 0 {
34 s.Col--
35 }
36 case '\t':
37 // HT moves to the next tab stop without modifying the frame. Clamp at
38 // the right edge rather than triggering deferred wrap or a scroll.
39 if s.frame.Width > 0 {
40 s.Col = min(s.Col+8-s.Col%8, s.frame.Width-1)
41 }
42 default:
43 // Drop C1 control characters (U+0080-U+009F).
44 if r >= 0x80 && r <= 0x9F {
45 return
46 }
47 if r >= 0x20 {
48 s.putPrintable(s.translateCharset(r))
49 }
50 }
51}
52
53// relinkWrap restores the soft link a deferred wrap just set when index()
54// scrolled the row out from under it. buffer.scrollUp severs the last moved
55// row on purpose — a linefeed there really does stop preceding anything — but
56// a wrap is the one case where that row flowed into the row below it, so the
57// link has to be put back after the rotation moved it to s.Row-1. movedUp is
58// false when nothing survived the scroll, and then there is no row to link.
59func (s *Screen) relinkWrap(movedUp bool) {
60 if movedUp {
61 s.buffer.continueRow(s.Row - 1)
62 }
63}
64
65func (s *Screen) putPrintable(r rune) {
66 w := renderer.RuneWidth(r)
67 // Skip combining marks and zero-width characters.
68 if w == 0 {
69 return
70 }
71 if s.frame.Width == 0 || s.frame.Height == 0 {
72 return
73 }
74 s.lastPrinted = r
75 // Deferred wrap: cursor sits past the last column. Disabling DECAWM
76 // cancels a pending wrap and leaves subsequent output at the right margin.
77 if s.Col >= s.frame.Width {
78 if s.autoWrapMode {
79 s.buffer.soft(s.Row)
80 s.Col = 0
81 s.relinkWrap(s.index())
82 } else {
83 s.Col = s.frame.Width - 1
84 }
85 }
86 if s.Row >= s.frame.Height {
87 s.Row = s.frame.Height - 1
88 }
89
90 // A wide rune must never straddle the right edge. If it does not fit on the
91 // current line, clear the abandoned last cell and wrap to the next line.
92 if w == 2 && s.Col+1 >= s.frame.Width {
93 if s.frame.Width < 2 || !s.autoWrapMode {
94 // A wide rune cannot be represented at the right margin without an
95 // orphaned continuation cell. Use the narrow replacement glyph.
96 r = '\uFFFD'
97 w = renderer.RuneWidth(r)
98 } else {
99 // The abandoned cell may itself be the continuation of a pair whose
100 // left half sits one column back; report damage over the full span.
101 cx := s.Col
102 if s.Col > 0 && s.frame.At(s.Col, s.Row).Continuation {
103 cx = s.Col - 1
104 }
105 s.clearWidePairAt(s.Col, s.Row)
106 s.frame.Set(s.Col, s.Row, renderer.BlankCell())
107 s.buffer.truncate(s.Row, cx)
108 s.buffer.continueRow(s.Row)
109 s.record(renderer.Damage{Kind: renderer.DamageText, X: cx, Y: s.Row, Width: s.Col - cx + 1, Height: 1, Count: 1})
110 s.Col = 0
111 s.relinkWrap(s.index())
112 if s.Row >= s.frame.Height {
113 s.Row = s.frame.Height - 1
114 }
115 }
116 }
117
118 insertDamageX := s.Col
119 insertDamageWidth := 0
120 if s.insertMode {
121 leftSplit := s.Col > 0 && s.frame.IsContinuation(s.Col, s.Row)
122 s.frame.CopyRow(s.Row, s.Col+w, s.Col, s.frame.Width-s.Col-w)
123 s.frame.FillRow(s.Row, s.Col, s.Col+w, renderer.BlankCell())
124 s.repairRow(s.Row)
125 s.buffer.insert(s.Row, s.Col, w)
126 if leftSplit {
127 insertDamageX = s.Col - 1
128 }
129 insertDamageWidth = s.frame.Width - insertDamageX
130 }
131
132 // Determine the range of cells actually modified, extending over any wide
133 // pair the write lands on so no orphaned half is left behind.
134 // Cells inside [Col, Col+w) are overwritten below, so only the orphaned
135 // halves just outside it need blanking.
136 lo, hi := s.Col, s.Col+w-1
137 if s.frame.IsContinuation(s.Col, s.Row) {
138 lo = s.Col - 1
139 s.frame.Set(lo, s.Row, renderer.BlankCell())
140 }
141 if right := s.Col + w; right < s.frame.Width && s.frame.IsContinuation(right, s.Row) {
142 hi = right
143 s.frame.Set(right, s.Row, renderer.BlankCell())
144 }
145 s.frame.Set(s.Col, s.Row, renderer.Cell{Rune: r, Style: s.Style})
146 s.buffer.content(s.Row, s.Col+w)
147 if w == 2 {
148 s.frame.Set(s.Col+1, s.Row, renderer.Cell{Continuation: true, Style: s.Style})
149 }
150 if insertDamageWidth > 0 {
151 lo = min(lo, insertDamageX)
152 hi = max(hi, insertDamageX+insertDamageWidth-1)
153 }
154 s.record(renderer.Damage{Kind: renderer.DamageText, X: lo, Y: s.Row, Width: hi - lo + 1, Height: 1, Count: 1})
155 s.Col += w
156 if !s.autoWrapMode && s.Col >= s.frame.Width {
157 s.Col = s.frame.Width - 1
158 }
159}
160
161// clearWidePairAt blanks both halves of a wide-character pair when the cell at
162// (x,y) is either the left (wide) half or the right (continuation) half. It is
163// O(1) and assumes the pair invariant (a continuation is preceded by its wide
164// left half) holds — which the writer maintains.
165
166func (s *Screen) clearWidePairAt(x, y int) {
167 if x < 0 || x >= s.frame.Width || y < 0 || y >= s.frame.Height {
168 return
169 }
170 if s.frame.At(x, y).Continuation {
171 if x-1 >= 0 {
172 s.frame.Set(x-1, y, renderer.BlankCell())
173 }
174 s.frame.Set(x, y, renderer.BlankCell())
175 return
176 }
177 if x+1 < s.frame.Width && s.frame.At(x+1, y).Continuation {
178 s.frame.Set(x, y, renderer.BlankCell())
179 s.frame.Set(x+1, y, renderer.BlankCell())
180 }
181}
182
183func (s *Screen) eraseCell() renderer.Cell {
184 style := renderer.DefaultStyle()
185 if s.Style.HasBackgroundRGB {
186 style.HasBackgroundRGB = true
187 style.BackgroundRGB = s.Style.BackgroundRGB
188 } else {
189 style.Background = s.Style.Background
190 }
191 return renderer.Cell{Rune: ' ', Style: style}
192}
193
194// clearRow blanks cells [x0,x1) on row y, extending the range to swallow either
195// half of a wide pair that straddles a boundary so no orphan half is left. It
196// returns the actual modified span [start, start+width).
197
198func (s *Screen) clearRow(y, x0, x1 int) (start, width int) {
199 if y < 0 || y >= s.frame.Height {
200 return x0, 0
201 }
202 if x0 < 0 {
203 x0 = 0
204 }
205 if x1 > s.frame.Width {
206 x1 = s.frame.Width
207 }
208 if x0 >= x1 {
209 return x0, 0
210 }
211 // Left boundary: a continuation at x0 means its wide left half sits at x0-1.
212 if x0 > 0 && s.frame.At(x0, y).Continuation {
213 x0--
214 }
215 // Right boundary: a continuation at x1 means its wide left half (at x1-1) is
216 // inside the range and will be blanked, so swallow the continuation too.
217 if x1 < s.frame.Width && s.frame.At(x1, y).Continuation {
218 x1++
219 }
220 fullRow := x0 <= 0 && x1 >= s.frame.Width
221 blank := s.eraseCell()
222 s.frame.FillRow(y, x0, x1, blank)
223 s.buffer.clear(y, x0, x1)
224 if fullRow {
225 s.buffer.rowIDs[y] = s.nextRowIDValue()
226 }
227 return x0, x1 - x0
228}
229
230// repairRow blanks any orphaned wide-character halves on row y: a continuation
231// cell not preceded by its wide left half, or a wide rune not followed by its
232// continuation. Used after erase/insert/delete operations that may split a pair
233// at a boundary or by shifting cells.
234
235func (s *Screen) repairRow(y int) {
236 repairFrameRow(s.frame, y)
237}
238
239func repairFrameRow(frame renderer.Frame, y int) {
240 if y < 0 || y >= frame.Height {
241 return
242 }
243 w := frame.Width
244 for x := range w {
245 c := frame.At(x, y)
246 if c.Continuation {
247 if x == 0 {
248 frame.Set(x, y, renderer.BlankCell())
249 continue
250 }
251 left := frame.At(x-1, y)
252 if left.Continuation || renderer.RuneWidth(left.Rune) != 2 {
253 frame.Set(x, y, renderer.BlankCell())
254 }
255 continue
256 }
257 if renderer.RuneWidth(c.Rune) == 2 {
258 if x+1 >= w || !frame.At(x+1, y).Continuation {
259 frame.Set(x, y, renderer.BlankCell())
260 }
261 }
262 }
263}