v0.7.1 ff50ee4

kittygraphics.go RawHistoryFolder

1package kittygraphics
2
3import (
4	"errors"
5	"math"
6	"sort"
7	"sync"
8
9	"github.com/bnema/vev-vt/graphics"
10)
11
12var (
13	ErrInvalidAPC        = errors.New("invalid kitty graphics APC")
14	ErrAPCTooLarge       = errors.New("kitty graphics APC is too large")
15	ErrAPCTruncated      = errors.New("truncated kitty graphics APC")
16	ErrInvalidCommand    = errors.New("invalid kitty graphics command")
17	ErrUnknownAction     = errors.New("unknown kitty graphics action")
18	ErrUnknownControl    = errors.New("unknown kitty graphics control")
19	ErrDuplicateControl  = errors.New("duplicate kitty graphics control")
20	ErrInvalidInteger    = errors.New("invalid kitty graphics integer")
21	ErrIntegerOverflow   = errors.New("kitty graphics integer overflow")
22	ErrInvalidBase64     = errors.New("invalid kitty graphics base64")
23	ErrInvalidPNG        = errors.New("invalid kitty graphics PNG")
24	ErrPayloadTooLarge   = errors.New("kitty graphics payload is too large")
25	ErrTooManyChunks     = errors.New("too many kitty graphics continuation chunks")
26	ErrInterleavedUpload = errors.New("interleaved kitty graphics upload")
27	ErrUnsupported       = errors.New("unsupported kitty graphics command")
28	ErrImageNotFound     = errors.New("kitty graphics image not found")
29	ErrPlacementNotFound = errors.New("kitty graphics placement not found")
30	ErrNoScene           = errors.New("kitty graphics scene is nil")
31)
32
33const (
34	// DefaultMaxAPCBytes bounds the complete framed APC, including its
35	// controls and encoded payload.
36	DefaultMaxAPCBytes = uint64(32 << 20)
37	// DefaultMaxPayloadBytes bounds encoded bytes in one image, before base64
38	// decoding. The graphics scene applies its own encoded-byte budget too.
39	DefaultMaxPayloadBytes = uint64(24 << 20)
40	DefaultMaxUploadBytes  = uint64(16 << 20)
41	DefaultMaxChunks       = uint64(1024)
42	DefaultMaxPixels       = uint64(128 << 20)
43	DefaultMaxDecodedBytes = uint64(256 << 20)
44	DefaultMaxDimension    = uint64(1 << 20)
45)
46
47// Limits bounds parser and adapter resource use. Zero values select bounded
48// defaults. The limits are independent from the graphics scene limits: both
49// layers enforce their budgets.
50type Limits struct {
51	MaxAPCBytes      uint64
52	MaxPayloadBytes  uint64
53	MaxUploadBytes   uint64
54	MaxChunks        uint64
55	MaxDecodedPixels uint64
56	MaxDecodedBytes  uint64
57	MaxDimension     uint64
58	MaxResponseBytes uint64
59	MaxImages        uint64
60	MaxPlacements    uint64
61}
62
63// Config is an alias retained for callers that use constructor terminology.
64type Config = Limits
65
66func normalizeLimits(l Limits) Limits {
67	if l.MaxAPCBytes == 0 {
68		l.MaxAPCBytes = DefaultMaxAPCBytes
69	}
70	if l.MaxPayloadBytes == 0 {
71		l.MaxPayloadBytes = DefaultMaxPayloadBytes
72	}
73	if l.MaxUploadBytes == 0 {
74		l.MaxUploadBytes = DefaultMaxUploadBytes
75	}
76	if l.MaxChunks == 0 {
77		l.MaxChunks = DefaultMaxChunks
78	}
79	if l.MaxDecodedPixels == 0 {
80		l.MaxDecodedPixels = DefaultMaxPixels
81	}
82	if l.MaxDecodedBytes == 0 {
83		l.MaxDecodedBytes = DefaultMaxDecodedBytes
84	}
85	if l.MaxDimension == 0 {
86		l.MaxDimension = DefaultMaxDimension
87	}
88	if l.MaxResponseBytes == 0 {
89		l.MaxResponseBytes = 4096
90	}
91	if l.MaxImages == 0 {
92		l.MaxImages = 1024
93	}
94	if l.MaxPlacements == 0 {
95		l.MaxPlacements = 65536
96	}
97	return l
98}
99
100// Action is the closed set of Kitty graphics actions understood by this
101// adapter. The underlying byte values are the protocol values.
102type Action byte
103
104const (
105	ActionTransmit        Action = 't'
106	ActionTransmitDisplay Action = 'T'
107	ActionPut             Action = 'p'
108	ActionQuery           Action = 'q'
109	ActionDelete          Action = 'd'
110	ActionFrame           Action = 'f'
111	ActionCompose         Action = 'a'
112
113	// Descriptive aliases used by callers that prefer the protocol names.
114	ActionTransmitAndDisplay = ActionTransmitDisplay
115	ActionDisplay            = ActionPut
116)
117
118func (a Action) Valid() bool {
119	switch a {
120	case ActionTransmit, ActionTransmitDisplay, ActionPut, ActionQuery, ActionDelete, ActionFrame, ActionCompose:
121		return true
122	default:
123		return false
124	}
125}
126
127func (a Action) String() string {
128	if !a.Valid() {
129		return "unknown"
130	}
131	return string([]byte{byte(a)})
132}
133
134// Format is the closed set of Kitty image formats supported by this adapter.
135type Format uint64
136
137const (
138	FormatRGB  Format = 24
139	FormatRGBA Format = 32
140	FormatPNG  Format = 100
141
142	FormatRGB24  = FormatRGB
143	FormatRGBA32 = FormatRGBA
144)
145
146func (f Format) Valid() bool { return f == FormatRGB || f == FormatRGBA || f == FormatPNG }
147
148// Compression is the optional compression applied to raw image data.
149type Compression byte
150
151const CompressionZlib Compression = 'z'
152
153// Quiet controls protocol response emission. Kitty uses q=1 for successful
154// operations only and q=2 for all responses.
155type Quiet uint8
156
157const (
158	QuietNever   Quiet = 0
159	QuietSuccess Quiet = 1
160	QuietAll     Quiet = 2
161
162	QuietNone       = QuietNever
163	QuietErrorsOnly = QuietSuccess
164	QuietEverything = QuietAll
165)
166
167// DeleteTarget is the closed set of targets accepted by a delete action.
168type DeleteTarget byte
169
170const (
171	DeleteImage       DeleteTarget = 'i'
172	DeleteImageNumber DeleteTarget = 'I'
173	DeleteCell        DeleteTarget = 'p'
174	DeleteAll         DeleteTarget = 'a'
175	DeleteAllImages   DeleteTarget = 'A'
176	DeleteCellAll     DeleteTarget = 'P'
177)
178
179// Transmission is the direct transmission mode. File, temporary-file and
180// shared-memory transmissions are intentionally not accepted by this adapter.
181type Transmission byte
182
183const TransmissionDirect Transmission = 'd'
184
185// ControlKey identifies a recognized Kitty control field.
186type ControlKey byte
187
188const (
189	ControlAction       ControlKey = 'a'
190	ControlCompression  ControlKey = 'o'
191	ControlFormat       ControlKey = 'f'
192	ControlImageID      ControlKey = 'i'
193	ControlImageNumber  ControlKey = 'I'
194	ControlMore         ControlKey = 'm'
195	ControlQuiet        ControlKey = 'q'
196	ControlWidth        ControlKey = 's'
197	ControlHeight       ControlKey = 'v'
198	ControlColumns      ControlKey = 'c'
199	ControlRows         ControlKey = 'r'
200	ControlX            ControlKey = 'x'
201	ControlY            ControlKey = 'y'
202	ControlSourceWidth  ControlKey = 'w'
203	ControlSourceHeight ControlKey = 'h'
204	ControlLayer        ControlKey = 'z'
205	ControlPlacementID  ControlKey = 'p'
206	ControlDelete       ControlKey = 'd'
207	ControlTransmission ControlKey = 't'
208	ControlCursor       ControlKey = 'C'
209	ControlSourceX      ControlKey = 'X'
210	ControlSourceY      ControlKey = 'Y'
211	ControlCellOffsetX  ControlKey = 'H'
212	ControlCellOffsetY  ControlKey = 'V'
213	ControlParent       ControlKey = 'P'
214)
215
216// Controls is the typed, closed representation of a Kitty control header.
217// Has* fields preserve the distinction between an omitted field and zero.
218type Controls struct {
219	Action          Action
220	HasAction       bool
221	Compression     Compression
222	HasCompression  bool
223	Format          Format
224	HasFormat       bool
225	ImageID         uint64
226	HasImageID      bool
227	ImageNumber     uint64
228	HasImageNumber  bool
229	More            uint64
230	HasMore         bool
231	Quiet           Quiet
232	HasQuiet        bool
233	Width           uint64
234	HasWidth        bool
235	Height          uint64
236	HasHeight       bool
237	Columns         uint64
238	HasColumns      bool
239	Rows            uint64
240	HasRows         bool
241	X               uint64
242	HasX            bool
243	Y               uint64
244	HasY            bool
245	SourceWidth     uint64
246	HasSourceWidth  bool
247	SourceHeight    uint64
248	HasSourceHeight bool
249	Layer           int64
250	HasLayer        bool
251	PlacementID     uint64
252	HasPlacementID  bool
253	Delete          DeleteTarget
254	HasDelete       bool
255	Transmission    Transmission
256	HasTransmission bool
257	Cursor          uint64
258	HasCursor       bool
259	SourceX         uint64
260	HasSourceX      bool
261	SourceY         uint64
262	HasSourceY      bool
263	CellOffsetX     int64
264	HasCellOffsetX  bool
265	CellOffsetY     int64
266	HasCellOffsetY  bool
267	Parent          uint64
268	HasParent       bool
269}
270
271// Command is one complete APC graphics command. Payload is the encoded
272// command payload and is copied from the parser input.
273type Command struct {
274	Controls Controls
275	Payload  []byte
276}
277
278// EventKind identifies parser output. Text events contain bytes which are not
279// Kitty graphics APCs; command events contain complete commands.
280type EventKind uint8
281
282const (
283	EventText EventKind = iota + 1
284	EventCommand
285	EventError
286)
287
288// Event is emitted by Parser. Text and command payloads are owned by the
289// event and remain valid after the next Feed call.
290type Event struct {
291	Kind    EventKind
292	Text    []byte
293	Command Command
294	Err     error
295}
296
297// ParserConfig is a descriptive alias for Limits.
298type ParserConfig = Limits
299
300// Parser incrementally recognizes ESC _ G APCs. It never interprets ordinary
301// text or other escape sequences and can be fed arbitrary byte-sized chunks.
302type Parser struct {
303	limits    Limits
304	candidate []byte
305	apc       []byte
306	inAPC     bool
307	escaped   bool
308	oversize  bool
309}
310
311// NewParser creates an incremental bounded parser.
312func NewParser(config ...Limits) *Parser {
313	var l Limits
314	if len(config) != 0 {
315		l = config[0]
316	}
317	return &Parser{limits: normalizeLimits(l)}
318}
319
320// Limits reports the parser limits.
321func (p *Parser) Limits() Limits {
322	if p == nil {
323		return Limits{}
324	}
325	return p.limits
326}
327
328// MaxInt64 is exported for callers constructing checked protocol geometry.
329const MaxInt64 = int64(math.MaxInt64)
330
331// MaxKittyID is the largest protocol image or placement identifier.
332const MaxKittyID = uint64(^uint32(0))
333
334// Session owns protocol-to-scene mappings. It is safe for one owner to call
335// Feed and query methods serially; the mutex also makes read-only mapping
336// access safe while a caller publishes a snapshot.
337type Session struct {
338	mu            sync.Mutex
339	scene         *graphics.Scene
340	limits        Limits
341	parser        *Parser
342	images        map[uint64]graphics.AssetID
343	imageNumbers  map[uint64]uint64
344	placements    map[placementKey]graphics.PlacementID
345	children      map[uint64]Child
346	nextChild     uint64
347	nextPlacement uint64
348	upload        *upload
349	origin        placementOrigin
350}
351
352// Adapter is an alias for the stateful Kitty graphics session.
353type Adapter = Session
354
355// Child describes a session-local image child and its graphics asset. Child
356// IDs are never sent to the terminal; they make the protocol mapping explicit
357// and stable for consumers.
358type Child struct {
359	ID      uint64
360	ImageID uint64
361	AssetID graphics.AssetID
362}
363
364// NewSession constructs an adapter over scene. A nil scene is allowed so the
365// parser can still be used for validation, but commands which mutate a scene
366// return ErrNoScene.
367func NewSession(scene *graphics.Scene, config ...Limits) *Session {
368	var l Limits
369	if len(config) != 0 {
370		l = config[0]
371	}
372	l = normalizeLimits(l)
373	return &Session{
374		scene:         scene,
375		limits:        l,
376		parser:        NewParser(l),
377		images:        make(map[uint64]graphics.AssetID),
378		imageNumbers:  make(map[uint64]uint64),
379		placements:    make(map[placementKey]graphics.PlacementID),
380		children:      make(map[uint64]Child),
381		nextChild:     1,
382		nextPlacement: 1,
383	}
384}
385
386// NewAdapter is an adapter-named constructor alias.
387func NewAdapter(scene *graphics.Scene, config ...Limits) *Session {
388	return NewSession(scene, config...)
389}
390
391// New is a concise constructor alias for NewSession.
392func New(scene *graphics.Scene, config ...Limits) *Session { return NewSession(scene, config...) }
393
394// Image returns the graphics asset mapped from a Kitty image ID.
395func (s *Session) Image(imageID uint64) (graphics.AssetID, bool) {
396	if s == nil {
397		return graphics.AssetID{}, false
398	}
399	s.mu.Lock()
400	defer s.mu.Unlock()
401	id, ok := s.images[imageID]
402	return id, ok
403}
404
405// Asset is an alias for Image.
406func (s *Session) Asset(imageID uint64) (graphics.AssetID, bool) { return s.Image(imageID) }
407
408// Placement returns the graphics placement mapped from one Kitty image and
409// placement-ID pair. Kitty placement IDs are scoped to their image ID.
410func (s *Session) Placement(imageID, placementID uint64) (graphics.PlacementID, bool) {
411	if s == nil {
412		return graphics.PlacementID{}, false
413	}
414	s.mu.Lock()
415	defer s.mu.Unlock()
416	id, ok := s.placements[placementKey{imageID: imageID, placementID: placementID}]
417	return id, ok
418}
419
420// PlacementID is an alias for Placement.
421func (s *Session) PlacementID(imageID, placementID uint64) (graphics.PlacementID, bool) {
422	return s.Placement(imageID, placementID)
423}
424
425// Child returns the oldest session child associated with an image ID.
426func (s *Session) Child(imageID uint64) (Child, bool) {
427	if s == nil {
428		return Child{}, false
429	}
430	s.mu.Lock()
431	defer s.mu.Unlock()
432	var selected Child
433	found := false
434	for _, child := range s.children {
435		if child.ImageID == imageID && (!found || child.ID < selected.ID) {
436			selected, found = child, true
437		}
438	}
439	return selected, found
440}
441
442// ChildByID returns a session child by its adapter-local ID.
443func (s *Session) ChildByID(childID uint64) (Child, bool) {
444	if s == nil {
445		return Child{}, false
446	}
447	s.mu.Lock()
448	defer s.mu.Unlock()
449	child, ok := s.children[childID]
450	return child, ok
451}
452
453// ChildID returns the adapter-local child ID associated with an image ID.
454func (s *Session) ChildID(imageID uint64) (uint64, bool) {
455	child, ok := s.Child(imageID)
456	if !ok {
457		return 0, false
458	}
459	return child.ID, true
460}
461
462// Children returns a stable copy of all current session children in child-ID
463// order.
464func (s *Session) Children() []Child {
465	if s == nil {
466		return nil
467	}
468	s.mu.Lock()
469	defer s.mu.Unlock()
470	children := make([]Child, 0, len(s.children))
471	for _, child := range s.children {
472		children = append(children, child)
473	}
474	sort.Slice(children, func(i, j int) bool { return children[i].ID < children[j].ID })
475	return children
476}

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