v0.6.0 b250c5e

controls.go RawHistoryFolder

1package kittygraphics
2
3import (
4	"fmt"
5)
6
7// ParseControls parses the comma-separated control header of an APC.
8func ParseControls(header []byte) (Controls, error) { return parseControls(header) }
9
10// ParseHeader is an alias for ParseControls.
11func ParseHeader(header []byte) (Controls, error) { return ParseControls(header) }
12
13func parseControls(header []byte) (Controls, error) {
14	var c Controls
15	if len(header) == 0 {
16		return c, nil
17	}
18	var seen [256]bool
19	start := 0
20	for i := 0; i <= len(header); i++ {
21		if i != len(header) && header[i] != ',' {
22			continue
23		}
24		field := header[start:i]
25		if len(field) < 3 || field[1] != '=' {
26			return Controls{}, fmt.Errorf("%w: control %q", ErrInvalidCommand, field)
27		}
28		key := field[0]
29		if seen[key] {
30			return Controls{}, fmt.Errorf("%w: %c", ErrDuplicateControl, key)
31		}
32		seen[key] = true
33		value := field[2:]
34		if err := parseControl(&c, key, value); err != nil {
35			return Controls{}, err
36		}
37		start = i + 1
38	}
39	if c.HasAction && !c.Action.Valid() {
40		return Controls{}, fmt.Errorf("%w: %q", ErrUnknownAction, c.Action)
41	}
42	if c.HasImageID && c.HasImageNumber {
43		return Controls{}, fmt.Errorf("%w: image id and image number are mutually exclusive", ErrInvalidCommand)
44	}
45	if c.HasQuiet && c.Quiet > QuietAll {
46		return Controls{}, fmt.Errorf("%w: quiet=%d", ErrInvalidCommand, c.Quiet)
47	}
48	if c.HasMore && c.More > 1 {
49		return Controls{}, fmt.Errorf("%w: m=%d", ErrInvalidCommand, c.More)
50	}
51	if c.HasCursor && c.Cursor > 1 {
52		return Controls{}, fmt.Errorf("%w: C=%d", ErrInvalidCommand, c.Cursor)
53	}
54	return c, nil
55}
56
57func parseControl(c *Controls, key byte, value []byte) error {
58	if len(value) == 0 {
59		return fmt.Errorf("%w: empty %c value", ErrInvalidInteger, key)
60	}
61	switch ControlKey(key) {
62	case ControlAction:
63		if len(value) != 1 {
64			return fmt.Errorf("%w: action %q", ErrUnknownAction, value)
65		}
66		c.Action, c.HasAction = Action(value[0]), true
67	case ControlCompression:
68		if len(value) != 1 {
69			return fmt.Errorf("%w: o=%q", ErrInvalidCommand, value)
70		}
71		// Preserve unknown one-byte algorithms so Session can return ENOTSUP.
72		c.Compression, c.HasCompression = Compression(value[0]), true
73	case ControlFormat:
74		n, err := parseUint(value)
75		if err != nil {
76			return fieldError(key, err)
77		}
78		f := Format(n)
79		if !f.Valid() {
80			return fmt.Errorf("%w: f=%d", ErrInvalidCommand, n)
81		}
82		c.Format, c.HasFormat = f, true
83	case ControlImageID:
84		return setID(&c.ImageID, &c.HasImageID, key, value)
85	case ControlImageNumber:
86		return setID(&c.ImageNumber, &c.HasImageNumber, key, value)
87	case ControlMore:
88		return setUint(&c.More, &c.HasMore, key, value)
89	case ControlQuiet:
90		n, err := parseUint(value)
91		if err != nil {
92			return fieldError(key, err)
93		}
94		if n > uint64(QuietAll) {
95			return fmt.Errorf("%w: quiet=%d", ErrInvalidCommand, n)
96		}
97		c.Quiet, c.HasQuiet = Quiet(n), true
98	case ControlWidth:
99		return setUint(&c.Width, &c.HasWidth, key, value)
100	case ControlHeight:
101		return setUint(&c.Height, &c.HasHeight, key, value)
102	case ControlColumns:
103		return setUint(&c.Columns, &c.HasColumns, key, value)
104	case ControlRows:
105		return setUint(&c.Rows, &c.HasRows, key, value)
106	case ControlX:
107		return setUint(&c.X, &c.HasX, key, value)
108	case ControlY:
109		return setUint(&c.Y, &c.HasY, key, value)
110	case ControlSourceWidth:
111		return setUint(&c.SourceWidth, &c.HasSourceWidth, key, value)
112	case ControlSourceHeight:
113		return setUint(&c.SourceHeight, &c.HasSourceHeight, key, value)
114	case ControlLayer:
115		return setInt(&c.Layer, &c.HasLayer, key, value)
116	case ControlPlacementID:
117		return setID(&c.PlacementID, &c.HasPlacementID, key, value)
118	case ControlDelete:
119		if len(value) != 1 {
120			return fmt.Errorf("%w: d=%q", ErrInvalidCommand, value)
121		}
122		d := DeleteTarget(value[0])
123		switch d {
124		case DeleteImage, DeleteImageNumber, DeleteCell, DeleteAll, DeleteAllImages, DeleteCellAll:
125			c.Delete, c.HasDelete = d, true
126		default:
127			return fmt.Errorf("%w: d=%q", ErrInvalidCommand, value)
128		}
129	case ControlTransmission:
130		if len(value) != 1 {
131			return fmt.Errorf("%w: t=%q", ErrInvalidCommand, value)
132		}
133		// Preserve unsupported transports long enough for Session to emit the
134		// protocol ENOTSUP response. Rejecting them during header parsing leaves
135		// capability probes such as kitten icat waiting until timeout.
136		c.Transmission, c.HasTransmission = Transmission(value[0]), true
137	case ControlCursor:
138		return setUint(&c.Cursor, &c.HasCursor, key, value)
139	case ControlSourceX:
140		return setUint(&c.SourceX, &c.HasSourceX, key, value)
141	case ControlSourceY:
142		return setUint(&c.SourceY, &c.HasSourceY, key, value)
143	case ControlCellOffsetX:
144		return setInt(&c.CellOffsetX, &c.HasCellOffsetX, key, value)
145	case ControlCellOffsetY:
146		return setInt(&c.CellOffsetY, &c.HasCellOffsetY, key, value)
147	case ControlParent:
148		return setUint(&c.Parent, &c.HasParent, key, value)
149	default:
150		// Kitty reserves unknown controls for forward-compatible extensions.
151		// Commands that depend on an unsupported control are rejected later by
152		// their action-specific validation.
153		return nil
154	}
155	return nil
156}
157
158func fieldError(key byte, err error) error {
159	return fmt.Errorf("%w: %c: %w", ErrInvalidCommand, key, err)
160}
161
162func setID(dst *uint64, present *bool, key byte, value []byte) error {
163	if err := setUint(dst, present, key, value); err != nil {
164		return err
165	}
166	if *dst > uint64(^uint32(0)) {
167		return fieldError(key, ErrIntegerOverflow)
168	}
169	return nil
170}
171
172func setUint(dst *uint64, present *bool, key byte, value []byte) error {
173	n, err := parseUint(value)
174	if err != nil {
175		return fieldError(key, err)
176	}
177	if n > MaxKittyID {
178		return fieldError(key, ErrIntegerOverflow)
179	}
180	*dst = n
181	if present != nil {
182		*present = true
183	}
184	return nil
185}
186
187func setInt(dst *int64, present *bool, key byte, value []byte) error {
188	n, err := parseInt(value)
189	if err != nil {
190		return fieldError(key, err)
191	}
192	if n < -1<<31 || n > 1<<31-1 {
193		return fieldError(key, ErrIntegerOverflow)
194	}
195	*dst = n
196	if present != nil {
197		*present = true
198	}
199	return nil
200}
201
202func parseUint(value []byte) (uint64, error) {
203	if len(value) == 0 {
204		return 0, ErrInvalidInteger
205	}
206	var n uint64
207	for _, b := range value {
208		if b < '0' || b > '9' {
209			return 0, ErrInvalidInteger
210		}
211		digit := uint64(b - '0')
212		if n > (^uint64(0)-digit)/10 {
213			return 0, ErrIntegerOverflow
214		}
215		n = n*10 + digit
216	}
217	return n, nil
218}
219
220func parseInt(value []byte) (int64, error) {
221	if len(value) == 0 {
222		return 0, ErrInvalidInteger
223	}
224	negative := value[0] == '-'
225	if negative {
226		value = value[1:]
227		if len(value) == 0 {
228			return 0, ErrInvalidInteger
229		}
230	}
231	n, err := parseUint(value)
232	if err != nil {
233		return 0, err
234	}
235	if negative {
236		if n > uint64(1)<<63 {
237			return 0, ErrIntegerOverflow
238		}
239		if n == uint64(1)<<63 {
240			return -1 << 63, nil
241		}
242		return -int64(n), nil
243	}
244	if n > uint64(^uint64(0)>>1) {
245		return 0, ErrIntegerOverflow
246	}
247	return int64(n), nil
248}

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