Kitty Graphics Protocol in OCaml
terminal graphics ocaml
1(** Kitty Terminal Graphics Protocol 2 3 This library implements the Kitty terminal graphics protocol, allowing 4 OCaml programs to display images in terminals that support the protocol 5 (Kitty, WezTerm, Konsole, Ghostty, etc.). 6 7 {1 Protocol Overview} 8 9 The Kitty Graphics Protocol is a flexible, performant protocol for rendering 10 arbitrary pixel (raster) graphics in terminal emulators. Key features: 11 12 - No requirement for terminal emulators to understand image formats 13 - Pixel-level positioning of graphics 14 - Integration with text (graphics can be drawn below/above text with alpha blending) 15 - Automatic scrolling with text 16 - Animation support with frame deltas for efficiency 17 18 {2 Escape Sequence Format} 19 20 All graphics commands use the Application Programming Command (APC) format: 21 22 {v <ESC>_G<control data>;<payload><ESC> v} 23 24 Where: 25 - [ESC _G] is the APC start sequence (bytes 0x1B 0x5F 0x47) 26 - Control data is comma-separated key=value pairs 27 - Payload is base64-encoded binary data (RFC-4648) 28 - [ESC] is the APC terminator (bytes 0x1B 0x5C) 29 30 Most terminal emulators ignore unrecognized APC sequences, making the 31 protocol safe to use even in unsupported terminals. 32 33 {2 Terminal Responses} 34 35 When an image ID is specified, the terminal responds: 36 - On success: [ESC _Gi=ID;OK ESC] 37 - On failure: [ESC _Gi=ID;error ESC] 38 39 Common error codes include [ENOENT] (image not found), [EINVAL] (invalid 40 parameter), and [ENOSPC] (storage quota exceeded). 41 42 {2 Image Storage} 43 44 Terminal emulators maintain a storage quota for images (typically ~320MB). 45 When the quota is exceeded, older images are deleted to make room for new 46 ones. Images without active placements are preferred for deletion. 47 48 For animations, frame data is stored separately with a larger quota 49 (typically 5x the base quota). 50 51 {2 Basic Usage} 52 53 {[ 54 (* Display a PNG image *) 55 let png_data = read_file "image.png" in 56 let cmd = Kgp.transmit_and_display ~format:`Png () in 57 let buf = Buffer.create 1024 in 58 Kgp.write buf cmd ~data:png_data; 59 print_string (Buffer.contents buf) 60 ]} 61 62 {[ 63 (* Transmit an image, then display it multiple times *) 64 let png_data = read_file "icon.png" in 65 let cmd = Kgp.transmit ~image_id:1 ~format:`Png () in 66 Kgp.write buf cmd ~data:png_data; 67 68 (* Display at different positions *) 69 let cmd = Kgp.display ~image_id:1 () in 70 Kgp.write buf cmd ~data:""; 71 ]} 72 73 {2 Protocol Reference} 74 75 See {{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics Protocol} 76 for the full specification. *) 77 78(** {1 Type Modules} *) 79 80module Format = Kgp_format 81module Transmission = Kgp_transmission 82module Compression = Kgp_compression 83module Quiet = Kgp_quiet 84module Cursor = Kgp_cursor 85module Composition = Kgp_composition 86module Delete = Kgp_delete 87module Animation_state = Kgp_animation_state 88 89(** {1 Configuration Modules} *) 90 91module Placement = Kgp_placement 92module Frame = Kgp_frame 93module Animation = Kgp_animation 94module Compose = Kgp_compose 95 96(** {1 Commands} *) 97 98type command = Kgp_command.t 99(** A graphics protocol command. Commands are built using the functions below 100 and then serialized using {!write} or {!to_string}. *) 101 102(** {2 Image Transmission} 103 104 Images can be transmitted to the terminal for storage and later display. 105 The terminal assigns storage and responds with success or failure. 106 107 For large images, the library automatically handles chunked transmission 108 (splitting data into 4096-byte base64-encoded chunks). *) 109 110val transmit : 111 ?image_id:int -> 112 ?image_number:int -> 113 ?format:Format.t -> 114 ?transmission:Transmission.t -> 115 ?compression:Compression.t -> 116 ?width:int -> 117 ?height:int -> 118 ?size:int -> 119 ?offset:int -> 120 ?quiet:Quiet.t -> 121 unit -> 122 command 123(** Transmit image data without displaying. 124 125 The image is stored by the terminal and can be displayed later using 126 {!val:display} with the same [image_id]. 127 128 @param image_id Unique identifier (1-4294967295) for later reference. 129 If specified, the terminal responds with success/failure. 130 @param image_number Alternative to [image_id] where the terminal assigns 131 a unique ID and returns it in the response. Useful when multiple 132 programs share the terminal. 133 @param format Pixel format of the data. Default is [`Rgba32]. 134 @param transmission How data is sent. Default is [`Direct] (inline). 135 @param compression Compression applied to data. Default is [`None]. 136 @param width Image width in pixels (required for raw RGB/RGBA formats). 137 @param height Image height in pixels (required for raw RGB/RGBA formats). 138 @param size Size in bytes when reading from file. 139 @param offset Byte offset when reading from file. 140 @param quiet Response suppression level. *) 141 142val transmit_and_display : 143 ?image_id:int -> 144 ?image_number:int -> 145 ?format:Format.t -> 146 ?transmission:Transmission.t -> 147 ?compression:Compression.t -> 148 ?width:int -> 149 ?height:int -> 150 ?size:int -> 151 ?offset:int -> 152 ?quiet:Quiet.t -> 153 ?placement:Placement.t -> 154 unit -> 155 command 156(** Transmit image data and display it immediately. 157 158 Combines transmission and display in a single command. The image is 159 rendered at the current cursor position unless placement options 160 specify otherwise. 161 162 See {!transmit} for parameter descriptions. The [placement] parameter 163 controls display position and scaling. *) 164 165val query : 166 ?format:Format.t -> 167 ?transmission:Transmission.t -> 168 ?width:int -> 169 ?height:int -> 170 ?quiet:Quiet.t -> 171 unit -> 172 command 173(** Query terminal support without storing the image. 174 175 Performs the same validation as {!transmit} but does not store the 176 image. Useful for testing whether the terminal supports the graphics 177 protocol and specific formats. 178 179 To detect graphics support, send a query and check for a response: 180 {[ 181 (* Send query with a tiny 1x1 RGB image *) 182 let cmd = Kgp.query ~format:`Rgb24 ~width:1 ~height:1 () in 183 Kgp.write buf cmd ~data:"\x00\x00\x00" 184 (* If terminal responds, it supports the protocol *) 185 ]} *) 186 187(** {2 Display} 188 189 Previously transmitted images can be displayed multiple times at 190 different positions with different cropping and scaling options. *) 191 192val display : 193 ?image_id:int -> 194 ?image_number:int -> 195 ?placement:Placement.t -> 196 ?quiet:Quiet.t -> 197 unit -> 198 command 199(** Display a previously transmitted image. 200 201 The image is rendered at the current cursor position. Use [placement] 202 to control cropping, scaling, z-index, and other display options. 203 204 Each display creates a "placement" of the image. Multiple placements 205 of the same image share the underlying image data. 206 207 @param image_id ID of a previously transmitted image. 208 @param image_number Image number (acts on the newest image with this number). 209 @param placement Display configuration (position, size, z-index, etc.). *) 210 211(** {2 Deletion} 212 213 Images and placements can be deleted to free terminal resources. 214 By default, only placements are removed and image data is retained 215 for potential reuse. Use [~free:true] to also release the image data. *) 216 217val delete : ?free:bool -> ?quiet:Quiet.t -> Delete.t -> command 218(** Delete images or placements. 219 220 See {!Delete} for the full list of deletion targets. 221 222 @param free If true, also free the image data from memory (default: false). 223 Without [~free:true], only placements are removed and the image data 224 can be reused for new placements. 225 226 Examples: 227 {[ 228 (* Delete all visible images, keep data *) 229 Kgp.delete `All_visible 230 231 (* Delete specific image, keeping data for reuse *) 232 Kgp.delete (`By_id (42, None)) 233 234 (* Delete specific image and free its data *) 235 Kgp.delete ~free:true (`By_id (42, None)) 236 237 (* Delete all placements at a specific cell *) 238 Kgp.delete (`At_cell (10, 5)) 239 ]} *) 240 241(** {2 Animation} 242 243 The protocol supports both client-driven and terminal-driven animations. 244 Animations are created by first transmitting a base image, then adding 245 frames with optional delta encoding for efficiency. 246 247 Frame numbers are 1-based: frame 1 is the root (base) image, frame 2 248 is the first added frame, etc. *) 249 250val frame : 251 ?image_id:int -> 252 ?image_number:int -> 253 ?format:Format.t -> 254 ?transmission:Transmission.t -> 255 ?compression:Compression.t -> 256 ?width:int -> 257 ?height:int -> 258 ?quiet:Quiet.t -> 259 frame:Frame.t -> 260 unit -> 261 command 262(** Transmit animation frame data. 263 264 Adds a new frame to an existing image or edits an existing frame. 265 The frame can be a full image or a partial update (rectangle). 266 267 Use {!Frame.make} to configure the frame's position, timing, and 268 composition options. 269 270 @param frame Frame configuration including timing and composition. *) 271 272val animate : 273 ?image_id:int -> 274 ?image_number:int -> 275 ?quiet:Quiet.t -> 276 Animation.t -> 277 command 278(** Control animation playback. 279 280 For terminal-driven animation: 281 {[ 282 (* Start infinite loop animation *) 283 Kgp.animate ~image_id:1 (Animation.set_state ~loops:1 `Run) 284 285 (* Stop animation *) 286 Kgp.animate ~image_id:1 (Animation.set_state `Stop) 287 288 (* Change frame timing *) 289 Kgp.animate ~image_id:1 (Animation.set_gap ~frame:3 ~gap_ms:100) 290 ]} 291 292 For client-driven animation: 293 {[ 294 (* Manually advance to specific frame *) 295 Kgp.animate ~image_id:1 (Animation.set_current_frame 5) 296 ]} *) 297 298val compose : 299 ?image_id:int -> 300 ?image_number:int -> 301 ?quiet:Quiet.t -> 302 Compose.t -> 303 command 304(** Compose animation frames. 305 306 Copies a rectangular region from one frame onto another. Useful for 307 building complex frames from simpler components. 308 309 {[ 310 (* Copy a 50x50 region from frame 2 to frame 5 *) 311 let comp = Compose.make 312 ~source_frame:2 ~dest_frame:5 313 ~width:50 ~height:50 314 ~source_x:10 ~source_y:10 315 ~dest_x:20 ~dest_y:20 () in 316 Kgp.compose ~image_id:1 comp 317 ]} *) 318 319(** {2 Output} 320 321 Commands are serialized to escape sequences that can be written 322 to the terminal. *) 323 324val write : Buffer.t -> command -> data:string -> unit 325(** Write the command to a buffer. 326 327 The [data] parameter contains the raw image/frame data (before base64 328 encoding). Pass an empty string for commands that don't include payload 329 data (like {!val:display}, {!val:delete}, {!val:animate}). 330 331 The library handles base64 encoding and chunking automatically. *) 332 333val to_string : command -> data:string -> string 334(** Convert command to a string. 335 336 Convenience wrapper around {!write} that returns the serialized 337 command as a string. *) 338 339val write_tmux : Buffer.t -> command -> data:string -> unit 340(** Write the command to a buffer with tmux passthrough support. 341 342 If running inside tmux (detected via [TMUX] environment variable), 343 wraps the graphics command in a DCS passthrough sequence so it 344 reaches the underlying terminal. Otherwise, behaves like {!write}. 345 346 Requires tmux 3.3+ with [allow-passthrough] enabled. *) 347 348val to_string_tmux : command -> data:string -> string 349(** Convert command to a string with tmux passthrough support. 350 351 Convenience wrapper around {!write_tmux}. If running inside tmux, 352 wraps the output for passthrough. Otherwise, behaves like {!to_string}. *) 353 354(** {1 Response} *) 355 356module Response = Kgp_response 357 358(** {1 Utilities} *) 359 360module Unicode_placeholder = Kgp_unicode 361module Detect = Kgp_detect 362 363module Tmux = Kgp_tmux 364(** Tmux passthrough support. Provides functions to detect if running 365 inside tmux and to wrap escape sequences for passthrough. *) 366 367module Terminal = Kgp_terminal 368(** Terminal environment detection. Provides functions to detect terminal 369 capabilities, pager mode, and resolve graphics output mode. *) 370