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// Quiet controls protocol response emission. Kitty uses q=1 for successful
149// operations only and q=2 for all responses.
150type Quiet uint8
151
152const (
153 QuietNever Quiet = 0
154 QuietSuccess Quiet = 1
155 QuietAll Quiet = 2
156
157 QuietNone = QuietNever
158 QuietErrorsOnly = QuietSuccess
159 QuietEverything = QuietAll
160)
161
162// DeleteTarget is the closed set of targets accepted by a delete action.
163type DeleteTarget byte
164
165const (
166 DeleteImage DeleteTarget = 'i'
167 DeleteImageNumber DeleteTarget = 'I'
168 DeleteCell DeleteTarget = 'p'
169 DeleteAll DeleteTarget = 'a'
170 DeleteAllImages DeleteTarget = 'A'
171 DeleteCellAll DeleteTarget = 'P'
172)
173
174// Transmission is the direct transmission mode. File, temporary-file and
175// shared-memory transmissions are intentionally not accepted by this adapter.
176type Transmission byte
177
178const TransmissionDirect Transmission = 'd'
179
180// ControlKey identifies a recognized Kitty control field.
181type ControlKey byte
182
183const (
184 ControlAction ControlKey = 'a'
185 ControlCompression ControlKey = 'o'
186 ControlFormat ControlKey = 'f'
187 ControlImageID ControlKey = 'i'
188 ControlImageNumber ControlKey = 'I'
189 ControlMore ControlKey = 'm'
190 ControlQuiet ControlKey = 'q'
191 ControlWidth ControlKey = 's'
192 ControlHeight ControlKey = 'v'
193 ControlColumns ControlKey = 'c'
194 ControlRows ControlKey = 'r'
195 ControlX ControlKey = 'x'
196 ControlY ControlKey = 'y'
197 ControlSourceWidth ControlKey = 'w'
198 ControlSourceHeight ControlKey = 'h'
199 ControlLayer ControlKey = 'z'
200 ControlPlacementID ControlKey = 'p'
201 ControlDelete ControlKey = 'd'
202 ControlTransmission ControlKey = 't'
203 ControlCursor ControlKey = 'C'
204 ControlSourceX ControlKey = 'X'
205 ControlSourceY ControlKey = 'Y'
206 ControlCellOffsetX ControlKey = 'H'
207 ControlCellOffsetY ControlKey = 'V'
208 ControlParent ControlKey = 'P'
209)
210
211// Controls is the typed, closed representation of a Kitty control header.
212// Has* fields preserve the distinction between an omitted field and zero.
213type Controls struct {
214 Action Action
215 HasAction bool
216 Compression uint64
217 HasCompression bool
218 Format Format
219 HasFormat bool
220 ImageID uint64
221 HasImageID bool
222 ImageNumber uint64
223 HasImageNumber bool
224 More uint64
225 HasMore bool
226 Quiet Quiet
227 HasQuiet bool
228 Width uint64
229 HasWidth bool
230 Height uint64
231 HasHeight bool
232 Columns uint64
233 HasColumns bool
234 Rows uint64
235 HasRows bool
236 X uint64
237 HasX bool
238 Y uint64
239 HasY bool
240 SourceWidth uint64
241 HasSourceWidth bool
242 SourceHeight uint64
243 HasSourceHeight bool
244 Layer int64
245 HasLayer bool
246 PlacementID uint64
247 HasPlacementID bool
248 Delete DeleteTarget
249 HasDelete bool
250 Transmission Transmission
251 HasTransmission bool
252 Cursor uint64
253 HasCursor bool
254 SourceX uint64
255 HasSourceX bool
256 SourceY uint64
257 HasSourceY bool
258 CellOffsetX int64
259 HasCellOffsetX bool
260 CellOffsetY int64
261 HasCellOffsetY bool
262 Parent uint64
263 HasParent bool
264}
265
266// Command is one complete APC graphics command. Payload is the encoded
267// command payload and is copied from the parser input.
268type Command struct {
269 Controls Controls
270 Payload []byte
271}
272
273// EventKind identifies parser output. Text events contain bytes which are not
274// Kitty graphics APCs; command events contain complete commands.
275type EventKind uint8
276
277const (
278 EventText EventKind = iota + 1
279 EventCommand
280 EventError
281)
282
283// Event is emitted by Parser. Text and command payloads are owned by the
284// event and remain valid after the next Feed call.
285type Event struct {
286 Kind EventKind
287 Text []byte
288 Command Command
289 Err error
290}
291
292// ParserConfig is a descriptive alias for Limits.
293type ParserConfig = Limits
294
295// Parser incrementally recognizes ESC _ G APCs. It never interprets ordinary
296// text or other escape sequences and can be fed arbitrary byte-sized chunks.
297type Parser struct {
298 limits Limits
299 candidate []byte
300 apc []byte
301 inAPC bool
302 escaped bool
303 oversize bool
304}
305
306// NewParser creates an incremental bounded parser.
307func NewParser(config ...Limits) *Parser {
308 var l Limits
309 if len(config) != 0 {
310 l = config[0]
311 }
312 return &Parser{limits: normalizeLimits(l)}
313}
314
315// Limits reports the parser limits.
316func (p *Parser) Limits() Limits {
317 if p == nil {
318 return Limits{}
319 }
320 return p.limits
321}
322
323// MaxInt64 is exported for callers constructing checked protocol geometry.
324const MaxInt64 = int64(math.MaxInt64)
325
326// MaxKittyID is the largest protocol image or placement identifier.
327const MaxKittyID = uint64(^uint32(0))
328
329// Session owns protocol-to-scene mappings. It is safe for one owner to call
330// Feed and query methods serially; the mutex also makes read-only mapping
331// access safe while a caller publishes a snapshot.
332type Session struct {
333 mu sync.Mutex
334 scene *graphics.Scene
335 limits Limits
336 parser *Parser
337 images map[uint64]graphics.AssetID
338 imageNumbers map[uint64]uint64
339 placements map[placementKey]graphics.PlacementID
340 children map[uint64]Child
341 nextChild uint64
342 nextPlacement uint64
343 upload *upload
344 origin placementOrigin
345}
346
347// Adapter is an alias for the stateful Kitty graphics session.
348type Adapter = Session
349
350// Child describes a session-local image child and its graphics asset. Child
351// IDs are never sent to the terminal; they make the protocol mapping explicit
352// and stable for consumers.
353type Child struct {
354 ID uint64
355 ImageID uint64
356 AssetID graphics.AssetID
357}
358
359// NewSession constructs an adapter over scene. A nil scene is allowed so the
360// parser can still be used for validation, but commands which mutate a scene
361// return ErrNoScene.
362func NewSession(scene *graphics.Scene, config ...Limits) *Session {
363 var l Limits
364 if len(config) != 0 {
365 l = config[0]
366 }
367 l = normalizeLimits(l)
368 return &Session{
369 scene: scene,
370 limits: l,
371 parser: NewParser(l),
372 images: make(map[uint64]graphics.AssetID),
373 imageNumbers: make(map[uint64]uint64),
374 placements: make(map[placementKey]graphics.PlacementID),
375 children: make(map[uint64]Child),
376 nextChild: 1,
377 nextPlacement: 1,
378 }
379}
380
381// NewAdapter is an adapter-named constructor alias.
382func NewAdapter(scene *graphics.Scene, config ...Limits) *Session {
383 return NewSession(scene, config...)
384}
385
386// New is a concise constructor alias for NewSession.
387func New(scene *graphics.Scene, config ...Limits) *Session { return NewSession(scene, config...) }
388
389// Image returns the graphics asset mapped from a Kitty image ID.
390func (s *Session) Image(imageID uint64) (graphics.AssetID, bool) {
391 if s == nil {
392 return graphics.AssetID{}, false
393 }
394 s.mu.Lock()
395 defer s.mu.Unlock()
396 id, ok := s.images[imageID]
397 return id, ok
398}
399
400// Asset is an alias for Image.
401func (s *Session) Asset(imageID uint64) (graphics.AssetID, bool) { return s.Image(imageID) }
402
403// Placement returns the graphics placement mapped from one Kitty image and
404// placement-ID pair. Kitty placement IDs are scoped to their image ID.
405func (s *Session) Placement(imageID, placementID uint64) (graphics.PlacementID, bool) {
406 if s == nil {
407 return graphics.PlacementID{}, false
408 }
409 s.mu.Lock()
410 defer s.mu.Unlock()
411 id, ok := s.placements[placementKey{imageID: imageID, placementID: placementID}]
412 return id, ok
413}
414
415// PlacementID is an alias for Placement.
416func (s *Session) PlacementID(imageID, placementID uint64) (graphics.PlacementID, bool) {
417 return s.Placement(imageID, placementID)
418}
419
420// Child returns the oldest session child associated with an image ID.
421func (s *Session) Child(imageID uint64) (Child, bool) {
422 if s == nil {
423 return Child{}, false
424 }
425 s.mu.Lock()
426 defer s.mu.Unlock()
427 var selected Child
428 found := false
429 for _, child := range s.children {
430 if child.ImageID == imageID && (!found || child.ID < selected.ID) {
431 selected, found = child, true
432 }
433 }
434 return selected, found
435}
436
437// ChildByID returns a session child by its adapter-local ID.
438func (s *Session) ChildByID(childID uint64) (Child, bool) {
439 if s == nil {
440 return Child{}, false
441 }
442 s.mu.Lock()
443 defer s.mu.Unlock()
444 child, ok := s.children[childID]
445 return child, ok
446}
447
448// ChildID returns the adapter-local child ID associated with an image ID.
449func (s *Session) ChildID(imageID uint64) (uint64, bool) {
450 child, ok := s.Child(imageID)
451 if !ok {
452 return 0, false
453 }
454 return child.ID, true
455}
456
457// Children returns a stable copy of all current session children in child-ID
458// order.
459func (s *Session) Children() []Child {
460 if s == nil {
461 return nil
462 }
463 s.mu.Lock()
464 defer s.mu.Unlock()
465 children := make([]Child, 0, len(s.children))
466 for _, child := range s.children {
467 children = append(children, child)
468 }
469 sort.Slice(children, func(i, j int) bool { return children[i].ID < children[j].ID })
470 return children
471}