···11(* Kitty Graphics Protocol Demo - Matching kgp/examples/demo *)
2233-module K = Kitty_graphics
33+module K = Kgp
4455(* Helper: Generate a solid color RGBA image *)
66let solid_color_rgba ~width ~height ~r ~g ~b ~a =
+1-1
example/test_output.ml
···11(* Simple test to show exact escape sequences without data *)
2233-module K = Kitty_graphics
33+module K = Kgp
4455let print_escaped s =
66 String.iter (fun c ->
+1-1
example/tiny_anim.ml
···11(* Tiny animation test - no chunking needed *)
22(* Uses 20x20 images which are ~1067 bytes base64 (well under 4096) *)
3344-module K = Kitty_graphics
44+module K = Kgp
5566let solid_color_rgba ~width ~height ~r ~g ~b ~a =
77 let pixels = Bytes.create (width * height * 4) in
···11+(** Kitty Terminal Graphics Protocol
22+33+ This library implements the Kitty terminal graphics protocol, allowing
44+ OCaml programs to display images in terminals that support the protocol
55+ (Kitty, WezTerm, Konsole, Ghostty, etc.).
66+77+ The protocol uses APC (Application Programming Command) escape sequences
88+ to transmit and display pixel graphics. Images can be transmitted as raw
99+ RGB/RGBA data or PNG, and displayed at specific positions with various
1010+ placement options.
1111+1212+ {2 Basic Usage}
1313+1414+ {[
1515+ (* Display a PNG image *)
1616+ let png_data = read_file "image.png" in
1717+ let cmd = Kgp.Command.transmit_and_display ~format:`Png () in
1818+ let buf = Buffer.create 1024 in
1919+ Kgp.Command.write buf cmd ~data:png_data;
2020+ print_string (Buffer.contents buf)
2121+ ]}
2222+2323+ {2 Protocol Reference}
2424+2525+ See {{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics Protocol}
2626+ for the full specification. *)
2727+2828+(** {1 Polymorphic Variant Types} *)
2929+3030+type format = Kgp_types.format
3131+(** Image data formats. [`Rgba32] is 32-bit RGBA (4 bytes per pixel),
3232+ [`Rgb24] is 24-bit RGB (3 bytes per pixel), [`Png] is PNG encoded data. *)
3333+3434+type transmission = Kgp_types.transmission
3535+(** Transmission methods. [`Direct] sends data inline, [`File] reads from a path,
3636+ [`Tempfile] reads from a temp file that the terminal deletes after reading. *)
3737+3838+type compression = Kgp_types.compression
3939+(** Compression options. [`None] for raw data, [`Zlib] for RFC 1950 compression. *)
4040+4141+type quiet = Kgp_types.quiet
4242+(** Response suppression. [`Noisy] sends all responses (default),
4343+ [`Errors_only] suppresses OK responses, [`Silent] suppresses all. *)
4444+4545+type cursor = Kgp_types.cursor
4646+(** Cursor movement after displaying. [`Move] advances cursor (default),
4747+ [`Static] keeps cursor in place. *)
4848+4949+type composition = Kgp_types.composition
5050+(** Composition modes. [`Alpha_blend] for full blending (default),
5151+ [`Overwrite] for simple pixel replacement. *)
5252+5353+type delete = Kgp_types.delete
5454+(** Delete target specification. Each variant has two forms: one that only
5555+ removes placements (e.g., [`All_visible]) and one that also frees the
5656+ image data (e.g., [`All_visible_and_free]). *)
5757+5858+type animation_state = Kgp_types.animation_state
5959+(** Animation playback state. [`Stop] halts animation, [`Loading] runs but
6060+ waits for new frames at end, [`Run] runs normally and loops. *)
6161+6262+(** {1 Type Modules} *)
6363+6464+module Format = Kgp_types.Format
6565+module Transmission = Kgp_types.Transmission
6666+module Compression = Kgp_types.Compression
6767+module Quiet = Kgp_types.Quiet
6868+module Cursor = Kgp_types.Cursor
6969+module Composition = Kgp_types.Composition
7070+module Delete = Kgp_types.Delete
7171+7272+(** {1 Configuration Modules} *)
7373+7474+module Placement = Kgp_placement
7575+module Frame = Kgp_frame
7676+module Animation = Kgp_animation
7777+module Compose = Kgp_compose
7878+7979+(** {1 Command and Response} *)
8080+8181+module Command = Kgp_command
8282+module Response = Kgp_response
8383+8484+(** {1 Utilities} *)
8585+8686+module Unicode_placeholder = Kgp_unicode
8787+module Detect = Kgp_detect
+12
lib/kgp_animation.ml
···11+(* Kitty Graphics Protocol Animation - Implementation *)
22+33+type state = Kgp_types.animation_state
44+55+type t =
66+ [ `Set_state of state * int option
77+ | `Set_gap of int * int
88+ | `Set_current of int ]
99+1010+let set_state ?loops state = `Set_state (state, loops)
1111+let set_gap ~frame ~gap_ms = `Set_gap (frame, gap_ms)
1212+let set_current_frame frame = `Set_current frame
+24
lib/kgp_animation.mli
···11+(** Kitty Graphics Protocol Animation
22+33+ Animation control operations. *)
44+55+type state = Kgp_types.animation_state
66+(** Animation playback state. *)
77+88+type t =
99+ [ `Set_state of state * int option
1010+ | `Set_gap of int * int
1111+ | `Set_current of int ]
1212+(** Animation control operations. *)
1313+1414+val set_state : ?loops:int -> state -> t
1515+(** Set animation state.
1616+ @param loops Number of loops: 0 = ignored, 1 = infinite, n = n-1 loops *)
1717+1818+val set_gap : frame:int -> gap_ms:int -> t
1919+(** Set the gap (delay) for a specific frame.
2020+ @param frame 1-based frame number
2121+ @param gap_ms Delay in milliseconds (negative = gapless) *)
2222+2323+val set_current_frame : int -> t
2424+(** Make a specific frame (1-based) the current displayed frame. *)
+332
lib/kgp_command.ml
···11+(* Kitty Graphics Protocol Command - Implementation *)
22+33+type action =
44+ [ `Transmit
55+ | `Transmit_and_display
66+ | `Query
77+ | `Display
88+ | `Delete
99+ | `Frame
1010+ | `Animate
1111+ | `Compose ]
1212+1313+type t = {
1414+ action : action;
1515+ format : Kgp_types.format option;
1616+ transmission : Kgp_types.transmission option;
1717+ compression : Kgp_types.compression option;
1818+ width : int option;
1919+ height : int option;
2020+ size : int option;
2121+ offset : int option;
2222+ quiet : Kgp_types.quiet option;
2323+ image_id : int option;
2424+ image_number : int option;
2525+ placement : Kgp_placement.t option;
2626+ delete : Kgp_types.delete option;
2727+ frame : Kgp_frame.t option;
2828+ animation : Kgp_animation.t option;
2929+ compose : Kgp_compose.t option;
3030+}
3131+3232+let make action =
3333+ {
3434+ action;
3535+ format = None;
3636+ transmission = None;
3737+ compression = None;
3838+ width = None;
3939+ height = None;
4040+ size = None;
4141+ offset = None;
4242+ quiet = None;
4343+ image_id = None;
4444+ image_number = None;
4545+ placement = None;
4646+ delete = None;
4747+ frame = None;
4848+ animation = None;
4949+ compose = None;
5050+ }
5151+5252+let transmit ?image_id ?image_number ?format ?transmission ?compression ?width
5353+ ?height ?size ?offset ?quiet () =
5454+ {
5555+ (make `Transmit) with
5656+ image_id;
5757+ image_number;
5858+ format;
5959+ transmission;
6060+ compression;
6161+ width;
6262+ height;
6363+ size;
6464+ offset;
6565+ quiet;
6666+ }
6767+6868+let transmit_and_display ?image_id ?image_number ?format ?transmission
6969+ ?compression ?width ?height ?size ?offset ?quiet ?placement () =
7070+ {
7171+ (make `Transmit_and_display) with
7272+ image_id;
7373+ image_number;
7474+ format;
7575+ transmission;
7676+ compression;
7777+ width;
7878+ height;
7979+ size;
8080+ offset;
8181+ quiet;
8282+ placement;
8383+ }
8484+8585+let query ?format ?transmission ?width ?height ?quiet () =
8686+ { (make `Query) with format; transmission; width; height; quiet }
8787+8888+let display ?image_id ?image_number ?placement ?quiet () =
8989+ { (make `Display) with image_id; image_number; placement; quiet }
9090+9191+let delete ?quiet del = { (make `Delete) with quiet; delete = Some del }
9292+9393+let frame ?image_id ?image_number ?format ?transmission ?compression ?width
9494+ ?height ?quiet ~frame () =
9595+ {
9696+ (make `Frame) with
9797+ image_id;
9898+ image_number;
9999+ format;
100100+ transmission;
101101+ compression;
102102+ width;
103103+ height;
104104+ quiet;
105105+ frame = Some frame;
106106+ }
107107+108108+let animate ?image_id ?image_number ?quiet anim =
109109+ { (make `Animate) with image_id; image_number; quiet; animation = Some anim }
110110+111111+let compose ?image_id ?image_number ?quiet comp =
112112+ { (make `Compose) with image_id; image_number; quiet; compose = Some comp }
113113+114114+(* Serialization helpers *)
115115+let apc_start = "\027_G"
116116+let apc_end = "\027\\"
117117+118118+(* Key-value writer with separator handling *)
119119+type kv_writer = { mutable first : bool; buf : Buffer.t }
120120+121121+let kv_writer buf = { first = true; buf }
122122+123123+let kv w key value =
124124+ if not w.first then Buffer.add_char w.buf ',';
125125+ w.first <- false;
126126+ Buffer.add_char w.buf key;
127127+ Buffer.add_char w.buf '=';
128128+ Buffer.add_string w.buf value
129129+130130+let kv_int w key value = kv w key (string_of_int value)
131131+let kv_int32 w key value = kv w key (Int32.to_string value)
132132+let kv_char w key value = kv w key (String.make 1 value)
133133+134134+(* Conditional writers using Option.iter *)
135135+let kv_int_opt w key = Option.iter (kv_int w key)
136136+let kv_int32_opt w key = Option.iter (kv_int32 w key)
137137+138138+let kv_int_if w key ~default opt =
139139+ Option.iter (fun v -> if v <> default then kv_int w key v) opt
140140+141141+let action_char : action -> char = function
142142+ | `Transmit -> 't'
143143+ | `Transmit_and_display -> 'T'
144144+ | `Query -> 'q'
145145+ | `Display -> 'p'
146146+ | `Delete -> 'd'
147147+ | `Frame -> 'f'
148148+ | `Animate -> 'a'
149149+ | `Compose -> 'c'
150150+151151+let delete_char : Kgp_types.delete -> char = function
152152+ | `All_visible -> 'a'
153153+ | `All_visible_and_free -> 'A'
154154+ | `By_id _ -> 'i'
155155+ | `By_id_and_free _ -> 'I'
156156+ | `By_number _ -> 'n'
157157+ | `By_number_and_free _ -> 'N'
158158+ | `At_cursor -> 'c'
159159+ | `At_cursor_and_free -> 'C'
160160+ | `At_cell _ -> 'p'
161161+ | `At_cell_and_free _ -> 'P'
162162+ | `At_cell_z _ -> 'q'
163163+ | `At_cell_z_and_free _ -> 'Q'
164164+ | `By_column _ -> 'x'
165165+ | `By_column_and_free _ -> 'X'
166166+ | `By_row _ -> 'y'
167167+ | `By_row_and_free _ -> 'Y'
168168+ | `By_z_index _ -> 'z'
169169+ | `By_z_index_and_free _ -> 'Z'
170170+ | `By_id_range _ -> 'r'
171171+ | `By_id_range_and_free _ -> 'R'
172172+ | `Frames -> 'f'
173173+ | `Frames_and_free -> 'F'
174174+175175+let write_placement w (p : Kgp_placement.t) =
176176+ kv_int_opt w 'x' (Kgp_placement.source_x p);
177177+ kv_int_opt w 'y' (Kgp_placement.source_y p);
178178+ kv_int_opt w 'w' (Kgp_placement.source_width p);
179179+ kv_int_opt w 'h' (Kgp_placement.source_height p);
180180+ kv_int_opt w 'X' (Kgp_placement.cell_x_offset p);
181181+ kv_int_opt w 'Y' (Kgp_placement.cell_y_offset p);
182182+ kv_int_opt w 'c' (Kgp_placement.columns p);
183183+ kv_int_opt w 'r' (Kgp_placement.rows p);
184184+ kv_int_opt w 'z' (Kgp_placement.z_index p);
185185+ kv_int_opt w 'p' (Kgp_placement.placement_id p);
186186+ Kgp_placement.cursor p
187187+ |> Option.iter (fun c ->
188188+ kv_int_if w 'C' ~default:0 (Some (Kgp_types.Cursor.to_int c)));
189189+ if Kgp_placement.unicode_placeholder p then kv_int w 'U' 1
190190+191191+let write_delete w (d : Kgp_types.delete) =
192192+ kv_char w 'd' (delete_char d);
193193+ match d with
194194+ | `By_id (id, pid) | `By_id_and_free (id, pid) ->
195195+ kv_int w 'i' id;
196196+ kv_int_opt w 'p' pid
197197+ | `By_number (n, pid) | `By_number_and_free (n, pid) ->
198198+ kv_int w 'I' n;
199199+ kv_int_opt w 'p' pid
200200+ | `At_cell (x, y) | `At_cell_and_free (x, y) ->
201201+ kv_int w 'x' x;
202202+ kv_int w 'y' y
203203+ | `At_cell_z (x, y, z) | `At_cell_z_and_free (x, y, z) ->
204204+ kv_int w 'x' x;
205205+ kv_int w 'y' y;
206206+ kv_int w 'z' z
207207+ | `By_column c | `By_column_and_free c -> kv_int w 'x' c
208208+ | `By_row r | `By_row_and_free r -> kv_int w 'y' r
209209+ | `By_z_index z | `By_z_index_and_free z -> kv_int w 'z' z
210210+ | `By_id_range (min_id, max_id) | `By_id_range_and_free (min_id, max_id) ->
211211+ kv_int w 'x' min_id;
212212+ kv_int w 'y' max_id
213213+ | `All_visible | `All_visible_and_free | `At_cursor | `At_cursor_and_free
214214+ | `Frames | `Frames_and_free ->
215215+ ()
216216+217217+let write_frame w (f : Kgp_frame.t) =
218218+ kv_int_opt w 'x' (Kgp_frame.x f);
219219+ kv_int_opt w 'y' (Kgp_frame.y f);
220220+ kv_int_opt w 'c' (Kgp_frame.base_frame f);
221221+ kv_int_opt w 'r' (Kgp_frame.edit_frame f);
222222+ kv_int_opt w 'z' (Kgp_frame.gap_ms f);
223223+ Kgp_frame.composition f
224224+ |> Option.iter (fun c ->
225225+ kv_int_if w 'X' ~default:0 (Some (Kgp_types.Composition.to_int c)));
226226+ kv_int32_opt w 'Y' (Kgp_frame.background_color f)
227227+228228+let write_animation w : Kgp_animation.t -> unit = function
229229+ | `Set_state (state, loops) ->
230230+ let s = match state with `Stop -> 1 | `Loading -> 2 | `Run -> 3 in
231231+ kv_int w 's' s;
232232+ kv_int_opt w 'v' loops
233233+ | `Set_gap (frame, gap_ms) ->
234234+ kv_int w 'r' frame;
235235+ kv_int w 'z' gap_ms
236236+ | `Set_current frame -> kv_int w 'c' frame
237237+238238+let write_compose w (c : Kgp_compose.t) =
239239+ kv_int w 'r' (Kgp_compose.source_frame c);
240240+ kv_int w 'c' (Kgp_compose.dest_frame c);
241241+ kv_int_opt w 'w' (Kgp_compose.width c);
242242+ kv_int_opt w 'h' (Kgp_compose.height c);
243243+ kv_int_opt w 'x' (Kgp_compose.dest_x c);
244244+ kv_int_opt w 'y' (Kgp_compose.dest_y c);
245245+ kv_int_opt w 'X' (Kgp_compose.source_x c);
246246+ kv_int_opt w 'Y' (Kgp_compose.source_y c);
247247+ Kgp_compose.composition c
248248+ |> Option.iter (fun comp ->
249249+ kv_int_if w 'C' ~default:0 (Some (Kgp_types.Composition.to_int comp)))
250250+251251+let write_control_data buf cmd =
252252+ let w = kv_writer buf in
253253+ (* Action *)
254254+ kv_char w 'a' (action_char cmd.action);
255255+ (* Quiet - only if non-default *)
256256+ cmd.quiet
257257+ |> Option.iter (fun q ->
258258+ kv_int_if w 'q' ~default:0 (Some (Kgp_types.Quiet.to_int q)));
259259+ (* Format *)
260260+ cmd.format
261261+ |> Option.iter (fun f -> kv_int w 'f' (Kgp_types.Format.to_int f));
262262+ (* Transmission - only for transmit/frame actions, always include t=d for compatibility *)
263263+ (match cmd.action with
264264+ | `Transmit | `Transmit_and_display | `Frame -> (
265265+ match cmd.transmission with
266266+ | Some t -> kv_char w 't' (Kgp_types.Transmission.to_char t)
267267+ | None -> kv_char w 't' 'd')
268268+ | _ -> ());
269269+ (* Compression *)
270270+ cmd.compression
271271+ |> Option.iter (fun c ->
272272+ Kgp_types.Compression.to_char c |> Option.iter (kv_char w 'o'));
273273+ (* Dimensions *)
274274+ kv_int_opt w 's' cmd.width;
275275+ kv_int_opt w 'v' cmd.height;
276276+ (* File size/offset *)
277277+ kv_int_opt w 'S' cmd.size;
278278+ kv_int_opt w 'O' cmd.offset;
279279+ (* Image ID/number *)
280280+ kv_int_opt w 'i' cmd.image_id;
281281+ kv_int_opt w 'I' cmd.image_number;
282282+ (* Complex options *)
283283+ cmd.placement |> Option.iter (write_placement w);
284284+ cmd.delete |> Option.iter (write_delete w);
285285+ cmd.frame |> Option.iter (write_frame w);
286286+ cmd.animation |> Option.iter (write_animation w);
287287+ cmd.compose |> Option.iter (write_compose w);
288288+ w
289289+290290+(* Use large chunk size to avoid chunking - Kitty animation doesn't handle chunks well *)
291291+let chunk_size = 1024 * 1024 (* 1MB - effectively no chunking *)
292292+293293+let write buf cmd ~data =
294294+ Buffer.add_string buf apc_start;
295295+ let w = write_control_data buf cmd in
296296+ if String.length data > 0 then begin
297297+ let encoded = Base64.encode_string data in
298298+ let len = String.length encoded in
299299+ if len <= chunk_size then (
300300+ Buffer.add_char buf ';';
301301+ Buffer.add_string buf encoded;
302302+ Buffer.add_string buf apc_end)
303303+ else begin
304304+ (* Multiple chunks *)
305305+ let rec write_chunks pos first =
306306+ if pos < len then begin
307307+ let remaining = len - pos in
308308+ let this_chunk = min chunk_size remaining in
309309+ let is_last = pos + this_chunk >= len in
310310+ if first then (
311311+ kv_int w 'm' 1;
312312+ Buffer.add_char buf ';';
313313+ Buffer.add_substring buf encoded pos this_chunk;
314314+ Buffer.add_string buf apc_end)
315315+ else (
316316+ Buffer.add_string buf apc_start;
317317+ Buffer.add_string buf (if is_last then "m=0" else "m=1");
318318+ Buffer.add_char buf ';';
319319+ Buffer.add_substring buf encoded pos this_chunk;
320320+ Buffer.add_string buf apc_end);
321321+ write_chunks (pos + this_chunk) false
322322+ end
323323+ in
324324+ write_chunks 0 true
325325+ end
326326+ end
327327+ else Buffer.add_string buf apc_end
328328+329329+let to_string cmd ~data =
330330+ let buf = Buffer.create 1024 in
331331+ write buf cmd ~data;
332332+ Buffer.contents buf
+106
lib/kgp_command.mli
···11+(** Kitty Graphics Protocol Commands
22+33+ This module provides functions for building and serializing graphics
44+ protocol commands. *)
55+66+type t
77+(** A graphics protocol command. *)
88+99+(** {1 Image Transmission} *)
1010+1111+val transmit :
1212+ ?image_id:int ->
1313+ ?image_number:int ->
1414+ ?format:Kgp_types.format ->
1515+ ?transmission:Kgp_types.transmission ->
1616+ ?compression:Kgp_types.compression ->
1717+ ?width:int ->
1818+ ?height:int ->
1919+ ?size:int ->
2020+ ?offset:int ->
2121+ ?quiet:Kgp_types.quiet ->
2222+ unit ->
2323+ t
2424+(** Transmit image data without displaying. *)
2525+2626+val transmit_and_display :
2727+ ?image_id:int ->
2828+ ?image_number:int ->
2929+ ?format:Kgp_types.format ->
3030+ ?transmission:Kgp_types.transmission ->
3131+ ?compression:Kgp_types.compression ->
3232+ ?width:int ->
3333+ ?height:int ->
3434+ ?size:int ->
3535+ ?offset:int ->
3636+ ?quiet:Kgp_types.quiet ->
3737+ ?placement:Kgp_placement.t ->
3838+ unit ->
3939+ t
4040+(** Transmit image data and display it immediately. *)
4141+4242+val query :
4343+ ?format:Kgp_types.format ->
4444+ ?transmission:Kgp_types.transmission ->
4545+ ?width:int ->
4646+ ?height:int ->
4747+ ?quiet:Kgp_types.quiet ->
4848+ unit ->
4949+ t
5050+(** Query terminal support without storing the image. *)
5151+5252+(** {1 Display} *)
5353+5454+val display :
5555+ ?image_id:int ->
5656+ ?image_number:int ->
5757+ ?placement:Kgp_placement.t ->
5858+ ?quiet:Kgp_types.quiet ->
5959+ unit ->
6060+ t
6161+(** Display a previously transmitted image. *)
6262+6363+(** {1 Deletion} *)
6464+6565+val delete : ?quiet:Kgp_types.quiet -> Kgp_types.delete -> t
6666+(** Delete images or placements. *)
6767+6868+(** {1 Animation} *)
6969+7070+val frame :
7171+ ?image_id:int ->
7272+ ?image_number:int ->
7373+ ?format:Kgp_types.format ->
7474+ ?transmission:Kgp_types.transmission ->
7575+ ?compression:Kgp_types.compression ->
7676+ ?width:int ->
7777+ ?height:int ->
7878+ ?quiet:Kgp_types.quiet ->
7979+ frame:Kgp_frame.t ->
8080+ unit ->
8181+ t
8282+(** Transmit animation frame data. *)
8383+8484+val animate :
8585+ ?image_id:int ->
8686+ ?image_number:int ->
8787+ ?quiet:Kgp_types.quiet ->
8888+ Kgp_animation.t ->
8989+ t
9090+(** Control animation playback. *)
9191+9292+val compose :
9393+ ?image_id:int ->
9494+ ?image_number:int ->
9595+ ?quiet:Kgp_types.quiet ->
9696+ Kgp_compose.t ->
9797+ t
9898+(** Compose animation frames. *)
9999+100100+(** {1 Output} *)
101101+102102+val write : Buffer.t -> t -> data:string -> unit
103103+(** Write the command to a buffer. *)
104104+105105+val to_string : t -> data:string -> string
106106+(** Convert command to a string. *)
+37
lib/kgp_compose.ml
···11+(* Kitty Graphics Protocol Compose - Implementation *)
22+33+type t = {
44+ source_frame : int;
55+ dest_frame : int;
66+ width : int option;
77+ height : int option;
88+ source_x : int option;
99+ source_y : int option;
1010+ dest_x : int option;
1111+ dest_y : int option;
1212+ composition : Kgp_types.composition option;
1313+}
1414+1515+let make ~source_frame ~dest_frame ?width ?height ?source_x ?source_y ?dest_x
1616+ ?dest_y ?composition () =
1717+ {
1818+ source_frame;
1919+ dest_frame;
2020+ width;
2121+ height;
2222+ source_x;
2323+ source_y;
2424+ dest_x;
2525+ dest_y;
2626+ composition;
2727+ }
2828+2929+let source_frame t = t.source_frame
3030+let dest_frame t = t.dest_frame
3131+let width t = t.width
3232+let height t = t.height
3333+let source_x t = t.source_x
3434+let source_y t = t.source_y
3535+let dest_x t = t.dest_x
3636+let dest_y t = t.dest_y
3737+let composition t = t.composition
+32
lib/kgp_compose.mli
···11+(** Kitty Graphics Protocol Compose
22+33+ Frame composition operations. *)
44+55+type t
66+(** Composition operation. *)
77+88+val make :
99+ source_frame:int ->
1010+ dest_frame:int ->
1111+ ?width:int ->
1212+ ?height:int ->
1313+ ?source_x:int ->
1414+ ?source_y:int ->
1515+ ?dest_x:int ->
1616+ ?dest_y:int ->
1717+ ?composition:Kgp_types.composition ->
1818+ unit ->
1919+ t
2020+(** Compose a rectangle from one frame onto another. *)
2121+2222+(** {1 Field Accessors} *)
2323+2424+val source_frame : t -> int
2525+val dest_frame : t -> int
2626+val width : t -> int option
2727+val height : t -> int option
2828+val source_x : t -> int option
2929+val source_y : t -> int option
3030+val dest_x : t -> int option
3131+val dest_y : t -> int option
3232+val composition : t -> Kgp_types.composition option
···11+(** Kitty Graphics Protocol Detection
22+33+ Detect terminal graphics support capabilities. *)
44+55+val make_query : unit -> string
66+(** Generate a query command to test graphics support. *)
77+88+val supports_graphics : Kgp_response.t option -> da1_received:bool -> bool
99+(** Determine if graphics are supported based on query results. *)
+33
lib/kgp_frame.ml
···11+(* Kitty Graphics Protocol Frame - Implementation *)
22+33+type t = {
44+ x : int option;
55+ y : int option;
66+ base_frame : int option;
77+ edit_frame : int option;
88+ gap_ms : int option;
99+ composition : Kgp_types.composition option;
1010+ background_color : int32 option;
1111+}
1212+1313+let empty =
1414+ {
1515+ x = None;
1616+ y = None;
1717+ base_frame = None;
1818+ edit_frame = None;
1919+ gap_ms = None;
2020+ composition = None;
2121+ background_color = None;
2222+ }
2323+2424+let make ?x ?y ?base_frame ?edit_frame ?gap_ms ?composition ?background_color () =
2525+ { x; y; base_frame; edit_frame; gap_ms; composition; background_color }
2626+2727+let x t = t.x
2828+let y t = t.y
2929+let base_frame t = t.base_frame
3030+let edit_frame t = t.edit_frame
3131+let gap_ms t = t.gap_ms
3232+let composition t = t.composition
3333+let background_color t = t.background_color
+39
lib/kgp_frame.mli
···11+(** Kitty Graphics Protocol Frame
22+33+ Animation frame configuration. *)
44+55+type t
66+(** Animation frame configuration. *)
77+88+val make :
99+ ?x:int ->
1010+ ?y:int ->
1111+ ?base_frame:int ->
1212+ ?edit_frame:int ->
1313+ ?gap_ms:int ->
1414+ ?composition:Kgp_types.composition ->
1515+ ?background_color:int32 ->
1616+ unit ->
1717+ t
1818+(** Create a frame specification.
1919+2020+ @param x Left edge where frame data is placed (pixels)
2121+ @param y Top edge where frame data is placed (pixels)
2222+ @param base_frame 1-based frame number to use as background canvas
2323+ @param edit_frame 1-based frame number to edit (0 = new frame)
2424+ @param gap_ms Delay before next frame in milliseconds
2525+ @param composition How to blend pixels onto the canvas
2626+ @param background_color 32-bit RGBA background when no base frame *)
2727+2828+val empty : t
2929+(** Empty frame spec with defaults. *)
3030+3131+(** {1 Field Accessors} *)
3232+3333+val x : t -> int option
3434+val y : t -> int option
3535+val base_frame : t -> int option
3636+val edit_frame : t -> int option
3737+val gap_ms : t -> int option
3838+val composition : t -> Kgp_types.composition option
3939+val background_color : t -> int32 option
+63
lib/kgp_placement.ml
···11+(* Kitty Graphics Protocol Placement - Implementation *)
22+33+type t = {
44+ source_x : int option;
55+ source_y : int option;
66+ source_width : int option;
77+ source_height : int option;
88+ cell_x_offset : int option;
99+ cell_y_offset : int option;
1010+ columns : int option;
1111+ rows : int option;
1212+ z_index : int option;
1313+ placement_id : int option;
1414+ cursor : Kgp_types.cursor option;
1515+ unicode_placeholder : bool;
1616+}
1717+1818+let empty =
1919+ {
2020+ source_x = None;
2121+ source_y = None;
2222+ source_width = None;
2323+ source_height = None;
2424+ cell_x_offset = None;
2525+ cell_y_offset = None;
2626+ columns = None;
2727+ rows = None;
2828+ z_index = None;
2929+ placement_id = None;
3030+ cursor = None;
3131+ unicode_placeholder = false;
3232+ }
3333+3434+let make ?source_x ?source_y ?source_width ?source_height ?cell_x_offset
3535+ ?cell_y_offset ?columns ?rows ?z_index ?placement_id ?cursor
3636+ ?(unicode_placeholder = false) () =
3737+ {
3838+ source_x;
3939+ source_y;
4040+ source_width;
4141+ source_height;
4242+ cell_x_offset;
4343+ cell_y_offset;
4444+ columns;
4545+ rows;
4646+ z_index;
4747+ placement_id;
4848+ cursor;
4949+ unicode_placeholder;
5050+ }
5151+5252+let source_x t = t.source_x
5353+let source_y t = t.source_y
5454+let source_width t = t.source_width
5555+let source_height t = t.source_height
5656+let cell_x_offset t = t.cell_x_offset
5757+let cell_y_offset t = t.cell_y_offset
5858+let columns t = t.columns
5959+let rows t = t.rows
6060+let z_index t = t.z_index
6161+let placement_id t = t.placement_id
6262+let cursor t = t.cursor
6363+let unicode_placeholder t = t.unicode_placeholder
+54
lib/kgp_placement.mli
···11+(** Kitty Graphics Protocol Placement
22+33+ Configuration for where and how to display images. *)
44+55+type t
66+(** Placement configuration. *)
77+88+val make :
99+ ?source_x:int ->
1010+ ?source_y:int ->
1111+ ?source_width:int ->
1212+ ?source_height:int ->
1313+ ?cell_x_offset:int ->
1414+ ?cell_y_offset:int ->
1515+ ?columns:int ->
1616+ ?rows:int ->
1717+ ?z_index:int ->
1818+ ?placement_id:int ->
1919+ ?cursor:Kgp_types.cursor ->
2020+ ?unicode_placeholder:bool ->
2121+ unit ->
2222+ t
2323+(** Create a placement configuration.
2424+2525+ @param source_x Left edge of source rectangle in pixels (default 0)
2626+ @param source_y Top edge of source rectangle in pixels (default 0)
2727+ @param source_width Width of source rectangle (default: full width)
2828+ @param source_height Height of source rectangle (default: full height)
2929+ @param cell_x_offset X offset within the first cell in pixels
3030+ @param cell_y_offset Y offset within the first cell in pixels
3131+ @param columns Number of columns to display over (scales image)
3232+ @param rows Number of rows to display over (scales image)
3333+ @param z_index Stacking order (negative = under text)
3434+ @param placement_id Unique ID for this placement
3535+ @param cursor Cursor movement policy after display
3636+ @param unicode_placeholder Create virtual placement for Unicode mode *)
3737+3838+val empty : t
3939+(** Empty placement with all defaults. *)
4040+4141+(** {1 Field Accessors} *)
4242+4343+val source_x : t -> int option
4444+val source_y : t -> int option
4545+val source_width : t -> int option
4646+val source_height : t -> int option
4747+val cell_x_offset : t -> int option
4848+val cell_y_offset : t -> int option
4949+val columns : t -> int option
5050+val rows : t -> int option
5151+val z_index : t -> int option
5252+val placement_id : t -> int option
5353+val cursor : t -> Kgp_types.cursor option
5454+val unicode_placeholder : t -> bool
+56
lib/kgp_response.ml
···11+(* Kitty Graphics Protocol Response - Implementation *)
22+33+type t = {
44+ message : string;
55+ image_id : int option;
66+ image_number : int option;
77+ placement_id : int option;
88+}
99+1010+let is_ok t = t.message = "OK"
1111+let message t = t.message
1212+1313+let error_code t =
1414+ if is_ok t then None
1515+ else
1616+ String.index_opt t.message ':'
1717+ |> Option.fold ~none:(Some t.message) ~some:(fun i ->
1818+ Some (String.sub t.message 0 i))
1919+2020+let image_id t = t.image_id
2121+let image_number t = t.image_number
2222+let placement_id t = t.placement_id
2323+2424+let parse s =
2525+ let ( let* ) = Option.bind in
2626+ let esc = '\027' in
2727+ let len = String.length s in
2828+ let* () =
2929+ if len >= 5 && s.[0] = esc && s.[1] = '_' && s.[2] = 'G' then Some ()
3030+ else None
3131+ in
3232+ let* semi_pos = String.index_from_opt s 3 ';' in
3333+ let rec find_end pos =
3434+ if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then Some pos
3535+ else if pos + 1 < len then find_end (pos + 1)
3636+ else None
3737+ in
3838+ let* end_pos = find_end (semi_pos + 1) in
3939+ let keys_str = String.sub s 3 (semi_pos - 3) in
4040+ let message = String.sub s (semi_pos + 1) (end_pos - semi_pos - 1) in
4141+ let parse_kv part =
4242+ if String.length part >= 3 && part.[1] = '=' then
4343+ Some (part.[0], String.sub part 2 (String.length part - 2))
4444+ else None
4545+ in
4646+ let keys = String.split_on_char ',' keys_str |> List.filter_map parse_kv in
4747+ let find_int key =
4848+ List.assoc_opt key keys |> Fun.flip Option.bind int_of_string_opt
4949+ in
5050+ Some
5151+ {
5252+ message;
5353+ image_id = find_int 'i';
5454+ image_number = find_int 'I';
5555+ placement_id = find_int 'p';
5656+ }
+27
lib/kgp_response.mli
···11+(** Kitty Graphics Protocol Response
22+33+ Parse and interpret terminal responses to graphics commands. *)
44+55+type t
66+(** A parsed terminal response. *)
77+88+val parse : string -> t option
99+(** Parse a response from terminal output. *)
1010+1111+val is_ok : t -> bool
1212+(** Check if the response indicates success. *)
1313+1414+val message : t -> string
1515+(** Get the response message. *)
1616+1717+val error_code : t -> string option
1818+(** Extract the error code if this is an error response. *)
1919+2020+val image_id : t -> int option
2121+(** Get the image ID from the response. *)
2222+2323+val image_number : t -> int option
2424+(** Get the image number from the response. *)
2525+2626+val placement_id : t -> int option
2727+(** Get the placement ID from the response. *)
+89
lib/kgp_types.ml
···11+(* Kitty Graphics Protocol Types - Implementation *)
22+33+type format = [ `Rgba32 | `Rgb24 | `Png ]
44+type transmission = [ `Direct | `File | `Tempfile ]
55+type compression = [ `None | `Zlib ]
66+type quiet = [ `Noisy | `Errors_only | `Silent ]
77+type cursor = [ `Move | `Static ]
88+type composition = [ `Alpha_blend | `Overwrite ]
99+1010+type delete =
1111+ [ `All_visible
1212+ | `All_visible_and_free
1313+ | `By_id of int * int option
1414+ | `By_id_and_free of int * int option
1515+ | `By_number of int * int option
1616+ | `By_number_and_free of int * int option
1717+ | `At_cursor
1818+ | `At_cursor_and_free
1919+ | `At_cell of int * int
2020+ | `At_cell_and_free of int * int
2121+ | `At_cell_z of int * int * int
2222+ | `At_cell_z_and_free of int * int * int
2323+ | `By_column of int
2424+ | `By_column_and_free of int
2525+ | `By_row of int
2626+ | `By_row_and_free of int
2727+ | `By_z_index of int
2828+ | `By_z_index_and_free of int
2929+ | `By_id_range of int * int
3030+ | `By_id_range_and_free of int * int
3131+ | `Frames
3232+ | `Frames_and_free ]
3333+3434+type animation_state = [ `Stop | `Loading | `Run ]
3535+3636+module Format = struct
3737+ type t = format
3838+3939+ let to_int : t -> int = function
4040+ | `Rgba32 -> 32
4141+ | `Rgb24 -> 24
4242+ | `Png -> 100
4343+end
4444+4545+module Transmission = struct
4646+ type t = transmission
4747+4848+ let to_char : t -> char = function
4949+ | `Direct -> 'd'
5050+ | `File -> 'f'
5151+ | `Tempfile -> 't'
5252+end
5353+5454+module Compression = struct
5555+ type t = compression
5656+5757+ let to_char : t -> char option = function
5858+ | `None -> None
5959+ | `Zlib -> Some 'z'
6060+end
6161+6262+module Quiet = struct
6363+ type t = quiet
6464+6565+ let to_int : t -> int = function
6666+ | `Noisy -> 0
6767+ | `Errors_only -> 1
6868+ | `Silent -> 2
6969+end
7070+7171+module Cursor = struct
7272+ type t = cursor
7373+7474+ let to_int : t -> int = function
7575+ | `Move -> 0
7676+ | `Static -> 1
7777+end
7878+7979+module Composition = struct
8080+ type t = composition
8181+8282+ let to_int : t -> int = function
8383+ | `Alpha_blend -> 0
8484+ | `Overwrite -> 1
8585+end
8686+8787+module Delete = struct
8888+ type t = delete
8989+end
+109
lib/kgp_types.mli
···11+(** Kitty Graphics Protocol Types
22+33+ This module defines the base polymorphic variant types used throughout
44+ the Kitty graphics protocol implementation. *)
55+66+(** {1 Polymorphic Variant Types} *)
77+88+type format = [ `Rgba32 | `Rgb24 | `Png ]
99+(** Image data formats. [`Rgba32] is 32-bit RGBA (4 bytes per pixel),
1010+ [`Rgb24] is 24-bit RGB (3 bytes per pixel), [`Png] is PNG encoded data. *)
1111+1212+type transmission = [ `Direct | `File | `Tempfile ]
1313+(** Transmission methods. [`Direct] sends data inline, [`File] reads from a path,
1414+ [`Tempfile] reads from a temp file that the terminal deletes after reading. *)
1515+1616+type compression = [ `None | `Zlib ]
1717+(** Compression options. [`None] for raw data, [`Zlib] for RFC 1950 compression. *)
1818+1919+type quiet = [ `Noisy | `Errors_only | `Silent ]
2020+(** Response suppression. [`Noisy] sends all responses (default),
2121+ [`Errors_only] suppresses OK responses, [`Silent] suppresses all. *)
2222+2323+type cursor = [ `Move | `Static ]
2424+(** Cursor movement after displaying. [`Move] advances cursor (default),
2525+ [`Static] keeps cursor in place. *)
2626+2727+type composition = [ `Alpha_blend | `Overwrite ]
2828+(** Composition modes. [`Alpha_blend] for full blending (default),
2929+ [`Overwrite] for simple pixel replacement. *)
3030+3131+type delete =
3232+ [ `All_visible
3333+ | `All_visible_and_free
3434+ | `By_id of int * int option
3535+ | `By_id_and_free of int * int option
3636+ | `By_number of int * int option
3737+ | `By_number_and_free of int * int option
3838+ | `At_cursor
3939+ | `At_cursor_and_free
4040+ | `At_cell of int * int
4141+ | `At_cell_and_free of int * int
4242+ | `At_cell_z of int * int * int
4343+ | `At_cell_z_and_free of int * int * int
4444+ | `By_column of int
4545+ | `By_column_and_free of int
4646+ | `By_row of int
4747+ | `By_row_and_free of int
4848+ | `By_z_index of int
4949+ | `By_z_index_and_free of int
5050+ | `By_id_range of int * int
5151+ | `By_id_range_and_free of int * int
5252+ | `Frames
5353+ | `Frames_and_free ]
5454+(** Delete target specification. Each variant has two forms: one that only
5555+ removes placements (e.g., [`All_visible]) and one that also frees the
5656+ image data (e.g., [`All_visible_and_free]). Tuple variants contain
5757+ (image_id, optional_placement_id) or (x, y) coordinates. *)
5858+5959+type animation_state = [ `Stop | `Loading | `Run ]
6060+(** Animation playback state. [`Stop] halts animation, [`Loading] runs but
6161+ waits for new frames at end, [`Run] runs normally and loops. *)
6262+6363+(** {1 Type Modules} *)
6464+6565+module Format : sig
6666+ type t = format
6767+6868+ val to_int : t -> int
6969+ (** Convert to protocol integer value (32, 24, or 100). *)
7070+end
7171+7272+module Transmission : sig
7373+ type t = transmission
7474+7575+ val to_char : t -> char
7676+ (** Convert to protocol character ('d', 'f', or 't'). *)
7777+end
7878+7979+module Compression : sig
8080+ type t = compression
8181+8282+ val to_char : t -> char option
8383+ (** Convert to protocol character ([None] or [Some 'z']). *)
8484+end
8585+8686+module Quiet : sig
8787+ type t = quiet
8888+8989+ val to_int : t -> int
9090+ (** Convert to protocol integer (0, 1, or 2). *)
9191+end
9292+9393+module Cursor : sig
9494+ type t = cursor
9595+9696+ val to_int : t -> int
9797+ (** Convert to protocol integer (0 or 1). *)
9898+end
9999+100100+module Composition : sig
101101+ type t = composition
102102+103103+ val to_int : t -> int
104104+ (** Convert to protocol integer (0 or 1). *)
105105+end
106106+107107+module Delete : sig
108108+ type t = delete
109109+end
+94
lib/kgp_unicode.ml
···11+(* Kitty Graphics Protocol Unicode Placeholders - Implementation *)
22+33+let placeholder_char = Uchar.of_int 0x10EEEE
44+55+let diacritics =
66+ [|
77+ 0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F;
88+ 0x0346; 0x034A; 0x034B; 0x034C; 0x0350; 0x0351; 0x0352; 0x0357;
99+ 0x035B; 0x0363; 0x0364; 0x0365; 0x0366; 0x0367; 0x0368; 0x0369;
1010+ 0x036A; 0x036B; 0x036C; 0x036D; 0x036E; 0x036F; 0x0483; 0x0484;
1111+ 0x0485; 0x0486; 0x0487; 0x0592; 0x0593; 0x0594; 0x0595; 0x0597;
1212+ 0x0598; 0x0599; 0x059C; 0x059D; 0x059E; 0x059F; 0x05A0; 0x05A1;
1313+ 0x05A8; 0x05A9; 0x05AB; 0x05AC; 0x05AF; 0x05C4; 0x0610; 0x0611;
1414+ 0x0612; 0x0613; 0x0614; 0x0615; 0x0616; 0x0617; 0x0657; 0x0658;
1515+ 0x0659; 0x065A; 0x065B; 0x065D; 0x065E; 0x06D6; 0x06D7; 0x06D8;
1616+ 0x06D9; 0x06DA; 0x06DB; 0x06DC; 0x06DF; 0x06E0; 0x06E1; 0x06E2;
1717+ 0x06E4; 0x06E7; 0x06E8; 0x06EB; 0x06EC; 0x0730; 0x0732; 0x0733;
1818+ 0x0735; 0x0736; 0x073A; 0x073D; 0x073F; 0x0740; 0x0741; 0x0743;
1919+ 0x0745; 0x0747; 0x0749; 0x074A; 0x07EB; 0x07EC; 0x07ED; 0x07EE;
2020+ 0x07EF; 0x07F0; 0x07F1; 0x07F3; 0x0816; 0x0817; 0x0818; 0x0819;
2121+ 0x081B; 0x081C; 0x081D; 0x081E; 0x081F; 0x0820; 0x0821; 0x0822;
2222+ 0x0823; 0x0825; 0x0826; 0x0827; 0x0829; 0x082A; 0x082B; 0x082C;
2323+ 0x082D; 0x0951; 0x0953; 0x0954; 0x0F82; 0x0F83; 0x0F86; 0x0F87;
2424+ 0x135D; 0x135E; 0x135F; 0x17DD; 0x193A; 0x1A17; 0x1A75; 0x1A76;
2525+ 0x1A77; 0x1A78; 0x1A79; 0x1A7A; 0x1A7B; 0x1A7C; 0x1B6B; 0x1B6D;
2626+ 0x1B6E; 0x1B6F; 0x1B70; 0x1B71; 0x1B72; 0x1B73; 0x1CD0; 0x1CD1;
2727+ 0x1CD2; 0x1CDA; 0x1CDB; 0x1CE0; 0x1DC0; 0x1DC1; 0x1DC3; 0x1DC4;
2828+ 0x1DC5; 0x1DC6; 0x1DC7; 0x1DC8; 0x1DC9; 0x1DCB; 0x1DCC; 0x1DD1;
2929+ 0x1DD2; 0x1DD3; 0x1DD4; 0x1DD5; 0x1DD6; 0x1DD7; 0x1DD8; 0x1DD9;
3030+ 0x1DDA; 0x1DDB; 0x1DDC; 0x1DDD; 0x1DDE; 0x1DDF; 0x1DE0; 0x1DE1;
3131+ 0x1DE2; 0x1DE3; 0x1DE4; 0x1DE5; 0x1DE6; 0x1DFE; 0x20D0; 0x20D1;
3232+ 0x20D4; 0x20D5; 0x20D6; 0x20D7; 0x20DB; 0x20DC; 0x20E1; 0x20E7;
3333+ 0x20E9; 0x20F0; 0xA66F; 0xA67C; 0xA67D; 0xA6F0; 0xA6F1; 0xA8E0;
3434+ 0xA8E1; 0xA8E2; 0xA8E3; 0xA8E4; 0xA8E5; 0xA8E6; 0xA8E7; 0xA8E8;
3535+ 0xA8E9; 0xA8EA; 0xA8EB; 0xA8EC; 0xA8ED; 0xA8EE; 0xA8EF; 0xA8F0;
3636+ 0xA8F1; 0xAAB0; 0xAAB2; 0xAAB3; 0xAAB7; 0xAAB8; 0xAABE; 0xAABF;
3737+ 0xAAC1; 0xFE20; 0xFE21; 0xFE22; 0xFE23; 0xFE24; 0xFE25; 0xFE26;
3838+ 0x10A0F; 0x10A38; 0x1D185; 0x1D186; 0x1D187; 0x1D188; 0x1D189;
3939+ 0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD; 0x1D242; 0x1D243; 0x1D244;
4040+ |]
4141+4242+let diacritic n = Uchar.of_int diacritics.(n mod Array.length diacritics)
4343+let row_diacritic = diacritic
4444+let column_diacritic = diacritic
4545+let id_high_byte_diacritic = diacritic
4646+4747+let add_uchar buf u =
4848+ let code = Uchar.to_int u in
4949+ let put = Buffer.add_char buf in
5050+ if code < 0x80 then put (Char.chr code)
5151+ else if code < 0x800 then (
5252+ put (Char.chr (0xC0 lor (code lsr 6)));
5353+ put (Char.chr (0x80 lor (code land 0x3F))))
5454+ else if code < 0x10000 then (
5555+ put (Char.chr (0xE0 lor (code lsr 12)));
5656+ put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
5757+ put (Char.chr (0x80 lor (code land 0x3F))))
5858+ else (
5959+ put (Char.chr (0xF0 lor (code lsr 18)));
6060+ put (Char.chr (0x80 lor ((code lsr 12) land 0x3F)));
6161+ put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
6262+ put (Char.chr (0x80 lor (code land 0x3F))))
6363+6464+let write buf ~image_id ?placement_id ~rows ~cols () =
6565+ (* Set foreground color *)
6666+ Printf.bprintf buf "\027[38;2;%d;%d;%dm"
6767+ ((image_id lsr 16) land 0xFF)
6868+ ((image_id lsr 8) land 0xFF)
6969+ (image_id land 0xFF);
7070+ (* Optional placement ID in underline color *)
7171+ placement_id
7272+ |> Option.iter (fun pid ->
7373+ Printf.bprintf buf "\027[58;2;%d;%d;%dm"
7474+ ((pid lsr 16) land 0xFF)
7575+ ((pid lsr 8) land 0xFF)
7676+ (pid land 0xFF));
7777+ (* High byte diacritic *)
7878+ let high_byte = (image_id lsr 24) land 0xFF in
7979+ let high_diac =
8080+ if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None
8181+ in
8282+ (* Write grid *)
8383+ for row = 0 to rows - 1 do
8484+ for col = 0 to cols - 1 do
8585+ add_uchar buf placeholder_char;
8686+ add_uchar buf (row_diacritic row);
8787+ add_uchar buf (column_diacritic col);
8888+ high_diac |> Option.iter (add_uchar buf)
8989+ done;
9090+ if row < rows - 1 then Buffer.add_string buf "\n\r"
9191+ done;
9292+ (* Reset colors *)
9393+ Buffer.add_string buf "\027[39m";
9494+ if Option.is_some placement_id then Buffer.add_string buf "\027[59m"
+26
lib/kgp_unicode.mli
···11+(** Kitty Graphics Protocol Unicode Placeholders
22+33+ Support for invisible Unicode placeholder characters that encode
44+ image position metadata for accessibility and compatibility. *)
55+66+val placeholder_char : Uchar.t
77+(** The Unicode placeholder character U+10EEEE. *)
88+99+val write :
1010+ Buffer.t ->
1111+ image_id:int ->
1212+ ?placement_id:int ->
1313+ rows:int ->
1414+ cols:int ->
1515+ unit ->
1616+ unit
1717+(** Write placeholder characters to a buffer. *)
1818+1919+val row_diacritic : int -> Uchar.t
2020+(** Get the combining diacritic for a row number (0-based). *)
2121+2222+val column_diacritic : int -> Uchar.t
2323+(** Get the combining diacritic for a column number (0-based). *)
2424+2525+val id_high_byte_diacritic : int -> Uchar.t
2626+(** Get the diacritic for the high byte of a 32-bit image ID. *)
-687
lib/kitty_graphics.ml
···11-(* Kitty Terminal Graphics Protocol - Implementation *)
22-33-(* Polymorphic variant types *)
44-type format = [ `Rgba32 | `Rgb24 | `Png ]
55-type transmission = [ `Direct | `File | `Tempfile ]
66-type compression = [ `None | `Zlib ]
77-type quiet = [ `Noisy | `Errors_only | `Silent ]
88-type cursor = [ `Move | `Static ]
99-type composition = [ `Alpha_blend | `Overwrite ]
1010-1111-type delete =
1212- [ `All_visible
1313- | `All_visible_and_free
1414- | `By_id of int * int option
1515- | `By_id_and_free of int * int option
1616- | `By_number of int * int option
1717- | `By_number_and_free of int * int option
1818- | `At_cursor
1919- | `At_cursor_and_free
2020- | `At_cell of int * int
2121- | `At_cell_and_free of int * int
2222- | `At_cell_z of int * int * int
2323- | `At_cell_z_and_free of int * int * int
2424- | `By_column of int
2525- | `By_column_and_free of int
2626- | `By_row of int
2727- | `By_row_and_free of int
2828- | `By_z_index of int
2929- | `By_z_index_and_free of int
3030- | `By_id_range of int * int
3131- | `By_id_range_and_free of int * int
3232- | `Frames
3333- | `Frames_and_free ]
3434-3535-type animation_state = [ `Stop | `Loading | `Run ]
3636-3737-(* Modules re-export the types with conversion functions *)
3838-module Format = struct
3939- type t = format
4040-4141- let to_int : t -> int = function
4242- | `Rgba32 -> 32
4343- | `Rgb24 -> 24
4444- | `Png -> 100
4545-end
4646-4747-module Transmission = struct
4848- type t = transmission
4949-5050- let to_char : t -> char = function
5151- | `Direct -> 'd'
5252- | `File -> 'f'
5353- | `Tempfile -> 't'
5454-end
5555-5656-module Compression = struct
5757- type t = compression
5858-5959- let to_char : t -> char option = function
6060- | `None -> None
6161- | `Zlib -> Some 'z'
6262-end
6363-6464-module Quiet = struct
6565- type t = quiet
6666-6767- let to_int : t -> int = function
6868- | `Noisy -> 0
6969- | `Errors_only -> 1
7070- | `Silent -> 2
7171-end
7272-7373-module Cursor = struct
7474- type t = cursor
7575-7676- let to_int : t -> int = function
7777- | `Move -> 0
7878- | `Static -> 1
7979-end
8080-8181-module Composition = struct
8282- type t = composition
8383-8484- let to_int : t -> int = function
8585- | `Alpha_blend -> 0
8686- | `Overwrite -> 1
8787-end
8888-8989-module Delete = struct
9090- type t = delete
9191-end
9292-9393-module Placement = struct
9494- type t = {
9595- source_x : int option;
9696- source_y : int option;
9797- source_width : int option;
9898- source_height : int option;
9999- cell_x_offset : int option;
100100- cell_y_offset : int option;
101101- columns : int option;
102102- rows : int option;
103103- z_index : int option;
104104- placement_id : int option;
105105- cursor : cursor option;
106106- unicode_placeholder : bool;
107107- }
108108-109109- let empty =
110110- {
111111- source_x = None;
112112- source_y = None;
113113- source_width = None;
114114- source_height = None;
115115- cell_x_offset = None;
116116- cell_y_offset = None;
117117- columns = None;
118118- rows = None;
119119- z_index = None;
120120- placement_id = None;
121121- cursor = None;
122122- unicode_placeholder = false;
123123- }
124124-125125- let make ?source_x ?source_y ?source_width ?source_height ?cell_x_offset
126126- ?cell_y_offset ?columns ?rows ?z_index ?placement_id ?cursor
127127- ?(unicode_placeholder = false) () =
128128- {
129129- source_x;
130130- source_y;
131131- source_width;
132132- source_height;
133133- cell_x_offset;
134134- cell_y_offset;
135135- columns;
136136- rows;
137137- z_index;
138138- placement_id;
139139- cursor;
140140- unicode_placeholder;
141141- }
142142-end
143143-144144-module Frame = struct
145145- type t = {
146146- x : int option;
147147- y : int option;
148148- base_frame : int option;
149149- edit_frame : int option;
150150- gap_ms : int option;
151151- composition : composition option;
152152- background_color : int32 option;
153153- }
154154-155155- let empty =
156156- {
157157- x = None;
158158- y = None;
159159- base_frame = None;
160160- edit_frame = None;
161161- gap_ms = None;
162162- composition = None;
163163- background_color = None;
164164- }
165165-166166- let make ?x ?y ?base_frame ?edit_frame ?gap_ms ?composition ?background_color
167167- () =
168168- { x; y; base_frame; edit_frame; gap_ms; composition; background_color }
169169-end
170170-171171-module Animation = struct
172172- type state = animation_state
173173-174174- type t =
175175- [ `Set_state of state * int option
176176- | `Set_gap of int * int
177177- | `Set_current of int ]
178178-179179- let set_state ?loops state = `Set_state (state, loops)
180180- let set_gap ~frame ~gap_ms = `Set_gap (frame, gap_ms)
181181- let set_current_frame frame = `Set_current frame
182182-end
183183-184184-module Compose = struct
185185- type t = {
186186- source_frame : int;
187187- dest_frame : int;
188188- width : int option;
189189- height : int option;
190190- source_x : int option;
191191- source_y : int option;
192192- dest_x : int option;
193193- dest_y : int option;
194194- composition : composition option;
195195- }
196196-197197- let make ~source_frame ~dest_frame ?width ?height ?source_x ?source_y ?dest_x
198198- ?dest_y ?composition () =
199199- {
200200- source_frame;
201201- dest_frame;
202202- width;
203203- height;
204204- source_x;
205205- source_y;
206206- dest_x;
207207- dest_y;
208208- composition;
209209- }
210210-end
211211-212212-module Command = struct
213213- type action =
214214- [ `Transmit
215215- | `Transmit_and_display
216216- | `Query
217217- | `Display
218218- | `Delete
219219- | `Frame
220220- | `Animate
221221- | `Compose ]
222222-223223- type t = {
224224- action : action;
225225- format : format option;
226226- transmission : transmission option;
227227- compression : compression option;
228228- width : int option;
229229- height : int option;
230230- size : int option;
231231- offset : int option;
232232- quiet : quiet option;
233233- image_id : int option;
234234- image_number : int option;
235235- placement : Placement.t option;
236236- delete : delete option;
237237- frame : Frame.t option;
238238- animation : Animation.t option;
239239- compose : Compose.t option;
240240- }
241241-242242- let make action =
243243- {
244244- action;
245245- format = None;
246246- transmission = None;
247247- compression = None;
248248- width = None;
249249- height = None;
250250- size = None;
251251- offset = None;
252252- quiet = None;
253253- image_id = None;
254254- image_number = None;
255255- placement = None;
256256- delete = None;
257257- frame = None;
258258- animation = None;
259259- compose = None;
260260- }
261261-262262- let transmit ?image_id ?image_number ?format ?transmission ?compression ?width
263263- ?height ?size ?offset ?quiet () =
264264- {
265265- (make `Transmit) with
266266- image_id;
267267- image_number;
268268- format;
269269- transmission;
270270- compression;
271271- width;
272272- height;
273273- size;
274274- offset;
275275- quiet;
276276- }
277277-278278- let transmit_and_display ?image_id ?image_number ?format ?transmission
279279- ?compression ?width ?height ?size ?offset ?quiet ?placement () =
280280- {
281281- (make `Transmit_and_display) with
282282- image_id;
283283- image_number;
284284- format;
285285- transmission;
286286- compression;
287287- width;
288288- height;
289289- size;
290290- offset;
291291- quiet;
292292- placement;
293293- }
294294-295295- let query ?format ?transmission ?width ?height ?quiet () =
296296- { (make `Query) with format; transmission; width; height; quiet }
297297-298298- let display ?image_id ?image_number ?placement ?quiet () =
299299- { (make `Display) with image_id; image_number; placement; quiet }
300300-301301- let delete ?quiet del = { (make `Delete) with quiet; delete = Some del }
302302-303303- let frame ?image_id ?image_number ?format ?transmission ?compression ?width
304304- ?height ?quiet ~frame () =
305305- {
306306- (make `Frame) with
307307- image_id;
308308- image_number;
309309- format;
310310- transmission;
311311- compression;
312312- width;
313313- height;
314314- quiet;
315315- frame = Some frame;
316316- }
317317-318318- let animate ?image_id ?image_number ?quiet anim =
319319- { (make `Animate) with image_id; image_number; quiet; animation = Some anim }
320320-321321- let compose ?image_id ?image_number ?quiet comp =
322322- { (make `Compose) with image_id; image_number; quiet; compose = Some comp }
323323-324324- (* Serialization helpers *)
325325- let apc_start = "\027_G"
326326- let apc_end = "\027\\"
327327-328328- (* Key-value writer with separator handling *)
329329- type kv_writer = { mutable first : bool; buf : Buffer.t }
330330-331331- let kv_writer buf = { first = true; buf }
332332-333333- let kv w key value =
334334- if not w.first then Buffer.add_char w.buf ',';
335335- w.first <- false;
336336- Buffer.add_char w.buf key;
337337- Buffer.add_char w.buf '=';
338338- Buffer.add_string w.buf value
339339-340340- let kv_int w key value = kv w key (string_of_int value)
341341- let kv_int32 w key value = kv w key (Int32.to_string value)
342342- let kv_char w key value = kv w key (String.make 1 value)
343343-344344- (* Conditional writers using Option.iter *)
345345- let kv_int_opt w key = Option.iter (kv_int w key)
346346- let kv_int32_opt w key = Option.iter (kv_int32 w key)
347347-348348- let kv_int_if w key ~default opt =
349349- Option.iter (fun v -> if v <> default then kv_int w key v) opt
350350-351351- let action_char : action -> char = function
352352- | `Transmit -> 't'
353353- | `Transmit_and_display -> 'T'
354354- | `Query -> 'q'
355355- | `Display -> 'p'
356356- | `Delete -> 'd'
357357- | `Frame -> 'f'
358358- | `Animate -> 'a'
359359- | `Compose -> 'c'
360360-361361- let delete_char : delete -> char = function
362362- | `All_visible -> 'a'
363363- | `All_visible_and_free -> 'A'
364364- | `By_id _ -> 'i'
365365- | `By_id_and_free _ -> 'I'
366366- | `By_number _ -> 'n'
367367- | `By_number_and_free _ -> 'N'
368368- | `At_cursor -> 'c'
369369- | `At_cursor_and_free -> 'C'
370370- | `At_cell _ -> 'p'
371371- | `At_cell_and_free _ -> 'P'
372372- | `At_cell_z _ -> 'q'
373373- | `At_cell_z_and_free _ -> 'Q'
374374- | `By_column _ -> 'x'
375375- | `By_column_and_free _ -> 'X'
376376- | `By_row _ -> 'y'
377377- | `By_row_and_free _ -> 'Y'
378378- | `By_z_index _ -> 'z'
379379- | `By_z_index_and_free _ -> 'Z'
380380- | `By_id_range _ -> 'r'
381381- | `By_id_range_and_free _ -> 'R'
382382- | `Frames -> 'f'
383383- | `Frames_and_free -> 'F'
384384-385385- let write_placement w (p : Placement.t) =
386386- kv_int_opt w 'x' p.source_x;
387387- kv_int_opt w 'y' p.source_y;
388388- kv_int_opt w 'w' p.source_width;
389389- kv_int_opt w 'h' p.source_height;
390390- kv_int_opt w 'X' p.cell_x_offset;
391391- kv_int_opt w 'Y' p.cell_y_offset;
392392- kv_int_opt w 'c' p.columns;
393393- kv_int_opt w 'r' p.rows;
394394- kv_int_opt w 'z' p.z_index;
395395- kv_int_opt w 'p' p.placement_id;
396396- p.cursor |> Option.iter (fun c -> kv_int_if w 'C' ~default:0 (Some (Cursor.to_int c)));
397397- if p.unicode_placeholder then kv_int w 'U' 1
398398-399399- let write_delete w (d : delete) =
400400- kv_char w 'd' (delete_char d);
401401- match d with
402402- | `By_id (id, pid) | `By_id_and_free (id, pid) ->
403403- kv_int w 'i' id;
404404- kv_int_opt w 'p' pid
405405- | `By_number (n, pid) | `By_number_and_free (n, pid) ->
406406- kv_int w 'I' n;
407407- kv_int_opt w 'p' pid
408408- | `At_cell (x, y) | `At_cell_and_free (x, y) ->
409409- kv_int w 'x' x;
410410- kv_int w 'y' y
411411- | `At_cell_z (x, y, z) | `At_cell_z_and_free (x, y, z) ->
412412- kv_int w 'x' x;
413413- kv_int w 'y' y;
414414- kv_int w 'z' z
415415- | `By_column c | `By_column_and_free c -> kv_int w 'x' c
416416- | `By_row r | `By_row_and_free r -> kv_int w 'y' r
417417- | `By_z_index z | `By_z_index_and_free z -> kv_int w 'z' z
418418- | `By_id_range (min_id, max_id) | `By_id_range_and_free (min_id, max_id) ->
419419- kv_int w 'x' min_id;
420420- kv_int w 'y' max_id
421421- | `All_visible | `All_visible_and_free | `At_cursor | `At_cursor_and_free
422422- | `Frames | `Frames_and_free ->
423423- ()
424424-425425- let write_frame w (f : Frame.t) =
426426- kv_int_opt w 'x' f.x;
427427- kv_int_opt w 'y' f.y;
428428- kv_int_opt w 'c' f.base_frame;
429429- kv_int_opt w 'r' f.edit_frame;
430430- kv_int_opt w 'z' f.gap_ms;
431431- f.composition
432432- |> Option.iter (fun c -> kv_int_if w 'X' ~default:0 (Some (Composition.to_int c)));
433433- kv_int32_opt w 'Y' f.background_color
434434-435435- let write_animation w : Animation.t -> unit = function
436436- | `Set_state (state, loops) ->
437437- let s = match state with `Stop -> 1 | `Loading -> 2 | `Run -> 3 in
438438- kv_int w 's' s;
439439- kv_int_opt w 'v' loops
440440- | `Set_gap (frame, gap_ms) ->
441441- kv_int w 'r' frame;
442442- kv_int w 'z' gap_ms
443443- | `Set_current frame -> kv_int w 'c' frame
444444-445445- let write_compose w (c : Compose.t) =
446446- kv_int w 'r' c.source_frame;
447447- kv_int w 'c' c.dest_frame;
448448- kv_int_opt w 'w' c.width;
449449- kv_int_opt w 'h' c.height;
450450- kv_int_opt w 'x' c.dest_x;
451451- kv_int_opt w 'y' c.dest_y;
452452- kv_int_opt w 'X' c.source_x;
453453- kv_int_opt w 'Y' c.source_y;
454454- c.composition
455455- |> Option.iter (fun comp -> kv_int_if w 'C' ~default:0 (Some (Composition.to_int comp)))
456456-457457- let write_control_data buf cmd =
458458- let w = kv_writer buf in
459459- (* Action *)
460460- kv_char w 'a' (action_char cmd.action);
461461- (* Quiet - only if non-default *)
462462- cmd.quiet |> Option.iter (fun q -> kv_int_if w 'q' ~default:0 (Some (Quiet.to_int q)));
463463- (* Format *)
464464- cmd.format |> Option.iter (fun f -> kv_int w 'f' (Format.to_int f));
465465- (* Transmission - only for transmit/frame actions, always include t=d for compatibility *)
466466- (match cmd.action with
467467- | `Transmit | `Transmit_and_display | `Frame ->
468468- (match cmd.transmission with
469469- | Some t -> kv_char w 't' (Transmission.to_char t)
470470- | None -> kv_char w 't' 'd')
471471- | _ -> ());
472472- (* Compression *)
473473- cmd.compression |> Option.iter (fun c -> Compression.to_char c |> Option.iter (kv_char w 'o'));
474474- (* Dimensions *)
475475- kv_int_opt w 's' cmd.width;
476476- kv_int_opt w 'v' cmd.height;
477477- (* File size/offset *)
478478- kv_int_opt w 'S' cmd.size;
479479- kv_int_opt w 'O' cmd.offset;
480480- (* Image ID/number *)
481481- kv_int_opt w 'i' cmd.image_id;
482482- kv_int_opt w 'I' cmd.image_number;
483483- (* Complex options *)
484484- cmd.placement |> Option.iter (write_placement w);
485485- cmd.delete |> Option.iter (write_delete w);
486486- cmd.frame |> Option.iter (write_frame w);
487487- cmd.animation |> Option.iter (write_animation w);
488488- cmd.compose |> Option.iter (write_compose w);
489489- w
490490-491491- (* Use large chunk size to avoid chunking - Kitty animation doesn't handle chunks well *)
492492- let chunk_size = 1024 * 1024 (* 1MB - effectively no chunking *)
493493-494494- let write buf cmd ~data =
495495- Buffer.add_string buf apc_start;
496496- let w = write_control_data buf cmd in
497497- if String.length data > 0 then begin
498498- let encoded = Base64.encode_string data in
499499- let len = String.length encoded in
500500- if len <= chunk_size then (
501501- Buffer.add_char buf ';';
502502- Buffer.add_string buf encoded;
503503- Buffer.add_string buf apc_end)
504504- else begin
505505- (* Multiple chunks *)
506506- let rec write_chunks pos first =
507507- if pos < len then begin
508508- let remaining = len - pos in
509509- let this_chunk = min chunk_size remaining in
510510- let is_last = pos + this_chunk >= len in
511511- if first then (
512512- kv_int w 'm' 1;
513513- Buffer.add_char buf ';';
514514- Buffer.add_substring buf encoded pos this_chunk;
515515- Buffer.add_string buf apc_end)
516516- else (
517517- Buffer.add_string buf apc_start;
518518- Buffer.add_string buf (if is_last then "m=0" else "m=1");
519519- Buffer.add_char buf ';';
520520- Buffer.add_substring buf encoded pos this_chunk;
521521- Buffer.add_string buf apc_end);
522522- write_chunks (pos + this_chunk) false
523523- end
524524- in
525525- write_chunks 0 true
526526- end
527527- end
528528- else Buffer.add_string buf apc_end
529529-530530- let to_string cmd ~data =
531531- let buf = Buffer.create 1024 in
532532- write buf cmd ~data;
533533- Buffer.contents buf
534534-end
535535-536536-module Response = struct
537537- type t = {
538538- message : string;
539539- image_id : int option;
540540- image_number : int option;
541541- placement_id : int option;
542542- }
543543-544544- let is_ok t = t.message = "OK"
545545- let message t = t.message
546546-547547- let error_code t =
548548- if is_ok t then None
549549- else String.index_opt t.message ':' |> Option.fold ~none:(Some t.message) ~some:(fun i -> Some (String.sub t.message 0 i))
550550-551551- let image_id t = t.image_id
552552- let image_number t = t.image_number
553553- let placement_id t = t.placement_id
554554-555555- let parse s =
556556- let ( let* ) = Option.bind in
557557- let esc = '\027' in
558558- let len = String.length s in
559559- let* () = if len >= 5 && s.[0] = esc && s.[1] = '_' && s.[2] = 'G' then Some () else None in
560560- let* semi_pos = String.index_from_opt s 3 ';' in
561561- let rec find_end pos =
562562- if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then Some pos
563563- else if pos + 1 < len then find_end (pos + 1)
564564- else None
565565- in
566566- let* end_pos = find_end (semi_pos + 1) in
567567- let keys_str = String.sub s 3 (semi_pos - 3) in
568568- let message = String.sub s (semi_pos + 1) (end_pos - semi_pos - 1) in
569569- let parse_kv part =
570570- if String.length part >= 3 && part.[1] = '=' then
571571- Some (part.[0], String.sub part 2 (String.length part - 2))
572572- else None
573573- in
574574- let keys = String.split_on_char ',' keys_str |> List.filter_map parse_kv in
575575- let find_int key = List.assoc_opt key keys |> Fun.flip Option.bind int_of_string_opt in
576576- Some
577577- {
578578- message;
579579- image_id = find_int 'i';
580580- image_number = find_int 'I';
581581- placement_id = find_int 'p';
582582- }
583583-end
584584-585585-module Unicode_placeholder = struct
586586- let placeholder_char = Uchar.of_int 0x10EEEE
587587-588588- let diacritics =
589589- [|
590590- 0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F;
591591- 0x0346; 0x034A; 0x034B; 0x034C; 0x0350; 0x0351; 0x0352; 0x0357;
592592- 0x035B; 0x0363; 0x0364; 0x0365; 0x0366; 0x0367; 0x0368; 0x0369;
593593- 0x036A; 0x036B; 0x036C; 0x036D; 0x036E; 0x036F; 0x0483; 0x0484;
594594- 0x0485; 0x0486; 0x0487; 0x0592; 0x0593; 0x0594; 0x0595; 0x0597;
595595- 0x0598; 0x0599; 0x059C; 0x059D; 0x059E; 0x059F; 0x05A0; 0x05A1;
596596- 0x05A8; 0x05A9; 0x05AB; 0x05AC; 0x05AF; 0x05C4; 0x0610; 0x0611;
597597- 0x0612; 0x0613; 0x0614; 0x0615; 0x0616; 0x0617; 0x0657; 0x0658;
598598- 0x0659; 0x065A; 0x065B; 0x065D; 0x065E; 0x06D6; 0x06D7; 0x06D8;
599599- 0x06D9; 0x06DA; 0x06DB; 0x06DC; 0x06DF; 0x06E0; 0x06E1; 0x06E2;
600600- 0x06E4; 0x06E7; 0x06E8; 0x06EB; 0x06EC; 0x0730; 0x0732; 0x0733;
601601- 0x0735; 0x0736; 0x073A; 0x073D; 0x073F; 0x0740; 0x0741; 0x0743;
602602- 0x0745; 0x0747; 0x0749; 0x074A; 0x07EB; 0x07EC; 0x07ED; 0x07EE;
603603- 0x07EF; 0x07F0; 0x07F1; 0x07F3; 0x0816; 0x0817; 0x0818; 0x0819;
604604- 0x081B; 0x081C; 0x081D; 0x081E; 0x081F; 0x0820; 0x0821; 0x0822;
605605- 0x0823; 0x0825; 0x0826; 0x0827; 0x0829; 0x082A; 0x082B; 0x082C;
606606- 0x082D; 0x0951; 0x0953; 0x0954; 0x0F82; 0x0F83; 0x0F86; 0x0F87;
607607- 0x135D; 0x135E; 0x135F; 0x17DD; 0x193A; 0x1A17; 0x1A75; 0x1A76;
608608- 0x1A77; 0x1A78; 0x1A79; 0x1A7A; 0x1A7B; 0x1A7C; 0x1B6B; 0x1B6D;
609609- 0x1B6E; 0x1B6F; 0x1B70; 0x1B71; 0x1B72; 0x1B73; 0x1CD0; 0x1CD1;
610610- 0x1CD2; 0x1CDA; 0x1CDB; 0x1CE0; 0x1DC0; 0x1DC1; 0x1DC3; 0x1DC4;
611611- 0x1DC5; 0x1DC6; 0x1DC7; 0x1DC8; 0x1DC9; 0x1DCB; 0x1DCC; 0x1DD1;
612612- 0x1DD2; 0x1DD3; 0x1DD4; 0x1DD5; 0x1DD6; 0x1DD7; 0x1DD8; 0x1DD9;
613613- 0x1DDA; 0x1DDB; 0x1DDC; 0x1DDD; 0x1DDE; 0x1DDF; 0x1DE0; 0x1DE1;
614614- 0x1DE2; 0x1DE3; 0x1DE4; 0x1DE5; 0x1DE6; 0x1DFE; 0x20D0; 0x20D1;
615615- 0x20D4; 0x20D5; 0x20D6; 0x20D7; 0x20DB; 0x20DC; 0x20E1; 0x20E7;
616616- 0x20E9; 0x20F0; 0xA66F; 0xA67C; 0xA67D; 0xA6F0; 0xA6F1; 0xA8E0;
617617- 0xA8E1; 0xA8E2; 0xA8E3; 0xA8E4; 0xA8E5; 0xA8E6; 0xA8E7; 0xA8E8;
618618- 0xA8E9; 0xA8EA; 0xA8EB; 0xA8EC; 0xA8ED; 0xA8EE; 0xA8EF; 0xA8F0;
619619- 0xA8F1; 0xAAB0; 0xAAB2; 0xAAB3; 0xAAB7; 0xAAB8; 0xAABE; 0xAABF;
620620- 0xAAC1; 0xFE20; 0xFE21; 0xFE22; 0xFE23; 0xFE24; 0xFE25; 0xFE26;
621621- 0x10A0F; 0x10A38; 0x1D185; 0x1D186; 0x1D187; 0x1D188; 0x1D189;
622622- 0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD; 0x1D242; 0x1D243; 0x1D244;
623623- |]
624624-625625- let diacritic n =
626626- Uchar.of_int diacritics.(n mod Array.length diacritics)
627627-628628- let row_diacritic = diacritic
629629- let column_diacritic = diacritic
630630- let id_high_byte_diacritic = diacritic
631631-632632- let add_uchar buf u =
633633- let code = Uchar.to_int u in
634634- let put = Buffer.add_char buf in
635635- if code < 0x80 then put (Char.chr code)
636636- else if code < 0x800 then (
637637- put (Char.chr (0xC0 lor (code lsr 6)));
638638- put (Char.chr (0x80 lor (code land 0x3F))))
639639- else if code < 0x10000 then (
640640- put (Char.chr (0xE0 lor (code lsr 12)));
641641- put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
642642- put (Char.chr (0x80 lor (code land 0x3F))))
643643- else (
644644- put (Char.chr (0xF0 lor (code lsr 18)));
645645- put (Char.chr (0x80 lor ((code lsr 12) land 0x3F)));
646646- put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
647647- put (Char.chr (0x80 lor (code land 0x3F))))
648648-649649- let write buf ~image_id ?placement_id ~rows ~cols () =
650650- (* Set foreground color *)
651651- Printf.bprintf buf "\027[38;2;%d;%d;%dm"
652652- ((image_id lsr 16) land 0xFF)
653653- ((image_id lsr 8) land 0xFF)
654654- (image_id land 0xFF);
655655- (* Optional placement ID in underline color *)
656656- placement_id
657657- |> Option.iter (fun pid ->
658658- Printf.bprintf buf "\027[58;2;%d;%d;%dm"
659659- ((pid lsr 16) land 0xFF)
660660- ((pid lsr 8) land 0xFF)
661661- (pid land 0xFF));
662662- (* High byte diacritic *)
663663- let high_byte = (image_id lsr 24) land 0xFF in
664664- let high_diac = if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None in
665665- (* Write grid *)
666666- for row = 0 to rows - 1 do
667667- for col = 0 to cols - 1 do
668668- add_uchar buf placeholder_char;
669669- add_uchar buf (row_diacritic row);
670670- add_uchar buf (column_diacritic col);
671671- high_diac |> Option.iter (add_uchar buf)
672672- done;
673673- if row < rows - 1 then Buffer.add_string buf "\n\r"
674674- done;
675675- (* Reset colors *)
676676- Buffer.add_string buf "\027[39m";
677677- if Option.is_some placement_id then Buffer.add_string buf "\027[59m"
678678-end
679679-680680-module Detect = struct
681681- let make_query () =
682682- let cmd = Command.query ~format:`Rgb24 ~transmission:`Direct ~width:1 ~height:1 () in
683683- Command.to_string cmd ~data:"\x00\x00\x00"
684684-685685- let supports_graphics response ~da1_received =
686686- response |> Option.map Response.is_ok |> Option.value ~default:(not da1_received)
687687-end
-402
lib/kitty_graphics.mli
···11-(** Kitty Terminal Graphics Protocol
22-33- This library implements the Kitty terminal graphics protocol, allowing
44- OCaml programs to display images in terminals that support the protocol
55- (Kitty, WezTerm, Konsole, Ghostty, etc.).
66-77- The protocol uses APC (Application Programming Command) escape sequences
88- to transmit and display pixel graphics. Images can be transmitted as raw
99- RGB/RGBA data or PNG, and displayed at specific positions with various
1010- placement options.
1111-1212- {2 Basic Usage}
1313-1414- {[
1515- (* Display a PNG image *)
1616- let png_data = read_file "image.png" in
1717- let cmd = Kitty_graphics.Command.transmit_and_display ~format:`Png () in
1818- let buf = Buffer.create 1024 in
1919- Kitty_graphics.Command.write buf cmd ~data:png_data;
2020- print_string (Buffer.contents buf)
2121- ]}
2222-2323- {2 Protocol Reference}
2424-2525- See {{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics Protocol}
2626- for the full specification. *)
2727-2828-(** {1 Polymorphic Variant Types} *)
2929-3030-type format = [ `Rgba32 | `Rgb24 | `Png ]
3131-(** Image data formats. [`Rgba32] is 32-bit RGBA (4 bytes per pixel),
3232- [`Rgb24] is 24-bit RGB (3 bytes per pixel), [`Png] is PNG encoded data. *)
3333-3434-type transmission = [ `Direct | `File | `Tempfile ]
3535-(** Transmission methods. [`Direct] sends data inline, [`File] reads from a path,
3636- [`Tempfile] reads from a temp file that the terminal deletes after reading. *)
3737-3838-type compression = [ `None | `Zlib ]
3939-(** Compression options. [`None] for raw data, [`Zlib] for RFC 1950 compression. *)
4040-4141-type quiet = [ `Noisy | `Errors_only | `Silent ]
4242-(** Response suppression. [`Noisy] sends all responses (default),
4343- [`Errors_only] suppresses OK responses, [`Silent] suppresses all. *)
4444-4545-type cursor = [ `Move | `Static ]
4646-(** Cursor movement after displaying. [`Move] advances cursor (default),
4747- [`Static] keeps cursor in place. *)
4848-4949-type composition = [ `Alpha_blend | `Overwrite ]
5050-(** Composition modes. [`Alpha_blend] for full blending (default),
5151- [`Overwrite] for simple pixel replacement. *)
5252-5353-type delete =
5454- [ `All_visible
5555- | `All_visible_and_free
5656- | `By_id of int * int option
5757- | `By_id_and_free of int * int option
5858- | `By_number of int * int option
5959- | `By_number_and_free of int * int option
6060- | `At_cursor
6161- | `At_cursor_and_free
6262- | `At_cell of int * int
6363- | `At_cell_and_free of int * int
6464- | `At_cell_z of int * int * int
6565- | `At_cell_z_and_free of int * int * int
6666- | `By_column of int
6767- | `By_column_and_free of int
6868- | `By_row of int
6969- | `By_row_and_free of int
7070- | `By_z_index of int
7171- | `By_z_index_and_free of int
7272- | `By_id_range of int * int
7373- | `By_id_range_and_free of int * int
7474- | `Frames
7575- | `Frames_and_free ]
7676-(** Delete target specification. Each variant has two forms: one that only
7777- removes placements (e.g., [`All_visible]) and one that also frees the
7878- image data (e.g., [`All_visible_and_free]). Tuple variants contain
7979- (image_id, optional_placement_id) or (x, y) coordinates. *)
8080-8181-type animation_state = [ `Stop | `Loading | `Run ]
8282-(** Animation playback state. [`Stop] halts animation, [`Loading] runs but
8383- waits for new frames at end, [`Run] runs normally and loops. *)
8484-8585-(** {1 Type Modules} *)
8686-8787-module Format : sig
8888- type t = format
8989-9090- val to_int : t -> int
9191- (** Convert to protocol integer value (32, 24, or 100). *)
9292-end
9393-9494-module Transmission : sig
9595- type t = transmission
9696-9797- val to_char : t -> char
9898- (** Convert to protocol character ('d', 'f', or 't'). *)
9999-end
100100-101101-module Compression : sig
102102- type t = compression
103103-104104- val to_char : t -> char option
105105- (** Convert to protocol character ([None] or [Some 'z']). *)
106106-end
107107-108108-module Quiet : sig
109109- type t = quiet
110110-111111- val to_int : t -> int
112112- (** Convert to protocol integer (0, 1, or 2). *)
113113-end
114114-115115-module Cursor : sig
116116- type t = cursor
117117-118118- val to_int : t -> int
119119- (** Convert to protocol integer (0 or 1). *)
120120-end
121121-122122-module Composition : sig
123123- type t = composition
124124-125125- val to_int : t -> int
126126- (** Convert to protocol integer (0 or 1). *)
127127-end
128128-129129-module Delete : sig
130130- type t = delete
131131-end
132132-133133-(** {1 Placement Options} *)
134134-135135-module Placement : sig
136136- type t
137137- (** Placement configuration. *)
138138-139139- val make :
140140- ?source_x:int ->
141141- ?source_y:int ->
142142- ?source_width:int ->
143143- ?source_height:int ->
144144- ?cell_x_offset:int ->
145145- ?cell_y_offset:int ->
146146- ?columns:int ->
147147- ?rows:int ->
148148- ?z_index:int ->
149149- ?placement_id:int ->
150150- ?cursor:cursor ->
151151- ?unicode_placeholder:bool ->
152152- unit ->
153153- t
154154- (** Create a placement configuration.
155155-156156- @param source_x Left edge of source rectangle in pixels (default 0)
157157- @param source_y Top edge of source rectangle in pixels (default 0)
158158- @param source_width Width of source rectangle (default: full width)
159159- @param source_height Height of source rectangle (default: full height)
160160- @param cell_x_offset X offset within the first cell in pixels
161161- @param cell_y_offset Y offset within the first cell in pixels
162162- @param columns Number of columns to display over (scales image)
163163- @param rows Number of rows to display over (scales image)
164164- @param z_index Stacking order (negative = under text)
165165- @param placement_id Unique ID for this placement
166166- @param cursor Cursor movement policy after display
167167- @param unicode_placeholder Create virtual placement for Unicode mode *)
168168-169169- val empty : t
170170- (** Empty placement with all defaults. *)
171171-end
172172-173173-(** {1 Animation} *)
174174-175175-module Frame : sig
176176- type t
177177- (** Animation frame configuration. *)
178178-179179- val make :
180180- ?x:int ->
181181- ?y:int ->
182182- ?base_frame:int ->
183183- ?edit_frame:int ->
184184- ?gap_ms:int ->
185185- ?composition:composition ->
186186- ?background_color:int32 ->
187187- unit ->
188188- t
189189- (** Create a frame specification.
190190-191191- @param x Left edge where frame data is placed (pixels)
192192- @param y Top edge where frame data is placed (pixels)
193193- @param base_frame 1-based frame number to use as background canvas
194194- @param edit_frame 1-based frame number to edit (0 = new frame)
195195- @param gap_ms Delay before next frame in milliseconds
196196- @param composition How to blend pixels onto the canvas
197197- @param background_color 32-bit RGBA background when no base frame *)
198198-199199- val empty : t
200200- (** Empty frame spec with defaults. *)
201201-end
202202-203203-module Animation : sig
204204- type state = animation_state
205205-206206- type t =
207207- [ `Set_state of state * int option
208208- | `Set_gap of int * int
209209- | `Set_current of int ]
210210- (** Animation control operations. *)
211211-212212- val set_state : ?loops:int -> state -> t
213213- (** Set animation state.
214214- @param loops Number of loops: 0 = ignored, 1 = infinite, n = n-1 loops *)
215215-216216- val set_gap : frame:int -> gap_ms:int -> t
217217- (** Set the gap (delay) for a specific frame.
218218- @param frame 1-based frame number
219219- @param gap_ms Delay in milliseconds (negative = gapless) *)
220220-221221- val set_current_frame : int -> t
222222- (** Make a specific frame (1-based) the current displayed frame. *)
223223-end
224224-225225-module Compose : sig
226226- type t
227227- (** Composition operation. *)
228228-229229- val make :
230230- source_frame:int ->
231231- dest_frame:int ->
232232- ?width:int ->
233233- ?height:int ->
234234- ?source_x:int ->
235235- ?source_y:int ->
236236- ?dest_x:int ->
237237- ?dest_y:int ->
238238- ?composition:composition ->
239239- unit ->
240240- t
241241- (** Compose a rectangle from one frame onto another. *)
242242-end
243243-244244-(** {1 Commands} *)
245245-246246-module Command : sig
247247- type t
248248- (** A graphics protocol command. *)
249249-250250- (** {2 Image Transmission} *)
251251-252252- val transmit :
253253- ?image_id:int ->
254254- ?image_number:int ->
255255- ?format:format ->
256256- ?transmission:transmission ->
257257- ?compression:compression ->
258258- ?width:int ->
259259- ?height:int ->
260260- ?size:int ->
261261- ?offset:int ->
262262- ?quiet:quiet ->
263263- unit ->
264264- t
265265- (** Transmit image data without displaying. *)
266266-267267- val transmit_and_display :
268268- ?image_id:int ->
269269- ?image_number:int ->
270270- ?format:format ->
271271- ?transmission:transmission ->
272272- ?compression:compression ->
273273- ?width:int ->
274274- ?height:int ->
275275- ?size:int ->
276276- ?offset:int ->
277277- ?quiet:quiet ->
278278- ?placement:Placement.t ->
279279- unit ->
280280- t
281281- (** Transmit image data and display it immediately. *)
282282-283283- val query :
284284- ?format:format ->
285285- ?transmission:transmission ->
286286- ?width:int ->
287287- ?height:int ->
288288- ?quiet:quiet ->
289289- unit ->
290290- t
291291- (** Query terminal support without storing the image. *)
292292-293293- (** {2 Display} *)
294294-295295- val display :
296296- ?image_id:int ->
297297- ?image_number:int ->
298298- ?placement:Placement.t ->
299299- ?quiet:quiet ->
300300- unit ->
301301- t
302302- (** Display a previously transmitted image. *)
303303-304304- (** {2 Deletion} *)
305305-306306- val delete : ?quiet:quiet -> delete -> t
307307- (** Delete images or placements. *)
308308-309309- (** {2 Animation} *)
310310-311311- val frame :
312312- ?image_id:int ->
313313- ?image_number:int ->
314314- ?format:format ->
315315- ?transmission:transmission ->
316316- ?compression:compression ->
317317- ?width:int ->
318318- ?height:int ->
319319- ?quiet:quiet ->
320320- frame:Frame.t ->
321321- unit ->
322322- t
323323- (** Transmit animation frame data. *)
324324-325325- val animate : ?image_id:int -> ?image_number:int -> ?quiet:quiet -> Animation.t -> t
326326- (** Control animation playback. *)
327327-328328- val compose : ?image_id:int -> ?image_number:int -> ?quiet:quiet -> Compose.t -> t
329329- (** Compose animation frames. *)
330330-331331- (** {2 Output} *)
332332-333333- val write : Buffer.t -> t -> data:string -> unit
334334- (** Write the command to a buffer. *)
335335-336336- val to_string : t -> data:string -> string
337337- (** Convert command to a string. *)
338338-end
339339-340340-(** {1 Response Parsing} *)
341341-342342-module Response : sig
343343- type t
344344- (** A parsed terminal response. *)
345345-346346- val parse : string -> t option
347347- (** Parse a response from terminal output. *)
348348-349349- val is_ok : t -> bool
350350- (** Check if the response indicates success. *)
351351-352352- val message : t -> string
353353- (** Get the response message. *)
354354-355355- val error_code : t -> string option
356356- (** Extract the error code if this is an error response. *)
357357-358358- val image_id : t -> int option
359359- (** Get the image ID from the response. *)
360360-361361- val image_number : t -> int option
362362- (** Get the image number from the response. *)
363363-364364- val placement_id : t -> int option
365365- (** Get the placement ID from the response. *)
366366-end
367367-368368-(** {1 Unicode Placeholders} *)
369369-370370-module Unicode_placeholder : sig
371371- val placeholder_char : Uchar.t
372372- (** The Unicode placeholder character U+10EEEE. *)
373373-374374- val write :
375375- Buffer.t ->
376376- image_id:int ->
377377- ?placement_id:int ->
378378- rows:int ->
379379- cols:int ->
380380- unit ->
381381- unit
382382- (** Write placeholder characters to a buffer. *)
383383-384384- val row_diacritic : int -> Uchar.t
385385- (** Get the combining diacritic for a row number (0-based). *)
386386-387387- val column_diacritic : int -> Uchar.t
388388- (** Get the combining diacritic for a column number (0-based). *)
389389-390390- val id_high_byte_diacritic : int -> Uchar.t
391391- (** Get the diacritic for the high byte of a 32-bit image ID. *)
392392-end
393393-394394-(** {1 Terminal Detection} *)
395395-396396-module Detect : sig
397397- val make_query : unit -> string
398398- (** Generate a query command to test graphics support. *)
399399-400400- val supports_graphics : Response.t option -> da1_received:bool -> bool
401401- (** Determine if graphics are supported based on query results. *)
402402-end