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}