v0.4.0 0b9cfda

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		n, err := parseUint(value)
69		if err != nil {
70			return fieldError(key, err)
71		}
72		c.Compression, c.HasCompression = n, 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 || Transmission(value[0]) != TransmissionDirect {
131			return fmt.Errorf("%w: t=%q", ErrUnsupported, value)
132		}
133		c.Transmission, c.HasTransmission = TransmissionDirect, true
134	case ControlCursor:
135		return setUint(&c.Cursor, &c.HasCursor, key, value)
136	case ControlSourceX:
137		return setUint(&c.SourceX, &c.HasSourceX, key, value)
138	case ControlSourceY:
139		return setUint(&c.SourceY, &c.HasSourceY, key, value)
140	case ControlCellOffsetX:
141		return setInt(&c.CellOffsetX, &c.HasCellOffsetX, key, value)
142	case ControlCellOffsetY:
143		return setInt(&c.CellOffsetY, &c.HasCellOffsetY, key, value)
144	case ControlParent:
145		return setUint(&c.Parent, &c.HasParent, key, value)
146	default:
147		// Kitty reserves unknown controls for forward-compatible extensions.
148		// Commands that depend on an unsupported control are rejected later by
149		// their action-specific validation.
150		return nil
151	}
152	return nil
153}
154
155func fieldError(key byte, err error) error {
156	return fmt.Errorf("%w: %c: %w", ErrInvalidCommand, key, err)
157}
158
159func setID(dst *uint64, present *bool, key byte, value []byte) error {
160	if err := setUint(dst, present, key, value); err != nil {
161		return err
162	}
163	if *dst > uint64(^uint32(0)) {
164		return fieldError(key, ErrIntegerOverflow)
165	}
166	return nil
167}
168
169func setUint(dst *uint64, present *bool, key byte, value []byte) error {
170	n, err := parseUint(value)
171	if err != nil {
172		return fieldError(key, err)
173	}
174	if n > MaxKittyID {
175		return fieldError(key, ErrIntegerOverflow)
176	}
177	*dst = n
178	if present != nil {
179		*present = true
180	}
181	return nil
182}
183
184func setInt(dst *int64, present *bool, key byte, value []byte) error {
185	n, err := parseInt(value)
186	if err != nil {
187		return fieldError(key, err)
188	}
189	if n < -1<<31 || n > 1<<31-1 {
190		return fieldError(key, ErrIntegerOverflow)
191	}
192	*dst = n
193	if present != nil {
194		*present = true
195	}
196	return nil
197}
198
199func parseUint(value []byte) (uint64, error) {
200	if len(value) == 0 {
201		return 0, ErrInvalidInteger
202	}
203	var n uint64
204	for _, b := range value {
205		if b < '0' || b > '9' {
206			return 0, ErrInvalidInteger
207		}
208		digit := uint64(b - '0')
209		if n > (^uint64(0)-digit)/10 {
210			return 0, ErrIntegerOverflow
211		}
212		n = n*10 + digit
213	}
214	return n, nil
215}
216
217func parseInt(value []byte) (int64, error) {
218	if len(value) == 0 {
219		return 0, ErrInvalidInteger
220	}
221	negative := value[0] == '-'
222	if negative {
223		value = value[1:]
224		if len(value) == 0 {
225			return 0, ErrInvalidInteger
226		}
227	}
228	n, err := parseUint(value)
229	if err != nil {
230		return 0, err
231	}
232	if negative {
233		if n > uint64(1)<<63 {
234			return 0, ErrIntegerOverflow
235		}
236		if n == uint64(1)<<63 {
237			return -1 << 63, nil
238		}
239		return -int64(n), nil
240	}
241	if n > uint64(^uint64(0)>>1) {
242		return 0, ErrIntegerOverflow
243	}
244	return int64(n), nil
245}

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