Kitty Graphics Protocol Discussion #5683
anthonykim1
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Here's the updated inventory in the exact same format — only changed items that the code now supports:
Kitty Graphics Protocol — Feature Inventory
Spec: https://sw.kovidgoyal.net/kitty/graphics-protocol/
Coming from: #5619
Key files:
KittyGraphicsHandler.tsKittyGraphicsTypes.tsKittyImageStorage.tsImageStorage.tsImageRenderer.tsflowchart TD A["Shell sends Kitty escape sequence<br/>ESC _ G a=T,f=32,s=2,v=2,i=1;<br/>base64... ESC \\"] A --> B B["ApcParser matches 'G'<br/>→ KittyGraphicsHandler<br/>registered in ImageAddon.ts"] B --> C C["start — reset state"] C --> D D["put — two-phase parsing<br/>1. Scan for semicolon,<br/>buffer keys<br/>2. parseKittyCommand<br/>extracts keys<br/>3. Stream payload into<br/>Base64Decoder"] D --> E E["end — finalize"] E --> F{"More chunks?<br/>m=1"} F -->|"Yes"| G["Park decoder in<br/>_pendingTransmissions<br/>keeps decoder alive<br/>across sequences"] G -.->|"Next sequence"| C F -->|"No"| H{"Action?"} H -->|"a=t transmit"| I["_handleTransmit<br/>KittyImageStorage.storeImage<br/>Blob copies bytes off JS heap"] H -->|"a=T transmit+display"| J["_handleTransmitDisplay<br/>storeImage as Blob<br/>then render"] H -->|"a=q query"| K["_handleQuery<br/>validate format/size<br/>respond OK or EINVAL"] H -->|"a=p placement"| P["_handlePlacement<br/>getImage from storage<br/>_displayImage<br/>respond OK or ENOENT"] H -->|"a=d delete"| L J --> M["_decodeAndDisplay"] P --> M M --> M1{"Image format?"} M1 -->|"f=100 PNG"| M1a["Wrap as image/png Blob<br/>browser decodes natively"] M1 -->|"f=32 RGBA"| M1b["Raw bytes → ImageData<br/>already has alpha"] M1 -->|"f=24 RGB"| M1c["uint32 block interleave<br/>RGB → RGBA<br/>adds alpha=0xFF000000"] M1a --> CROP M1b --> CROP M1c --> CROP CROP["Source crop x,y,w,h<br/>createImageBitmap crop<br/>if specified"] CROP --> BMP BMP["createImageBitmap<br/>GPU-backed render-ready<br/>object for efficient<br/>canvas drawing"] BMP --> M2["Resize via<br/>createImageBitmap<br/>if c/r columns/rows<br/>specified"] M2 --> OFFSET["Sub-cell offset X,Y<br/>Composite onto padded<br/>canvas if specified"] OFFSET --> M3["KittyImageStorage.addImage<br/>→ ImageStorage.addImage<br/>Tiles bitmap into cells,<br/>marks each with<br/>image+tile ID.<br/>Adds scroll marker<br/>for cleanup."] M3 --> M4["KittyImageStorage<br/>bridges Kitty ID<br/>→ internal storage ID<br/>via _kittyIdToStorageId<br/>+ _storageIdToKittyId"] M4 --> M5{"Cursor<br/>movement<br/>C key"} M5 -->|"C=0 default"| M6["Advance cursor<br/>past image"] M5 -->|"C=1"| M7["Restore cursor<br/>with scroll correction"] subgraph LAYER["Layer Selection"] Z1{"z key?"} Z1 -->|"z < 0"| Z2["bottom layer<br/>(behind text)"] Z1 -->|"z >= 0 or absent"| Z3["top layer<br/>(on top of text)"] end M2 --> Z1 Z2 --> M3 Z3 --> M3 subgraph DEL["_handleDelete — selector dispatch"] L1{"d key selector?"} L1 -->|"d=a / d=A"| L2["Abort ALL in-flight decoders<br/>KittyImageStorage.deleteAll()"] L1 -->|"d=i / d=I"| L3["Abort targeted decoder<br/>KittyImageStorage.deleteById()"] L1 -->|"default (no d key)"| L2 L1 -->|"other (c,n,p,…)"| L4["Ignored (not yet supported)"] L2 --> L5["Delete Blob from _images<br/>Delete bitmap from ImageStorage<br/>Clear kittyId↔storageId maps"] L3 --> L5 end L --> L1 subgraph EVICT["Eviction & Cleanup (KittyImageStorage)"] EV1["onImageDeleted callback<br/>fires when ImageStorage<br/>evicts/deletes an image"] EV1 --> EV2["_handleStorageImageDeleted<br/>cleans _kittyIdToStorageId<br/>_storageIdToKittyId<br/>_images"] EV3["storeImage with same ID<br/>deletes old placement<br/>from ImageStorage first"] EV4["_evictUndisplayedImages<br/>when _images exceeds 256<br/>preferentially removes<br/>images without placements"] end I --> RESP M5 --> RESP K --> RESP RESP["_sendResponse<br/>OK or EINVAL to client<br/>q=1 suppresses OK<br/>q=2 suppresses errors"] RESP --> DONE["Client receives response"]Legend: ✅ Done |⚠️ Partial | ❌ TODO | 🚫 Blocked (renderer/architecture limitation)
1. Escape Code Format (APC)
ESC_G<control>;<payload>ESC\registerApcHandler(0x47, ...)inImageAddon.tskey=valuecontrol data parsingparseKittyCommand()inKittyGraphicsTypes.tsBase64DecoderinKittyGraphicsHandler.ts2. Actions (
akey)a=tKittyImageStorage.storeImage(). SendsGi=<id>;OKwheniprovideda=Ta=qa=p_handlePlacementretrieves stored image via_kittyStorage.getImage(), calls_displayImage(). Supports all display features (c/r, C, z, x/y/w/h, X/Y). Responds OK on success, ENOENT when image not found. 20 integration testsa=da=ddispatches to_handleDelete.dkey (delete selector) is fully parsed —d=a/d=Adeletes all,d=i/d=Ideletes by image ID. Default selector is'a'per spec. Cascades to ImageStorage viaKittyImageStorage. 7 unit tests ford/pkey parsing + 14 integration tests for delete dispatcha=fa=aa=c3. Image Formats (
fkey)f=32KittyFormat.RGBAper specf=24f=100Blob(type:'image/png')→createImageBitmap4. Transmission Medium (
tkey)t=dtkey parsed astransmission. Explicitly checks and acceptst=d. 13 integration tests for medium rejectiont=ft=tt=s5. Compression (
okey)o=zDecompressionStream('deflate')with fallback to'deflate-raw'. Applied after base64 decode, before pixel interpretationSkey validation for PNG + compressionSkey." Functionally works without it but conformance gap6. Chunked Transmission (
mkey)m=1/m=0)_pendingTransmissionsmap accumulates chunks via shared decoderIPendingTransmission.totalEncodedSizem+qpending.cmdstores first chunk's command. Subsequent chunks' extra keys are silently ignored._lastPendingKeytracks most recent pending upload for chunks withouti=a=f7. Image Display Layout
KittyImageStorage.addImage()→ImageStorage.addImage()c,rcreateImageBitmap; stretches to fill placement rectangle per spec. If only one of c/r specified, computes the other from image aspect ratiox,y,w,hx,yused for crop origin,w,hparsed assourceWidth/sourceHeightfor crop dimensions. UsescreateImageBitmap(bitmap, cropX, cropY, finalCropW, finalCropH). Out-of-boundsx/yproduces no display.w=0/h=0treated as unset (full width/height). Negative values clamped to 0. 8 integration testsX,YX/Yparsed asxOffset/yOffset. Composited onto padded canvas viadrawImage(bitmap, xOffset, yOffset). Clamped to cell dimensions. When c/r explicit, placement area stays c×cw × r×ch and offset image is clipped. 4 integration testsCCkey parsed. Default (C=0): cursor at (originX + cols, last row) per spec. C=1 restores cursor with scroll correction. 10 integration testszzkey parsed. Negative z → bottom layer (behind text), z≥0 → top layer. Draw calls sorted by z-index so lower z draws first and higher z renders on top. 9 integration tests8. Image IDs & Placement IDs
iKittyImageStorage._imagesmap keyinot specified_nextImageId++inKittyImageStorageIi+Iconflict validated. ButGi=<id>,I=<num>;OKresponse not sent, andI-only commands don't do image number lookupi+Iconflict → EINVALput()and_handleNoPayloadCommand()pplacementIdinparseKittyCommand(). Used in_sendResponse(response includes,p=<pid>when provided). Placement-level delete (d=i with p=) not yet implementeda=pallows placing the same transmitted image multiple times at different cursor positions. Each placement creates a separate entry in shared ImageStorage. Image data persists across placements. 2 integration testsKittyImageStoragemanages bidirectional_kittyIdToStorageId/_storageIdToKittyIdmaps.onImageDeletedcallback keeps maps in sync when ImageStorage evicts images. Used by delete, re-transmit, and eviction flowsKittyImageStorage.storeImage()checks for existing_kittyIdToStorageIdentry and calls_storage.deleteImage()on old placement before storing new data. 2 integration tests (re-transmit with a=T, re-transmit with a=t then a=T)9. Delete Commands (
a=d,dkey)a/Adkey parsed viadeleteSelector. Default selector is'a'per spec.KittyImageStorage.deleteAll()clears_images, iterates_kittyIdToStorageIdto delete from ImageStorage, clears maps. Also aborts all in-flight uploads. 4 integration tests (d=a,d=A, storage cleanup, pixel cleanup)i/Id=i/d=Idispatched via switch/case.KittyImageStorage.deleteById()removes from_images, cascades to ImageStorage. Aborts only the targeted in-flight upload. 6 integration tests (d=i,d=I, storage cleanup, pixel cleanup, no-id edge case, targeted abort)i/I + ppkey parsed but placement IDs not fully implementedn/Nc/Cp/Pq/Qr/Rx/Xy/Yz/Zf/FKittyGraphicsHandler.tsand KittyGraphics.test.ts_handleDeletereleases decoders and clears_pendingTransmissionsbefore deleting images.d=a/d=Aaborts all uploads;d=i/d=Iaborts only the targeted upload. 4 integration tests10. Response / Acknowledgement System
ESC_Gi=<id>;OK ESC\)a=qa=tsends OK synchronously after storing blob.a=Tsends OK after async display via.then(). Only responds whencmd.idprovided. Respectsq=1/q=2i+Iconflict, missing dimensions, unsupported transmission medium, unsupported actionq=1(suppress OK)q=2(suppress errors)a=p_handlePlacementsendsENOENT:image not foundwhen_kittyStorage.getImage()returns undefined. q=1 still reports ENOENT (only suppresses OK). q=2 suppresses ENOENT. 3 integration testsGi=<id>,p=<pid>;OK)_sendResponseincludes,p=<pid>whenplacementIdis provided. 1 integration test verifying\x1b_Gi=224,p=42;OK\x1b\\formatGi=<id>,I=<num>;OK)11. Unicode Placeholders
U+10EEEEplaceholder characterU=1)12. Relative Placements
P,QH,V13. Animation
a=fcx,y,s,vzY(RGBA)X=1a=a, s, va=a,c=<frame>a=cz=<neg>14. Terminal Interaction
reset()clearsKittyImageStoragemaps and_pendingTransmissions. Wired via DECSTR, RIS,onRequestResetESC[2JonImageDeletedcallback →KittyImageStorage._handleStorageImageDeletedcascades cleanup to_images,_kittyIdToStorageId,_storageIdToKittyId. No stale entrieswipeAlternate()calls_delImg()which firesonImageDeleted→KittyImageStorage._handleStorageImageDeletedcleans kitty maps. Fully cascadedESC[14t/ESC[16t)15. Memory & Storage
KittyImageStorageenforces_maxStoredImages = 256cap.onImageDeletedcallback keeps kitty maps in sync when ImageStorage evicts. 2 integration tests (memory limit eviction, scrollback eviction)IKittyImageData.dataisBlobkittySizeLimitoption enforced during streaming decodeKittyImageStorage._evictUndisplayedImages()preferentially removes images that have no_kittyIdToStorageIdentry (i.e., not displayed)KittyImageStorage._imagesAND renderedImageBitmaplives inImageStorage. Two separate stores but now coordinated viaonImageDeletedcallback. Still two copies of data — post-MVP to unify16. Control Data Keys — Parse Status
Click to expand full key table
at,T,q,p,dhandled.f,a,cnot handledfttransmission.t=daccepted;t=f,t=t,t=srejected with EINVAL. 13 integration testssvSoz(zlib/deflate)miIpplacementId. Used in_sendResponse(included in OK/error response). Placement-level deletion not yet implementedqx_decodeAndDisplayy_decodeAndDisplaywsourceWidth. Crop width in_decodeAndDisplay. 0 treated as unsethsourceHeight. Crop height in_decodeAndDisplay. 0 treated as unsetXxOffset. Sub-cell pixel offset within first cell. Composited via padded canvasYyOffset. Sub-cell pixel offset within first cell. Composited via padded canvascrCzzIndex. Negative → bottom layer, ≥0 → top layer. Stored in ImageSpec. Draw calls sorted by z-index. 9 integration testsddeleteSelector. Dispatchesa/A(all),i/I(by ID). Default'a'per specPQHVUMVP Checklist (Feb 9 – 20)
Items from the chart above that need work before initial merge. Ordered by priority.
P0 — Clients break without these
Ckey parsed. Default (C=0): cursor at (originX + cols, lastRow). C=1: cursor restored. Fixed proportional resize to spec-correct stretch. 10 integration tests (5 unskipped + 5 updated). 2 unit tests for C key parsing.P1 — Important for correctness
KittyGraphicsHandlerandIIPHandlercreate independent wasmBase64Decoderinstances. Avoid duplicate wasm memory. (PR feedback)Done
_handlePlacementretrieves stored image via_kittyStorage.getImage(), calls_displayImage()with full display pipeline (crop, resize, offset, z-index, cursor). Responds OK on success, ENOENT when image not found. Supports c/r, C, z, x/y/w/h, X/Y, and placement ID in response. 20 integration tests.x,yas crop origin.w,hparsed assourceWidth/sourceHeightfor crop dimensions. UsescreateImageBitmap(bitmap, cropX, cropY, finalCropW, finalCropH). Out-of-bounds x/y produces no display. w=0/h=0 treated as unset. Negative values clamped to 0. Combinable with c/r and X/Y. 8 integration tests.X/Yparsed asxOffset/yOffset. Composited onto padded canvas. Clamped to cell dimensions. When c/r explicit, placement area stays c×cw × r×ch and offset image is clipped. Without c/r, padded canvas determines natural cell dimensions. 4 integration tests._sendResponseincludes,p=<pid>whenplacementIdprovided. 1 integration test.a=pallows placing same transmitted image at different cursor positions. Each placement creates separate ImageStorage entry. Image data persists across placements. 2 integration tests.a=p—_handlePlacementsendsENOENT:image not foundwhen image not in storage. q=1 still reports ENOENT (only suppresses OK). q=2 suppresses ENOENT. 3 integration tests._sendResponseincludes,p=<pid>whenplacementIdprovided. 1 integration test verifying\x1b_Gi=224,p=42;OK\x1b\\format.KittyImageStoragemanages bidirectional_kittyIdToStorageId/_storageIdToKittyId.onImageDeletedcallback keeps maps in sync when ImageStorage evicts. Delete cascades to ImageStorage.KittyImageStorage.storeImage()checks for existing_kittyIdToStorageIdentry and calls_storage.deleteImage()on old placement before storing new data. 2 integration tests.dkey + delete dispatch —DELETE_SELECTORandPLACEMENT_IDadded toKittyKey.deleteSelectorparsed as string,placementIdparsed as number._handleDeletedispatches via switch/case:d=a/d=A→KittyImageStorage.deleteAll(),d=i/d=I→KittyImageStorage.deleteById(). Default selector is'a'per spec.d=aaborts all in-flight uploads;d=iaborts only targeted upload. 7 unit tests ford/pkey parsing. 14 integration tests for delete dispatch including pixel-level verification. TODOs for lowercase/uppercase distinction.KittyImageStorage.deleteById()calls_storage.deleteImage()for displayed images.deleteImage()public API on ImageStorage — Thin wrapper around_delImg+ marker dispose.a=t/a=T—a=tsends OK in dispatcher after_handleTransmit.a=Tsends OK after async display via.then(). Only whencmd.idprovided. Respectsq=1/q=2._handleTransmitnow defaults toKittyFormat.RGBA(f=32) per spec instead of PNG._handleDeletereleases decoders and clears_pendingTransmissions.d=a/d=Aaborts all;d=i/d=Iaborts only targeted. 4 integration tests.Ckey parsed. Default (C=0): cursor at (originX + cols, lastRow). C=1: cursor restored. Fixed proportional resize to spec-correct stretch. 10 integration tests (5 unskipped + 5 updated). 2 unit tests for C key parsing.ESC[2J— ImageStorage's marker disposal firesonImageDeletedcallback →KittyImageStorage._handleStorageImageDeletedcascades cleanup to_images,_kittyIdToStorageId,_storageIdToKittyId. No more stale entries.wipeAlternate()calls_delImg()which firesonImageDeleted→KittyImageStorage._handleStorageImageDeletedcleans kitty maps. Fully cascaded.KittyImageStorageenforces_maxStoredImages = 256cap with_evictUndisplayedImages().onImageDeletedcallback keeps kitty maps in sync when ImageStorage evicts by pixel limit or scrollback. 2 integration tests (memory limit eviction, scrollback eviction).KittyImageStorage._evictUndisplayedImages()preferentially removes images that have no_kittyIdToStorageIdentry.zkey parsed. Negative z → bottom layer (behind text), z≥0 → top layer.ImageStorage.addImage()acceptslayerandzIndexparams. Draw calls sorted by z-index. 9 integration tests including DOM order verification.tkey parsing + rejection —tkey parsed astransmissionviaKittyKey.TRANSMISSION.t=daccepted;t=f/t=t/t=srejected with EINVAL for both transmit and query actions. 13 integration tests.Beta Was this translation helpful? Give feedback.
All reactions