main ab6686c

screen_write.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close