v0.7.0 36c429d

screen_write.go RawHistoryFolder

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}

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