Kitty Graphics Protocol in OCaml
terminal
graphics
ocaml
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. *)