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.
65 if s.Col >= s.frame.Width {
66 s.buffer.soft(s.Row)
67 s.Col = 0
68 s.relinkWrap(s.index())
69 }
70 if s.Row >= s.frame.Height {
71 s.Row = s.frame.Height - 1
72 }
73
74 // A wide rune must never straddle the right edge. If it does not fit on the
75 // current line, clear the abandoned last cell and wrap to the next line.
76 if w == 2 && s.Col+1 >= s.frame.Width {
77 if s.frame.Width < 2 {
78 // The screen is too narrow to ever hold a wide rune; store a narrow
79 // replacement so the cell's renderer width matches its layout.
80 r = '\uFFFD'
81 w = renderer.RuneWidth(r)
82 } else {
83 // The abandoned cell may itself be the continuation of a pair whose
84 // left half sits one column back; report damage over the full span.
85 cx := s.Col
86 if s.Col > 0 && s.frame.At(s.Col, s.Row).Continuation {
87 cx = s.Col - 1
88 }
89 s.clearWidePairAt(s.Col, s.Row)
90 s.frame.Set(s.Col, s.Row, renderer.BlankCell())
91 s.buffer.truncate(s.Row, cx)
92 s.buffer.continueRow(s.Row)
93 s.record(renderer.Damage{Kind: renderer.DamageText, X: cx, Y: s.Row, Width: s.Col - cx + 1, Height: 1, Count: 1})
94 s.Col = 0
95 s.relinkWrap(s.index())
96 if s.Row >= s.frame.Height {
97 s.Row = s.frame.Height - 1
98 }
99 }
100 }
101
102 insertDamageX := s.Col
103 insertDamageWidth := 0
104 if s.insertMode {
105 leftSplit := s.Col > 0 && s.frame.At(s.Col, s.Row).Continuation
106 s.frame.CopyRow(s.Row, s.Col+w, s.Col, s.frame.Width-s.Col-w)
107 s.frame.FillRow(s.Row, s.Col, s.Col+w, renderer.BlankCell())
108 s.repairRow(s.Row)
109 s.buffer.insert(s.Row, s.Col, w)
110 if leftSplit {
111 insertDamageX = s.Col - 1
112 }
113 insertDamageWidth = s.frame.Width - insertDamageX
114 }
115
116 // Determine the range of cells actually modified, extending over any wide
117 // pair the write lands on so no orphaned half is left behind.
118 lo, hi := s.Col, s.Col+w-1
119 if s.frame.At(s.Col, s.Row).Continuation {
120 lo = s.Col - 1
121 }
122 if right := s.Col + w; right < s.frame.Width && s.frame.At(right, s.Row).Continuation {
123 hi = right
124 }
125 for x := lo; x <= hi; x++ {
126 s.frame.Set(x, s.Row, renderer.BlankCell())
127 }
128 s.frame.Set(s.Col, s.Row, renderer.Cell{Rune: r, Style: s.Style})
129 s.buffer.content(s.Row, s.Col+w)
130 if w == 2 {
131 s.frame.Set(s.Col+1, s.Row, renderer.Cell{Continuation: true, Style: s.Style})
132 }
133 if insertDamageWidth > 0 {
134 lo = min(lo, insertDamageX)
135 hi = max(hi, insertDamageX+insertDamageWidth-1)
136 }
137 s.record(renderer.Damage{Kind: renderer.DamageText, X: lo, Y: s.Row, Width: hi - lo + 1, Height: 1, Count: 1})
138 s.Col += w
139}
140
141// clearWidePairAt blanks both halves of a wide-character pair when the cell at
142// (x,y) is either the left (wide) half or the right (continuation) half. It is
143// O(1) and assumes the pair invariant (a continuation is preceded by its wide
144// left half) holds — which the writer maintains.
145
146func (s *Screen) clearWidePairAt(x, y int) {
147 if x < 0 || x >= s.frame.Width || y < 0 || y >= s.frame.Height {
148 return
149 }
150 if s.frame.At(x, y).Continuation {
151 if x-1 >= 0 {
152 s.frame.Set(x-1, y, renderer.BlankCell())
153 }
154 s.frame.Set(x, y, renderer.BlankCell())
155 return
156 }
157 if x+1 < s.frame.Width && s.frame.At(x+1, y).Continuation {
158 s.frame.Set(x, y, renderer.BlankCell())
159 s.frame.Set(x+1, y, renderer.BlankCell())
160 }
161}
162
163func (s *Screen) eraseCell() renderer.Cell {
164 style := renderer.DefaultStyle()
165 if s.Style.HasBackgroundRGB {
166 style.HasBackgroundRGB = true
167 style.BackgroundRGB = s.Style.BackgroundRGB
168 } else {
169 style.Background = s.Style.Background
170 }
171 return renderer.Cell{Rune: ' ', Style: style}
172}
173
174// clearRow blanks cells [x0,x1) on row y, extending the range to swallow either
175// half of a wide pair that straddles a boundary so no orphan half is left. It
176// returns the actual modified span [start, start+width).
177
178func (s *Screen) clearRow(y, x0, x1 int) (start, width int) {
179 if y < 0 || y >= s.frame.Height {
180 return x0, 0
181 }
182 if x0 < 0 {
183 x0 = 0
184 }
185 if x1 > s.frame.Width {
186 x1 = s.frame.Width
187 }
188 if x0 >= x1 {
189 return x0, 0
190 }
191 // Left boundary: a continuation at x0 means its wide left half sits at x0-1.
192 if x0 > 0 && s.frame.At(x0, y).Continuation {
193 x0--
194 }
195 // Right boundary: a continuation at x1 means its wide left half (at x1-1) is
196 // inside the range and will be blanked, so swallow the continuation too.
197 if x1 < s.frame.Width && s.frame.At(x1, y).Continuation {
198 x1++
199 }
200 fullRow := x0 <= 0 && x1 >= s.frame.Width
201 blank := s.eraseCell()
202 s.frame.FillRow(y, x0, x1, blank)
203 s.buffer.clear(y, x0, x1)
204 if fullRow {
205 s.buffer.rowIDs[y] = s.nextRowIDValue()
206 }
207 return x0, x1 - x0
208}
209
210// repairRow blanks any orphaned wide-character halves on row y: a continuation
211// cell not preceded by its wide left half, or a wide rune not followed by its
212// continuation. Used after erase/insert/delete operations that may split a pair
213// at a boundary or by shifting cells.
214
215func (s *Screen) repairRow(y int) {
216 repairFrameRow(s.frame, y)
217}
218
219func repairFrameRow(frame renderer.Frame, y int) {
220 if y < 0 || y >= frame.Height {
221 return
222 }
223 w := frame.Width
224 for x := range w {
225 c := frame.At(x, y)
226 if c.Continuation {
227 if x == 0 {
228 frame.Set(x, y, renderer.BlankCell())
229 continue
230 }
231 left := frame.At(x-1, y)
232 if left.Continuation || renderer.RuneWidth(left.Rune) != 2 {
233 frame.Set(x, y, renderer.BlankCell())
234 }
235 continue
236 }
237 if renderer.RuneWidth(c.Rune) == 2 {
238 if x+1 >= w || !frame.At(x+1, y).Continuation {
239 frame.Set(x, y, renderer.BlankCell())
240 }
241 }
242 }
243}