detached ab6686c

screen_osc.go RawHistoryFolder

1package vt
2
3import (
4	"strconv"
5	"strings"
6
7	renderer "github.com/bnema/vev-vt/core"
8)
9
10const (
11	progressStateClear = iota
12	progressStateNormal
13	progressStateError
14	progressStateIndeterminate
15	progressStatePaused
16)
17
18func (s *Screen) consumeOSC(data []byte) (consumed int, partial bool) {
19	for i := 2; i < len(data); i++ {
20		switch data[i] {
21		case 0x07:
22			s.applyOSC(data[2:i], []byte{0x07})
23			s.handleOSC(data[2:i])
24			return i + 1, false
25		case 0x1b:
26			if i+1 < len(data) && data[i+1] == '\\' {
27				s.applyOSC(data[2:i], []byte{0x1b, '\\'})
28				s.handleOSC(data[2:i])
29				return i + 2, false
30			}
31		}
32	}
33	return 0, true
34}
35
36func (s *Screen) applyOSC(payload, terminator []byte) {
37	if !s.defaultColorsKnown {
38		return
39	}
40
41	var color renderer.RGB
42	switch string(payload) {
43	case "10;?":
44		color = s.defaultFG
45	case "11;?":
46		color = s.defaultBG
47	default:
48		return
49	}
50
51	resp := make([]byte, 0, len(payload)+len("\x1b];rgb:0000/0000/0000")+len(terminator))
52	resp = append(resp, "\x1b]"...)
53	resp = append(resp, payload[:2]...)
54	resp = append(resp, ";rgb:"...)
55	resp = appendOSCColorComponent(resp, color.R)
56	resp = append(resp, '/')
57	resp = appendOSCColorComponent(resp, color.G)
58	resp = append(resp, '/')
59	resp = appendOSCColorComponent(resp, color.B)
60	resp = append(resp, terminator...)
61	s.respond(resp)
62}
63
64func appendOSCColorComponent(dst []byte, c uint8) []byte {
65	const hex = "0123456789abcdef"
66	v := uint16(c)<<8 | uint16(c)
67	return append(dst, hex[v>>12&0xf], hex[v>>8&0xf], hex[v>>4&0xf], hex[v&0xf])
68}
69
70// handleOSC inspects a complete OSC payload (between "ESC ]" and its
71// terminator). Terminal titles, notification sequences (OSC 9, OSC 777
72// "notify"), and clipboard set requests (OSC 52) are acted on; every other OSC
73// is discarded.
74
75func (s *Screen) handleOSC(payload []byte) {
76	if len(payload) >= len("52;") && payload[0] == '5' && payload[1] == '2' && payload[2] == ';' {
77		s.handleOSC52(string(payload[len("52;"):]))
78		return
79	}
80	p := string(payload)
81	if selector, title, ok := strings.Cut(p, ";"); ok && (selector == "0" || selector == "2") {
82		s.terminalTitle = title
83		return
84	}
85	if p == "9;4" || strings.HasPrefix(p, "9;4;") {
86		s.handleProgress(p[len("9;4"):])
87		return
88	}
89	if s.OnNotify == nil {
90		return
91	}
92	switch {
93	case strings.HasPrefix(p, "9;"):
94		s.OnNotify("", p[len("9;"):])
95	case strings.HasPrefix(p, "777;"):
96		parts := strings.SplitN(p[len("777;"):], ";", 3)
97		if parts[0] != "notify" {
98			return
99		}
100		var title, body string
101		if len(parts) > 1 {
102			title = parts[1]
103		}
104		if len(parts) > 2 {
105			body = parts[2]
106		}
107		s.OnNotify(title, body)
108	}
109}
110
111func (s *Screen) handleProgress(rest string) {
112	rest = strings.TrimPrefix(rest, ";")
113	if rest == "" {
114		return
115	}
116	token, _, _ := strings.Cut(rest, ";")
117	state, err := strconv.Atoi(token)
118	if err != nil {
119		return
120	}
121	switch state {
122	case progressStateNormal, progressStateIndeterminate, progressStatePaused:
123		s.progressState = state
124	case progressStateClear:
125		previous := s.progressState
126		s.progressState = state
127		if previous == progressStateNormal || previous == progressStateIndeterminate || previous == progressStatePaused {
128			if s.OnProgress != nil {
129				s.OnProgress(false)
130			}
131		}
132	case progressStateError:
133		previous := s.progressState
134		s.progressState = state
135		if previous != progressStateError {
136			if s.OnProgress != nil {
137				s.OnProgress(true)
138			}
139		}
140	}
141}
142
143// handleOSC52 parses the "<selection>;<data>" remainder of an OSC 52
144// clipboard payload (selection may be empty; split on the first ";"). A
145// clipboard query (data == "?") is always ignored — vev never answers
146// clipboard queries. A payload with no second ";" is malformed and ignored.
147
148func (s *Screen) handleOSC52(rest string) {
149	_, data, ok := strings.Cut(rest, ";")
150	if !ok {
151		return
152	}
153	if data == "?" {
154		return
155	}
156	if s.OnClipboard != nil {
157		s.OnClipboard(data)
158	}
159}

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