Kitty Graphics Protocol in OCaml
terminal graphics ocaml

break out libraries

+2 -2
dune-project
···
(lang dune 3.20)
-
(name kitty_graphics)
+
(name kgp)
(package
-
(name kitty_graphics)
+
(name kgp)
(synopsis "OCaml implementation of the Kitty terminal graphics protocol")
(description
"A standalone library for rendering images in terminals that support the Kitty graphics protocol. Supports image transmission, display, animation, Unicode placeholders, and terminal capability detection.")
+1 -1
example/anim_test.ml
···
(* Minimal animation test - shows exact bytes sent *)
-
module K = Kitty_graphics
+
module K = Kgp
let solid_color_rgba ~width ~height ~r ~g ~b ~a =
let pixels = Bytes.create (width * height * 4) in
+1 -1
example/debug_anim.ml
···
(* Debug: Output animation escape sequences for comparison with Go *)
-
module K = Kitty_graphics
+
module K = Kgp
let solid_color_rgba ~width ~height ~r ~g ~b ~a =
let pixels = Bytes.create (width * height * 4) in
+5 -5
example/dune
···
(executable
(name example)
-
(libraries kitty_graphics unix))
+
(libraries kgp unix))
(executable
(name debug_anim)
-
(libraries kitty_graphics))
+
(libraries kgp))
(executable
(name test_output)
-
(libraries kitty_graphics))
+
(libraries kgp))
(executable
(name anim_test)
-
(libraries kitty_graphics))
+
(libraries kgp))
(executable
(name tiny_anim)
-
(libraries kitty_graphics))
+
(libraries kgp))
+1 -1
example/example.ml
···
(* Kitty Graphics Protocol Demo - Matching kgp/examples/demo *)
-
module K = Kitty_graphics
+
module K = Kgp
(* Helper: Generate a solid color RGBA image *)
let solid_color_rgba ~width ~height ~r ~g ~b ~a =
+1 -1
example/test_output.ml
···
(* Simple test to show exact escape sequences without data *)
-
module K = Kitty_graphics
+
module K = Kgp
let print_escaped s =
String.iter (fun c ->
+1 -1
example/tiny_anim.ml
···
(* Tiny animation test - no chunking needed *)
(* Uses 20x20 images which are ~1067 bytes base64 (well under 4096) *)
-
module K = Kitty_graphics
+
module K = Kgp
let solid_color_rgba ~width ~height ~r ~g ~b ~a =
let pixels = Bytes.create (width * height * 4) in
+2 -2
lib/dune
···
(library
-
(name kitty_graphics)
-
(public_name kitty_graphics)
+
(name kgp)
+
(public_name kgp)
(libraries base64))
+34
lib/kgp.ml
···
+
(* Kitty Terminal Graphics Protocol - Main Module *)
+
+
(* Re-export polymorphic variant types *)
+
type format = Kgp_types.format
+
type transmission = Kgp_types.transmission
+
type compression = Kgp_types.compression
+
type quiet = Kgp_types.quiet
+
type cursor = Kgp_types.cursor
+
type composition = Kgp_types.composition
+
type delete = Kgp_types.delete
+
type animation_state = Kgp_types.animation_state
+
+
(* Type conversion modules *)
+
module Format = Kgp_types.Format
+
module Transmission = Kgp_types.Transmission
+
module Compression = Kgp_types.Compression
+
module Quiet = Kgp_types.Quiet
+
module Cursor = Kgp_types.Cursor
+
module Composition = Kgp_types.Composition
+
module Delete = Kgp_types.Delete
+
+
(* Configuration modules *)
+
module Placement = Kgp_placement
+
module Frame = Kgp_frame
+
module Animation = Kgp_animation
+
module Compose = Kgp_compose
+
+
(* Core modules *)
+
module Command = Kgp_command
+
module Response = Kgp_response
+
+
(* Utility modules *)
+
module Unicode_placeholder = Kgp_unicode
+
module Detect = Kgp_detect
+87
lib/kgp.mli
···
+
(** Kitty Terminal Graphics Protocol
+
+
This library implements the Kitty terminal graphics protocol, allowing
+
OCaml programs to display images in terminals that support the protocol
+
(Kitty, WezTerm, Konsole, Ghostty, etc.).
+
+
The protocol uses APC (Application Programming Command) escape sequences
+
to transmit and display pixel graphics. Images can be transmitted as raw
+
RGB/RGBA data or PNG, and displayed at specific positions with various
+
placement options.
+
+
{2 Basic Usage}
+
+
{[
+
(* Display a PNG image *)
+
let png_data = read_file "image.png" in
+
let cmd = Kgp.Command.transmit_and_display ~format:`Png () in
+
let buf = Buffer.create 1024 in
+
Kgp.Command.write buf cmd ~data:png_data;
+
print_string (Buffer.contents buf)
+
]}
+
+
{2 Protocol Reference}
+
+
See {{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics Protocol}
+
for the full specification. *)
+
+
(** {1 Polymorphic Variant Types} *)
+
+
type format = Kgp_types.format
+
(** 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 = Kgp_types.transmission
+
(** 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 = Kgp_types.compression
+
(** Compression options. [`None] for raw data, [`Zlib] for RFC 1950 compression. *)
+
+
type quiet = Kgp_types.quiet
+
(** Response suppression. [`Noisy] sends all responses (default),
+
[`Errors_only] suppresses OK responses, [`Silent] suppresses all. *)
+
+
type cursor = Kgp_types.cursor
+
(** Cursor movement after displaying. [`Move] advances cursor (default),
+
[`Static] keeps cursor in place. *)
+
+
type composition = Kgp_types.composition
+
(** Composition modes. [`Alpha_blend] for full blending (default),
+
[`Overwrite] for simple pixel replacement. *)
+
+
type delete = Kgp_types.delete
+
(** 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]). *)
+
+
type animation_state = Kgp_types.animation_state
+
(** 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 = Kgp_types.Format
+
module Transmission = Kgp_types.Transmission
+
module Compression = Kgp_types.Compression
+
module Quiet = Kgp_types.Quiet
+
module Cursor = Kgp_types.Cursor
+
module Composition = Kgp_types.Composition
+
module Delete = Kgp_types.Delete
+
+
(** {1 Configuration Modules} *)
+
+
module Placement = Kgp_placement
+
module Frame = Kgp_frame
+
module Animation = Kgp_animation
+
module Compose = Kgp_compose
+
+
(** {1 Command and Response} *)
+
+
module Command = Kgp_command
+
module Response = Kgp_response
+
+
(** {1 Utilities} *)
+
+
module Unicode_placeholder = Kgp_unicode
+
module Detect = Kgp_detect
+12
lib/kgp_animation.ml
···
+
(* Kitty Graphics Protocol Animation - Implementation *)
+
+
type state = Kgp_types.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
+24
lib/kgp_animation.mli
···
+
(** Kitty Graphics Protocol Animation
+
+
Animation control operations. *)
+
+
type state = Kgp_types.animation_state
+
(** Animation playback 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) *)
+
+
val set_current_frame : int -> t
+
(** Make a specific frame (1-based) the current displayed frame. *)
+332
lib/kgp_command.ml
···
+
(* Kitty Graphics Protocol Command - Implementation *)
+
+
type action =
+
[ `Transmit
+
| `Transmit_and_display
+
| `Query
+
| `Display
+
| `Delete
+
| `Frame
+
| `Animate
+
| `Compose ]
+
+
type t = {
+
action : action;
+
format : Kgp_types.format option;
+
transmission : Kgp_types.transmission option;
+
compression : Kgp_types.compression option;
+
width : int option;
+
height : int option;
+
size : int option;
+
offset : int option;
+
quiet : Kgp_types.quiet option;
+
image_id : int option;
+
image_number : int option;
+
placement : Kgp_placement.t option;
+
delete : Kgp_types.delete option;
+
frame : Kgp_frame.t option;
+
animation : Kgp_animation.t option;
+
compose : Kgp_compose.t option;
+
}
+
+
let make action =
+
{
+
action;
+
format = None;
+
transmission = None;
+
compression = None;
+
width = None;
+
height = None;
+
size = None;
+
offset = None;
+
quiet = None;
+
image_id = None;
+
image_number = None;
+
placement = None;
+
delete = None;
+
frame = None;
+
animation = None;
+
compose = None;
+
}
+
+
let transmit ?image_id ?image_number ?format ?transmission ?compression ?width
+
?height ?size ?offset ?quiet () =
+
{
+
(make `Transmit) with
+
image_id;
+
image_number;
+
format;
+
transmission;
+
compression;
+
width;
+
height;
+
size;
+
offset;
+
quiet;
+
}
+
+
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;
+
transmission;
+
compression;
+
width;
+
height;
+
size;
+
offset;
+
quiet;
+
placement;
+
}
+
+
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;
+
transmission;
+
compression;
+
width;
+
height;
+
quiet;
+
frame = Some frame;
+
}
+
+
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 : Kgp_types.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 : Kgp_placement.t) =
+
kv_int_opt w 'x' (Kgp_placement.source_x p);
+
kv_int_opt w 'y' (Kgp_placement.source_y p);
+
kv_int_opt w 'w' (Kgp_placement.source_width p);
+
kv_int_opt w 'h' (Kgp_placement.source_height p);
+
kv_int_opt w 'X' (Kgp_placement.cell_x_offset p);
+
kv_int_opt w 'Y' (Kgp_placement.cell_y_offset p);
+
kv_int_opt w 'c' (Kgp_placement.columns p);
+
kv_int_opt w 'r' (Kgp_placement.rows p);
+
kv_int_opt w 'z' (Kgp_placement.z_index p);
+
kv_int_opt w 'p' (Kgp_placement.placement_id p);
+
Kgp_placement.cursor p
+
|> Option.iter (fun c ->
+
kv_int_if w 'C' ~default:0 (Some (Kgp_types.Cursor.to_int c)));
+
if Kgp_placement.unicode_placeholder p then kv_int w 'U' 1
+
+
let write_delete w (d : Kgp_types.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 : Kgp_frame.t) =
+
kv_int_opt w 'x' (Kgp_frame.x f);
+
kv_int_opt w 'y' (Kgp_frame.y f);
+
kv_int_opt w 'c' (Kgp_frame.base_frame f);
+
kv_int_opt w 'r' (Kgp_frame.edit_frame f);
+
kv_int_opt w 'z' (Kgp_frame.gap_ms f);
+
Kgp_frame.composition f
+
|> Option.iter (fun c ->
+
kv_int_if w 'X' ~default:0 (Some (Kgp_types.Composition.to_int c)));
+
kv_int32_opt w 'Y' (Kgp_frame.background_color f)
+
+
let write_animation w : Kgp_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 : Kgp_compose.t) =
+
kv_int w 'r' (Kgp_compose.source_frame c);
+
kv_int w 'c' (Kgp_compose.dest_frame c);
+
kv_int_opt w 'w' (Kgp_compose.width c);
+
kv_int_opt w 'h' (Kgp_compose.height c);
+
kv_int_opt w 'x' (Kgp_compose.dest_x c);
+
kv_int_opt w 'y' (Kgp_compose.dest_y c);
+
kv_int_opt w 'X' (Kgp_compose.source_x c);
+
kv_int_opt w 'Y' (Kgp_compose.source_y c);
+
Kgp_compose.composition c
+
|> Option.iter (fun comp ->
+
kv_int_if w 'C' ~default:0 (Some (Kgp_types.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 (Kgp_types.Quiet.to_int q)));
+
(* Format *)
+
cmd.format
+
|> Option.iter (fun f -> kv_int w 'f' (Kgp_types.Format.to_int f));
+
(* Transmission - only for transmit/frame actions, always include t=d for compatibility *)
+
(match cmd.action with
+
| `Transmit | `Transmit_and_display | `Frame -> (
+
match cmd.transmission with
+
| Some t -> kv_char w 't' (Kgp_types.Transmission.to_char t)
+
| None -> kv_char w 't' 'd')
+
| _ -> ());
+
(* Compression *)
+
cmd.compression
+
|> Option.iter (fun c ->
+
Kgp_types.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
+
+
(* Use large chunk size to avoid chunking - Kitty animation doesn't handle chunks well *)
+
let chunk_size = 1024 * 1024 (* 1MB - effectively no chunking *)
+
+
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
+
if len <= chunk_size then (
+
Buffer.add_char buf ';';
+
Buffer.add_string buf encoded;
+
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 to_string cmd ~data =
+
let buf = Buffer.create 1024 in
+
write buf cmd ~data;
+
Buffer.contents buf
+106
lib/kgp_command.mli
···
+
(** Kitty Graphics Protocol Commands
+
+
This module provides functions for building and serializing graphics
+
protocol commands. *)
+
+
type t
+
(** A graphics protocol command. *)
+
+
(** {1 Image Transmission} *)
+
+
val transmit :
+
?image_id:int ->
+
?image_number:int ->
+
?format:Kgp_types.format ->
+
?transmission:Kgp_types.transmission ->
+
?compression:Kgp_types.compression ->
+
?width:int ->
+
?height:int ->
+
?size:int ->
+
?offset:int ->
+
?quiet:Kgp_types.quiet ->
+
unit ->
+
t
+
(** Transmit image data without displaying. *)
+
+
val transmit_and_display :
+
?image_id:int ->
+
?image_number:int ->
+
?format:Kgp_types.format ->
+
?transmission:Kgp_types.transmission ->
+
?compression:Kgp_types.compression ->
+
?width:int ->
+
?height:int ->
+
?size:int ->
+
?offset:int ->
+
?quiet:Kgp_types.quiet ->
+
?placement:Kgp_placement.t ->
+
unit ->
+
t
+
(** Transmit image data and display it immediately. *)
+
+
val query :
+
?format:Kgp_types.format ->
+
?transmission:Kgp_types.transmission ->
+
?width:int ->
+
?height:int ->
+
?quiet:Kgp_types.quiet ->
+
unit ->
+
t
+
(** Query terminal support without storing the image. *)
+
+
(** {1 Display} *)
+
+
val display :
+
?image_id:int ->
+
?image_number:int ->
+
?placement:Kgp_placement.t ->
+
?quiet:Kgp_types.quiet ->
+
unit ->
+
t
+
(** Display a previously transmitted image. *)
+
+
(** {1 Deletion} *)
+
+
val delete : ?quiet:Kgp_types.quiet -> Kgp_types.delete -> t
+
(** Delete images or placements. *)
+
+
(** {1 Animation} *)
+
+
val frame :
+
?image_id:int ->
+
?image_number:int ->
+
?format:Kgp_types.format ->
+
?transmission:Kgp_types.transmission ->
+
?compression:Kgp_types.compression ->
+
?width:int ->
+
?height:int ->
+
?quiet:Kgp_types.quiet ->
+
frame:Kgp_frame.t ->
+
unit ->
+
t
+
(** Transmit animation frame data. *)
+
+
val animate :
+
?image_id:int ->
+
?image_number:int ->
+
?quiet:Kgp_types.quiet ->
+
Kgp_animation.t ->
+
t
+
(** Control animation playback. *)
+
+
val compose :
+
?image_id:int ->
+
?image_number:int ->
+
?quiet:Kgp_types.quiet ->
+
Kgp_compose.t ->
+
t
+
(** Compose animation frames. *)
+
+
(** {1 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. *)
+37
lib/kgp_compose.ml
···
+
(* Kitty Graphics Protocol Compose - Implementation *)
+
+
type t = {
+
source_frame : int;
+
dest_frame : int;
+
width : int option;
+
height : int option;
+
source_x : int option;
+
source_y : int option;
+
dest_x : int option;
+
dest_y : int option;
+
composition : Kgp_types.composition option;
+
}
+
+
let make ~source_frame ~dest_frame ?width ?height ?source_x ?source_y ?dest_x
+
?dest_y ?composition () =
+
{
+
source_frame;
+
dest_frame;
+
width;
+
height;
+
source_x;
+
source_y;
+
dest_x;
+
dest_y;
+
composition;
+
}
+
+
let source_frame t = t.source_frame
+
let dest_frame t = t.dest_frame
+
let width t = t.width
+
let height t = t.height
+
let source_x t = t.source_x
+
let source_y t = t.source_y
+
let dest_x t = t.dest_x
+
let dest_y t = t.dest_y
+
let composition t = t.composition
+32
lib/kgp_compose.mli
···
+
(** Kitty Graphics Protocol Compose
+
+
Frame composition operations. *)
+
+
type t
+
(** Composition operation. *)
+
+
val make :
+
source_frame:int ->
+
dest_frame:int ->
+
?width:int ->
+
?height:int ->
+
?source_x:int ->
+
?source_y:int ->
+
?dest_x:int ->
+
?dest_y:int ->
+
?composition:Kgp_types.composition ->
+
unit ->
+
t
+
(** Compose a rectangle from one frame onto another. *)
+
+
(** {1 Field Accessors} *)
+
+
val source_frame : t -> int
+
val dest_frame : t -> int
+
val width : t -> int option
+
val height : t -> int option
+
val source_x : t -> int option
+
val source_y : t -> int option
+
val dest_x : t -> int option
+
val dest_y : t -> int option
+
val composition : t -> Kgp_types.composition option
+12
lib/kgp_detect.ml
···
+
(* Kitty Graphics Protocol Detection - Implementation *)
+
+
let make_query () =
+
let cmd =
+
Kgp_command.query ~format:`Rgb24 ~transmission:`Direct ~width:1 ~height:1 ()
+
in
+
Kgp_command.to_string cmd ~data:"\x00\x00\x00"
+
+
let supports_graphics response ~da1_received =
+
response
+
|> Option.map Kgp_response.is_ok
+
|> Option.value ~default:(not da1_received)
+9
lib/kgp_detect.mli
···
+
(** Kitty Graphics Protocol Detection
+
+
Detect terminal graphics support capabilities. *)
+
+
val make_query : unit -> string
+
(** Generate a query command to test graphics support. *)
+
+
val supports_graphics : Kgp_response.t option -> da1_received:bool -> bool
+
(** Determine if graphics are supported based on query results. *)
+33
lib/kgp_frame.ml
···
+
(* Kitty Graphics Protocol Frame - Implementation *)
+
+
type t = {
+
x : int option;
+
y : int option;
+
base_frame : int option;
+
edit_frame : int option;
+
gap_ms : int option;
+
composition : Kgp_types.composition option;
+
background_color : int32 option;
+
}
+
+
let empty =
+
{
+
x = None;
+
y = None;
+
base_frame = None;
+
edit_frame = None;
+
gap_ms = None;
+
composition = None;
+
background_color = None;
+
}
+
+
let make ?x ?y ?base_frame ?edit_frame ?gap_ms ?composition ?background_color () =
+
{ x; y; base_frame; edit_frame; gap_ms; composition; background_color }
+
+
let x t = t.x
+
let y t = t.y
+
let base_frame t = t.base_frame
+
let edit_frame t = t.edit_frame
+
let gap_ms t = t.gap_ms
+
let composition t = t.composition
+
let background_color t = t.background_color
+39
lib/kgp_frame.mli
···
+
(** Kitty Graphics Protocol Frame
+
+
Animation frame configuration. *)
+
+
type t
+
(** Animation frame configuration. *)
+
+
val make :
+
?x:int ->
+
?y:int ->
+
?base_frame:int ->
+
?edit_frame:int ->
+
?gap_ms:int ->
+
?composition:Kgp_types.composition ->
+
?background_color:int32 ->
+
unit ->
+
t
+
(** Create a frame specification.
+
+
@param x Left edge where frame data is placed (pixels)
+
@param y Top edge where frame data is placed (pixels)
+
@param base_frame 1-based frame number to use as background canvas
+
@param edit_frame 1-based frame number to edit (0 = new frame)
+
@param gap_ms Delay before next frame in milliseconds
+
@param composition How to blend pixels onto the canvas
+
@param background_color 32-bit RGBA background when no base frame *)
+
+
val empty : t
+
(** Empty frame spec with defaults. *)
+
+
(** {1 Field Accessors} *)
+
+
val x : t -> int option
+
val y : t -> int option
+
val base_frame : t -> int option
+
val edit_frame : t -> int option
+
val gap_ms : t -> int option
+
val composition : t -> Kgp_types.composition option
+
val background_color : t -> int32 option
+63
lib/kgp_placement.ml
···
+
(* Kitty Graphics Protocol Placement - Implementation *)
+
+
type t = {
+
source_x : int option;
+
source_y : int option;
+
source_width : int option;
+
source_height : int option;
+
cell_x_offset : int option;
+
cell_y_offset : int option;
+
columns : int option;
+
rows : int option;
+
z_index : int option;
+
placement_id : int option;
+
cursor : Kgp_types.cursor option;
+
unicode_placeholder : bool;
+
}
+
+
let empty =
+
{
+
source_x = None;
+
source_y = None;
+
source_width = None;
+
source_height = None;
+
cell_x_offset = None;
+
cell_y_offset = None;
+
columns = None;
+
rows = None;
+
z_index = None;
+
placement_id = None;
+
cursor = None;
+
unicode_placeholder = false;
+
}
+
+
let make ?source_x ?source_y ?source_width ?source_height ?cell_x_offset
+
?cell_y_offset ?columns ?rows ?z_index ?placement_id ?cursor
+
?(unicode_placeholder = false) () =
+
{
+
source_x;
+
source_y;
+
source_width;
+
source_height;
+
cell_x_offset;
+
cell_y_offset;
+
columns;
+
rows;
+
z_index;
+
placement_id;
+
cursor;
+
unicode_placeholder;
+
}
+
+
let source_x t = t.source_x
+
let source_y t = t.source_y
+
let source_width t = t.source_width
+
let source_height t = t.source_height
+
let cell_x_offset t = t.cell_x_offset
+
let cell_y_offset t = t.cell_y_offset
+
let columns t = t.columns
+
let rows t = t.rows
+
let z_index t = t.z_index
+
let placement_id t = t.placement_id
+
let cursor t = t.cursor
+
let unicode_placeholder t = t.unicode_placeholder
+54
lib/kgp_placement.mli
···
+
(** Kitty Graphics Protocol Placement
+
+
Configuration for where and how to display images. *)
+
+
type t
+
(** Placement configuration. *)
+
+
val make :
+
?source_x:int ->
+
?source_y:int ->
+
?source_width:int ->
+
?source_height:int ->
+
?cell_x_offset:int ->
+
?cell_y_offset:int ->
+
?columns:int ->
+
?rows:int ->
+
?z_index:int ->
+
?placement_id:int ->
+
?cursor:Kgp_types.cursor ->
+
?unicode_placeholder:bool ->
+
unit ->
+
t
+
(** Create a placement configuration.
+
+
@param source_x Left edge of source rectangle in pixels (default 0)
+
@param source_y Top edge of source rectangle in pixels (default 0)
+
@param source_width Width of source rectangle (default: full width)
+
@param source_height Height of source rectangle (default: full height)
+
@param cell_x_offset X offset within the first cell in pixels
+
@param cell_y_offset Y offset within the first cell in pixels
+
@param columns Number of columns to display over (scales image)
+
@param rows Number of rows to display over (scales image)
+
@param z_index Stacking order (negative = under text)
+
@param placement_id Unique ID for this placement
+
@param cursor Cursor movement policy after display
+
@param unicode_placeholder Create virtual placement for Unicode mode *)
+
+
val empty : t
+
(** Empty placement with all defaults. *)
+
+
(** {1 Field Accessors} *)
+
+
val source_x : t -> int option
+
val source_y : t -> int option
+
val source_width : t -> int option
+
val source_height : t -> int option
+
val cell_x_offset : t -> int option
+
val cell_y_offset : t -> int option
+
val columns : t -> int option
+
val rows : t -> int option
+
val z_index : t -> int option
+
val placement_id : t -> int option
+
val cursor : t -> Kgp_types.cursor option
+
val unicode_placeholder : t -> bool
+56
lib/kgp_response.ml
···
+
(* Kitty Graphics Protocol Response - Implementation *)
+
+
type t = {
+
message : string;
+
image_id : int option;
+
image_number : int option;
+
placement_id : int option;
+
}
+
+
let is_ok t = t.message = "OK"
+
let message t = t.message
+
+
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';
+
}
+27
lib/kgp_response.mli
···
+
(** Kitty Graphics Protocol Response
+
+
Parse and interpret terminal responses to graphics commands. *)
+
+
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. *)
+89
lib/kgp_types.ml
···
+
(* Kitty Graphics Protocol Types - Implementation *)
+
+
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 ]
+
+
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
+109
lib/kgp_types.mli
···
+
(** Kitty Graphics Protocol Types
+
+
This module defines the base polymorphic variant types used throughout
+
the Kitty graphics protocol implementation. *)
+
+
(** {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
+94
lib/kgp_unicode.ml
···
+
(* Kitty Graphics Protocol Unicode Placeholders - Implementation *)
+
+
let placeholder_char = Uchar.of_int 0x10EEEE
+
+
let diacritics =
+
[|
+
0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F;
+
0x0346; 0x034A; 0x034B; 0x034C; 0x0350; 0x0351; 0x0352; 0x0357;
+
0x035B; 0x0363; 0x0364; 0x0365; 0x0366; 0x0367; 0x0368; 0x0369;
+
0x036A; 0x036B; 0x036C; 0x036D; 0x036E; 0x036F; 0x0483; 0x0484;
+
0x0485; 0x0486; 0x0487; 0x0592; 0x0593; 0x0594; 0x0595; 0x0597;
+
0x0598; 0x0599; 0x059C; 0x059D; 0x059E; 0x059F; 0x05A0; 0x05A1;
+
0x05A8; 0x05A9; 0x05AB; 0x05AC; 0x05AF; 0x05C4; 0x0610; 0x0611;
+
0x0612; 0x0613; 0x0614; 0x0615; 0x0616; 0x0617; 0x0657; 0x0658;
+
0x0659; 0x065A; 0x065B; 0x065D; 0x065E; 0x06D6; 0x06D7; 0x06D8;
+
0x06D9; 0x06DA; 0x06DB; 0x06DC; 0x06DF; 0x06E0; 0x06E1; 0x06E2;
+
0x06E4; 0x06E7; 0x06E8; 0x06EB; 0x06EC; 0x0730; 0x0732; 0x0733;
+
0x0735; 0x0736; 0x073A; 0x073D; 0x073F; 0x0740; 0x0741; 0x0743;
+
0x0745; 0x0747; 0x0749; 0x074A; 0x07EB; 0x07EC; 0x07ED; 0x07EE;
+
0x07EF; 0x07F0; 0x07F1; 0x07F3; 0x0816; 0x0817; 0x0818; 0x0819;
+
0x081B; 0x081C; 0x081D; 0x081E; 0x081F; 0x0820; 0x0821; 0x0822;
+
0x0823; 0x0825; 0x0826; 0x0827; 0x0829; 0x082A; 0x082B; 0x082C;
+
0x082D; 0x0951; 0x0953; 0x0954; 0x0F82; 0x0F83; 0x0F86; 0x0F87;
+
0x135D; 0x135E; 0x135F; 0x17DD; 0x193A; 0x1A17; 0x1A75; 0x1A76;
+
0x1A77; 0x1A78; 0x1A79; 0x1A7A; 0x1A7B; 0x1A7C; 0x1B6B; 0x1B6D;
+
0x1B6E; 0x1B6F; 0x1B70; 0x1B71; 0x1B72; 0x1B73; 0x1CD0; 0x1CD1;
+
0x1CD2; 0x1CDA; 0x1CDB; 0x1CE0; 0x1DC0; 0x1DC1; 0x1DC3; 0x1DC4;
+
0x1DC5; 0x1DC6; 0x1DC7; 0x1DC8; 0x1DC9; 0x1DCB; 0x1DCC; 0x1DD1;
+
0x1DD2; 0x1DD3; 0x1DD4; 0x1DD5; 0x1DD6; 0x1DD7; 0x1DD8; 0x1DD9;
+
0x1DDA; 0x1DDB; 0x1DDC; 0x1DDD; 0x1DDE; 0x1DDF; 0x1DE0; 0x1DE1;
+
0x1DE2; 0x1DE3; 0x1DE4; 0x1DE5; 0x1DE6; 0x1DFE; 0x20D0; 0x20D1;
+
0x20D4; 0x20D5; 0x20D6; 0x20D7; 0x20DB; 0x20DC; 0x20E1; 0x20E7;
+
0x20E9; 0x20F0; 0xA66F; 0xA67C; 0xA67D; 0xA6F0; 0xA6F1; 0xA8E0;
+
0xA8E1; 0xA8E2; 0xA8E3; 0xA8E4; 0xA8E5; 0xA8E6; 0xA8E7; 0xA8E8;
+
0xA8E9; 0xA8EA; 0xA8EB; 0xA8EC; 0xA8ED; 0xA8EE; 0xA8EF; 0xA8F0;
+
0xA8F1; 0xAAB0; 0xAAB2; 0xAAB3; 0xAAB7; 0xAAB8; 0xAABE; 0xAABF;
+
0xAAC1; 0xFE20; 0xFE21; 0xFE22; 0xFE23; 0xFE24; 0xFE25; 0xFE26;
+
0x10A0F; 0x10A38; 0x1D185; 0x1D186; 0x1D187; 0x1D188; 0x1D189;
+
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"
+26
lib/kgp_unicode.mli
···
+
(** Kitty Graphics Protocol Unicode Placeholders
+
+
Support for invisible Unicode placeholder characters that encode
+
image position metadata for accessibility and compatibility. *)
+
+
val placeholder_char : Uchar.t
+
(** The Unicode placeholder character U+10EEEE. *)
+
+
val write :
+
Buffer.t ->
+
image_id:int ->
+
?placement_id:int ->
+
rows:int ->
+
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). *)
+
+
val column_diacritic : int -> Uchar.t
+
(** Get the combining diacritic for a column number (0-based). *)
+
+
val id_high_byte_diacritic : int -> Uchar.t
+
(** Get the diacritic for the high byte of a 32-bit image ID. *)
-687
lib/kitty_graphics.ml
···
-
(* 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
-
type t = {
-
source_x : int option;
-
source_y : int option;
-
source_width : int option;
-
source_height : int option;
-
cell_x_offset : int option;
-
cell_y_offset : int option;
-
columns : int option;
-
rows : int option;
-
z_index : int option;
-
placement_id : int option;
-
cursor : cursor option;
-
unicode_placeholder : bool;
-
}
-
-
let empty =
-
{
-
source_x = None;
-
source_y = None;
-
source_width = None;
-
source_height = None;
-
cell_x_offset = None;
-
cell_y_offset = None;
-
columns = None;
-
rows = None;
-
z_index = None;
-
placement_id = None;
-
cursor = None;
-
unicode_placeholder = false;
-
}
-
-
let make ?source_x ?source_y ?source_width ?source_height ?cell_x_offset
-
?cell_y_offset ?columns ?rows ?z_index ?placement_id ?cursor
-
?(unicode_placeholder = false) () =
-
{
-
source_x;
-
source_y;
-
source_width;
-
source_height;
-
cell_x_offset;
-
cell_y_offset;
-
columns;
-
rows;
-
z_index;
-
placement_id;
-
cursor;
-
unicode_placeholder;
-
}
-
end
-
-
module Frame = struct
-
type t = {
-
x : int option;
-
y : int option;
-
base_frame : int option;
-
edit_frame : int option;
-
gap_ms : int option;
-
composition : composition option;
-
background_color : int32 option;
-
}
-
-
let empty =
-
{
-
x = None;
-
y = None;
-
base_frame = None;
-
edit_frame = None;
-
gap_ms = None;
-
composition = None;
-
background_color = None;
-
}
-
-
let make ?x ?y ?base_frame ?edit_frame ?gap_ms ?composition ?background_color
-
() =
-
{ x; y; base_frame; edit_frame; gap_ms; composition; background_color }
-
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
-
type t = {
-
source_frame : int;
-
dest_frame : int;
-
width : int option;
-
height : int option;
-
source_x : int option;
-
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
-
?dest_y ?composition () =
-
{
-
source_frame;
-
dest_frame;
-
width;
-
height;
-
source_x;
-
source_y;
-
dest_x;
-
dest_y;
-
composition;
-
}
-
end
-
-
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;
-
transmission = None;
-
compression = None;
-
width = None;
-
height = None;
-
size = None;
-
offset = None;
-
quiet = None;
-
image_id = None;
-
image_number = None;
-
placement = None;
-
delete = None;
-
frame = None;
-
animation = None;
-
compose = None;
-
}
-
-
let transmit ?image_id ?image_number ?format ?transmission ?compression ?width
-
?height ?size ?offset ?quiet () =
-
{
-
(make `Transmit) with
-
image_id;
-
image_number;
-
format;
-
transmission;
-
compression;
-
width;
-
height;
-
size;
-
offset;
-
quiet;
-
}
-
-
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;
-
transmission;
-
compression;
-
width;
-
height;
-
size;
-
offset;
-
quiet;
-
placement;
-
}
-
-
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;
-
transmission;
-
compression;
-
width;
-
height;
-
quiet;
-
frame = Some frame;
-
}
-
-
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 for transmit/frame actions, always include t=d for compatibility *)
-
(match cmd.action with
-
| `Transmit | `Transmit_and_display | `Frame ->
-
(match cmd.transmission with
-
| Some t -> kv_char w 't' (Transmission.to_char t)
-
| None -> kv_char w 't' 'd')
-
| _ -> ());
-
(* 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
-
-
(* Use large chunk size to avoid chunking - Kitty animation doesn't handle chunks well *)
-
let chunk_size = 1024 * 1024 (* 1MB - effectively no chunking *)
-
-
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
-
if len <= chunk_size then (
-
Buffer.add_char buf ';';
-
Buffer.add_string buf encoded;
-
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 to_string cmd ~data =
-
let buf = Buffer.create 1024 in
-
write buf cmd ~data;
-
Buffer.contents buf
-
end
-
-
module Response = struct
-
type t = {
-
message : string;
-
image_id : int option;
-
image_number : int option;
-
placement_id : int option;
-
}
-
-
let is_ok t = t.message = "OK"
-
let message t = t.message
-
-
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;
-
0x0346; 0x034A; 0x034B; 0x034C; 0x0350; 0x0351; 0x0352; 0x0357;
-
0x035B; 0x0363; 0x0364; 0x0365; 0x0366; 0x0367; 0x0368; 0x0369;
-
0x036A; 0x036B; 0x036C; 0x036D; 0x036E; 0x036F; 0x0483; 0x0484;
-
0x0485; 0x0486; 0x0487; 0x0592; 0x0593; 0x0594; 0x0595; 0x0597;
-
0x0598; 0x0599; 0x059C; 0x059D; 0x059E; 0x059F; 0x05A0; 0x05A1;
-
0x05A8; 0x05A9; 0x05AB; 0x05AC; 0x05AF; 0x05C4; 0x0610; 0x0611;
-
0x0612; 0x0613; 0x0614; 0x0615; 0x0616; 0x0617; 0x0657; 0x0658;
-
0x0659; 0x065A; 0x065B; 0x065D; 0x065E; 0x06D6; 0x06D7; 0x06D8;
-
0x06D9; 0x06DA; 0x06DB; 0x06DC; 0x06DF; 0x06E0; 0x06E1; 0x06E2;
-
0x06E4; 0x06E7; 0x06E8; 0x06EB; 0x06EC; 0x0730; 0x0732; 0x0733;
-
0x0735; 0x0736; 0x073A; 0x073D; 0x073F; 0x0740; 0x0741; 0x0743;
-
0x0745; 0x0747; 0x0749; 0x074A; 0x07EB; 0x07EC; 0x07ED; 0x07EE;
-
0x07EF; 0x07F0; 0x07F1; 0x07F3; 0x0816; 0x0817; 0x0818; 0x0819;
-
0x081B; 0x081C; 0x081D; 0x081E; 0x081F; 0x0820; 0x0821; 0x0822;
-
0x0823; 0x0825; 0x0826; 0x0827; 0x0829; 0x082A; 0x082B; 0x082C;
-
0x082D; 0x0951; 0x0953; 0x0954; 0x0F82; 0x0F83; 0x0F86; 0x0F87;
-
0x135D; 0x135E; 0x135F; 0x17DD; 0x193A; 0x1A17; 0x1A75; 0x1A76;
-
0x1A77; 0x1A78; 0x1A79; 0x1A7A; 0x1A7B; 0x1A7C; 0x1B6B; 0x1B6D;
-
0x1B6E; 0x1B6F; 0x1B70; 0x1B71; 0x1B72; 0x1B73; 0x1CD0; 0x1CD1;
-
0x1CD2; 0x1CDA; 0x1CDB; 0x1CE0; 0x1DC0; 0x1DC1; 0x1DC3; 0x1DC4;
-
0x1DC5; 0x1DC6; 0x1DC7; 0x1DC8; 0x1DC9; 0x1DCB; 0x1DCC; 0x1DD1;
-
0x1DD2; 0x1DD3; 0x1DD4; 0x1DD5; 0x1DD6; 0x1DD7; 0x1DD8; 0x1DD9;
-
0x1DDA; 0x1DDB; 0x1DDC; 0x1DDD; 0x1DDE; 0x1DDF; 0x1DE0; 0x1DE1;
-
0x1DE2; 0x1DE3; 0x1DE4; 0x1DE5; 0x1DE6; 0x1DFE; 0x20D0; 0x20D1;
-
0x20D4; 0x20D5; 0x20D6; 0x20D7; 0x20DB; 0x20DC; 0x20E1; 0x20E7;
-
0x20E9; 0x20F0; 0xA66F; 0xA67C; 0xA67D; 0xA6F0; 0xA6F1; 0xA8E0;
-
0xA8E1; 0xA8E2; 0xA8E3; 0xA8E4; 0xA8E5; 0xA8E6; 0xA8E7; 0xA8E8;
-
0xA8E9; 0xA8EA; 0xA8EB; 0xA8EC; 0xA8ED; 0xA8EE; 0xA8EF; 0xA8F0;
-
0xA8F1; 0xAAB0; 0xAAB2; 0xAAB3; 0xAAB7; 0xAAB8; 0xAABE; 0xAABF;
-
0xAAC1; 0xFE20; 0xFE21; 0xFE22; 0xFE23; 0xFE24; 0xFE25; 0xFE26;
-
0x10A0F; 0x10A38; 0x1D185; 0x1D186; 0x1D187; 0x1D188; 0x1D189;
-
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"
-
-
let supports_graphics response ~da1_received =
-
response |> Option.map Response.is_ok |> Option.value ~default:(not da1_received)
-
end
-402
lib/kitty_graphics.mli
···
-
(** Kitty Terminal Graphics Protocol
-
-
This library implements the Kitty terminal graphics protocol, allowing
-
OCaml programs to display images in terminals that support the protocol
-
(Kitty, WezTerm, Konsole, Ghostty, etc.).
-
-
The protocol uses APC (Application Programming Command) escape sequences
-
to transmit and display pixel graphics. Images can be transmitted as raw
-
RGB/RGBA data or PNG, and displayed at specific positions with various
-
placement options.
-
-
{2 Basic Usage}
-
-
{[
-
(* 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)
-
]}
-
-
{2 Protocol Reference}
-
-
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. *)
-
-
val make :
-
?source_x:int ->
-
?source_y:int ->
-
?source_width:int ->
-
?source_height:int ->
-
?cell_x_offset:int ->
-
?cell_y_offset:int ->
-
?columns:int ->
-
?rows:int ->
-
?z_index:int ->
-
?placement_id:int ->
-
?cursor:cursor ->
-
?unicode_placeholder:bool ->
-
unit ->
-
t
-
(** Create a placement configuration.
-
-
@param source_x Left edge of source rectangle in pixels (default 0)
-
@param source_y Top edge of source rectangle in pixels (default 0)
-
@param source_width Width of source rectangle (default: full width)
-
@param source_height Height of source rectangle (default: full height)
-
@param cell_x_offset X offset within the first cell in pixels
-
@param cell_y_offset Y offset within the first cell in pixels
-
@param columns Number of columns to display over (scales image)
-
@param rows Number of rows to display over (scales image)
-
@param z_index Stacking order (negative = under text)
-
@param placement_id Unique ID for this placement
-
@param cursor Cursor movement policy after display
-
@param unicode_placeholder Create virtual placement for Unicode mode *)
-
-
val empty : t
-
(** Empty placement with all defaults. *)
-
end
-
-
(** {1 Animation} *)
-
-
module Frame : sig
-
type t
-
(** Animation frame configuration. *)
-
-
val make :
-
?x:int ->
-
?y:int ->
-
?base_frame:int ->
-
?edit_frame:int ->
-
?gap_ms:int ->
-
?composition:composition ->
-
?background_color:int32 ->
-
unit ->
-
t
-
(** Create a frame specification.
-
-
@param x Left edge where frame data is placed (pixels)
-
@param y Top edge where frame data is placed (pixels)
-
@param base_frame 1-based frame number to use as background canvas
-
@param edit_frame 1-based frame number to edit (0 = new frame)
-
@param gap_ms Delay before next frame in milliseconds
-
@param composition How to blend pixels onto the canvas
-
@param background_color 32-bit RGBA background when no base frame *)
-
-
val empty : 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) *)
-
-
val set_current_frame : int -> t
-
(** Make a specific frame (1-based) the current displayed frame. *)
-
end
-
-
module Compose : sig
-
type t
-
(** Composition operation. *)
-
-
val make :
-
source_frame:int ->
-
dest_frame:int ->
-
?width:int ->
-
?height:int ->
-
?source_x:int ->
-
?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. *)
-
-
(** {2 Image Transmission} *)
-
-
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} *)
-
-
val 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. *)
-
end
-
-
(** {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. *)
-
-
val write :
-
Buffer.t ->
-
image_id:int ->
-
?placement_id:int ->
-
rows:int ->
-
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). *)
-
-
val column_diacritic : int -> Uchar.t
-
(** Get the combining diacritic for a column number (0-based). *)
-
-
val id_high_byte_diacritic : int -> Uchar.t
-
(** Get the diacritic for the high byte of a 32-bit image ID. *)
-
end
-
-
(** {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