main 7a3b92c

markup.go RawHistoryFolder

1package tray
2
3import (
4	"strconv"
5	"strings"
6	"unicode/utf8"
7)
8
9// maxTipBytes bounds a tooltip text sent to the bar.
10const maxTipBytes = 512
11
12// plainText turns the markup an application puts in a tooltip into plain
13// text: tags are dropped, <br> becomes a newline, and the common entities are
14// resolved. The result is cut to maxTipBytes at a rune boundary. A "<" with no
15// closing ">" is kept as text.
16func plainText(s string) string {
17	var b strings.Builder
18	for i := 0; i < len(s) && b.Len() < maxTipBytes+utf8.UTFMax; {
19		switch c := s[i]; {
20		case c == '<':
21			end := strings.IndexByte(s[i:], '>')
22			if end < 0 {
23				b.WriteByte(c)
24				i++
25				continue
26			}
27			if isBreak(s[i+1 : i+end]) {
28				b.WriteByte('\n')
29			}
30			i += end + 1
31		case c == '&':
32			if r, n, ok := entity(s[i:]); ok {
33				b.WriteRune(r)
34				i += n
35				continue
36			}
37			b.WriteByte(c)
38			i++
39		default:
40			b.WriteByte(c)
41			i++
42		}
43	}
44	return cutRunes(b.String(), maxTipBytes)
45}
46
47// isBreak reports whether the inside of a tag is <br>, <br/> or <br />.
48func isBreak(tag string) bool {
49	tag = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(tag), "/"))
50	return strings.EqualFold(tag, "br")
51}
52
53// entity decodes the entity at the start of s ("&amp;", "&#65;", "&#x41;").
54func entity(s string) (r rune, n int, ok bool) {
55	end := strings.IndexByte(s, ';')
56	if end < 2 || end > 10 {
57		return 0, 0, false
58	}
59	switch name := s[1:end]; name {
60	case "amp":
61		return '&', end + 1, true
62	case "lt":
63		return '<', end + 1, true
64	case "gt":
65		return '>', end + 1, true
66	case "quot":
67		return '"', end + 1, true
68	case "apos":
69		return '\'', end + 1, true
70	default:
71		if name[0] != '#' {
72			return 0, 0, false
73		}
74		num, base := name[1:], 10
75		if len(num) > 0 && (num[0] == 'x' || num[0] == 'X') {
76			num, base = num[1:], 16
77		}
78		v, err := strconv.ParseUint(num, base, 32)
79		if err != nil || !utf8.ValidRune(rune(v)) || v == 0 {
80			return 0, 0, false
81		}
82		return rune(v), end + 1, true
83	}
84}
85
86// cutRunes cuts s to at most max bytes without splitting a rune.
87func cutRunes(s string, max int) string {
88	if len(s) <= max {
89		return s
90	}
91	for max > 0 && !utf8.RuneStart(s[max]) {
92		max--
93	}
94	return s[:max]
95}

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 5

Tags 4

↑↓ moveenter openesc close