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