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