v0.1.0 af61ae3

screen.go RawHistoryFolder

1package vt
2
3import (
4	"unicode/utf8"
5
6	renderer "github.com/bnema/vev-vt/core"
7)
8
9// maxEscapeBufferLen must stay large enough for OSC 52 clipboard payloads
10// forwarded by internal/usecase/copy.OSC52MaxPayloadBytes after base64
11// expansion plus the OSC wrapper. pkg/vt cannot import internal/usecase/copy,
12// so keep this value in sync if that payload cap changes.
13const maxEscapeBufferLen = 128 * 1024
14
15const (
16	// ColorSchemeReportDark is the DEC 2031 dark-scheme report.
17	ColorSchemeReportDark = "\x1b[?997;1n"
18	// ColorSchemeReportLight is the DEC 2031 light-scheme report.
19	ColorSchemeReportLight = "\x1b[?997;2n"
20)
21
22type Screen struct {
23	Frame renderer.Frame
24	Row   int
25	Col   int
26	Style renderer.Style
27	// OnLineEvicted is called just before a full-width upward scroll recycles
28	// and blanks rows. The callback receives a stable copy of each evicted row.
29	OnLineEvicted func([]renderer.Cell)
30	// OnResponse is called synchronously from Write with reply bytes that the
31	// emulator must send back to the child process (DA, DSR, and ANSI/DEC mode
32	// query reports). The host wires it to the PTY input. Nil disables responses.
33	OnResponse func([]byte)
34	// OnBell is called synchronously from Write for each lone BEL (0x07)
35	// outside escape sequences. BELs that terminate an OSC never fire it.
36	// Nil disables bell reporting.
37	OnBell func()
38	// OnNotify is called synchronously from Write for explicit terminal
39	// notifications: OSC 9 (body only) and OSC 777 "notify" (title;body).
40	// Other non-clipboard OSC payloads remain discarded. Nil disables it.
41	OnNotify func(title, body string)
42	// OnProgress is called synchronously from Write for OSC 9;4 progress
43	// transitions that request attention: active progress cleared or first entry
44	// into error state. Nil disables progress reporting.
45	OnProgress func(errored bool)
46	// OnClipboard is called synchronously from Write for a complete OSC 52
47	// clipboard set request from the child. The OSC 52 selection field is
48	// accepted but ignored; the callback receives only the raw base64 payload.
49	// Clipboard queries (data == "?") and malformed payloads are ignored and
50	// never invoke it. Nil disables it.
51	OnClipboard func(b64 string)
52
53	history *History
54	buffer  *buffer
55
56	nextRowID RowID
57
58	defaultFG          renderer.RGB
59	defaultBG          renderer.RGB
60	defaultColorsKnown bool
61	terminalTitle      string
62
63	damage                 []renderer.Damage
64	damageGeneration       uint64
65	damageSaturated        bool
66	damageFullRedrawSticky bool
67	escapeBuf              []byte
68	csiScratch             []int
69	sgrScratch             []int
70
71	scrollTop        int
72	scrollBottom     int
73	savedCursor      cursorState
74	alternate        *screenState
75	syncUpdateActive bool
76	progressState    int
77	cursorVisible    bool
78	cursorStyle      int
79	cursorStyleSet   bool
80	mouseMode        int
81	mouseSGR         bool
82	bracketedPaste   bool
83	originMode       bool
84	insertMode       bool
85	colorSchemeMode  bool
86	colorSchemeLight bool
87	colorSchemeSet   bool
88}
89
90func NewScreen(width, height int) *Screen {
91	s := &Screen{
92		Style:            renderer.DefaultStyle(),
93		damage:           []renderer.Damage{renderer.FullRedraw()},
94		damageGeneration: 1,
95		cursorVisible:    true,
96	}
97	s.buffer = s.newBuffer(width, height)
98	s.Frame = s.buffer.frame
99	s.resetScrollRegion()
100	return s
101}
102
103// NewScreenWithHistory creates a screen that records rows evicted from its
104// primary screen into bounded immutable terminal history.
105func NewScreenWithHistory(width, height int, config HistoryConfig) *Screen {
106	s := NewScreen(width, height)
107	s.history = NewHistory(config)
108	return s
109}
110
111// History returns this screen's terminal history, or nil when history was not
112// configured with NewScreenWithHistory.
113func (s *Screen) History() *History { return s.history }
114
115// LineBounds returns an owned copy of the live grid's per-row logical extents,
116// indexed like Frame rows. It returns nil when the screen has no buffer.
117func (s *Screen) LineBounds() []LineBound {
118	if s == nil || s.buffer == nil {
119		return nil
120	}
121	return append([]LineBound(nil), s.buffer.boundaries...)
122}
123
124func (s *Screen) Write(data []byte) {
125	if len(s.escapeBuf) > 0 {
126		combined := make([]byte, 0, len(s.escapeBuf)+len(data))
127		combined = append(combined, s.escapeBuf...)
128		combined = append(combined, data...)
129		data = combined
130		s.escapeBuf = s.escapeBuf[:0]
131	}
132
133	for len(data) > 0 {
134		if data[0] == 0x1b {
135			consumed, partial := s.consumeEscape(data)
136			if consumed > 0 {
137				data = data[consumed:]
138				continue
139			}
140			if partial {
141				if len(data) <= maxEscapeBufferLen {
142					s.escapeBuf = append(s.escapeBuf[:0], data...)
143				}
144				return
145			}
146		}
147		r, size := utf8.DecodeRune(data)
148		if r == utf8.RuneError && size == 1 {
149			data = data[1:]
150			continue
151		}
152		s.putRune(r)
153		data = data[size:]
154	}
155}
156
157func (s *Screen) consumeEscape(data []byte) (consumed int, partial bool) {
158	if len(data) < 2 {
159		return 0, true
160	}
161	switch data[1] {
162	case ']':
163		return s.consumeOSC(data)
164	case 'P':
165		return consumeSTString(data)
166	case '_', '^', 'X':
167		return consumeSTString(data)
168	case '[':
169		return s.consumeCSI(data)
170	case '=':
171		return 2, false
172	case '>':
173		return 2, false
174	case '7':
175		s.saveCursor()
176		return 2, false
177	case '8':
178		s.restoreCursor()
179		return 2, false
180	case 'D':
181		s.buffer.hard(s.Row)
182		s.index()
183		return 2, false
184	case 'E':
185		s.nextLine()
186		return 2, false
187	case 'M':
188		s.reverseIndex()
189		return 2, false
190	case 'c':
191		s.reset()
192		return 2, false
193	case 'H':
194		return 2, false
195	case '(', ')', '*', '+', '-', '.', '/':
196		if len(data) < 3 {
197			return 0, true
198		}
199		return 3, false
200	default:
201		if data[1] >= 0x30 && data[1] <= 0x7e {
202			return 2, false
203		}
204		return 0, false
205	}
206}
207
208func consumeSTString(data []byte) (consumed int, partial bool) {
209	for i := 2; i < len(data); i++ {
210		if data[i] == 0x1b && i+1 < len(data) && data[i+1] == '\\' {
211			return i + 2, false
212		}
213	}
214	return 0, true
215}

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