main 7a3b92c

control.go RawHistoryFolder

1package module
2
3import (
4	"encoding/json"
5	"errors"
6	"fmt"
7	"unicode"
8	"unicode/utf8"
9)
10
11// Limits of a control line. ParseControl refuses what exceeds them.
12const (
13	// MaxMenuDepth is how many levels of submenus a menu may nest.
14	MaxMenuDepth = 8
15	// MaxMenuItems is how many items a whole menu may hold, submenus included.
16	MaxMenuItems = 512
17	// MaxLabel is the longest item label or tooltip title, in bytes; longer
18	// ones are cut at a rune boundary.
19	MaxLabel = 256
20	// MaxText is the longest tooltip body, in bytes; longer ones are cut at a
21	// rune boundary.
22	MaxText = 1024
23	// MaxRows and MaxCells bound the table of a tooltip; a cell is cut like a
24	// label.
25	MaxRows  = 32
26	MaxCells = 4
27	// maxColumn bounds the column and width of a control line.
28	maxColumn = 1 << 20
29)
30
31// Control types.
32const (
33	ControlTooltip = "tooltip"
34	ControlMenu    = "menu"
35	ControlClose   = "close"
36)
37
38// Menu item kinds.
39const (
40	KindNormal    = "normal"
41	KindSeparator = "separator"
42	KindCheck     = "check"
43	KindRadio     = "radio"
44)
45
46// Control is one parsed control line of a script.
47type Control struct {
48	Type  string
49	Col   int // first cell of the anchor inside the module
50	Width int // cells of the anchor
51	// Title and Body are the text of a tooltip, Rows its table: one entry
52	// per row, one string per cell.
53	Title, Body string
54	Rows        [][]string
55	// Click is the token of the press a menu answers.
56	Click uint32
57	Items []MenuItem
58}
59
60// MenuItem is one entry of a menu.
61type MenuItem struct {
62	ID      int32
63	Label   string
64	Kind    string
65	Enabled bool
66	Checked bool
67	Items   []MenuItem // a submenu
68}
69
70type wireControl struct {
71	Type  string     `json:"type"`
72	Col   int        `json:"col"`
73	Width int        `json:"width"`
74	Title string     `json:"title"`
75	Body  string     `json:"body"`
76	Rows  [][]string `json:"rows"`
77	Click uint32     `json:"click"`
78	Items []wireItem `json:"items"`
79}
80
81type wireItem struct {
82	ID      int32      `json:"id"`
83	Label   string     `json:"label"`
84	Kind    string     `json:"kind"`
85	Enabled *bool      `json:"enabled"`
86	Checked bool       `json:"checked"`
87	Items   []wireItem `json:"items"`
88}
89
90// ParseControl reads the JSON of a control line (see TakeControl) and checks
91// every limit. Control characters are removed from the texts, labels are cut
92// to MaxLabel bytes, and an item without "enabled" is enabled.
93func ParseControl(b []byte) (Control, error) {
94	if len(b) > MaxControl {
95		return Control{}, fmt.Errorf("control line of %d bytes exceeds %d", len(b), MaxControl)
96	}
97	var w wireControl
98	if err := json.Unmarshal(b, &w); err != nil {
99		return Control{}, fmt.Errorf("control line: %w", err)
100	}
101	if w.Col < 0 || w.Col > maxColumn || w.Width < 0 || w.Width > maxColumn {
102		return Control{}, errors.New("control line: column or width out of range")
103	}
104	c := Control{Type: w.Type, Col: w.Col, Width: w.Width}
105	switch w.Type {
106	case ControlClose:
107		return c, nil
108	case ControlTooltip:
109		if w.Width == 0 {
110			return Control{}, errors.New("tooltip: width must be positive")
111		}
112		c.Title = clean(w.Title, MaxLabel, false)
113		c.Body = clean(w.Body, MaxText, true)
114		if len(w.Rows) > MaxRows {
115			return Control{}, fmt.Errorf("tooltip: more than %d rows", MaxRows)
116		}
117		if len(w.Rows) > 0 {
118			c.Rows = make([][]string, 0, len(w.Rows))
119		}
120		text := c.Title != "" || c.Body != ""
121		for _, row := range w.Rows {
122			if len(row) > MaxCells {
123				return Control{}, fmt.Errorf("tooltip: more than %d cells in a row", MaxCells)
124			}
125			cells := make([]string, len(row))
126			for i, s := range row {
127				cells[i] = clean(s, MaxLabel, false)
128				text = text || cells[i] != ""
129			}
130			c.Rows = append(c.Rows, cells)
131		}
132		if !text {
133			return Control{}, errors.New("tooltip: no text")
134		}
135		return c, nil
136	case ControlMenu:
137		if w.Width == 0 {
138			return Control{}, errors.New("menu: width must be positive")
139		}
140		c.Click = w.Click
141		count := 0
142		items, err := convertItems(w.Items, 1, &count)
143		if err != nil {
144			return Control{}, fmt.Errorf("menu: %w", err)
145		}
146		if len(items) == 0 {
147			return Control{}, errors.New("menu: no items")
148		}
149		c.Items = items
150		return c, nil
151	}
152	return Control{}, fmt.Errorf("control line: unknown type %q", w.Type)
153}
154
155func convertItems(in []wireItem, depth int, count *int) ([]MenuItem, error) {
156	if len(in) == 0 {
157		return nil, nil
158	}
159	if depth > MaxMenuDepth {
160		return nil, fmt.Errorf("more than %d levels", MaxMenuDepth)
161	}
162	out := make([]MenuItem, 0, len(in))
163	for i := range in {
164		if *count++; *count > MaxMenuItems {
165			return nil, fmt.Errorf("more than %d items", MaxMenuItems)
166		}
167		w := &in[i]
168		it := MenuItem{ID: w.ID, Label: clean(w.Label, MaxLabel, false), Kind: w.Kind, Enabled: w.Enabled == nil || *w.Enabled, Checked: w.Checked}
169		if w.ID < 0 {
170			return nil, fmt.Errorf("item %d: negative id", w.ID)
171		}
172		switch w.Kind {
173		case "":
174			it.Kind = KindNormal
175		case KindNormal, KindSeparator, KindCheck, KindRadio:
176		default:
177			return nil, fmt.Errorf("item %d: unknown kind %q", w.ID, w.Kind)
178		}
179		sub, err := convertItems(w.Items, depth+1, count)
180		if err != nil {
181			return nil, err
182		}
183		it.Items = sub
184		out = append(out, it)
185	}
186	return out, nil
187}
188
189// clean drops control characters (and anything Unicode calls a format or
190// unassigned control, such as bidi overrides), keeps newlines when keepNL, and
191// cuts the result to max bytes at a rune boundary.
192func clean(s string, max int, keepNL bool) string {
193	needs := len(s) > max
194	for _, r := range s {
195		if r == utf8.RuneError || unicode.IsControl(r) || unicode.Is(unicode.Cf, r) {
196			needs = true
197			break
198		}
199	}
200	if !needs {
201		return s
202	}
203	out := make([]byte, 0, min(len(s), max))
204	for _, r := range s {
205		switch {
206		case r == '\n' && keepNL:
207		case r == utf8.RuneError, unicode.IsControl(r), unicode.Is(unicode.Cf, r):
208			continue
209		}
210		if len(out)+utf8.RuneLen(r) > max {
211			break
212		}
213		out = utf8.AppendRune(out, r)
214	}
215	return string(out)
216}

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