v0.4.1 03674e0

screen_modes.go RawHistoryFolder

1package vt
2
3import (
4	renderer "github.com/bnema/vev-vt/core"
5	"github.com/bnema/vev-vt/protocol/kittygraphics"
6)
7
8type screenState struct {
9	frame               renderer.Frame
10	buffer              *buffer
11	row                 int
12	col                 int
13	style               renderer.Style
14	scrollTop           int
15	scrollBottom        int
16	savedCursor         cursorState
17	graphics            *screenGraphicsState
18	kittyPendingDisplay *kittygraphics.Controls
19	originMode          bool
20	insertMode          bool
21}
22
23// SyncUpdateActive reports whether DEC private mode 2026 (synchronized update)
24// is currently enabled by the child process.
25func (s *Screen) SyncUpdateActive() bool { return s.syncUpdateActive }
26
27// BracketedPasteMode reports whether DEC private mode 2004 is currently enabled
28// by the child process.
29func (s *Screen) BracketedPasteMode() bool { return s.bracketedPaste }
30
31// ColorSchemeMode reports whether DEC private mode 2031 is currently enabled.
32func (s *Screen) ColorSchemeMode() bool { return s.colorSchemeMode }
33
34// SetColorScheme updates the host color scheme and notifies subscribed child apps.
35func (s *Screen) SetColorScheme(light bool) {
36	if s.colorSchemeSet && s.colorSchemeLight == light {
37		return
38	}
39	s.colorSchemeSet = true
40	s.colorSchemeLight = light
41	if s.colorSchemeMode {
42		s.respond(s.colorSchemeReport())
43	}
44}
45
46// ClearColorScheme marks the host color scheme as unknown. Future child color
47// scheme queries are silent until SetColorScheme supplies a known value again.
48func (s *Screen) ClearColorScheme() {
49	s.colorSchemeSet = false
50}
51
52// ForceSyncEnd forcibly leaves DEC private mode 2026 (synchronized update).
53// Hosts use this as a safety valve if a child enters synchronized update mode
54// and never sends the matching end sequence.
55func (s *Screen) ForceSyncEnd() { s.syncUpdateActive = false }
56
57// SetDefaultColors sets the terminal default foreground/background colors used
58// to answer child OSC 10/11 color queries. Passing ok=false makes color queries
59// silent until known colors are supplied again.
60func (s *Screen) SetDefaultColors(fg, bg renderer.RGB, ok bool) {
61	s.defaultFG = fg
62	s.defaultBG = bg
63	s.defaultColorsKnown = ok
64}
65
66func (s *Screen) CursorRow() int { return s.Row }
67func (s *Screen) CursorCol() int { return s.Col }
68func (s *Screen) CursorVisible() bool {
69	return s.cursorVisible
70}
71func (s *Screen) CursorStyle() (int, bool) { return s.cursorStyle, s.cursorStyleSet }
72func (s *Screen) MouseMode() (int, bool)   { return s.mouseMode, s.mouseSGR }
73func (s *Screen) AltScreenActive() bool    { return s.alternate != nil }
74
75// TerminalTitle returns the latest title set by OSC 0 or OSC 2.
76func (s *Screen) TerminalTitle() string { return s.terminalTitle }
77
78func (s *Screen) respond(b []byte) {
79	if s.OnResponse != nil {
80		s.OnResponse(b)
81	}
82}
83
84func (s *Screen) colorSchemeReport() []byte {
85	if !s.colorSchemeSet {
86		return nil
87	}
88	if s.colorSchemeLight {
89		return []byte(ColorSchemeReportLight)
90	}
91	return []byte(ColorSchemeReportDark)
92}
93
94func (s *Screen) reset() {
95	s.buffer = s.newBuffer(s.Frame.Width, s.Frame.Height)
96	s.Frame = s.buffer.frame
97	s.Row, s.Col = 0, 0
98	s.Style = renderer.DefaultStyle()
99	s.escapeBuf = s.escapeBuf[:0]
100	s.kittyDiscard = false
101	s.kittyDiscardEscaped = false
102	s.kittyPendingDisplay = nil
103	s.savedCursor = cursorState{}
104	s.alternate = nil
105	s.graphics = nil
106	s.originMode = false
107	s.insertMode = false
108	s.cursorVisible = true
109	s.cursorStyle = 0
110	s.cursorStyleSet = false
111	s.mouseMode = 0
112	s.mouseSGR = false
113	s.bracketedPaste = false
114	s.colorSchemeMode = false
115	s.resetScrollRegion()
116	s.fullRedraw()
117}
118
119func (s *Screen) setScrollRegion(parts []int) {
120	if s.Frame.Height == 0 {
121		return
122	}
123	top, bottom := 1, s.Frame.Height
124	if len(parts) > 0 && parts[0] > 0 {
125		top = parts[0]
126	}
127	if len(parts) > 1 && parts[1] > 0 {
128		bottom = parts[1]
129	}
130	if top >= bottom {
131		s.resetScrollRegion()
132	} else {
133		s.scrollTop = clamp(top-1, 0, s.Frame.Height-1)
134		s.scrollBottom = clamp(bottom-1, 0, s.Frame.Height-1)
135		if s.scrollTop >= s.scrollBottom {
136			s.resetScrollRegion()
137		}
138	}
139	s.homeCursor()
140}
141
142func (s *Screen) resetScrollRegion() {
143	s.scrollTop = 0
144	if s.Frame.Height > 0 {
145		s.scrollBottom = s.Frame.Height - 1
146	} else {
147		s.scrollBottom = 0
148	}
149}
150
151func (s *Screen) setMode(private bool, parts []int, enabled bool) {
152	if !private {
153		for _, mode := range parts {
154			switch mode {
155			case 4:
156				s.insertMode = enabled
157			default:
158				// Other ANSI modes (for example LNM) are intentionally ignored
159				// until the screen model implements their observable behavior.
160				continue
161			}
162		}
163		return
164	}
165	for _, mode := range parts {
166		switch mode {
167		case 6:
168			s.originMode = enabled
169			s.homeCursor()
170		case 47, 1047, 1049:
171			if enabled {
172				s.enterAlternateScreen()
173			} else {
174				s.exitAlternateScreen()
175			}
176		case 2026:
177			s.syncUpdateActive = enabled
178		case 2031:
179			s.colorSchemeMode = enabled
180		case 25:
181			s.cursorVisible = enabled
182		case 1000, 1002, 1003:
183			if enabled {
184				s.mouseMode = mode
185			} else if s.mouseMode == mode {
186				s.mouseMode = 0
187			}
188		case 1006:
189			s.mouseSGR = enabled
190		case 2004:
191			s.bracketedPaste = enabled
192		case 1, 1004, 1005:
193			// Trackable terminal modes that do not directly affect the current
194			// cell model yet. Consuming them prevents mode bytes from leaking.
195			continue
196		}
197	}
198}
199
200func (s *Screen) enterAlternateScreen() {
201	if s.alternate == nil {
202		s.alternate = &screenState{
203			frame:               cloneFrame(s.Frame),
204			buffer:              s.buffer.clone(),
205			row:                 s.Row,
206			col:                 s.Col,
207			style:               s.Style,
208			scrollTop:           s.scrollTop,
209			scrollBottom:        s.scrollBottom,
210			savedCursor:         s.savedCursor,
211			graphics:            s.graphics,
212			kittyPendingDisplay: s.kittyPendingDisplay,
213			originMode:          s.originMode,
214			insertMode:          s.insertMode,
215		}
216	}
217	s.graphics = nil
218	s.kittyPendingDisplay = nil
219	s.buffer = s.newBuffer(s.Frame.Width, s.Frame.Height)
220	s.Frame = s.buffer.frame
221	s.Row, s.Col = 0, 0
222	s.Style = renderer.DefaultStyle()
223	s.savedCursor = cursorState{}
224	s.resetScrollRegion()
225	s.fullRedraw()
226}
227
228func (s *Screen) exitAlternateScreen() {
229	if s.alternate == nil {
230		return
231	}
232	state := s.alternate
233	s.buffer = state.buffer
234	if s.buffer == nil {
235		s.buffer = bufferFromFrame(cloneFrame(state.frame))
236		s.fillMissingRowIDs(s.buffer)
237	}
238	s.Frame = s.buffer.frame
239	s.Row = clamp(state.row, 0, s.Frame.Height-1)
240	s.Col = clamp(state.col, 0, s.Frame.Width-1)
241	s.Style = state.style
242	s.scrollTop = state.scrollTop
243	s.scrollBottom = state.scrollBottom
244	s.savedCursor = state.savedCursor
245	s.graphics = state.graphics
246	s.kittyPendingDisplay = state.kittyPendingDisplay
247	s.originMode = state.originMode
248	s.insertMode = state.insertMode
249	s.alternate = nil
250	s.fullRedraw()
251}

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