My agentic slop goes here. Not intended for anyone else!

sync

Changed files
+643 -867
stack
kitty_graphics
+204 -93
stack/kitty_graphics/example/example.ml
···
-
(* Example usage of the Kitty Graphics Protocol library *)
-
(* Create a 64x64 colorful gradient image in RGBA format *)
-
let test_rgba_image () =
-
let size = 64 in
-
let pixels = Bytes.create (size * size * 4) in
-
for y = 0 to size - 1 do
-
for x = 0 to size - 1 do
-
let offset = (y * size + x) * 4 in
-
(* Red gradient left to right *)
-
Bytes.set pixels offset (Char.chr (x * 4 land 0xFF));
-
(* Green gradient top to bottom *)
-
Bytes.set pixels (offset + 1) (Char.chr (y * 4 land 0xFF));
-
(* Blue diagonal gradient *)
-
Bytes.set pixels (offset + 2) (Char.chr ((x + y) * 2 land 0xFF));
-
(* Fully opaque *)
-
Bytes.set pixels (offset + 3) '\xff'
-
done
done;
-
(size, Bytes.to_string pixels)
let () =
-
print_endline "Kitty Graphics Protocol Example";
-
print_endline "================================";
print_newline ();
-
(* Example 1: Display a simple RGBA image *)
-
print_endline "1. Displaying a 64x64 RGBA gradient image:";
print_newline ();
-
flush stdout;
-
let (size, image_data) = test_rgba_image () in
-
let cmd =
-
Kitty_graphics.Command.transmit_and_display
-
~format:Kitty_graphics.Format.Rgba32
-
~width:size ~height:size
-
()
in
-
let buf = Buffer.create 4096 in
-
Kitty_graphics.Command.write buf cmd ~data:image_data;
-
print_string (Buffer.contents buf);
-
flush stdout;
print_newline ();
print_newline ();
-
(* Example 2: Display scaled to specific cell size *)
-
print_endline "2. Same image scaled to 20 columns x 10 rows:";
print_newline ();
-
flush stdout;
-
let placement =
-
Kitty_graphics.Placement.make ~columns:20 ~rows:10 ()
-
in
-
let cmd =
-
Kitty_graphics.Command.transmit_and_display
-
~format:Kitty_graphics.Format.Rgba32
-
~width:size ~height:size
-
~placement
-
()
-
in
-
Buffer.clear buf;
-
Kitty_graphics.Command.write buf cmd ~data:image_data;
-
print_string (Buffer.contents buf);
flush stdout;
-
print_newline ();
-
print_newline ();
-
(* Example 3: Query terminal support *)
-
print_endline "3. Query command (to test graphics support):";
-
let query = Kitty_graphics.Detect.make_query () in
-
Printf.printf " Query escape sequence: %S\n" query;
-
print_newline ();
-
(* Example 4: Delete command *)
-
print_endline "4. Delete all visible images:";
-
let del_cmd =
-
Kitty_graphics.Command.delete Kitty_graphics.Delete.All_visible
-
in
-
Buffer.clear buf;
-
Kitty_graphics.Command.write buf del_cmd ~data:"";
-
Printf.printf " Delete escape sequence: %S\n" (Buffer.contents buf);
-
print_newline ();
-
(* Example 5: Unicode placeholder *)
-
print_endline "5. Unicode placeholder (for tmux compatibility):";
-
print_newline ();
-
Buffer.clear buf;
-
Kitty_graphics.Unicode_placeholder.write buf ~image_id:42 ~rows:2 ~cols:4 ();
-
print_string (Buffer.contents buf);
print_newline ();
print_newline ();
-
(* Example 6: Parse a response *)
-
print_endline "6. Parsing terminal responses:";
-
let test_response = "\027_Gi=123;OK\027\\" in
-
(match Kitty_graphics.Response.parse test_response with
-
| Some r ->
-
Printf.printf " Parsed response: is_ok=%b, image_id=%s\n"
-
(Kitty_graphics.Response.is_ok r)
-
(match Kitty_graphics.Response.image_id r with
-
| Some id -> string_of_int id
-
| None -> "none")
-
| None -> print_endline " Failed to parse");
-
let error_response = "\027_Gi=456;ENOENT:Image not found\027\\" in
-
(match Kitty_graphics.Response.parse error_response with
-
| Some r ->
-
Printf.printf " Error response: code=%s, message=%s\n"
-
(match Kitty_graphics.Response.error_code r with
-
| Some c -> c
-
| None -> "none")
-
(Kitty_graphics.Response.message r)
-
| None -> print_endline " Failed to parse");
-
print_newline ();
-
print_endline "Done!"
···
+
(* Kitty Graphics Protocol Demo - Matching kgp/examples/demo workflow *)
+
module K = Kitty_graphics
+
+
(* Generate a solid color RGBA frame *)
+
let make_solid_frame ~width ~height ~r ~g ~b =
+
let pixels = Bytes.create (width * height * 4) in
+
for i = 0 to (width * height) - 1 do
+
let idx = i * 4 in
+
Bytes.set pixels idx (Char.chr r);
+
Bytes.set pixels (idx + 1) (Char.chr g);
+
Bytes.set pixels (idx + 2) (Char.chr b);
+
Bytes.set pixels (idx + 3) '\xff'
done;
+
Bytes.to_string pixels
+
+
let send cmd ~data =
+
print_string (K.Command.to_string cmd ~data);
+
flush stdout
+
+
let wait_for_enter () =
+
print_string "Press Enter to continue...";
+
flush stdout;
+
let _ = read_line () in
+
print_newline ()
+
+
let clear_screen () =
+
print_string "\x1b[2J\x1b[H";
+
for _ = 1 to 5 do print_newline () done;
+
flush stdout
let () =
+
clear_screen ();
+
print_endline "Kitty Graphics Protocol - OCaml Demo";
+
print_endline "=====================================";
+
print_newline ();
+
print_endline "Press Enter to proceed through each demo...";
print_newline ();
+
wait_for_enter ();
+
(* Demo 1: Basic RGBA format *)
+
clear_screen ();
+
print_endline "Demo 1: Image Format - RGBA (32-bit)";
+
let blue_data = make_solid_frame ~width:100 ~height:100 ~r:0 ~g:0 ~b:255 in
+
send
+
(K.Command.transmit_and_display
+
~image_id:1
+
~format:`Rgba32
+
~width:100 ~height:100
+
~quiet:`Errors_only
+
())
+
~data:blue_data;
+
print_endline "Blue square displayed using raw RGBA format";
print_newline ();
+
wait_for_enter ();
+
(* Demo 2: Basic RGB format *)
+
clear_screen ();
+
print_endline "Demo 2: Image Format - RGB (24-bit)";
+
(* RGB is 3 bytes per pixel *)
+
let green_rgb =
+
let pixels = Bytes.create (100 * 100 * 3) in
+
for i = 0 to (100 * 100) - 1 do
+
let idx = i * 3 in
+
Bytes.set pixels idx '\x00'; (* R *)
+
Bytes.set pixels (idx + 1) '\xff'; (* G *)
+
Bytes.set pixels (idx + 2) '\x00' (* B *)
+
done;
+
Bytes.to_string pixels
in
+
send
+
(K.Command.transmit_and_display
+
~image_id:2
+
~format:`Rgb24
+
~width:100 ~height:100
+
~quiet:`Errors_only
+
())
+
~data:green_rgb;
+
print_endline "Green square displayed using raw RGB format (no alpha)";
print_newline ();
+
wait_for_enter ();
+
+
(* Demo 3: Multiple placements - transmit once, display multiple times *)
+
clear_screen ();
+
print_endline "Demo 3: Multiple Placements";
+
let cyan_data = make_solid_frame ~width:80 ~height:80 ~r:0 ~g:255 ~b:255 in
+
(* Transmit only (a=t) *)
+
send
+
(K.Command.transmit
+
~image_id:100
+
~format:`Rgba32
+
~width:80 ~height:80
+
~quiet:`Errors_only
+
())
+
~data:cyan_data;
+
(* Display first placement *)
+
send
+
(K.Command.display
+
~image_id:100
+
~placement:(K.Placement.make ~columns:10 ~rows:5 ())
+
~quiet:`Errors_only
+
())
+
~data:"";
+
print_string " ";
+
(* Display second placement *)
+
send
+
(K.Command.display
+
~image_id:100
+
~placement:(K.Placement.make ~columns:5 ~rows:3 ())
+
~quiet:`Errors_only
+
())
+
~data:"";
print_newline ();
+
print_endline "Same image displayed twice at different sizes";
+
print_newline ();
+
wait_for_enter ();
+
(* Demo 4: Z-index layering *)
+
clear_screen ();
+
print_endline "Demo 4: Z-Index Layering";
+
let orange_data = make_solid_frame ~width:200 ~height:100 ~r:255 ~g:165 ~b:0 in
+
send
+
(K.Command.transmit_and_display
+
~image_id:200
+
~format:`Rgba32
+
~width:200 ~height:100
+
~placement:(K.Placement.make ~z_index:(-1) ~cursor:`Static ())
+
~quiet:`Errors_only
+
())
+
~data:orange_data;
+
print_endline "This orange square should appear behind the text!";
print_newline ();
+
wait_for_enter ();
+
(* Demo 5: Animation - matching kgp demo exactly *)
+
clear_screen ();
+
print_endline "Demo 5: Animation - Color-changing square";
+
print_endline "Creating animated sequence...";
flush stdout;
+
(* Using small size to avoid chunking - 10x10 = 400 bytes raw *)
+
let width, height = 10, 10 in
+
let image_id = 300 in
+
(* Step 1: Create base frame (red) - transmit only, don't display yet *)
+
let red_frame = make_solid_frame ~width ~height ~r:255 ~g:0 ~b:0 in
+
send
+
(K.Command.transmit
+
~image_id
+
~format:`Rgba32
+
~width ~height
+
~quiet:`Errors_only
+
())
+
~data:red_frame;
+
+
(* Step 2: Add frame 2 (orange) with gap and composition replace *)
+
let orange_frame = make_solid_frame ~width ~height ~r:255 ~g:165 ~b:0 in
+
send
+
(K.Command.frame
+
~image_id
+
~format:`Rgba32
+
~width ~height
+
~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
+
~quiet:`Errors_only
+
())
+
~data:orange_frame;
+
(* Step 3: Add frame 3 (yellow) *)
+
let yellow_frame = make_solid_frame ~width ~height ~r:255 ~g:255 ~b:0 in
+
send
+
(K.Command.frame
+
~image_id
+
~format:`Rgba32
+
~width ~height
+
~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
+
~quiet:`Errors_only
+
())
+
~data:yellow_frame;
+
+
(* Step 4: Add frame 4 (green) *)
+
let green_frame = make_solid_frame ~width ~height ~r:0 ~g:255 ~b:0 in
+
send
+
(K.Command.frame
+
~image_id
+
~format:`Rgba32
+
~width ~height
+
~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
+
~quiet:`Errors_only
+
())
+
~data:green_frame;
+
+
(* Step 5: Create placement to display the animation *)
+
(* Add columns/rows to scale up the small image for visibility *)
+
send
+
(K.Command.display
+
~image_id
+
~placement:(K.Placement.make
+
~placement_id:1
+
~columns:10
+
~rows:5
+
~cursor:`Static
+
())
+
~quiet:`Errors_only
+
())
+
~data:"";
+
+
(* Step 6: Start animation with infinite looping (s=3, v=1) *)
+
send
+
(K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run))
+
~data:"";
+
print_newline ();
+
print_endline "Animation playing: Red -> Orange -> Yellow -> Green";
print_newline ();
+
wait_for_enter ();
+
(* Stop the animation *)
+
send
+
(K.Command.animate ~image_id (K.Animation.set_state `Stop))
+
~data:"";
+
print_endline "Animation stopped.";
+
print_newline ();
+
(* Cleanup *)
+
print_endline "Demo complete!";
+
()
+327 -544
stack/kitty_graphics/lib/kitty_graphics.ml
···
(* Kitty Terminal Graphics Protocol - Implementation *)
module Format = struct
-
type t = Rgba32 | Rgb24 | Png
-
let to_int = function Rgba32 -> 32 | Rgb24 -> 24 | Png -> 100
end
module Transmission = struct
-
type t = Direct | File | Tempfile
-
let to_char = function Direct -> 'd' | File -> 'f' | Tempfile -> 't'
end
module Compression = struct
-
type t = None | Zlib
-
let to_char = function None -> Option.none | Zlib -> Some 'z'
end
module Quiet = struct
-
type t = Noisy | Errors_only | Silent
-
let to_int = function Noisy -> 0 | Errors_only -> 1 | Silent -> 2
end
module Cursor = struct
-
type t = Move | Static
-
let to_int = function Move -> 0 | Static -> 1
end
module Composition = struct
-
type t = Alpha_blend | Overwrite
-
let to_int = function Alpha_blend -> 0 | Overwrite -> 1
end
module Delete = struct
-
type t =
-
| All_visible
-
| All_visible_and_free
-
| By_id of { image_id : int; placement_id : int option }
-
| By_id_and_free of { image_id : int; placement_id : int option }
-
| By_number of { image_number : int; placement_id : int option }
-
| By_number_and_free of { image_number : int; placement_id : int option }
-
| At_cursor
-
| At_cursor_and_free
-
| At_cell of { x : int; y : int }
-
| At_cell_and_free of { x : int; y : int }
-
| At_cell_z of { x : int; y : int; z : int }
-
| At_cell_z_and_free of { x : int; y : int; z : int }
-
| By_column of int
-
| By_column_and_free of int
-
| By_row of int
-
| By_row_and_free of int
-
| By_z_index of int
-
| By_z_index_and_free of int
-
| By_id_range of { min_id : int; max_id : int }
-
| By_id_range_and_free of { min_id : int; max_id : int }
-
| Frames
-
| Frames_and_free
end
module Placement = struct
···
rows : int option;
z_index : int option;
placement_id : int option;
-
cursor : Cursor.t option;
unicode_placeholder : bool;
}
···
base_frame : int option;
edit_frame : int option;
gap_ms : int option;
-
composition : Composition.t option;
background_color : int32 option;
}
···
end
module Animation = struct
-
type state = Stop | Loading | Run
type t =
-
| Set_state of { state : state; loops : int option }
-
| Set_gap of { frame : int; gap_ms : int }
-
| Set_current of int
-
let set_state ?loops state = Set_state { state; loops }
-
let set_gap ~frame ~gap_ms = Set_gap { frame; gap_ms }
-
let set_current_frame frame = Set_current frame
end
module Compose = struct
···
source_y : int option;
dest_x : int option;
dest_y : int option;
-
composition : Composition.t option;
}
let make ~source_frame ~dest_frame ?width ?height ?source_x ?source_y ?dest_x
···
module Command = struct
type action =
-
| Transmit
-
| Transmit_and_display
-
| Query
-
| Display
-
| Delete
-
| Frame
-
| Animate
-
| Compose
type t = {
action : action;
-
format : Format.t option;
-
transmission : Transmission.t option;
-
compression : Compression.t option;
width : int option;
height : int option;
size : int option;
offset : int option;
-
quiet : Quiet.t option;
image_id : int option;
image_number : int option;
placement : Placement.t option;
-
delete : Delete.t option;
frame : Frame.t option;
animation : Animation.t option;
compose : Compose.t option;
}
-
let make_base action =
{
action;
format = None;
···
let transmit ?image_id ?image_number ?format ?transmission ?compression ?width
?height ?size ?offset ?quiet () =
{
-
(make_base Transmit) with
image_id;
image_number;
format;
···
let transmit_and_display ?image_id ?image_number ?format ?transmission
?compression ?width ?height ?size ?offset ?quiet ?placement () =
{
-
(make_base Transmit_and_display) with
image_id;
image_number;
format;
···
}
let query ?format ?transmission ?width ?height ?quiet () =
-
{ (make_base Query) with format; transmission; width; height; quiet }
let display ?image_id ?image_number ?placement ?quiet () =
-
{ (make_base Display) with image_id; image_number; placement; quiet }
-
let delete ?quiet del =
-
{ (make_base Delete) with quiet; delete = Some del }
let frame ?image_id ?image_number ?format ?transmission ?compression ?width
?height ?quiet ~frame () =
{
-
(make_base Frame) with
image_id;
image_number;
format;
···
}
let animate ?image_id ?image_number ?quiet anim =
-
{ (make_base Animate) with image_id; image_number; quiet; animation = Some anim }
let compose ?image_id ?image_number ?quiet comp =
-
{ (make_base Compose) with image_id; image_number; quiet; compose = Some comp }
-
(* APC escape sequences *)
let apc_start = "\027_G"
let apc_end = "\027\\"
-
(* Helper to add key=value pairs *)
-
let add_kv buf key value =
-
Buffer.add_char buf key;
-
Buffer.add_char buf '=';
-
Buffer.add_string buf value
-
let add_kv_int buf key value =
-
Buffer.add_char buf key;
-
Buffer.add_char buf '=';
-
Buffer.add_string buf (string_of_int value)
-
let add_kv_int32 buf key value =
-
Buffer.add_char buf key;
-
Buffer.add_char buf '=';
-
Buffer.add_string buf (Int32.to_string value)
-
let add_comma buf = Buffer.add_char buf ','
-
let action_char = function
-
| Transmit -> 't'
-
| Transmit_and_display -> 'T'
-
| Query -> 'q'
-
| Display -> 'p'
-
| Delete -> 'd'
-
| Frame -> 'f'
-
| Animate -> 'a'
-
| Compose -> 'c'
-
let delete_char = function
-
| Delete.All_visible -> 'a'
-
| All_visible_and_free -> 'A'
-
| By_id _ -> 'i'
-
| By_id_and_free _ -> 'I'
-
| By_number _ -> 'n'
-
| By_number_and_free _ -> 'N'
-
| At_cursor -> 'c'
-
| At_cursor_and_free -> 'C'
-
| At_cell _ -> 'p'
-
| At_cell_and_free _ -> 'P'
-
| At_cell_z _ -> 'q'
-
| At_cell_z_and_free _ -> 'Q'
-
| By_column _ -> 'x'
-
| By_column_and_free _ -> 'X'
-
| By_row _ -> 'y'
-
| By_row_and_free _ -> 'Y'
-
| By_z_index _ -> 'z'
-
| By_z_index_and_free _ -> 'Z'
-
| By_id_range _ -> 'r'
-
| By_id_range_and_free _ -> 'R'
-
| Frames -> 'f'
-
| Frames_and_free -> 'F'
let write_control_data buf cmd =
-
let first = ref true in
-
let sep () =
-
if !first then first := false else add_comma buf
-
in
(* Action *)
-
sep ();
-
add_kv buf 'a' (String.make 1 (action_char cmd.action));
-
(* Quiet *)
-
Option.iter
-
(fun q ->
-
let v = Quiet.to_int q in
-
if v <> 0 then (
-
sep ();
-
add_kv_int buf 'q' v))
-
cmd.quiet;
(* Format *)
-
Option.iter
-
(fun f ->
-
sep ();
-
add_kv_int buf 'f' (Format.to_int f))
-
cmd.format;
-
(* Transmission *)
-
Option.iter
-
(fun t ->
-
let c = Transmission.to_char t in
-
if c <> 'd' then (
-
sep ();
-
add_kv buf 't' (String.make 1 c)))
-
cmd.transmission;
(* Compression *)
-
Option.iter
-
(fun c ->
-
match Compression.to_char c with
-
| Some ch ->
-
sep ();
-
add_kv buf 'o' (String.make 1 ch)
-
| None -> ())
-
cmd.compression;
(* Dimensions *)
-
Option.iter
-
(fun w ->
-
sep ();
-
add_kv_int buf 's' w)
-
cmd.width;
-
Option.iter
-
(fun h ->
-
sep ();
-
add_kv_int buf 'v' h)
-
cmd.height;
(* File size/offset *)
-
Option.iter
-
(fun s ->
-
sep ();
-
add_kv_int buf 'S' s)
-
cmd.size;
-
Option.iter
-
(fun o ->
-
sep ();
-
add_kv_int buf 'O' o)
-
cmd.offset;
-
(* Image ID *)
-
Option.iter
-
(fun id ->
-
sep ();
-
add_kv_int buf 'i' id)
-
cmd.image_id;
-
(* Image number *)
-
Option.iter
-
(fun n ->
-
sep ();
-
add_kv_int buf 'I' n)
-
cmd.image_number;
-
(* Placement options *)
-
Option.iter
-
(fun (p : Placement.t) ->
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'x' v)
-
p.source_x;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'y' v)
-
p.source_y;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'w' v)
-
p.source_width;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'h' v)
-
p.source_height;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'X' v)
-
p.cell_x_offset;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'Y' v)
-
p.cell_y_offset;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'c' v)
-
p.columns;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'r' v)
-
p.rows;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'z' v)
-
p.z_index;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'p' v)
-
p.placement_id;
-
Option.iter
-
(fun c ->
-
let v = Cursor.to_int c in
-
if v <> 0 then (
-
sep ();
-
add_kv_int buf 'C' v))
-
p.cursor;
-
if p.unicode_placeholder then (
-
sep ();
-
add_kv_int buf 'U' 1))
-
cmd.placement;
-
(* Delete options *)
-
Option.iter
-
(fun d ->
-
sep ();
-
add_kv buf 'd' (String.make 1 (delete_char d));
-
match d with
-
| Delete.By_id { image_id; placement_id }
-
| Delete.By_id_and_free { image_id; placement_id } ->
-
sep ();
-
add_kv_int buf 'i' image_id;
-
Option.iter
-
(fun p ->
-
sep ();
-
add_kv_int buf 'p' p)
-
placement_id
-
| Delete.By_number { image_number; placement_id }
-
| Delete.By_number_and_free { image_number; placement_id } ->
-
sep ();
-
add_kv_int buf 'I' image_number;
-
Option.iter
-
(fun p ->
-
sep ();
-
add_kv_int buf 'p' p)
-
placement_id
-
| Delete.At_cell { x; y } | Delete.At_cell_and_free { x; y } ->
-
sep ();
-
add_kv_int buf 'x' x;
-
sep ();
-
add_kv_int buf 'y' y
-
| Delete.At_cell_z { x; y; z }
-
| Delete.At_cell_z_and_free { x; y; z } ->
-
sep ();
-
add_kv_int buf 'x' x;
-
sep ();
-
add_kv_int buf 'y' y;
-
sep ();
-
add_kv_int buf 'z' z
-
| Delete.By_column c | Delete.By_column_and_free c ->
-
sep ();
-
add_kv_int buf 'x' c
-
| Delete.By_row r | Delete.By_row_and_free r ->
-
sep ();
-
add_kv_int buf 'y' r
-
| Delete.By_z_index z | Delete.By_z_index_and_free z ->
-
sep ();
-
add_kv_int buf 'z' z
-
| Delete.By_id_range { min_id; max_id }
-
| Delete.By_id_range_and_free { min_id; max_id } ->
-
sep ();
-
add_kv_int buf 'x' min_id;
-
sep ();
-
add_kv_int buf 'y' max_id
-
| _ -> ())
-
cmd.delete;
-
(* Frame options *)
-
Option.iter
-
(fun (f : Frame.t) ->
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'x' v)
-
f.x;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'y' v)
-
f.y;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'c' v)
-
f.base_frame;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'r' v)
-
f.edit_frame;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'z' v)
-
f.gap_ms;
-
Option.iter
-
(fun c ->
-
let v = Composition.to_int c in
-
if v <> 0 then (
-
sep ();
-
add_kv_int buf 'X' v))
-
f.composition;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int32 buf 'Y' v)
-
f.background_color)
-
cmd.frame;
-
(* Animation options *)
-
Option.iter
-
(fun a ->
-
match a with
-
| Animation.Set_state { state; loops } ->
-
let s =
-
match state with
-
| Animation.Stop -> 1
-
| Animation.Loading -> 2
-
| Animation.Run -> 3
-
in
-
sep ();
-
add_kv_int buf 's' s;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'v' v)
-
loops
-
| Animation.Set_gap { frame; gap_ms } ->
-
sep ();
-
add_kv_int buf 'r' frame;
-
sep ();
-
add_kv_int buf 'z' gap_ms
-
| Animation.Set_current frame ->
-
sep ();
-
add_kv_int buf 'c' frame)
-
cmd.animation;
-
(* Compose options *)
-
Option.iter
-
(fun (c : Compose.t) ->
-
sep ();
-
add_kv_int buf 'r' c.source_frame;
-
sep ();
-
add_kv_int buf 'c' c.dest_frame;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'w' v)
-
c.width;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'h' v)
-
c.height;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'x' v)
-
c.dest_x;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'y' v)
-
c.dest_y;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'X' v)
-
c.source_x;
-
Option.iter
-
(fun v ->
-
sep ();
-
add_kv_int buf 'Y' v)
-
c.source_y;
-
Option.iter
-
(fun comp ->
-
let v = Composition.to_int comp in
-
if v <> 0 then (
-
sep ();
-
add_kv_int buf 'C' v))
-
c.composition)
-
cmd.compose
let chunk_size = 4096
let write buf cmd ~data =
Buffer.add_string buf apc_start;
-
write_control_data buf cmd;
if String.length data > 0 then begin
let encoded = Base64.encode_string data in
let len = String.length encoded in
···
Buffer.add_string buf apc_end)
else begin
(* Multiple chunks *)
-
let pos = ref 0 in
-
let first = ref true in
-
while !pos < len do
-
let remaining = len - !pos in
-
let this_chunk = min chunk_size remaining in
-
let is_last = !pos + this_chunk >= len in
-
if !first then (
-
(* First chunk *)
-
first := false;
-
add_comma buf;
-
add_kv_int buf 'm' 1;
-
Buffer.add_char buf ';';
-
Buffer.add_substring buf encoded !pos this_chunk;
-
Buffer.add_string buf apc_end)
-
else (
-
(* Continuation chunk *)
-
Buffer.add_string buf apc_start;
-
add_kv_int buf 'm' (if is_last then 0 else 1);
-
Buffer.add_char buf ';';
-
Buffer.add_substring buf encoded !pos this_chunk;
-
Buffer.add_string buf apc_end);
-
pos := !pos + this_chunk
-
done
end
end
else Buffer.add_string buf apc_end
···
let error_code t =
if is_ok t then None
-
else
-
match String.index_opt t.message ':' with
-
| Some i -> Some (String.sub t.message 0 i)
-
| None -> Some t.message
let image_id t = t.image_id
let image_number t = t.image_number
let placement_id t = t.placement_id
let parse s =
-
(* Format: <ESC>_G<keys>;message<ESC>\ *)
let esc = '\027' in
let len = String.length s in
-
if len < 5 then None
-
else if s.[0] <> esc || s.[1] <> '_' || s.[2] <> 'G' then None
-
else
-
(* Find the semicolon and end *)
-
match String.index_from_opt s 3 ';' with
-
| None -> None
-
| Some semi_pos -> (
-
(* Find the APC terminator *)
-
let rec find_end pos =
-
if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then
-
Some pos
-
else if pos + 1 < len then find_end (pos + 1)
-
else None
-
in
-
match find_end (semi_pos + 1) with
-
| None -> None
-
| Some end_pos ->
-
let keys_str = String.sub s 3 (semi_pos - 3) in
-
let message =
-
String.sub s (semi_pos + 1) (end_pos - semi_pos - 1)
-
in
-
(* Parse keys *)
-
let image_id = ref None in
-
let image_number = ref None in
-
let placement_id = ref None in
-
let parts = String.split_on_char ',' keys_str in
-
List.iter
-
(fun part ->
-
if String.length part >= 3 && part.[1] = '=' then
-
let key = part.[0] in
-
let value = String.sub part 2 (String.length part - 2) in
-
match key with
-
| 'i' -> image_id := int_of_string_opt value
-
| 'I' -> image_number := int_of_string_opt value
-
| 'p' -> placement_id := int_of_string_opt value
-
| _ -> ())
-
parts;
-
Some
-
{
-
message;
-
image_id = !image_id;
-
image_number = !image_number;
-
placement_id = !placement_id;
-
})
end
module Unicode_placeholder = struct
let placeholder_char = Uchar.of_int 0x10EEEE
-
(* Row/column diacritics from the protocol spec *)
let diacritics =
[|
0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F;
···
0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD; 0x1D242; 0x1D243; 0x1D244;
|]
-
let row_diacritic n =
-
if n >= 0 && n < Array.length diacritics then
-
Uchar.of_int diacritics.(n)
-
else Uchar.of_int diacritics.(0)
-
let column_diacritic = row_diacritic
-
let id_high_byte_diacritic = row_diacritic
let add_uchar buf u =
-
let b = Bytes.create 4 in
-
let len = Uchar.utf_8_byte_length u in
-
let _ = Uchar.unsafe_to_char u in
-
(* Encode UTF-8 manually *)
let code = Uchar.to_int u in
-
if code < 0x80 then (
-
Bytes.set b 0 (Char.chr code);
-
Buffer.add_subbytes buf b 0 1)
else if code < 0x800 then (
-
Bytes.set b 0 (Char.chr (0xC0 lor (code lsr 6)));
-
Bytes.set b 1 (Char.chr (0x80 lor (code land 0x3F)));
-
Buffer.add_subbytes buf b 0 2)
else if code < 0x10000 then (
-
Bytes.set b 0 (Char.chr (0xE0 lor (code lsr 12)));
-
Bytes.set b 1 (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
-
Bytes.set b 2 (Char.chr (0x80 lor (code land 0x3F)));
-
Buffer.add_subbytes buf b 0 3)
else (
-
Bytes.set b 0 (Char.chr (0xF0 lor (code lsr 18)));
-
Bytes.set b 1 (Char.chr (0x80 lor ((code lsr 12) land 0x3F)));
-
Bytes.set b 2 (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
-
Bytes.set b 3 (Char.chr (0x80 lor (code land 0x3F)));
-
Buffer.add_subbytes buf b 0 len)
let write buf ~image_id ?placement_id ~rows ~cols () =
-
(* Set foreground color using 24-bit mode *)
-
let r = (image_id lsr 16) land 0xFF in
-
let g = (image_id lsr 8) land 0xFF in
-
let b = image_id land 0xFF in
-
Buffer.add_string buf (Printf.sprintf "\027[38;2;%d;%d;%dm" r g b);
-
(* Optionally set underline color for placement ID *)
-
(match placement_id with
-
| Some pid ->
-
let pr = (pid lsr 16) land 0xFF in
-
let pg = (pid lsr 8) land 0xFF in
-
let pb = pid land 0xFF in
-
Buffer.add_string buf (Printf.sprintf "\027[58;2;%d;%d;%dm" pr pg pb)
-
| None -> ());
-
(* High byte diacritic if needed *)
let high_byte = (image_id lsr 24) land 0xFF in
-
let high_diac =
-
if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None
-
in
-
(* Write placeholder grid *)
for row = 0 to rows - 1 do
for col = 0 to cols - 1 do
add_uchar buf placeholder_char;
add_uchar buf (row_diacritic row);
add_uchar buf (column_diacritic col);
-
Option.iter (add_uchar buf) high_diac
done;
if row < rows - 1 then Buffer.add_string buf "\n\r"
done;
(* Reset colors *)
Buffer.add_string buf "\027[39m";
-
match placement_id with Some _ -> Buffer.add_string buf "\027[59m" | None -> ()
end
module Detect = struct
let make_query () =
-
(* Send a 1x1 transparent pixel query *)
-
let cmd =
-
Command.query ~format:Format.Rgb24 ~transmission:Transmission.Direct
-
~width:1 ~height:1 ()
-
in
-
let data = "\x00\x00\x00" in
-
let query = Command.to_string cmd ~data in
-
(* Add DA1 query to detect non-supporting terminals *)
-
query ^ "\027[c"
let supports_graphics response ~da1_received =
-
match response with
-
| Some r -> Response.is_ok r
-
| None -> not da1_received
end
···
(* Kitty Terminal Graphics Protocol - Implementation *)
+
(* Polymorphic variant types *)
+
type format = [ `Rgba32 | `Rgb24 | `Png ]
+
type transmission = [ `Direct | `File | `Tempfile ]
+
type compression = [ `None | `Zlib ]
+
type quiet = [ `Noisy | `Errors_only | `Silent ]
+
type cursor = [ `Move | `Static ]
+
type composition = [ `Alpha_blend | `Overwrite ]
+
+
type delete =
+
[ `All_visible
+
| `All_visible_and_free
+
| `By_id of int * int option
+
| `By_id_and_free of int * int option
+
| `By_number of int * int option
+
| `By_number_and_free of int * int option
+
| `At_cursor
+
| `At_cursor_and_free
+
| `At_cell of int * int
+
| `At_cell_and_free of int * int
+
| `At_cell_z of int * int * int
+
| `At_cell_z_and_free of int * int * int
+
| `By_column of int
+
| `By_column_and_free of int
+
| `By_row of int
+
| `By_row_and_free of int
+
| `By_z_index of int
+
| `By_z_index_and_free of int
+
| `By_id_range of int * int
+
| `By_id_range_and_free of int * int
+
| `Frames
+
| `Frames_and_free ]
+
+
type animation_state = [ `Stop | `Loading | `Run ]
+
+
(* Modules re-export the types with conversion functions *)
module Format = struct
+
type t = format
+
let to_int : t -> int = function
+
| `Rgba32 -> 32
+
| `Rgb24 -> 24
+
| `Png -> 100
end
module Transmission = struct
+
type t = transmission
+
let to_char : t -> char = function
+
| `Direct -> 'd'
+
| `File -> 'f'
+
| `Tempfile -> 't'
end
module Compression = struct
+
type t = compression
+
let to_char : t -> char option = function
+
| `None -> None
+
| `Zlib -> Some 'z'
end
module Quiet = struct
+
type t = quiet
+
let to_int : t -> int = function
+
| `Noisy -> 0
+
| `Errors_only -> 1
+
| `Silent -> 2
end
module Cursor = struct
+
type t = cursor
+
let to_int : t -> int = function
+
| `Move -> 0
+
| `Static -> 1
end
module Composition = struct
+
type t = composition
+
let to_int : t -> int = function
+
| `Alpha_blend -> 0
+
| `Overwrite -> 1
end
module Delete = struct
+
type t = delete
end
module Placement = struct
···
rows : int option;
z_index : int option;
placement_id : int option;
+
cursor : cursor option;
unicode_placeholder : bool;
}
···
base_frame : int option;
edit_frame : int option;
gap_ms : int option;
+
composition : composition option;
background_color : int32 option;
}
···
end
module Animation = struct
+
type state = animation_state
type t =
+
[ `Set_state of state * int option
+
| `Set_gap of int * int
+
| `Set_current of int ]
+
let set_state ?loops state = `Set_state (state, loops)
+
let set_gap ~frame ~gap_ms = `Set_gap (frame, gap_ms)
+
let set_current_frame frame = `Set_current frame
end
module Compose = struct
···
source_y : int option;
dest_x : int option;
dest_y : int option;
+
composition : composition option;
}
let make ~source_frame ~dest_frame ?width ?height ?source_x ?source_y ?dest_x
···
module Command = struct
type action =
+
[ `Transmit
+
| `Transmit_and_display
+
| `Query
+
| `Display
+
| `Delete
+
| `Frame
+
| `Animate
+
| `Compose ]
type t = {
action : action;
+
format : format option;
+
transmission : transmission option;
+
compression : compression option;
width : int option;
height : int option;
size : int option;
offset : int option;
+
quiet : quiet option;
image_id : int option;
image_number : int option;
placement : Placement.t option;
+
delete : delete option;
frame : Frame.t option;
animation : Animation.t option;
compose : Compose.t option;
}
+
let make action =
{
action;
format = None;
···
let transmit ?image_id ?image_number ?format ?transmission ?compression ?width
?height ?size ?offset ?quiet () =
{
+
(make `Transmit) with
image_id;
image_number;
format;
···
let transmit_and_display ?image_id ?image_number ?format ?transmission
?compression ?width ?height ?size ?offset ?quiet ?placement () =
{
+
(make `Transmit_and_display) with
image_id;
image_number;
format;
···
}
let query ?format ?transmission ?width ?height ?quiet () =
+
{ (make `Query) with format; transmission; width; height; quiet }
let display ?image_id ?image_number ?placement ?quiet () =
+
{ (make `Display) with image_id; image_number; placement; quiet }
+
let delete ?quiet del = { (make `Delete) with quiet; delete = Some del }
let frame ?image_id ?image_number ?format ?transmission ?compression ?width
?height ?quiet ~frame () =
{
+
(make `Frame) with
image_id;
image_number;
format;
···
}
let animate ?image_id ?image_number ?quiet anim =
+
{ (make `Animate) with image_id; image_number; quiet; animation = Some anim }
let compose ?image_id ?image_number ?quiet comp =
+
{ (make `Compose) with image_id; image_number; quiet; compose = Some comp }
+
(* Serialization helpers *)
let apc_start = "\027_G"
let apc_end = "\027\\"
+
(* Key-value writer with separator handling *)
+
type kv_writer = { mutable first : bool; buf : Buffer.t }
+
let kv_writer buf = { first = true; buf }
+
let kv w key value =
+
if not w.first then Buffer.add_char w.buf ',';
+
w.first <- false;
+
Buffer.add_char w.buf key;
+
Buffer.add_char w.buf '=';
+
Buffer.add_string w.buf value
+
let kv_int w key value = kv w key (string_of_int value)
+
let kv_int32 w key value = kv w key (Int32.to_string value)
+
let kv_char w key value = kv w key (String.make 1 value)
+
(* Conditional writers using Option.iter *)
+
let kv_int_opt w key = Option.iter (kv_int w key)
+
let kv_int32_opt w key = Option.iter (kv_int32 w key)
+
let kv_int_if w key ~default opt =
+
Option.iter (fun v -> if v <> default then kv_int w key v) opt
+
+
let action_char : action -> char = function
+
| `Transmit -> 't'
+
| `Transmit_and_display -> 'T'
+
| `Query -> 'q'
+
| `Display -> 'p'
+
| `Delete -> 'd'
+
| `Frame -> 'f'
+
| `Animate -> 'a'
+
| `Compose -> 'c'
+
+
let delete_char : delete -> char = function
+
| `All_visible -> 'a'
+
| `All_visible_and_free -> 'A'
+
| `By_id _ -> 'i'
+
| `By_id_and_free _ -> 'I'
+
| `By_number _ -> 'n'
+
| `By_number_and_free _ -> 'N'
+
| `At_cursor -> 'c'
+
| `At_cursor_and_free -> 'C'
+
| `At_cell _ -> 'p'
+
| `At_cell_and_free _ -> 'P'
+
| `At_cell_z _ -> 'q'
+
| `At_cell_z_and_free _ -> 'Q'
+
| `By_column _ -> 'x'
+
| `By_column_and_free _ -> 'X'
+
| `By_row _ -> 'y'
+
| `By_row_and_free _ -> 'Y'
+
| `By_z_index _ -> 'z'
+
| `By_z_index_and_free _ -> 'Z'
+
| `By_id_range _ -> 'r'
+
| `By_id_range_and_free _ -> 'R'
+
| `Frames -> 'f'
+
| `Frames_and_free -> 'F'
+
+
let write_placement w (p : Placement.t) =
+
kv_int_opt w 'x' p.source_x;
+
kv_int_opt w 'y' p.source_y;
+
kv_int_opt w 'w' p.source_width;
+
kv_int_opt w 'h' p.source_height;
+
kv_int_opt w 'X' p.cell_x_offset;
+
kv_int_opt w 'Y' p.cell_y_offset;
+
kv_int_opt w 'c' p.columns;
+
kv_int_opt w 'r' p.rows;
+
kv_int_opt w 'z' p.z_index;
+
kv_int_opt w 'p' p.placement_id;
+
p.cursor |> Option.iter (fun c -> kv_int_if w 'C' ~default:0 (Some (Cursor.to_int c)));
+
if p.unicode_placeholder then kv_int w 'U' 1
+
+
let write_delete w (d : delete) =
+
kv_char w 'd' (delete_char d);
+
match d with
+
| `By_id (id, pid) | `By_id_and_free (id, pid) ->
+
kv_int w 'i' id;
+
kv_int_opt w 'p' pid
+
| `By_number (n, pid) | `By_number_and_free (n, pid) ->
+
kv_int w 'I' n;
+
kv_int_opt w 'p' pid
+
| `At_cell (x, y) | `At_cell_and_free (x, y) ->
+
kv_int w 'x' x;
+
kv_int w 'y' y
+
| `At_cell_z (x, y, z) | `At_cell_z_and_free (x, y, z) ->
+
kv_int w 'x' x;
+
kv_int w 'y' y;
+
kv_int w 'z' z
+
| `By_column c | `By_column_and_free c -> kv_int w 'x' c
+
| `By_row r | `By_row_and_free r -> kv_int w 'y' r
+
| `By_z_index z | `By_z_index_and_free z -> kv_int w 'z' z
+
| `By_id_range (min_id, max_id) | `By_id_range_and_free (min_id, max_id) ->
+
kv_int w 'x' min_id;
+
kv_int w 'y' max_id
+
| `All_visible | `All_visible_and_free | `At_cursor | `At_cursor_and_free
+
| `Frames | `Frames_and_free ->
+
()
+
+
let write_frame w (f : Frame.t) =
+
kv_int_opt w 'x' f.x;
+
kv_int_opt w 'y' f.y;
+
kv_int_opt w 'c' f.base_frame;
+
kv_int_opt w 'r' f.edit_frame;
+
kv_int_opt w 'z' f.gap_ms;
+
f.composition
+
|> Option.iter (fun c -> kv_int_if w 'X' ~default:0 (Some (Composition.to_int c)));
+
kv_int32_opt w 'Y' f.background_color
+
+
let write_animation w : Animation.t -> unit = function
+
| `Set_state (state, loops) ->
+
let s = match state with `Stop -> 1 | `Loading -> 2 | `Run -> 3 in
+
kv_int w 's' s;
+
kv_int_opt w 'v' loops
+
| `Set_gap (frame, gap_ms) ->
+
kv_int w 'r' frame;
+
kv_int w 'z' gap_ms
+
| `Set_current frame -> kv_int w 'c' frame
+
+
let write_compose w (c : Compose.t) =
+
kv_int w 'r' c.source_frame;
+
kv_int w 'c' c.dest_frame;
+
kv_int_opt w 'w' c.width;
+
kv_int_opt w 'h' c.height;
+
kv_int_opt w 'x' c.dest_x;
+
kv_int_opt w 'y' c.dest_y;
+
kv_int_opt w 'X' c.source_x;
+
kv_int_opt w 'Y' c.source_y;
+
c.composition
+
|> Option.iter (fun comp -> kv_int_if w 'C' ~default:0 (Some (Composition.to_int comp)))
let write_control_data buf cmd =
+
let w = kv_writer buf in
(* Action *)
+
kv_char w 'a' (action_char cmd.action);
+
(* Quiet - only if non-default *)
+
cmd.quiet |> Option.iter (fun q -> kv_int_if w 'q' ~default:0 (Some (Quiet.to_int q)));
(* Format *)
+
cmd.format |> Option.iter (fun f -> kv_int w 'f' (Format.to_int f));
+
(* Transmission - only if non-default *)
+
cmd.transmission
+
|> Option.iter (fun t ->
+
let c = Transmission.to_char t in
+
if c <> 'd' then kv_char w 't' c);
(* Compression *)
+
cmd.compression |> Option.iter (fun c -> Compression.to_char c |> Option.iter (kv_char w 'o'));
(* Dimensions *)
+
kv_int_opt w 's' cmd.width;
+
kv_int_opt w 'v' cmd.height;
(* File size/offset *)
+
kv_int_opt w 'S' cmd.size;
+
kv_int_opt w 'O' cmd.offset;
+
(* Image ID/number *)
+
kv_int_opt w 'i' cmd.image_id;
+
kv_int_opt w 'I' cmd.image_number;
+
(* Complex options *)
+
cmd.placement |> Option.iter (write_placement w);
+
cmd.delete |> Option.iter (write_delete w);
+
cmd.frame |> Option.iter (write_frame w);
+
cmd.animation |> Option.iter (write_animation w);
+
cmd.compose |> Option.iter (write_compose w);
+
w
let chunk_size = 4096
let write buf cmd ~data =
Buffer.add_string buf apc_start;
+
let w = write_control_data buf cmd in
if String.length data > 0 then begin
let encoded = Base64.encode_string data in
let len = String.length encoded in
···
Buffer.add_string buf apc_end)
else begin
(* Multiple chunks *)
+
let rec write_chunks pos first =
+
if pos < len then begin
+
let remaining = len - pos in
+
let this_chunk = min chunk_size remaining in
+
let is_last = pos + this_chunk >= len in
+
if first then (
+
kv_int w 'm' 1;
+
Buffer.add_char buf ';';
+
Buffer.add_substring buf encoded pos this_chunk;
+
Buffer.add_string buf apc_end)
+
else (
+
Buffer.add_string buf apc_start;
+
Buffer.add_string buf (if is_last then "m=0" else "m=1");
+
Buffer.add_char buf ';';
+
Buffer.add_substring buf encoded pos this_chunk;
+
Buffer.add_string buf apc_end);
+
write_chunks (pos + this_chunk) false
+
end
+
in
+
write_chunks 0 true
end
end
else Buffer.add_string buf apc_end
···
let error_code t =
if is_ok t then None
+
else String.index_opt t.message ':' |> Option.fold ~none:(Some t.message) ~some:(fun i -> Some (String.sub t.message 0 i))
let image_id t = t.image_id
let image_number t = t.image_number
let placement_id t = t.placement_id
let parse s =
+
let ( let* ) = Option.bind in
let esc = '\027' in
let len = String.length s in
+
let* () = if len >= 5 && s.[0] = esc && s.[1] = '_' && s.[2] = 'G' then Some () else None in
+
let* semi_pos = String.index_from_opt s 3 ';' in
+
let rec find_end pos =
+
if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then Some pos
+
else if pos + 1 < len then find_end (pos + 1)
+
else None
+
in
+
let* end_pos = find_end (semi_pos + 1) in
+
let keys_str = String.sub s 3 (semi_pos - 3) in
+
let message = String.sub s (semi_pos + 1) (end_pos - semi_pos - 1) in
+
let parse_kv part =
+
if String.length part >= 3 && part.[1] = '=' then
+
Some (part.[0], String.sub part 2 (String.length part - 2))
+
else None
+
in
+
let keys = String.split_on_char ',' keys_str |> List.filter_map parse_kv in
+
let find_int key = List.assoc_opt key keys |> Fun.flip Option.bind int_of_string_opt in
+
Some
+
{
+
message;
+
image_id = find_int 'i';
+
image_number = find_int 'I';
+
placement_id = find_int 'p';
+
}
end
module Unicode_placeholder = struct
let placeholder_char = Uchar.of_int 0x10EEEE
let diacritics =
[|
0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F;
···
0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD; 0x1D242; 0x1D243; 0x1D244;
|]
+
let diacritic n =
+
Uchar.of_int diacritics.(n mod Array.length diacritics)
+
let row_diacritic = diacritic
+
let column_diacritic = diacritic
+
let id_high_byte_diacritic = diacritic
let add_uchar buf u =
let code = Uchar.to_int u in
+
let put = Buffer.add_char buf in
+
if code < 0x80 then put (Char.chr code)
else if code < 0x800 then (
+
put (Char.chr (0xC0 lor (code lsr 6)));
+
put (Char.chr (0x80 lor (code land 0x3F))))
else if code < 0x10000 then (
+
put (Char.chr (0xE0 lor (code lsr 12)));
+
put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
+
put (Char.chr (0x80 lor (code land 0x3F))))
else (
+
put (Char.chr (0xF0 lor (code lsr 18)));
+
put (Char.chr (0x80 lor ((code lsr 12) land 0x3F)));
+
put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
+
put (Char.chr (0x80 lor (code land 0x3F))))
let write buf ~image_id ?placement_id ~rows ~cols () =
+
(* Set foreground color *)
+
Printf.bprintf buf "\027[38;2;%d;%d;%dm"
+
((image_id lsr 16) land 0xFF)
+
((image_id lsr 8) land 0xFF)
+
(image_id land 0xFF);
+
(* Optional placement ID in underline color *)
+
placement_id
+
|> Option.iter (fun pid ->
+
Printf.bprintf buf "\027[58;2;%d;%d;%dm"
+
((pid lsr 16) land 0xFF)
+
((pid lsr 8) land 0xFF)
+
(pid land 0xFF));
+
(* High byte diacritic *)
let high_byte = (image_id lsr 24) land 0xFF in
+
let high_diac = if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None in
+
(* Write grid *)
for row = 0 to rows - 1 do
for col = 0 to cols - 1 do
add_uchar buf placeholder_char;
add_uchar buf (row_diacritic row);
add_uchar buf (column_diacritic col);
+
high_diac |> Option.iter (add_uchar buf)
done;
if row < rows - 1 then Buffer.add_string buf "\n\r"
done;
(* Reset colors *)
Buffer.add_string buf "\027[39m";
+
if Option.is_some placement_id then Buffer.add_string buf "\027[59m"
end
module Detect = struct
let make_query () =
+
let cmd = Command.query ~format:`Rgb24 ~transmission:`Direct ~width:1 ~height:1 () in
+
Command.to_string cmd ~data:"\x00\x00\x00" ^ "\027[c"
let supports_graphics response ~da1_received =
+
response |> Option.map Response.is_ok |> Option.value ~default:(not da1_received)
end
+112 -230
stack/kitty_graphics/lib/kitty_graphics.mli
···
{[
(* Display a PNG image *)
let png_data = read_file "image.png" in
-
let cmd = Kitty_graphics.Command.transmit_and_display
-
~format:Kitty_graphics.Format.Png
-
()
-
in
let buf = Buffer.create 1024 in
Kitty_graphics.Command.write buf cmd ~data:png_data;
print_string (Buffer.contents buf)
···
See {{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics Protocol}
for the full specification. *)
-
(** {1 Core Types} *)
-
(** Image data formats. *)
module Format : sig
-
type t =
-
| Rgba32 (** 32-bit RGBA, 4 bytes per pixel *)
-
| Rgb24 (** 24-bit RGB, 3 bytes per pixel *)
-
| Png (** PNG encoded data *)
val to_int : t -> int
(** Convert to protocol integer value (32, 24, or 100). *)
end
-
(** Transmission methods for image data. *)
module Transmission : sig
-
type t =
-
| Direct (** Data transmitted inline in the escape sequence *)
-
| File (** Data read from a file path *)
-
| Tempfile (** Data read from a temp file, deleted after reading *)
val to_char : t -> char
(** Convert to protocol character ('d', 'f', or 't'). *)
end
-
(** Compression options for transmitted data. *)
module Compression : sig
-
type t =
-
| None (** No compression *)
-
| Zlib (** RFC 1950 zlib compression *)
val to_char : t -> char option
-
(** Convert to protocol character (None or Some 'z'). *)
end
-
(** Response suppression modes. *)
module Quiet : sig
-
type t =
-
| Noisy (** Terminal sends all responses (default) *)
-
| Errors_only (** Terminal only sends error responses *)
-
| Silent (** Terminal sends no responses *)
val to_int : t -> int
(** Convert to protocol integer (0, 1, or 2). *)
end
-
(** Cursor movement policy after displaying an image. *)
module Cursor : sig
-
type t =
-
| Move (** Move cursor after image (default) *)
-
| Static (** Keep cursor in place *)
val to_int : t -> int
(** Convert to protocol integer (0 or 1). *)
end
-
(** Composition modes for blending. *)
module Composition : sig
-
type t =
-
| Alpha_blend (** Full alpha blending (default) *)
-
| Overwrite (** Simple pixel replacement *)
val to_int : t -> int
(** Convert to protocol integer (0 or 1). *)
end
-
(** {1 Delete Operations} *)
-
-
(** Specifies what to delete when using delete commands. *)
module Delete : sig
-
(** Delete target specification.
-
-
Each variant has two forms: one that only removes placements (keeping
-
image data for potential reuse) and one that also frees the image data. *)
-
type t =
-
| All_visible
-
(** Delete all visible placements. *)
-
| All_visible_and_free
-
(** Delete all visible placements and free their image data. *)
-
| By_id of { image_id : int; placement_id : int option }
-
(** Delete placements for a specific image ID, optionally filtered
-
by placement ID. *)
-
| By_id_and_free of { image_id : int; placement_id : int option }
-
(** Delete and free by image ID. *)
-
| By_number of { image_number : int; placement_id : int option }
-
(** Delete by image number (newest with that number). *)
-
| By_number_and_free of { image_number : int; placement_id : int option }
-
(** Delete and free by image number. *)
-
| At_cursor
-
(** Delete placements intersecting cursor position. *)
-
| At_cursor_and_free
-
(** Delete and free at cursor position. *)
-
| At_cell of { x : int; y : int }
-
(** Delete placements intersecting a specific cell (1-based). *)
-
| At_cell_and_free of { x : int; y : int }
-
(** Delete and free at specific cell. *)
-
| At_cell_z of { x : int; y : int; z : int }
-
(** Delete at cell with specific z-index. *)
-
| At_cell_z_and_free of { x : int; y : int; z : int }
-
(** Delete and free at cell with z-index. *)
-
| By_column of int
-
(** Delete all placements intersecting a column (1-based). *)
-
| By_column_and_free of int
-
(** Delete and free by column. *)
-
| By_row of int
-
(** Delete all placements intersecting a row (1-based). *)
-
| By_row_and_free of int
-
(** Delete and free by row. *)
-
| By_z_index of int
-
(** Delete all placements with a specific z-index. *)
-
| By_z_index_and_free of int
-
(** Delete and free by z-index. *)
-
| By_id_range of { min_id : int; max_id : int }
-
(** Delete images with IDs in range [min_id, max_id]. *)
-
| By_id_range_and_free of { min_id : int; max_id : int }
-
(** Delete and free by ID range. *)
-
| Frames
-
(** Delete animation frames. *)
-
| Frames_and_free
-
(** Delete animation frames and free if no frames remain. *)
end
(** {1 Placement Options} *)
-
(** Image placement configuration.
-
-
Controls how an image is positioned and scaled when displayed. *)
module Placement : sig
type t
(** Placement configuration. *)
···
?rows:int ->
?z_index:int ->
?placement_id:int ->
-
?cursor:Cursor.t ->
?unicode_placeholder:bool ->
unit ->
t
···
(** {1 Animation} *)
-
(** Animation frame specification. *)
module Frame : sig
type t
(** Animation frame configuration. *)
···
?base_frame:int ->
?edit_frame:int ->
?gap_ms:int ->
-
?composition:Composition.t ->
?background_color:int32 ->
unit ->
t
···
(** Empty frame spec with defaults. *)
end
-
(** Animation control operations. *)
module Animation : sig
-
type state =
-
| Stop (** Stop the animation *)
-
| Loading (** Run but wait for new frames at end *)
-
| Run (** Run normally, loop at end *)
-
type t
-
(** Animation control configuration. *)
val set_state : ?loops:int -> state -> t
(** Set animation state.
-
@param loops Number of loops: 0 = ignored, 1 = infinite, n = n-1 loops *)
val set_gap : frame:int -> gap_ms:int -> t
(** Set the gap (delay) for a specific frame.
-
@param frame 1-based frame number
@param gap_ms Delay in milliseconds (negative = gapless) *)
···
(** Make a specific frame (1-based) the current displayed frame. *)
end
-
(** Frame composition for combining frame regions. *)
module Compose : sig
type t
(** Composition operation. *)
···
?source_y:int ->
?dest_x:int ->
?dest_y:int ->
-
?composition:Composition.t ->
unit ->
t
-
(** Compose a rectangle from one frame onto another.
-
-
@param source_frame 1-based source frame number
-
@param dest_frame 1-based destination frame number
-
@param width Rectangle width in pixels (default: full width)
-
@param height Rectangle height in pixels (default: full height)
-
@param source_x Left edge of source rectangle
-
@param source_y Top edge of source rectangle
-
@param dest_x Left edge of destination rectangle
-
@param dest_y Top edge of destination rectangle
-
@param composition Blend mode *)
end
(** {1 Commands} *)
-
(** Graphics command builder.
-
-
This is the main API for constructing graphics protocol commands.
-
Commands are built using the various constructors, then written to
-
a buffer with {!write}. *)
module Command : sig
type t
(** A graphics protocol command. *)
···
val transmit :
?image_id:int ->
?image_number:int ->
-
?format:Format.t ->
-
?transmission:Transmission.t ->
-
?compression:Compression.t ->
?width:int ->
?height:int ->
?size:int ->
?offset:int ->
-
?quiet:Quiet.t ->
unit ->
t
-
(** Transmit image data without displaying.
-
-
@param image_id Unique ID for the image (1-4294967295)
-
@param image_number Image number (terminal assigns ID)
-
@param format Pixel format of the data
-
@param transmission How data is transmitted
-
@param compression Compression applied to data
-
@param width Image width in pixels (required for RGB/RGBA)
-
@param height Image height in pixels (required for RGB/RGBA)
-
@param size Number of bytes to read (for file transmission)
-
@param offset Byte offset to start reading (for file transmission)
-
@param quiet Response suppression mode *)
val transmit_and_display :
?image_id:int ->
?image_number:int ->
-
?format:Format.t ->
-
?transmission:Transmission.t ->
-
?compression:Compression.t ->
?width:int ->
?height:int ->
?size:int ->
?offset:int ->
-
?quiet:Quiet.t ->
?placement:Placement.t ->
unit ->
t
-
(** Transmit image data and display it immediately.
-
-
This is the most common operation for displaying images.
-
See {!transmit} for transmission parameters and {!Placement}
-
for display options. *)
val query :
-
?format:Format.t ->
-
?transmission:Transmission.t ->
?width:int ->
?height:int ->
-
?quiet:Quiet.t ->
unit ->
t
-
(** Query terminal support without storing the image.
-
-
Send a small test image to check if the terminal supports
-
the graphics protocol. The terminal responds with OK or
-
an error without storing the image. *)
(** {2 Display} *)
···
?image_id:int ->
?image_number:int ->
?placement:Placement.t ->
-
?quiet:Quiet.t ->
unit ->
t
-
(** Display a previously transmitted image.
-
-
@param image_id ID of a previously transmitted image
-
@param image_number Number of the image to display
-
@param placement Display placement options
-
@param quiet Response suppression *)
(** {2 Deletion} *)
-
val delete : ?quiet:Quiet.t -> Delete.t -> t
-
(** Delete images or placements.
-
-
See {!Delete} for the various deletion modes. *)
(** {2 Animation} *)
val frame :
?image_id:int ->
?image_number:int ->
-
?format:Format.t ->
-
?transmission:Transmission.t ->
-
?compression:Compression.t ->
?width:int ->
?height:int ->
-
?quiet:Quiet.t ->
frame:Frame.t ->
unit ->
t
-
(** Transmit animation frame data.
-
-
Similar to {!transmit} but adds frame-specific parameters. *)
-
val animate :
-
?image_id:int ->
-
?image_number:int ->
-
?quiet:Quiet.t ->
-
Animation.t ->
-
t
(** Control animation playback. *)
-
val compose :
-
?image_id:int ->
-
?image_number:int ->
-
?quiet:Quiet.t ->
-
Compose.t ->
-
t
(** Compose animation frames. *)
(** {2 Output} *)
val write : Buffer.t -> t -> data:string -> unit
-
(** Write the command to a buffer.
-
-
@param data The payload data (image bytes, file path, etc.).
-
For {!display}, {!delete}, {!animate}, pass empty string. *)
val to_string : t -> data:string -> string
(** Convert command to a string. *)
···
(** {1 Response Parsing} *)
-
(** Terminal response parsing.
-
-
When the terminal processes a graphics command, it may send back
-
a response indicating success or failure. *)
module Response : sig
type t
(** A parsed terminal response. *)
val parse : string -> t option
-
(** Parse a response from terminal output.
-
-
Expects the format: [<ESC>_G...;message<ESC>\]
-
Returns [None] if the string is not a valid graphics response. *)
val is_ok : t -> bool
(** Check if the response indicates success. *)
val message : t -> string
-
(** Get the response message ("OK" or error description). *)
val error_code : t -> string option
-
(** Extract the error code if this is an error response.
-
-
Error codes include: ENOENT, EINVAL, ENOSPC, EBADPNG, etc. *)
val image_id : t -> int option
-
(** Get the image ID from the response, if present. *)
val image_number : t -> int option
-
(** Get the image number from the response, if present. *)
val placement_id : t -> int option
-
(** Get the placement ID from the response, if present. *)
end
(** {1 Unicode Placeholders} *)
-
(** Unicode placeholder generation for tmux/vim compatibility.
-
-
Unicode placeholders allow images to work with applications that
-
don't understand the graphics protocol but support Unicode and
-
foreground colors. The image is transmitted with a virtual placement,
-
then placeholder characters are written to the terminal. *)
module Unicode_placeholder : sig
val placeholder_char : Uchar.t
(** The Unicode placeholder character U+10EEEE. *)
···
cols:int ->
unit ->
unit
-
(** Write placeholder characters to a buffer.
-
-
The image ID is encoded in the foreground color (24-bit mode).
-
Row and column positions are encoded using combining diacritics.
-
-
@param image_id The image ID (should have non-zero bytes for 24-bit)
-
@param placement_id Optional placement ID (encoded in underline color)
-
@param rows Number of rows to fill
-
@param cols Number of columns per row *)
val row_diacritic : int -> Uchar.t
(** Get the combining diacritic for a row number (0-based). *)
···
(** {1 Terminal Detection} *)
-
(** Helpers for detecting terminal graphics support. *)
module Detect : sig
val make_query : unit -> string
-
(** Generate a query command to test graphics support.
-
-
Send this to stdout and read the terminal's response.
-
Follow with a DA1 query ([<ESC>[c]) to detect terminals
-
that don't support graphics (they'll answer DA1 but not
-
the graphics query). *)
val supports_graphics : Response.t option -> da1_received:bool -> bool
-
(** Determine if graphics are supported based on query results.
-
-
@param response The parsed graphics response, if any
-
@param da1_received Whether a DA1 response was received
-
-
Returns [true] if a graphics OK response was received,
-
or [false] if only DA1 was received (no graphics support). *)
end
···
{[
(* Display a PNG image *)
let png_data = read_file "image.png" in
+
let cmd = Kitty_graphics.Command.transmit_and_display ~format:`Png () in
let buf = Buffer.create 1024 in
Kitty_graphics.Command.write buf cmd ~data:png_data;
print_string (Buffer.contents buf)
···
See {{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics Protocol}
for the full specification. *)
+
(** {1 Polymorphic Variant Types} *)
+
+
type format = [ `Rgba32 | `Rgb24 | `Png ]
+
(** Image data formats. [`Rgba32] is 32-bit RGBA (4 bytes per pixel),
+
[`Rgb24] is 24-bit RGB (3 bytes per pixel), [`Png] is PNG encoded data. *)
+
+
type transmission = [ `Direct | `File | `Tempfile ]
+
(** Transmission methods. [`Direct] sends data inline, [`File] reads from a path,
+
[`Tempfile] reads from a temp file that the terminal deletes after reading. *)
+
+
type compression = [ `None | `Zlib ]
+
(** Compression options. [`None] for raw data, [`Zlib] for RFC 1950 compression. *)
+
+
type quiet = [ `Noisy | `Errors_only | `Silent ]
+
(** Response suppression. [`Noisy] sends all responses (default),
+
[`Errors_only] suppresses OK responses, [`Silent] suppresses all. *)
+
+
type cursor = [ `Move | `Static ]
+
(** Cursor movement after displaying. [`Move] advances cursor (default),
+
[`Static] keeps cursor in place. *)
+
+
type composition = [ `Alpha_blend | `Overwrite ]
+
(** Composition modes. [`Alpha_blend] for full blending (default),
+
[`Overwrite] for simple pixel replacement. *)
+
type delete =
+
[ `All_visible
+
| `All_visible_and_free
+
| `By_id of int * int option
+
| `By_id_and_free of int * int option
+
| `By_number of int * int option
+
| `By_number_and_free of int * int option
+
| `At_cursor
+
| `At_cursor_and_free
+
| `At_cell of int * int
+
| `At_cell_and_free of int * int
+
| `At_cell_z of int * int * int
+
| `At_cell_z_and_free of int * int * int
+
| `By_column of int
+
| `By_column_and_free of int
+
| `By_row of int
+
| `By_row_and_free of int
+
| `By_z_index of int
+
| `By_z_index_and_free of int
+
| `By_id_range of int * int
+
| `By_id_range_and_free of int * int
+
| `Frames
+
| `Frames_and_free ]
+
(** Delete target specification. Each variant has two forms: one that only
+
removes placements (e.g., [`All_visible]) and one that also frees the
+
image data (e.g., [`All_visible_and_free]). Tuple variants contain
+
(image_id, optional_placement_id) or (x, y) coordinates. *)
+
+
type animation_state = [ `Stop | `Loading | `Run ]
+
(** Animation playback state. [`Stop] halts animation, [`Loading] runs but
+
waits for new frames at end, [`Run] runs normally and loops. *)
+
+
(** {1 Type Modules} *)
+
module Format : sig
+
type t = format
val to_int : t -> int
(** Convert to protocol integer value (32, 24, or 100). *)
end
module Transmission : sig
+
type t = transmission
val to_char : t -> char
(** Convert to protocol character ('d', 'f', or 't'). *)
end
module Compression : sig
+
type t = compression
val to_char : t -> char option
+
(** Convert to protocol character ([None] or [Some 'z']). *)
end
module Quiet : sig
+
type t = quiet
val to_int : t -> int
(** Convert to protocol integer (0, 1, or 2). *)
end
module Cursor : sig
+
type t = cursor
val to_int : t -> int
(** Convert to protocol integer (0 or 1). *)
end
module Composition : sig
+
type t = composition
val to_int : t -> int
(** Convert to protocol integer (0 or 1). *)
end
module Delete : sig
+
type t = delete
end
(** {1 Placement Options} *)
module Placement : sig
type t
(** Placement configuration. *)
···
?rows:int ->
?z_index:int ->
?placement_id:int ->
+
?cursor:cursor ->
?unicode_placeholder:bool ->
unit ->
t
···
(** {1 Animation} *)
module Frame : sig
type t
(** Animation frame configuration. *)
···
?base_frame:int ->
?edit_frame:int ->
?gap_ms:int ->
+
?composition:composition ->
?background_color:int32 ->
unit ->
t
···
(** Empty frame spec with defaults. *)
end
module Animation : sig
+
type state = animation_state
+
type t =
+
[ `Set_state of state * int option
+
| `Set_gap of int * int
+
| `Set_current of int ]
+
(** Animation control operations. *)
val set_state : ?loops:int -> state -> t
(** Set animation state.
@param loops Number of loops: 0 = ignored, 1 = infinite, n = n-1 loops *)
val set_gap : frame:int -> gap_ms:int -> t
(** Set the gap (delay) for a specific frame.
@param frame 1-based frame number
@param gap_ms Delay in milliseconds (negative = gapless) *)
···
(** Make a specific frame (1-based) the current displayed frame. *)
end
module Compose : sig
type t
(** Composition operation. *)
···
?source_y:int ->
?dest_x:int ->
?dest_y:int ->
+
?composition:composition ->
unit ->
t
+
(** Compose a rectangle from one frame onto another. *)
end
(** {1 Commands} *)
module Command : sig
type t
(** A graphics protocol command. *)
···
val transmit :
?image_id:int ->
?image_number:int ->
+
?format:format ->
+
?transmission:transmission ->
+
?compression:compression ->
?width:int ->
?height:int ->
?size:int ->
?offset:int ->
+
?quiet:quiet ->
unit ->
t
+
(** Transmit image data without displaying. *)
val transmit_and_display :
?image_id:int ->
?image_number:int ->
+
?format:format ->
+
?transmission:transmission ->
+
?compression:compression ->
?width:int ->
?height:int ->
?size:int ->
?offset:int ->
+
?quiet:quiet ->
?placement:Placement.t ->
unit ->
t
+
(** Transmit image data and display it immediately. *)
val query :
+
?format:format ->
+
?transmission:transmission ->
?width:int ->
?height:int ->
+
?quiet:quiet ->
unit ->
t
+
(** Query terminal support without storing the image. *)
(** {2 Display} *)
···
?image_id:int ->
?image_number:int ->
?placement:Placement.t ->
+
?quiet:quiet ->
unit ->
t
+
(** Display a previously transmitted image. *)
(** {2 Deletion} *)
+
val delete : ?quiet:quiet -> delete -> t
+
(** Delete images or placements. *)
(** {2 Animation} *)
val frame :
?image_id:int ->
?image_number:int ->
+
?format:format ->
+
?transmission:transmission ->
+
?compression:compression ->
?width:int ->
?height:int ->
+
?quiet:quiet ->
frame:Frame.t ->
unit ->
t
+
(** Transmit animation frame data. *)
+
val animate : ?image_id:int -> ?image_number:int -> ?quiet:quiet -> Animation.t -> t
(** Control animation playback. *)
+
val compose : ?image_id:int -> ?image_number:int -> ?quiet:quiet -> Compose.t -> t
(** Compose animation frames. *)
(** {2 Output} *)
val write : Buffer.t -> t -> data:string -> unit
+
(** Write the command to a buffer. *)
val to_string : t -> data:string -> string
(** Convert command to a string. *)
···
(** {1 Response Parsing} *)
module Response : sig
type t
(** A parsed terminal response. *)
val parse : string -> t option
+
(** Parse a response from terminal output. *)
val is_ok : t -> bool
(** Check if the response indicates success. *)
val message : t -> string
+
(** Get the response message. *)
val error_code : t -> string option
+
(** Extract the error code if this is an error response. *)
val image_id : t -> int option
+
(** Get the image ID from the response. *)
val image_number : t -> int option
+
(** Get the image number from the response. *)
val placement_id : t -> int option
+
(** Get the placement ID from the response. *)
end
(** {1 Unicode Placeholders} *)
module Unicode_placeholder : sig
val placeholder_char : Uchar.t
(** The Unicode placeholder character U+10EEEE. *)
···
cols:int ->
unit ->
unit
+
(** Write placeholder characters to a buffer. *)
val row_diacritic : int -> Uchar.t
(** Get the combining diacritic for a row number (0-based). *)
···
(** {1 Terminal Detection} *)
module Detect : sig
val make_query : unit -> string
+
(** Generate a query command to test graphics support. *)
val supports_graphics : Response.t option -> da1_received:bool -> bool
+
(** Determine if graphics are supported based on query results. *)
end