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