Kitty Graphics Protocol in OCaml
terminal graphics ocaml

Compare changes

Choose any two refs to compare.

+1
.ocamlformat
···
+
version=0.28.1
+49
.tangled/workflows/build.yml
···
+
when:
+
- event: ["push", "pull_request"]
+
branch: ["main"]
+
+
engine: nixery
+
+
dependencies:
+
nixpkgs:
+
- shell
+
- stdenv
+
- findutils
+
- binutils
+
- libunwind
+
- ncurses
+
- opam
+
- git
+
- gawk
+
- gnupatch
+
- gnum4
+
- gnumake
+
- gnutar
+
- gnused
+
- gnugrep
+
- diffutils
+
- gzip
+
- bzip2
+
- gcc
+
- ocaml
+
+
steps:
+
- name: opam
+
command: |
+
opam init --disable-sandboxing -any
+
- name: switch
+
command: |
+
opam install . --confirm-level=unsafe-yes --deps-only
+
- name: build
+
command: |
+
opam exec -- dune build
+
- name: switch-test
+
command: |
+
opam install . --confirm-level=unsafe-yes --deps-only --with-test
+
- name: test
+
command: |
+
opam exec -- dune runtest --verbose
+
- name: doc
+
command: |
+
opam install -y odoc
+
opam exec -- dune build @doc
+4
CHANGES.md
···
+
v1.0.0 (dev)
+
------------
+
+
- Initial public release (@avsm)
+18
LICENSE.md
···
+
(*
+
* ISC License
+
*
+
* Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>
+
*
+
* Permission to use, copy, modify, and distribute this software for any
+
* purpose with or without fee is hereby granted, provided that the above
+
* copyright notice and this permission notice appear in all copies.
+
*
+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
*
+
*)
+66
README.md
···
+
# kgp - Kitty Graphics Protocol for OCaml
+
+
An OCaml library for displaying images in terminals using the [Kitty Graphics Protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/).
+
+
This library can display PNG images in supported terminals (e.g. Kitty, WezTerm, Konsole, Ghostty) with varying levels of support depending on the terminal. Most core features work but some advanced things like animations might be only partially supported outside of Kitty.
+
+
Other features include:
+
- Image transmission with automatic chunking and base64 encoding
+
- Multiple placements of the same image
+
- Animation support with frame deltas
+
- Unicode placeholder mode for proper scrolling
+
- tmux passthrough support (requires tmux 3.3+)
+
- Terminal capability detection
+
+
The library provides a Cmdliner term via the `kgp.cli` package to make integration into other CLI tools easier.
+
+
## Installation
+
+
```
+
opam install kgp
+
```
+
+
## Usage
+
+
### Library
+
+
```ocaml
+
(* Display a PNG image *)
+
let png_data = read_file "image.png" in
+
let cmd = Kgp.transmit_and_display ~format:`Png () in
+
let buf = Buffer.create 1024 in
+
Kgp.write buf cmd ~data:png_data;
+
print_string (Buffer.contents buf)
+
```
+
+
```ocaml
+
(* Transmit once, display multiple times *)
+
let cmd = Kgp.transmit ~image_id:1 ~format:`Png () in
+
Kgp.write buf cmd ~data:png_data;
+
+
let cmd = Kgp.display ~image_id:1 () in
+
Kgp.write buf cmd ~data:""
+
```
+
+
### CLI
+
+
```
+
# Display an image
+
kgpcat image.png
+
+
# Display at specific size and position
+
kgpcat --place 40x20@10,5 image.png
+
+
# Display from stdin
+
curl -s https://example.com/image.png | kgpcat
+
+
# Detect terminal support
+
kgpcat --detect-support
+
+
# Clear displayed images
+
kgpcat --clear
+
```
+
+
## License
+
+
ISC
+1
TODO.md
···
+
- Support zlib via decompress or similar
+5
bin/dune
···
+
(executable
+
(name kgpcat)
+
(public_name kgpcat)
+
(package kgp)
+
(libraries kgp kgp.cli cmdliner unix))
+352
bin/kgpcat.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(* kgpcat - Display images in the terminal using Kitty Graphics Protocol *)
+
+
open Cmdliner
+
+
module K = Kgp
+
+
type align = Center | Left | Right
+
type fit = Width | Height | Both | None_
+
+
type config = {
+
files : string list;
+
clear : bool;
+
clear_all : bool;
+
detect_support : bool;
+
align : align; [@warning "-69"]
+
place : (int * int * int * int) option;
+
scale_up : bool; [@warning "-69"]
+
fit : fit; [@warning "-69"]
+
z_index : int;
+
unicode_placeholder : bool;
+
no_trailing_newline : bool;
+
hold : bool;
+
graphics_mode : K.Terminal.graphics_mode;
+
}
+
+
(* Default image size in cells when not using --place *)
+
let default_rows = 10
+
let default_cols = 20
+
+
(* Read file contents *)
+
let read_file filename =
+
let ic = open_in_bin filename in
+
let n = in_channel_length ic in
+
let s = really_input_string ic n in
+
close_in ic;
+
s
+
+
(* Read from stdin *)
+
let read_stdin () =
+
let buf = Buffer.create 4096 in
+
try
+
while true do
+
Buffer.add_channel buf stdin 4096
+
done;
+
assert false
+
with End_of_file -> Buffer.contents buf
+
+
(* Detect if stdin has data *)
+
let stdin_has_data () =
+
let fd = Unix.descr_of_in_channel stdin in
+
not (Unix.isatty fd)
+
+
(* Send command to terminal *)
+
let send ?(tmux = false) cmd ~data =
+
let s = if tmux then K.to_string_tmux cmd ~data else K.to_string cmd ~data in
+
print_string s;
+
flush stdout
+
+
(* Check if file is a supported format (PNG only) *)
+
let is_supported_format filename =
+
let ext =
+
try
+
let dot = String.rindex filename '.' in
+
String.lowercase_ascii (String.sub filename (dot + 1) (String.length filename - dot - 1))
+
with Not_found -> ""
+
in
+
ext = "png" || filename = "stdin"
+
+
(* Display a single image *)
+
let display_image config filename data =
+
let resolved_mode = K.Terminal.resolve_mode config.graphics_mode in
+
match resolved_mode with
+
| `Placeholder ->
+
(* No graphics support - show placeholder text *)
+
Printf.printf "[Image: %s (%d bytes)]\n" filename (String.length data)
+
| `Graphics | `Tmux ->
+
let use_tmux = resolved_mode = `Tmux in
+
(* Use unicode placeholders if requested or if in tmux mode *)
+
let use_unicode = config.unicode_placeholder || use_tmux in
+
let cols, rows =
+
match config.place with
+
| Some (w, h, _, _) -> (w, h)
+
| None -> (default_cols, default_rows)
+
in
+
if use_unicode then (
+
(* Unicode placeholder mode: transmit image, then output placeholder chars *)
+
let image_id = K.next_image_id () in
+
let placement =
+
match config.place with
+
| Some (w, h, x, y) ->
+
K.Placement.make ~columns:w ~rows:h ~cell_x_offset:x ~cell_y_offset:y
+
~z_index:config.z_index ~unicode_placeholder:true ~cursor:`Static ()
+
| None ->
+
let z = if config.z_index <> 0 then Some config.z_index else None in
+
K.Placement.make ~columns:cols ~rows ?z_index:z ~unicode_placeholder:true
+
~cursor:`Static ()
+
in
+
(* Transmit the image data with virtual placement *)
+
let cmd =
+
K.transmit_and_display ~image_id ~format:`Png ~placement ~quiet:`Errors_only ()
+
in
+
send ~tmux:use_tmux cmd ~data;
+
(* Output unicode placeholder characters that reference the image *)
+
let buf = Buffer.create 256 in
+
K.Unicode_placeholder.write buf ~image_id ~rows ~cols ();
+
print_string (Buffer.contents buf);
+
if not config.no_trailing_newline then print_newline ()
+
) else (
+
(* Direct graphics mode *)
+
let image_id = K.next_image_id () in
+
let placement =
+
match config.place with
+
| Some (w, h, x, y) ->
+
Some
+
(K.Placement.make ~columns:w ~rows:h ~cell_x_offset:x ~cell_y_offset:y
+
~z_index:config.z_index ~cursor:`Static ())
+
| None ->
+
let z = if config.z_index <> 0 then Some config.z_index else None in
+
Some
+
(K.Placement.make ?z_index:z
+
~cursor:(if config.no_trailing_newline then `Static else `Move) ())
+
in
+
let cmd =
+
K.transmit_and_display ~image_id ~format:`Png ?placement ~quiet:`Errors_only ()
+
in
+
send ~tmux:use_tmux cmd ~data;
+
if not config.no_trailing_newline && config.place = None then print_newline ()
+
)
+
+
(* Clear all images *)
+
let do_clear () =
+
send (K.delete `All_visible) ~data:""
+
+
(* Clear all images including scrollback *)
+
let do_clear_all () =
+
(* Delete all images by ID range 1 to max, freeing data *)
+
send (K.delete ~free:true (`By_id_range (1, 4294967295))) ~data:""
+
+
(* Detect terminal support *)
+
let do_detect_support () =
+
if K.Terminal.is_graphics_terminal () then (
+
let mode =
+
if K.Terminal.is_tmux () then "tmux"
+
else if K.Terminal.is_kitty () then "kitty"
+
else if K.Terminal.is_wezterm () then "wezterm"
+
else if K.Terminal.is_ghostty () then "ghostty"
+
else "stream"
+
in
+
Printf.eprintf "%s\n" mode;
+
0
+
) else (
+
Printf.eprintf "not supported\n";
+
1
+
)
+
+
(* Main run function *)
+
let run config =
+
(* Handle clear operations first *)
+
if config.clear_all then do_clear_all ()
+
else if config.clear then do_clear ();
+
+
(* Handle detect support *)
+
if config.detect_support then exit (do_detect_support ());
+
+
(* Process files *)
+
let files =
+
if config.files = [] && stdin_has_data () then [ "-" ] else config.files
+
in
+
if files = [] && not config.clear && not config.clear_all then (
+
Printf.eprintf "Usage: kgpcat [OPTIONS] IMAGE_FILE...\n";
+
Printf.eprintf "Try 'kgpcat --help' for more information.\n";
+
exit 1);
+
+
List.iter
+
(fun file ->
+
let name = if file = "-" then "stdin" else file in
+
if not (is_supported_format name) then
+
Printf.eprintf "Error: %s is not a PNG file (only PNG format is supported)\n" file
+
else
+
try
+
let data =
+
if file = "-" then read_stdin () else read_file file
+
in
+
display_image config name data
+
with
+
| Sys_error msg ->
+
Printf.eprintf "Error reading %s: %s\n" file msg
+
| exn ->
+
Printf.eprintf "Error processing %s: %s\n" file (Printexc.to_string exn))
+
files;
+
+
(* Ensure all output is flushed before exiting *)
+
flush stdout;
+
+
if config.hold then (
+
Printf.eprintf "Press Enter to exit...";
+
flush stderr;
+
ignore (read_line ()))
+
+
(* Cmdliner argument definitions *)
+
+
let files_arg =
+
Arg.(value & pos_all file [] & info [] ~docv:"IMAGE" ~doc:"Image files to display.")
+
+
let clear_arg =
+
let doc = "Remove all images currently displayed on the screen." in
+
Arg.(value & flag & info [ "clear" ] ~doc)
+
+
let clear_all_arg =
+
let doc = "Remove all images from screen and scrollback." in
+
Arg.(value & flag & info [ "clear-all" ] ~doc)
+
+
let detect_support_arg =
+
let doc =
+
"Detect support for image display in the terminal. Exits with code 0 if \
+
supported, 1 otherwise. Prints the supported transfer mode to stderr."
+
in
+
Arg.(value & flag & info [ "detect-support" ] ~doc)
+
+
let align_arg =
+
let doc = "Horizontal alignment for the displayed image." in
+
let align_enum = Arg.enum [ ("center", Center); ("left", Left); ("right", Right) ] in
+
Arg.(value & opt align_enum Center & info [ "align" ] ~doc ~docv:"ALIGN")
+
+
let place_arg =
+
let doc =
+
"Display image in specified rectangle. Format: WxH@X,Y where W and H are \
+
width and height in cells, and X,Y is the position. Example: 40x20@10,5"
+
in
+
let parse s =
+
try
+
let at_pos = String.index s '@' in
+
let size_part = String.sub s 0 at_pos in
+
let pos_part = String.sub s (at_pos + 1) (String.length s - at_pos - 1) in
+
let x_pos = String.index size_part 'x' in
+
let w = int_of_string (String.sub size_part 0 x_pos) in
+
let h = int_of_string (String.sub size_part (x_pos + 1) (String.length size_part - x_pos - 1)) in
+
let comma_pos = String.index pos_part ',' in
+
let x = int_of_string (String.sub pos_part 0 comma_pos) in
+
let y = int_of_string (String.sub pos_part (comma_pos + 1) (String.length pos_part - comma_pos - 1)) in
+
Ok (w, h, x, y)
+
with _ -> Error (`Msg "Invalid place format. Use WxH@X,Y (e.g., 40x20@10,5)")
+
in
+
let print ppf (w, h, x, y) = Format.fprintf ppf "%dx%d@%d,%d" w h x y in
+
let place_conv = Arg.conv (parse, print) in
+
Arg.(value & opt (some place_conv) None & info [ "place" ] ~doc ~docv:"WxH@X,Y")
+
+
let scale_up_arg =
+
let doc =
+
"Scale up images smaller than the specified area to use as much of the \
+
area as possible."
+
in
+
Arg.(value & flag & info [ "scale-up" ] ~doc)
+
+
let fit_arg =
+
let doc = "Control how the image is scaled relative to the screen." in
+
let fit_enum =
+
Arg.enum [ ("width", Width); ("height", Height); ("both", Both); ("none", None_) ]
+
in
+
Arg.(value & opt fit_enum Width & info [ "fit" ] ~doc ~docv:"FIT")
+
+
let z_index_arg =
+
let doc =
+
"Z-index of the image. Negative values display text on top of the image."
+
in
+
Arg.(value & opt int 0 & info [ "z"; "z-index" ] ~doc ~docv:"Z")
+
+
let unicode_placeholder_arg =
+
let doc =
+
"Use Unicode placeholder method to display images. This allows images to \
+
scroll properly with text in terminals and multiplexers. Automatically \
+
enabled when using tmux passthrough mode."
+
in
+
Arg.(value & flag & info [ "unicode-placeholder" ] ~doc)
+
+
let no_trailing_newline_arg =
+
let doc = "Don't move cursor to next line after displaying an image." in
+
Arg.(value & flag & info [ "n"; "no-trailing-newline" ] ~doc)
+
+
let hold_arg =
+
let doc = "Wait for a key press before exiting after displaying images." in
+
Arg.(value & flag & info [ "hold" ] ~doc)
+
+
let config_term =
+
let combine files clear clear_all detect_support align place scale_up fit z_index
+
unicode_placeholder no_trailing_newline hold graphics_mode =
+
{
+
files;
+
clear;
+
clear_all;
+
detect_support;
+
align;
+
place;
+
scale_up;
+
fit;
+
z_index;
+
unicode_placeholder;
+
no_trailing_newline;
+
hold;
+
graphics_mode;
+
}
+
in
+
Term.(
+
const combine
+
$ files_arg
+
$ clear_arg
+
$ clear_all_arg
+
$ detect_support_arg
+
$ align_arg
+
$ place_arg
+
$ scale_up_arg
+
$ fit_arg
+
$ z_index_arg
+
$ unicode_placeholder_arg
+
$ no_trailing_newline_arg
+
$ hold_arg
+
$ Kgp_cli.graphics_term)
+
+
let cmd =
+
let doc = "Display images in the terminal using Kitty Graphics Protocol" in
+
let man =
+
[
+
`S Manpage.s_description;
+
`P
+
"$(tname) displays images in terminals that support the Kitty Graphics \
+
Protocol (Kitty, WezTerm, Konsole, Ghostty, etc.).";
+
`P
+
"You can specify multiple image files. If no files are given and stdin \
+
is not a terminal, image data is read from stdin.";
+
`S Manpage.s_examples;
+
`P "Display an image:";
+
`Pre " $(tname) photo.png";
+
`P "Display multiple images:";
+
`Pre " $(tname) *.png";
+
`P "Display image from stdin:";
+
`Pre " curl -s https://example.com/image.png | $(tname)";
+
`P "Display image at specific position:";
+
`Pre " $(tname) --place 40x20@10,5 photo.png";
+
`P "Clear all displayed images:";
+
`Pre " $(tname) --clear";
+
`S Kgp_cli.graphics_docs;
+
]
+
in
+
let info = Cmd.info "kgpcat" ~version:"%%VERSION%%" ~doc ~man in
+
Cmd.v info Term.(const run $ config_term)
+
+
let () = exit (Cmd.eval cmd)
+11 -1
dune-project
···
(lang dune 3.20)
(name kgp)
+
(generate_opam_files true)
+
+
(license ISC)
+
(authors "Anil Madhavapeddy")
+
(homepage "https://tangled.org/@anil.recoil.org/ocaml-kgp")
+
(maintainers "Anil Madhavapeddy <anil@recoil.org>")
+
(bug_reports "https://tangled.org/@anil.recoil.org/ocaml-kgp/issues")
+
(maintenance_intent "(latest)")
+
(package
(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.")
+
"Library for rendering images in terminals that support the Kitty graphics protocol. Supports image transmission, display, animation, Unicode placeholders, and terminal capability detection.")
(depends
(ocaml (>= 4.14.0))
+
cmdliner
base64))
-97
example/anim_test.ml
···
-
(* Minimal animation test - shows exact bytes sent *)
-
-
module K = Kgp
-
-
let solid_color_rgba ~width ~height ~r ~g ~b ~a =
-
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) (Char.chr a)
-
done;
-
Bytes.to_string pixels
-
-
let send cmd ~data =
-
print_string (K.Command.to_string cmd ~data);
-
flush stdout
-
-
let () =
-
let width, height = 40, 40 in (* Smaller for faster testing *)
-
let image_id = 500 in
-
-
(* Clear any existing image *)
-
send (K.Command.delete ~quiet:`Errors_only (`All_visible_and_free)) ~data:"";
-
-
(* Step 1: Transmit base frame (red) *)
-
let red_frame = solid_color_rgba ~width ~height ~r:255 ~g:0 ~b:0 ~a:255 in
-
send
-
(K.Command.transmit
-
~image_id
-
~format:`Rgba32
-
~width ~height
-
~quiet:`Errors_only
-
())
-
~data:red_frame;
-
-
(* Step 2: Add frame (blue) *)
-
let blue_frame = solid_color_rgba ~width ~height ~r:0 ~g:0 ~b:255 ~a:255 in
-
send
-
(K.Command.frame
-
~image_id
-
~format:`Rgba32
-
~width ~height
-
~frame:(K.Frame.make ~gap_ms:500 ~composition:`Overwrite ())
-
~quiet:`Errors_only
-
())
-
~data:blue_frame;
-
-
(* Step 3: Add frame (green) *)
-
let green_frame = solid_color_rgba ~width ~height ~r:0 ~g:255 ~b:0 ~a:255 in
-
send
-
(K.Command.frame
-
~image_id
-
~format:`Rgba32
-
~width ~height
-
~frame:(K.Frame.make ~gap_ms:500 ~composition:`Overwrite ())
-
~quiet:`Errors_only
-
())
-
~data:green_frame;
-
-
(* Step 4: Create placement *)
-
send
-
(K.Command.display
-
~image_id
-
~placement:(K.Placement.make
-
~placement_id:1
-
~cursor:`Static
-
())
-
~quiet:`Errors_only
-
())
-
~data:"";
-
-
(* Step 5: Set root frame gap - IMPORTANT: root frame has no gap by default *)
-
send
-
(K.Command.animate ~image_id (K.Animation.set_gap ~frame:1 ~gap_ms:500))
-
~data:"";
-
-
(* Step 6: Start animation *)
-
send
-
(K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run))
-
~data:"";
-
-
print_endline "";
-
print_endline "Animation should be playing (red -> blue -> green).";
-
print_endline "Press Enter to stop...";
-
flush stdout;
-
let _ = read_line () in
-
-
(* Stop animation *)
-
send
-
(K.Command.animate ~image_id (K.Animation.set_state `Stop))
-
~data:"";
-
-
(* Clean up *)
-
send (K.Command.delete ~quiet:`Errors_only (`All_visible_and_free)) ~data:"";
-
print_endline "Done."
example/camel.png

This is a binary file and will not be displayed.

-94
example/debug_anim.ml
···
-
(* Debug: Output animation escape sequences for comparison with Go *)
-
-
module K = Kgp
-
-
let solid_color_rgba ~width ~height ~r ~g ~b ~a =
-
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) (Char.chr a)
-
done;
-
Bytes.to_string pixels
-
-
let send cmd ~data =
-
let s = K.Command.to_string cmd ~data in
-
(* Print escaped version for debugging *)
-
String.iter (fun c ->
-
let code = Char.code c in
-
if code = 27 then print_string "\\x1b"
-
else if code < 32 || code > 126 then Printf.printf "\\x%02x" code
-
else print_char c
-
) s;
-
print_newline ()
-
-
let () =
-
let width, height = 80, 80 in
-
let image_id = 300 in
-
-
print_endline "=== OCaml Animation Debug ===\n";
-
-
(* Step 1: Transmit base frame *)
-
print_endline "1. Transmit base frame (a=t):";
-
let red_frame = solid_color_rgba ~width ~height ~r:255 ~g:0 ~b:0 ~a:255 in
-
send
-
(K.Command.transmit
-
~image_id
-
~format:`Rgba32
-
~width ~height
-
~quiet:`Errors_only
-
())
-
~data:red_frame;
-
print_newline ();
-
-
(* Step 2: Add frame *)
-
print_endline "2. Add frame (a=f):";
-
let orange_frame = solid_color_rgba ~width ~height ~r:255 ~g:165 ~b:0 ~a:255 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;
-
print_newline ();
-
-
(* Step 3: Put/display placement *)
-
print_endline "3. Create placement (a=p):";
-
send
-
(K.Command.display
-
~image_id
-
~placement:(K.Placement.make
-
~placement_id:1
-
~cell_x_offset:0
-
~cell_y_offset:0
-
~cursor:`Static
-
())
-
~quiet:`Errors_only
-
())
-
~data:"";
-
print_newline ();
-
-
(* Step 4: Set root frame gap *)
-
print_endline "4. Set root frame gap (a=a,r=1,z=100):";
-
send
-
(K.Command.animate ~image_id (K.Animation.set_gap ~frame:1 ~gap_ms:100))
-
~data:"";
-
print_newline ();
-
-
(* Step 5: Animate *)
-
print_endline "5. Start animation (a=a,s=3,v=1):";
-
send
-
(K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run))
-
~data:"";
-
print_newline ();
-
-
(* Step 6: Stop animation *)
-
print_endline "6. Stop animation:";
-
send
-
(K.Command.animate ~image_id (K.Animation.set_state `Stop))
-
~data:""
+3 -11
example/dune
···
(name example)
(libraries kgp unix))
-
(executable
-
(name debug_anim)
-
(libraries kgp))
-
-
(executable
-
(name test_output)
-
(libraries kgp))
-
-
(executable
-
(name anim_test)
-
(libraries kgp))
+
(alias
+
(name example)
+
(deps example.exe camel.png))
(executable
(name tiny_anim)
+97 -161
example/example.ml
···
-
(* Kitty Graphics Protocol Demo - Matching kgp/examples/demo *)
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(* Kitty Graphics Protocol Demo *)
module K = Kgp
···
let pixels = Bytes.create (width * height * 4) in
for y = 0 to height - 1 do
for x = 0 to width - 1 do
-
let idx = (y * width + x) * 4 in
+
let idx = ((y * width) + x) * 4 in
let r = 255 * x / width in
let b = 255 * (width - x) / width in
Bytes.set pixels idx (Char.chr r);
···
s
let send cmd ~data =
-
print_string (K.Command.to_string cmd ~data);
+
print_string (K.to_string cmd ~data);
flush stdout
let wait_for_enter () =
···
let clear_screen () =
print_string "\x1b[2J\x1b[H";
-
for _ = 1 to 5 do print_newline () done;
+
for _ = 1 to 5 do
+
print_newline ()
+
done;
flush stdout
let () =
···
(* Demo 1: Basic formats - PNG *)
clear_screen ();
print_endline "Demo 1: Image Formats - PNG format";
-
(* Read sf.png and display a small portion as demo *)
+
(* Read camel.png and display a small portion as demo *)
(try
-
let png_data = read_file "sf.png" in
+
let png_data = read_file "camel.png" in
send
-
(K.Command.transmit_and_display
-
~image_id:1
-
~format:`Png
-
~quiet:`Errors_only
+
(K.transmit_and_display ~image_id:1 ~format:`Png ~quiet:`Errors_only
~placement:(K.Placement.make ~columns:15 ~rows:8 ())
())
~data:png_data;
-
print_endline "sf.png displayed using PNG format"
+
print_endline "camel.png displayed using PNG format"
with _ ->
(* Fallback: red square as RGBA *)
-
let red_data = solid_color_rgba ~width:100 ~height:100 ~r:255 ~g:0 ~b:0 ~a:255 in
+
let red_data =
+
solid_color_rgba ~width:100 ~height:100 ~r:255 ~g:0 ~b:0 ~a:255
+
in
send
-
(K.Command.transmit_and_display
-
~image_id:1
-
~format:`Rgba32
-
~width:100 ~height:100
-
~quiet:`Errors_only
-
())
+
(K.transmit_and_display ~image_id:1 ~format:`Rgba32 ~width:100
+
~height:100 ~quiet:`Errors_only ())
~data:red_data;
-
print_endline "Red square displayed (sf.png not found)");
+
print_endline "Red square displayed (camel.png not found)");
print_newline ();
wait_for_enter ();
(* Demo 2: Basic formats - RGBA *)
clear_screen ();
print_endline "Demo 2: Image Formats - RGBA format (32-bit)";
-
let blue_data = solid_color_rgba ~width:100 ~height:100 ~r:0 ~g:0 ~b:255 ~a:255 in
+
let blue_data =
+
solid_color_rgba ~width:100 ~height:100 ~r:0 ~g:0 ~b:255 ~a:255
+
in
send
-
(K.Command.transmit_and_display
-
~image_id:2
-
~format:`Rgba32
-
~width:100 ~height:100
-
~quiet:`Errors_only
-
())
+
(K.transmit_and_display ~image_id:2 ~format:`Rgba32 ~width:100 ~height:100
+
~quiet:`Errors_only ())
~data:blue_data;
print_endline "Blue square displayed using raw RGBA format";
print_newline ();
···
print_endline "Demo 3: Image Formats - RGB format (24-bit)";
let green_data = solid_color_rgb ~width:100 ~height:100 ~r:0 ~g:255 ~b:0 in
send
-
(K.Command.transmit_and_display
-
~image_id:3
-
~format:`Rgb24
-
~width:100 ~height:100
-
~quiet:`Errors_only
-
())
+
(K.transmit_and_display ~image_id:3 ~format:`Rgb24 ~width:100 ~height:100
+
~quiet:`Errors_only ())
~data:green_data;
print_endline "Green square displayed using raw RGB format (no alpha channel)";
print_newline ();
···
(* Demo 4: Compression - Note: would need zlib library for actual compression *)
clear_screen ();
print_endline "Demo 4: Large Image (compression requires zlib library)";
-
let orange_data = solid_color_rgba ~width:200 ~height:200 ~r:255 ~g:165 ~b:0 ~a:255 in
+
let orange_data =
+
solid_color_rgba ~width:200 ~height:200 ~r:255 ~g:165 ~b:0 ~a:255
+
in
send
-
(K.Command.transmit_and_display
-
~image_id:4
-
~format:`Rgba32
-
~width:200 ~height:200
-
~quiet:`Errors_only
-
())
+
(K.transmit_and_display ~image_id:4 ~format:`Rgba32 ~width:200 ~height:200
+
~quiet:`Errors_only ())
~data:orange_data;
-
Printf.printf "Orange square (200x200) - %d bytes uncompressed\n" (String.length orange_data);
+
Printf.printf "Orange square (200x200) - %d bytes uncompressed\n"
+
(String.length orange_data);
print_newline ();
wait_for_enter ();
(* Demo 5: Load and display external PNG file *)
clear_screen ();
-
print_endline "Demo 5: Loading external PNG file (sf.png)";
+
print_endline "Demo 5: Loading external PNG file (camel.png)";
(try
-
let png_data = read_file "sf.png" in
+
let png_data = read_file "camel.png" in
send
-
(K.Command.transmit_and_display
-
~image_id:10
-
~format:`Png
-
~quiet:`Errors_only
-
())
+
(K.transmit_and_display ~image_id:10 ~format:`Png ~quiet:`Errors_only ())
~data:png_data;
-
print_endline "sf.png loaded and displayed"
-
with Sys_error msg ->
-
Printf.printf "sf.png not found: %s\n" msg);
+
print_endline "camel.png loaded and displayed"
+
with Sys_error msg -> Printf.printf "camel.png not found: %s\n" msg);
print_newline ();
wait_for_enter ();
···
print_endline "Demo 6: Cropping and Scaling - Display part of an image";
let gradient = gradient_rgba ~width:200 ~height:200 in
send
-
(K.Command.transmit_and_display
-
~image_id:20
-
~format:`Rgba32
-
~width:200 ~height:200
-
~placement:(K.Placement.make
-
~source_x:50 ~source_y:50
-
~source_width:100 ~source_height:100
-
~columns:10 ~rows:10
-
())
-
~quiet:`Errors_only
-
())
+
(K.transmit_and_display ~image_id:20 ~format:`Rgba32 ~width:200 ~height:200
+
~placement:
+
(K.Placement.make ~source_x:50 ~source_y:50 ~source_width:100
+
~source_height:100 ~columns:10 ~rows:10 ())
+
~quiet:`Errors_only ())
~data:gradient;
print_endline "Cropped to center 100x100 region of a 200x200 gradient";
print_newline ();
···
(* Demo 7: Multiple placements *)
clear_screen ();
print_endline "Demo 7: Multiple Placements - One image, multiple displays";
-
let cyan_data = solid_color_rgba ~width:80 ~height:80 ~r:0 ~g:255 ~b:255 ~a:255 in
+
let cyan_data =
+
solid_color_rgba ~width:80 ~height:80 ~r:0 ~g:255 ~b:255 ~a:255
+
in
(* Transmit once with an ID *)
send
-
(K.Command.transmit
-
~image_id:100
-
~format:`Rgba32
-
~width:80 ~height:80
-
~quiet:`Errors_only
-
())
+
(K.transmit ~image_id:100 ~format:`Rgba32 ~width:80 ~height:80
+
~quiet:`Errors_only ())
~data:cyan_data;
(* Create first placement *)
send
-
(K.Command.display
-
~image_id:100
+
(K.display ~image_id:100
~placement:(K.Placement.make ~columns:10 ~rows:5 ())
-
~quiet:`Errors_only
-
())
+
~quiet:`Errors_only ())
~data:"";
(* Create second placement *)
send
-
(K.Command.display
-
~image_id:100
+
(K.display ~image_id:100
~placement:(K.Placement.make ~columns:5 ~rows:3 ())
-
~quiet:`Errors_only
-
())
+
~quiet:`Errors_only ())
~data:"";
print_newline ();
wait_for_enter ();
···
let grad_small = gradient_rgba ~width:100 ~height:100 in
(* Transmit once *)
send
-
(K.Command.transmit
-
~image_id:160
-
~format:`Rgba32
-
~width:100 ~height:100
-
~quiet:`Errors_only
-
())
+
(K.transmit ~image_id:160 ~format:`Rgba32 ~width:100 ~height:100
+
~quiet:`Errors_only ())
~data:grad_small;
(* Place same image three times at different sizes *)
send
-
(K.Command.display
-
~image_id:160
+
(K.display ~image_id:160
~placement:(K.Placement.make ~columns:5 ~rows:5 ())
-
~quiet:`Errors_only
-
())
+
~quiet:`Errors_only ())
~data:"";
print_string " ";
send
-
(K.Command.display
-
~image_id:160
+
(K.display ~image_id:160
~placement:(K.Placement.make ~columns:8 ~rows:8 ())
-
~quiet:`Errors_only
-
())
+
~quiet:`Errors_only ())
~data:"";
print_string " ";
send
-
(K.Command.display
-
~image_id:160
+
(K.display ~image_id:160
~placement:(K.Placement.make ~columns:12 ~rows:12 ())
-
~quiet:`Errors_only
-
())
+
~quiet:`Errors_only ())
~data:"";
print_newline ();
print_newline ();
···
(* Demo 9: Z-index layering *)
clear_screen ();
print_endline "Demo 9: Z-Index Layering - Images above/below text";
-
let bg_data = solid_color_rgba ~width:200 ~height:100 ~r:255 ~g:165 ~b:0 ~a:128 in
+
let bg_data =
+
solid_color_rgba ~width:200 ~height:100 ~r:255 ~g:165 ~b:0 ~a:128
+
in
send
-
(K.Command.transmit_and_display
-
~image_id:200
-
~format:`Rgba32
-
~width:200 ~height:100
+
(K.transmit_and_display ~image_id:200 ~format:`Rgba32 ~width:200 ~height:100
~placement:(K.Placement.make ~z_index:(-1) ~cursor:`Static ())
-
~quiet:`Errors_only
-
())
+
~quiet:`Errors_only ())
~data:bg_data;
print_endline "This orange square should appear behind the text!";
print_newline ();
···
print_endline "Demo 11: Animation - Color-changing square";
print_endline "Creating animated sequence with 4 colors...";
-
let width, height = 80, 80 in
+
let width, height = (80, 80) in
let image_id = 300 in
(* Create base frame (red) - transmit without displaying *)
let red_frame = solid_color_rgba ~width ~height ~r:255 ~g:0 ~b:0 ~a:255 in
send
-
(K.Command.transmit
-
~image_id
-
~format:`Rgba32
-
~width ~height
-
~quiet:`Errors_only
-
())
+
(K.transmit ~image_id ~format:`Rgba32 ~width ~height ~quiet:`Errors_only ())
~data:red_frame;
(* Add frames with composition replace *)
-
let orange_frame = solid_color_rgba ~width ~height ~r:255 ~g:165 ~b:0 ~a:255 in
+
let orange_frame =
+
solid_color_rgba ~width ~height ~r:255 ~g:165 ~b:0 ~a:255
+
in
send
-
(K.Command.frame
-
~image_id
-
~format:`Rgba32
-
~width ~height
+
(K.frame ~image_id ~format:`Rgba32 ~width ~height
~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
-
~quiet:`Errors_only
-
())
+
~quiet:`Errors_only ())
~data:orange_frame;
-
let yellow_frame = solid_color_rgba ~width ~height ~r:255 ~g:255 ~b:0 ~a:255 in
+
let yellow_frame =
+
solid_color_rgba ~width ~height ~r:255 ~g:255 ~b:0 ~a:255
+
in
send
-
(K.Command.frame
-
~image_id
-
~format:`Rgba32
-
~width ~height
+
(K.frame ~image_id ~format:`Rgba32 ~width ~height
~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
-
~quiet:`Errors_only
-
())
+
~quiet:`Errors_only ())
~data:yellow_frame;
let green_frame = solid_color_rgba ~width ~height ~r:0 ~g:255 ~b:0 ~a:255 in
send
-
(K.Command.frame
-
~image_id
-
~format:`Rgba32
-
~width ~height
+
(K.frame ~image_id ~format:`Rgba32 ~width ~height
~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
-
~quiet:`Errors_only
-
())
+
~quiet:`Errors_only ())
~data:green_frame;
(* Create placement and start animation *)
send
-
(K.Command.display
-
~image_id
-
~placement:(K.Placement.make
-
~placement_id:1
-
~cell_x_offset:0
-
~cell_y_offset:0
-
~cursor:`Static
-
())
-
~quiet:`Errors_only
-
())
+
(K.display ~image_id
+
~placement:
+
(K.Placement.make ~placement_id:1 ~cell_x_offset:0 ~cell_y_offset:0
+
~cursor:`Static ())
+
~quiet:`Errors_only ())
~data:"";
(* Set root frame gap - root frame has no gap by default per Kitty protocol *)
-
send
-
(K.Command.animate ~image_id (K.Animation.set_gap ~frame:1 ~gap_ms:100))
-
~data:"";
+
send (K.animate ~image_id (K.Animation.set_gap ~frame:1 ~gap_ms:100)) ~data:"";
(* Start animation with infinite looping *)
-
send
-
(K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run))
-
~data:"";
+
send (K.animate ~image_id (K.Animation.set_state ~loops:1 `Run)) ~data:"";
print_newline ();
-
print_endline "Animation playing with colors: Red -> Orange -> Yellow -> Green";
+
print_endline
+
"Animation playing with colors: Red -> Orange -> Yellow -> Green";
print_newline ();
(* Simulate movement by deleting and recreating placement at different positions *)
···
Unix.sleepf 0.4;
(* Delete the current placement *)
-
send
-
(K.Command.delete ~quiet:`Errors_only (`By_id (image_id, Some 1)))
-
~data:"";
+
send (K.delete ~quiet:`Errors_only (`By_id (image_id, Some 1))) ~data:"";
(* Create new placement at next position *)
send
-
(K.Command.display
-
~image_id
-
~placement:(K.Placement.make
-
~placement_id:1
-
~cell_x_offset:(i * 5)
-
~cell_y_offset:0
-
~cursor:`Static
-
())
-
~quiet:`Errors_only
-
())
+
(K.display ~image_id
+
~placement:
+
(K.Placement.make ~placement_id:1 ~cell_x_offset:(i * 5)
+
~cell_y_offset:0 ~cursor:`Static ())
+
~quiet:`Errors_only ())
~data:""
done;
(* Stop the animation *)
-
send
-
(K.Command.animate ~image_id (K.Animation.set_state `Stop))
-
~data:"";
+
send (K.animate ~image_id (K.Animation.set_state `Stop)) ~data:"";
print_endline "Animation stopped.";
print_newline ();
-59
example/test_output.ml
···
-
(* Simple test to show exact escape sequences without data *)
-
-
module K = Kgp
-
-
let print_escaped s =
-
String.iter (fun c ->
-
let code = Char.code c in
-
if code = 27 then print_string "\\x1b"
-
else if code < 32 || code > 126 then Printf.printf "\\x%02x" code
-
else print_char c
-
) s;
-
print_newline ()
-
-
let () =
-
let image_id = 300 in
-
let width, height = 80, 80 in
-
-
print_endline "=== Animation Escape Sequences (no data) ===\n";
-
-
(* 1. Transmit base frame (no data for testing) *)
-
print_endline "1. Transmit (a=t):";
-
let cmd1 = K.Command.transmit
-
~image_id ~format:`Rgba32 ~width ~height ~quiet:`Errors_only () in
-
print_escaped (K.Command.to_string cmd1 ~data:"");
-
-
(* 2. Frame command *)
-
print_endline "\n2. Frame (a=f):";
-
let cmd2 = K.Command.frame
-
~image_id ~format:`Rgba32 ~width ~height
-
~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
-
~quiet:`Errors_only () in
-
print_escaped (K.Command.to_string cmd2 ~data:"");
-
-
(* 3. Put/display command *)
-
print_endline "\n3. Display/Put (a=p):";
-
let cmd3 = K.Command.display
-
~image_id
-
~placement:(K.Placement.make
-
~placement_id:1
-
~cell_x_offset:0
-
~cell_y_offset:0
-
~cursor:`Static ())
-
~quiet:`Errors_only () in
-
print_escaped (K.Command.to_string cmd3 ~data:"");
-
-
(* 4. Set root frame gap - IMPORTANT for animation! *)
-
print_endline "\n4. Set root frame gap (a=a, r=1, z=100):";
-
let cmd4 = K.Command.animate ~image_id (K.Animation.set_gap ~frame:1 ~gap_ms:100) in
-
print_escaped (K.Command.to_string cmd4 ~data:"");
-
-
(* 5. Animate - start *)
-
print_endline "\n5. Animate start (a=a, s=3, v=1):";
-
let cmd5 = K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run) in
-
print_escaped (K.Command.to_string cmd5 ~data:"");
-
-
(* 6. Animate - stop *)
-
print_endline "\n6. Animate stop (a=a, s=1):";
-
let cmd6 = K.Command.animate ~image_id (K.Animation.set_state `Stop) in
-
print_escaped (K.Command.to_string cmd6 ~data:"")
+33 -50
example/tiny_anim.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(* Tiny animation test - no chunking needed *)
(* Uses 20x20 images which are ~1067 bytes base64 (well under 4096) *)
···
Bytes.to_string pixels
let send cmd ~data =
-
print_string (K.Command.to_string cmd ~data);
+
print_string (K.to_string cmd ~data);
flush stdout
let () =
(* Use 20x20 to avoid chunking: 20*20*4 = 1600 bytes, base64 ~2134 bytes *)
-
let width, height = 20, 20 in
+
let width, height = (20, 20) in
let image_id = 999 in
(* Clear any existing images *)
-
send (K.Command.delete ~quiet:`Errors_only (`All_visible_and_free)) ~data:"";
+
send (K.delete ~free:true ~quiet:`Errors_only `All_visible) ~data:"";
-
(* Step 1: Transmit base frame (red) - matching Go's sequence *)
+
(* Step 1: Transmit base frame (red) *)
let red_frame = solid_color_rgba ~width ~height ~r:255 ~g:0 ~b:0 ~a:255 in
send
-
(K.Command.transmit
-
~image_id
-
~format:`Rgba32
-
~width ~height
-
~quiet:`Errors_only
-
())
+
(K.transmit ~image_id ~format:`Rgba32 ~width ~height ~quiet:`Errors_only ())
~data:red_frame;
-
(* Step 2: Add frame (orange) with 100ms gap - like Go *)
-
let orange_frame = solid_color_rgba ~width ~height ~r:255 ~g:165 ~b:0 ~a:255 in
+
(* Step 2: Add frame (orange) with 100ms gap *)
+
let orange_frame =
+
solid_color_rgba ~width ~height ~r:255 ~g:165 ~b:0 ~a:255
+
in
send
-
(K.Command.frame
-
~image_id
-
~format:`Rgba32
-
~width ~height
+
(K.frame ~image_id ~format:`Rgba32 ~width ~height
~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
-
~quiet:`Errors_only
-
())
+
~quiet:`Errors_only ())
~data:orange_frame;
(* Step 3: Add frame (yellow) *)
-
let yellow_frame = solid_color_rgba ~width ~height ~r:255 ~g:255 ~b:0 ~a:255 in
+
let yellow_frame =
+
solid_color_rgba ~width ~height ~r:255 ~g:255 ~b:0 ~a:255
+
in
send
-
(K.Command.frame
-
~image_id
-
~format:`Rgba32
-
~width ~height
+
(K.frame ~image_id ~format:`Rgba32 ~width ~height
~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
-
~quiet:`Errors_only
-
())
+
~quiet:`Errors_only ())
~data:yellow_frame;
(* Step 4: Add frame (green) *)
let green_frame = solid_color_rgba ~width ~height ~r:0 ~g:255 ~b:0 ~a:255 in
send
-
(K.Command.frame
-
~image_id
-
~format:`Rgba32
-
~width ~height
+
(K.frame ~image_id ~format:`Rgba32 ~width ~height
~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
-
~quiet:`Errors_only
-
())
+
~quiet:`Errors_only ())
~data:green_frame;
-
(* Step 5: Create placement - exactly like Go *)
+
(* Step 5: Create placement *)
send
-
(K.Command.display
-
~image_id
-
~placement:(K.Placement.make
-
~placement_id:1
-
~cell_x_offset:0
-
~cell_y_offset:0
-
~cursor:`Static
-
())
-
~quiet:`Errors_only
-
())
+
(K.display ~image_id
+
~placement:
+
(K.Placement.make ~placement_id:1 ~cell_x_offset:0 ~cell_y_offset:0
+
~cursor:`Static ())
+
~quiet:`Errors_only ())
~data:"";
-
(* Step 6: Start animation - exactly like Go (NO root frame gap) *)
-
send
-
(K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run))
-
~data:"";
+
(* Step 6: Start animation *)
+
send (K.animate ~image_id (K.Animation.set_state ~loops:1 `Run)) ~data:"";
print_endline "";
print_endline "Tiny animation (20x20) - Red -> Orange -> Yellow -> Green";
···
let _ = read_line () in
(* Stop animation *)
-
send
-
(K.Command.animate ~image_id (K.Animation.set_state `Stop))
-
~data:"";
+
send (K.animate ~image_id (K.Animation.set_state `Stop)) ~data:"";
(* Clean up *)
-
send (K.Command.delete ~quiet:`Errors_only (`All_visible_and_free)) ~data:"";
+
send (K.delete ~free:true ~quiet:`Errors_only `All_visible) ~data:"";
print_endline "Done."
+32
kgp.opam
···
+
# This file is generated by dune, edit dune-project instead
+
opam-version: "2.0"
+
synopsis: "OCaml implementation of the Kitty terminal graphics protocol"
+
description:
+
"Library for rendering images in terminals that support the Kitty graphics protocol. Supports image transmission, display, animation, Unicode placeholders, and terminal capability detection."
+
maintainer: ["Anil Madhavapeddy <anil@recoil.org>"]
+
authors: ["Anil Madhavapeddy"]
+
license: "ISC"
+
homepage: "https://tangled.org/@anil.recoil.org/ocaml-kgp"
+
bug-reports: "https://tangled.org/@anil.recoil.org/ocaml-kgp/issues"
+
depends: [
+
"dune" {>= "3.20"}
+
"ocaml" {>= "4.14.0"}
+
"cmdliner"
+
"base64"
+
"odoc" {with-doc}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]
+
x-maintenance-intent: ["(latest)"]
+1 -1
lib/dune
···
(library
(name kgp)
(public_name kgp)
-
(libraries base64))
+
(libraries base64 unix))
+10 -18
lib/kgp.ml
···
-
(* Kitty Terminal Graphics Protocol - Main Module *)
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
-
(* Type modules *)
module Format = Kgp_format
module Transmission = Kgp_transmission
module Compression = Kgp_compression
···
module Composition = Kgp_composition
module Delete = Kgp_delete
module Animation_state = Kgp_animation_state
-
-
(* Type aliases *)
-
type format = Format.t
-
type transmission = Transmission.t
-
type compression = Compression.t
-
type quiet = Quiet.t
-
type cursor = Cursor.t
-
type composition = Composition.t
-
type delete = Delete.t
-
type animation_state = Animation_state.t
-
-
(* Configuration modules *)
module Placement = Kgp_placement
module Frame = Kgp_frame
module Animation = Kgp_animation
module Compose = Kgp_compose
-
(* Command type and functions *)
type command = Kgp_command.t
let transmit = Kgp_command.transmit
···
let compose = Kgp_command.compose
let write = Kgp_command.write
let to_string = Kgp_command.to_string
+
let write_tmux = Kgp_command.write_tmux
+
let to_string_tmux = Kgp_command.to_string_tmux
-
(* Core modules *)
-
module Command = Kgp_command
module Response = Kgp_response
-
(* Utility modules *)
+
let next_image_id = Kgp_unicode.next_image_id
+
module Unicode_placeholder = Kgp_unicode
module Detect = Kgp_detect
+
module Tmux = Kgp_tmux
+
module Terminal = Kgp_terminal
+112 -82
lib/kgp.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** 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.).
+
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.).
{1 Protocol Overview}
···
- No requirement for terminal emulators to understand image formats
- Pixel-level positioning of graphics
-
- Integration with text (graphics can be drawn below/above text with alpha blending)
+
- Integration with text (graphics can be drawn below/above text with alpha
+
blending)
- Automatic scrolling with text
- Animation support with frame deltas for efficiency
···
All graphics commands use the Application Programming Command (APC) format:
-
{v <ESC>_G<control data>;<payload><ESC>\ v}
+
{v <ESC>_G<control data>;<payload><ESC> v}
Where:
- [ESC _G] is the APC start sequence (bytes 0x1B 0x5F 0x47)
···
(* Display at different positions *)
let cmd = Kgp.display ~image_id:1 () in
-
Kgp.write buf cmd ~data:"";
+
Kgp.write buf cmd ~data:""
]}
{2 Protocol Reference}
-
See {{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics Protocol}
-
for the full specification. *)
+
See
+
{{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics
+
Protocol} for the full specification. *)
(** {1 Type Modules} *)
···
(** {2 Image Transmission}
-
Images can be transmitted to the terminal for storage and later display.
-
The terminal assigns storage and responds with success or failure.
+
Images can be transmitted to the terminal for storage and later display. The
+
terminal assigns storage and responds with success or failure.
For large images, the library automatically handles chunked transmission
(splitting data into 4096-byte base64-encoded chunks). *)
···
The image is stored by the terminal and can be displayed later using
{!val:display} with the same [image_id].
-
@param image_id Unique identifier (1-4294967295) for later reference.
-
If specified, the terminal responds with success/failure.
-
@param image_number Alternative to [image_id] where the terminal assigns
-
a unique ID and returns it in the response. Useful when multiple
-
programs share the terminal.
+
@param image_id
+
Unique identifier (1-4294967295) for later reference. If specified, the
+
terminal responds with success/failure.
+
@param image_number
+
Alternative to [image_id] where the terminal assigns a unique ID and
+
returns it in the response. Useful when multiple programs share the
+
terminal.
@param format Pixel format of the data. Default is [`Rgba32].
@param transmission How data is sent. Default is [`Direct] (inline).
@param compression Compression applied to data. Default is [`None].
···
command
(** Transmit image data and display it immediately.
-
Combines transmission and display in a single command. The image is
-
rendered at the current cursor position unless placement options
-
specify otherwise.
+
Combines transmission and display in a single command. The image is rendered
+
at the current cursor position unless placement options specify otherwise.
See {!transmit} for parameter descriptions. The [placement] parameter
controls display position and scaling. *)
···
command
(** Query terminal support without storing the image.
-
Performs the same validation as {!transmit} but does not store the
-
image. Useful for testing whether the terminal supports the graphics
-
protocol and specific formats.
+
Performs the same validation as {!transmit} but does not store the image.
+
Useful for testing whether the terminal supports the graphics protocol and
+
specific formats.
To detect graphics support, send a query and check for a response:
{[
···
(** {2 Display}
-
Previously transmitted images can be displayed multiple times at
-
different positions with different cropping and scaling options. *)
+
Previously transmitted images can be displayed multiple times at different
+
positions with different cropping and scaling options. *)
val display :
?image_id:int ->
···
command
(** Display a previously transmitted image.
-
The image is rendered at the current cursor position. Use [placement]
-
to control cropping, scaling, z-index, and other display options.
+
The image is rendered at the current cursor position. Use [placement] to
+
control cropping, scaling, z-index, and other display options.
-
Each display creates a "placement" of the image. Multiple placements
-
of the same image share the underlying image data.
+
Each display creates a "placement" of the image. Multiple placements of the
+
same image share the underlying image data.
@param image_id ID of a previously transmitted image.
-
@param image_number Image number (acts on the newest image with this number).
+
@param image_number
+
Image number (acts on the newest image with this number).
@param placement Display configuration (position, size, z-index, etc.). *)
(** {2 Deletion}
-
Images and placements can be deleted to free terminal resources.
-
Lowercase delete commands remove placements but keep image data;
-
uppercase variants also free the image data. *)
+
Images and placements can be deleted to free terminal resources. By default,
+
only placements are removed and image data is retained for potential reuse.
+
Use [~free:true] to also release the image data. *)
-
val delete : ?quiet:Quiet.t -> Delete.t -> command
+
val delete : ?free:bool -> ?quiet:Quiet.t -> Delete.t -> command
(** Delete images or placements.
See {!Delete} for the full list of deletion targets.
+
@param free
+
If true, also free the image data from memory (default: false). Without
+
[~free:true], only placements are removed and the image data can be reused
+
for new placements.
+
Examples:
{[
-
(* Delete all visible images *)
+
(* Delete all visible images, keep data *)
Kgp.delete `All_visible
-
-
(* Delete specific image, keeping data for reuse *)
-
Kgp.delete (`By_id (42, None))
-
-
(* Delete specific image and free its data *)
-
Kgp.delete (`By_id_and_free (42, None))
-
-
(* Delete all placements at a specific cell *)
-
Kgp.delete (`At_cell (10, 5))
+
(* Delete specific image, keeping data for reuse *)
+
Kgp.delete
+
(`By_id (42, None))
+
(* Delete specific image and free its data *)
+
Kgp.delete ~free:true
+
(`By_id (42, None))
+
(* Delete all placements at a specific cell *)
+
Kgp.delete
+
(`At_cell (10, 5))
]} *)
(** {2 Animation}
···
Animations are created by first transmitting a base image, then adding
frames with optional delta encoding for efficiency.
-
Frame numbers are 1-based: frame 1 is the root (base) image, frame 2
-
is the first added frame, etc. *)
+
Frame numbers are 1-based: frame 1 is the root (base) image, frame 2 is the
+
first added frame, etc. *)
val frame :
?image_id:int ->
···
command
(** Transmit animation frame data.
-
Adds a new frame to an existing image or edits an existing frame.
-
The frame can be a full image or a partial update (rectangle).
+
Adds a new frame to an existing image or edits an existing frame. The frame
+
can be a full image or a partial update (rectangle).
-
Use {!Frame.make} to configure the frame's position, timing, and
-
composition options.
+
Use {!Frame.make} to configure the frame's position, timing, and composition
+
options.
@param frame Frame configuration including timing and composition. *)
val animate :
-
?image_id:int ->
-
?image_number:int ->
-
?quiet:Quiet.t ->
-
Animation.t ->
-
command
+
?image_id:int -> ?image_number:int -> ?quiet:Quiet.t -> Animation.t -> command
(** Control animation playback.
For terminal-driven animation:
{[
(* Start infinite loop animation *)
-
Kgp.animate ~image_id:1 (Animation.set_state ~loops:1 `Run)
-
-
(* Stop animation *)
-
Kgp.animate ~image_id:1 (Animation.set_state `Stop)
-
-
(* Change frame timing *)
-
Kgp.animate ~image_id:1 (Animation.set_gap ~frame:3 ~gap_ms:100)
+
Kgp.animate ~image_id:1
+
(Animation.set_state ~loops:1 `Run)
+
(* Stop animation *)
+
Kgp.animate ~image_id:1
+
(Animation.set_state `Stop)
+
(* Change frame timing *)
+
Kgp.animate ~image_id:1
+
(Animation.set_gap ~frame:3 ~gap_ms:100)
]}
For client-driven animation:
···
]} *)
val compose :
-
?image_id:int ->
-
?image_number:int ->
-
?quiet:Quiet.t ->
-
Compose.t ->
-
command
+
?image_id:int -> ?image_number:int -> ?quiet:Quiet.t -> Compose.t -> command
(** Compose animation frames.
-
Copies a rectangular region from one frame onto another. Useful for
-
building complex frames from simpler components.
+
Copies a rectangular region from one frame onto another. Useful for building
+
complex frames from simpler components.
{[
(* Copy a 50x50 region from frame 2 to frame 5 *)
-
let comp = Compose.make
-
~source_frame:2 ~dest_frame:5
-
~width:50 ~height:50
-
~source_x:10 ~source_y:10
-
~dest_x:20 ~dest_y:20 () in
+
let comp =
+
Compose.make ~source_frame:2 ~dest_frame:5 ~width:50 ~height:50
+
~source_x:10 ~source_y:10 ~dest_x:20 ~dest_y:20 ()
+
in
Kgp.compose ~image_id:1 comp
]} *)
(** {2 Output}
-
Commands are serialized to escape sequences that can be written
-
to the terminal. *)
+
Commands are serialized to escape sequences that can be written to the
+
terminal. *)
val write : Buffer.t -> command -> data:string -> unit
(** Write the command to a buffer.
The [data] parameter contains the raw image/frame data (before base64
-
encoding). Pass an empty string for commands that don't include payload
-
data (like {!val:display}, {!val:delete}, {!val:animate}).
+
encoding). Pass an empty string for commands that don't include payload data
+
(like {!val:display}, {!val:delete}, {!val:animate}).
The library handles base64 encoding and chunking automatically. *)
val to_string : command -> data:string -> string
(** Convert command to a string.
-
Convenience wrapper around {!write} that returns the serialized
-
command as a string. *)
+
Convenience wrapper around {!write} that returns the serialized command as a
+
string. *)
+
+
val write_tmux : Buffer.t -> command -> data:string -> unit
+
(** Write the command to a buffer with tmux passthrough support.
+
+
If running inside tmux (detected via [TMUX] environment variable), wraps the
+
graphics command in a DCS passthrough sequence so it reaches the underlying
+
terminal. Otherwise, behaves like {!write}.
+
+
Requires tmux 3.3+ with [allow-passthrough] enabled. *)
+
+
val to_string_tmux : command -> data:string -> string
+
(** Convert command to a string with tmux passthrough support.
+
+
Convenience wrapper around {!write_tmux}. If running inside tmux, wraps the
+
output for passthrough. Otherwise, behaves like {!to_string}. *)
(** {1 Response} *)
···
(** {1 Utilities} *)
+
val next_image_id : unit -> int
+
(** Generate a unique image ID suitable for use with all graphics commands.
+
+
Returns a random ID with non-zero bytes in all positions, making it
+
compatible with both regular display and Unicode placeholder modes.
+
Uses [Random] internally. *)
+
module Unicode_placeholder = Kgp_unicode
module Detect = Kgp_detect
-
(** {1 Low-level Access} *)
+
module Tmux = Kgp_tmux
+
(** Tmux passthrough support. Provides functions to detect if running inside
+
tmux and to wrap escape sequences for passthrough. *)
-
module Command = Kgp_command
-
(** Low-level command module. The command functions are also available
-
at the top level of this module for convenience. *)
+
module Terminal = Kgp_terminal
+
(** Terminal environment detection. Provides functions to detect terminal
+
capabilities, pager mode, and resolve graphics output mode. *)
+5
lib/kgp_animation.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
type t =
[ `Set_state of Kgp_animation_state.t * int option
| `Set_gap of int * int
+34 -26
lib/kgp_animation.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Animation Control
-
Operations for controlling animation playback. The protocol supports
-
both terminal-driven and client-driven animation modes.
+
Operations for controlling animation playback. The protocol supports both
+
terminal-driven and client-driven animation modes.
{2 Protocol Overview}
···
{[
(* Start infinite loop *)
-
Kgp.animate ~image_id:1 (Animation.set_state ~loops:1 `Run)
-
-
(* Run 3 times then stop *)
-
Kgp.animate ~image_id:1 (Animation.set_state ~loops:4 `Run)
-
-
(* Stop animation *)
-
Kgp.animate ~image_id:1 (Animation.set_state `Stop)
+
Kgp.animate ~image_id:1
+
(Animation.set_state ~loops:1 `Run)
+
(* Run 3 times then stop *)
+
Kgp.animate ~image_id:1
+
(Animation.set_state ~loops:4 `Run)
+
(* Stop animation *)
+
Kgp.animate ~image_id:1
+
(Animation.set_state `Stop)
]}
{2 Client-Driven Animation}
···
{[
(* Slow down frame 3 *)
-
Kgp.animate ~image_id:1 (Animation.set_gap ~frame:3 ~gap_ms:200)
-
-
(* Make frame 5 instant/gapless *)
-
Kgp.animate ~image_id:1 (Animation.set_gap ~frame:5 ~gap_ms:(-1))
+
Kgp.animate ~image_id:1
+
(Animation.set_gap ~frame:3 ~gap_ms:200)
+
(* Make frame 5 instant/gapless *)
+
Kgp.animate ~image_id:1
+
(Animation.set_gap ~frame:5 ~gap_ms:(-1))
]}
{2 Loop Counting}
···
| `Set_current of int ]
(** Animation control operations.
-
- [`Set_state (state, loops)] - Set animation playback state with
-
optional loop count.
+
- [`Set_state (state, loops)] - Set animation playback state with optional
+
loop count.
- [`Set_gap (frame, gap_ms)] - Set the delay for a specific frame.
- [`Set_current frame] - Jump to a specific frame (1-based). *)
val set_state : ?loops:int -> Kgp_animation_state.t -> t
(** Set animation playback state.
-
@param loops Loop count: 0 = ignored, 1 = infinite, n > 1 = (n-1) loops.
-
Protocol key: [v].
+
@param loops
+
Loop count: 0 = ignored, 1 = infinite, n > 1 = (n-1) loops. Protocol key:
+
[v].
@param state The target playback state.
Examples:
{[
-
set_state `Run (* Run with current loop setting *)
-
set_state ~loops:1 `Run (* Run infinitely *)
-
set_state ~loops:3 `Run (* Run twice, then stop *)
-
set_state `Stop (* Pause animation *)
-
set_state `Loading (* Run, wait for more frames at end *)
+
set_state `Run (* Run with current loop setting *) set_state ~loops:1 `Run
+
(* Run infinitely *) set_state ~loops:3 `Run
+
(* Run twice, then stop *) set_state `Stop (* Pause animation *)
+
set_state `Loading (* Run, wait for more frames at end *)
]} *)
val set_gap : frame:int -> gap_ms:int -> t
(** Set the gap (delay) for a specific frame.
@param frame 1-based frame number to modify. Protocol key: [r].
-
@param gap_ms Delay in milliseconds before next frame. Negative values
-
create gapless frames (not displayed, instant skip). Protocol key: [z].
+
@param gap_ms
+
Delay in milliseconds before next frame. Negative values create gapless
+
frames (not displayed, instant skip). Protocol key: [z].
Note: Frame 1 is the root/base image. Use 2+ for added frames. *)
···
@param frame 1-based frame number to display. Protocol key: [c].
-
Used for client-driven animation where the application controls
-
frame advancement rather than the terminal. *)
+
Used for client-driven animation where the application controls frame
+
advancement rather than the terminal. *)
+6 -4
lib/kgp_animation_state.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
type t = [ `Stop | `Loading | `Run ]
-
let to_int : t -> int = function
-
| `Stop -> 1
-
| `Loading -> 2
-
| `Run -> 3
+
let to_int : t -> int = function `Stop -> 1 | `Loading -> 2 | `Run -> 3
+30 -25
lib/kgp_animation_state.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Animation Playback State
Controls the playback state of animated images.
{2 Protocol Details}
-
The animation state is specified via the [s] key in the control data
-
when using action [a=a]:
+
The animation state is specified via the [s] key in the control data when
+
using action [a=a]:
- [s=1]: stop animation
- [s=2]: run in loading mode
- [s=3]: run normally
···
The protocol supports two animation approaches:
-
{b Terminal-driven animation}: The terminal automatically advances
-
frames based on the gap (delay) specified for each frame. Use
-
[{`Run}] or [{`Loading}] states.
+
{b Terminal-driven animation}: The terminal automatically advances frames
+
based on the gap (delay) specified for each frame. Use [{`Run}] or
+
[{`Loading}] states.
-
{b Client-driven animation}: The client manually sets the current
-
frame using [Kgp.Animation.set_current_frame]. Use [{`Stop}] state
-
to prevent automatic advancement.
+
{b Client-driven animation}: The client manually sets the current frame
+
using [Kgp.Animation.set_current_frame]. Use [{`Stop}] state to prevent
+
automatic advancement.
{2 Stop State}
-
[{`Stop}] halts automatic frame advancement. The animation freezes
-
on the current frame. Use this when:
+
[{`Stop}] halts automatic frame advancement. The animation freezes on the
+
current frame. Use this when:
- Implementing client-driven animation
- Pausing an animation
- Displaying a static frame from an animated image
{2 Loading State}
-
[{`Loading}] runs the animation but waits for new frames when reaching
-
the end instead of looping. Use this when:
+
[{`Loading}] runs the animation but waits for new frames when reaching the
+
end instead of looping. Use this when:
- Streaming animation frames progressively
- Building an animation while displaying it
- The animation is not yet complete
{2 Run State}
-
[{`Run}] runs the animation normally, looping back to the first frame
-
after the last. The loop count can be controlled via the [loops]
-
parameter in [Kgp.Animation.set_state]. *)
+
[{`Run}] runs the animation normally, looping back to the first frame after
+
the last. The loop count can be controlled via the [loops] parameter in
+
[Kgp.Animation.set_state]. *)
type t = [ `Stop | `Loading | `Run ]
(** Animation playback states.
-
- [`Stop] - Halt animation playback. The animation freezes on the
-
current frame and does not advance automatically.
-
- [`Loading] - Run animation but wait for new frames at end. When
-
the last frame is reached, the animation pauses until more frames
-
are added, then continues.
-
- [`Run] - Run animation normally and loop. After the last frame,
-
playback returns to the first frame (or stops after the specified
-
number of loops). *)
+
- [`Stop] - Halt animation playback. The animation freezes on the current
+
frame and does not advance automatically.
+
- [`Loading] - Run animation but wait for new frames at end. When the last
+
frame is reached, the animation pauses until more frames are added, then
+
continues.
+
- [`Run] - Run animation normally and loop. After the last frame, playback
+
returns to the first frame (or stops after the specified number of loops).
+
*)
val to_int : t -> int
(** Convert to protocol integer.
-
Returns 1 for [`Stop], 2 for [`Loading], or 3 for [`Run].
-
These values are used in the [s=] control data key. *)
+
Returns 1 for [`Stop], 2 for [`Loading], or 3 for [`Run]. These values are
+
used in the [s=] control data key. *)
+40 -22
lib/kgp_command.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
type action =
[ `Transmit
| `Transmit_and_display
···
image_number : int option;
placement : Kgp_placement.t option;
delete : Kgp_delete.t option;
+
delete_free : bool;
frame : Kgp_frame.t option;
animation : Kgp_animation.t option;
compose : Kgp_compose.t option;
···
image_number = None;
placement = None;
delete = None;
+
delete_free = false;
frame = None;
animation = None;
compose = None;
···
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 delete ?(free = false) ?quiet del =
+
{ (make `Delete) with quiet; delete = Some del; delete_free = free }
let frame ?image_id ?image_number ?format ?transmission ?compression ?width
?height ?quiet ~frame () =
···
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_cursor.to_int c)));
+
kv_int_if w 'C' ~default:0 (Some (Kgp_cursor.to_int c)));
if Kgp_placement.unicode_placeholder p then kv_int w 'U' 1
-
let write_delete w (d : Kgp_delete.t) =
-
kv_char w 'd' (Kgp_delete.to_char d);
+
let write_delete w ~free (d : Kgp_delete.t) =
+
kv_char w 'd' (Kgp_delete.to_char ~free d);
match d with
-
| `By_id (id, pid) | `By_id_and_free (id, pid) ->
+
| `By_id (id, pid) ->
kv_int w 'i' id;
kv_int_opt w 'p' pid
-
| `By_number (n, pid) | `By_number_and_free (n, pid) ->
+
| `By_number (n, pid) ->
kv_int w 'I' n;
kv_int_opt w 'p' pid
-
| `At_cell (x, y) | `At_cell_and_free (x, y) ->
+
| `At_cell (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) ->
+
| `At_cell_z (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) ->
+
| `By_column c -> kv_int w 'x' c
+
| `By_row r -> kv_int w 'y' r
+
| `By_z_index z -> kv_int w 'z' z
+
| `By_id_range (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 ->
-
()
+
| `All_visible | `At_cursor | `Frames -> ()
let write_frame w (f : Kgp_frame.t) =
kv_int_opt w 'x' (Kgp_frame.x 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_composition.to_int c)));
+
kv_int_if w 'X' ~default:0 (Some (Kgp_composition.to_int c)));
kv_int32_opt w 'Y' (Kgp_frame.background_color f)
let write_animation w : Kgp_animation.t -> unit = function
···
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_composition.to_int comp)))
+
kv_int_if w 'C' ~default:0 (Some (Kgp_composition.to_int comp)))
let write_control_data buf cmd =
let w = kv_writer buf in
···
(* Quiet - only if non-default *)
cmd.quiet
|> Option.iter (fun q ->
-
kv_int_if w 'q' ~default:0 (Some (Kgp_quiet.to_int q)));
+
kv_int_if w 'q' ~default:0 (Some (Kgp_quiet.to_int q)));
(* Format *)
-
cmd.format
-
|> Option.iter (fun f -> kv_int w 'f' (Kgp_format.to_int f));
+
cmd.format |> Option.iter (fun f -> kv_int w 'f' (Kgp_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 -> (
···
(* Compression *)
cmd.compression
|> Option.iter (fun c ->
-
Kgp_compression.to_char c |> Option.iter (kv_char w 'o'));
+
Kgp_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;
···
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.delete |> Option.iter (write_delete w ~free:cmd.delete_free);
cmd.frame |> Option.iter (write_frame w);
cmd.animation |> Option.iter (write_animation w);
cmd.compose |> Option.iter (write_compose w);
···
let buf = Buffer.create 1024 in
write buf cmd ~data;
Buffer.contents buf
+
+
let write_tmux buf cmd ~data =
+
if Kgp_tmux.is_active () then begin
+
let inner_buf = Buffer.create 1024 in
+
write inner_buf cmd ~data;
+
Kgp_tmux.write_wrapped buf (Buffer.contents inner_buf)
+
end
+
else write buf cmd ~data
+
+
let to_string_tmux cmd ~data =
+
let buf = Buffer.create 1024 in
+
write_tmux buf cmd ~data;
+
Buffer.contents buf
+26 -7
lib/kgp_command.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Kitty Graphics Protocol Commands
This module provides functions for building and serializing graphics
···
(** {1 Deletion} *)
-
val delete : ?quiet:Kgp_quiet.t -> Kgp_delete.t -> t
-
(** Delete images or placements. *)
+
val delete : ?free:bool -> ?quiet:Kgp_quiet.t -> Kgp_delete.t -> t
+
(** Delete images or placements.
+
+
@param free
+
If true, also free the image data from memory (default: false). Without
+
[~free:true], only placements are removed and the image data can be reused
+
for new placements. *)
(** {1 Animation} *)
···
(** Control animation playback. *)
val compose :
-
?image_id:int ->
-
?image_number:int ->
-
?quiet:Kgp_quiet.t ->
-
Kgp_compose.t ->
-
t
+
?image_id:int -> ?image_number:int -> ?quiet:Kgp_quiet.t -> Kgp_compose.t -> t
(** Compose animation frames. *)
(** {1 Output} *)
···
val to_string : t -> data:string -> string
(** Convert command to a string. *)
+
+
val write_tmux : Buffer.t -> t -> data:string -> unit
+
(** Write the command to a buffer with tmux passthrough wrapping.
+
+
If running inside tmux (detected via [TMUX] environment variable), wraps the
+
graphics command in a DCS passthrough sequence. Otherwise, behaves like
+
{!write}. *)
+
+
val to_string_tmux : t -> data:string -> string
+
(** Convert command to a string with tmux passthrough wrapping.
+
+
If running inside tmux, wraps the output for passthrough. Otherwise, behaves
+
like {!to_string}. *)
+5
lib/kgp_compose.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
type t = {
source_frame : int;
dest_frame : int;
+32 -29
lib/kgp_compose.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Frame Composition
Operations for compositing rectangular regions between animation frames.
···
{2 Protocol Overview}
-
Frame composition uses action [a=c] to copy a rectangular region from
-
one frame onto another. This is useful for:
+
Frame composition uses action [a=c] to copy a rectangular region from one
+
frame onto another. This is useful for:
- Building frames from reusable sprite components
- Applying partial updates to existing frames
···
{2 Coordinate System}
-
All coordinates are in pixels, relative to the top-left corner of
-
the respective frame:
+
All coordinates are in pixels, relative to the top-left corner of the
+
respective frame:
- [source_x], [source_y]: Top-left of rectangle in source frame
- [dest_x], [dest_y]: Top-left of destination in target frame
···
The terminal responds with errors for:
- [ENOENT]: Source or destination frame doesn't exist
-
- [EINVAL]: Rectangle out of bounds, or source equals destination
-
with overlapping regions
+
- [EINVAL]: Rectangle out of bounds, or source equals destination with
+
overlapping regions
- [ENOSPC]: Not enough storage after composition
{2 Example}
{[
(* Copy a 32x32 sprite from frame 2 to frame 5 *)
-
let comp = Compose.make
-
~source_frame:2 ~dest_frame:5
-
~width:32 ~height:32
-
~source_x:0 ~source_y:0 (* From top-left of source *)
-
~dest_x:100 ~dest_y:50 () (* To position in dest *)
+
let comp =
+
Compose.make ~source_frame:2 ~dest_frame:5 ~width:32 ~height:32
+
~source_x:0 ~source_y:0 (* From top-left of source *)
+
~dest_x:100 ~dest_y:50 () (* To position in dest *)
in
Kgp.compose ~image_id:1 comp
]} *)
···
t
(** Create a composition operation.
-
@param source_frame 1-based frame number to copy from. Required.
-
Protocol key: [r].
-
@param dest_frame 1-based frame number to copy onto. Required.
-
Protocol key: [c].
-
@param width Width of rectangle in pixels. Default is full frame.
-
Protocol key: [w].
-
@param height Height of rectangle in pixels. Default is full frame.
-
Protocol key: [h].
-
@param source_x Left edge of source rectangle (default 0).
-
Protocol key: [X].
-
@param source_y Top edge of source rectangle (default 0).
-
Protocol key: [Y].
-
@param dest_x Left edge of destination position (default 0).
-
Protocol key: [x].
-
@param dest_y Top edge of destination position (default 0).
-
Protocol key: [y].
-
@param composition Blending mode. Default is alpha blending.
-
Protocol key: [C]. *)
+
@param source_frame
+
1-based frame number to copy from. Required. Protocol key: [r].
+
@param dest_frame
+
1-based frame number to copy onto. Required. Protocol key: [c].
+
@param width
+
Width of rectangle in pixels. Default is full frame. Protocol key: [w].
+
@param height
+
Height of rectangle in pixels. Default is full frame. Protocol key: [h].
+
@param source_x
+
Left edge of source rectangle (default 0). Protocol key: [X].
+
@param source_y Top edge of source rectangle (default 0). Protocol key: [Y].
+
@param dest_x
+
Left edge of destination position (default 0). Protocol key: [x].
+
@param dest_y
+
Top edge of destination position (default 0). Protocol key: [y].
+
@param composition
+
Blending mode. Default is alpha blending. Protocol key: [C]. *)
(** {1 Field Accessors} *)
+6 -3
lib/kgp_composition.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
type t = [ `Alpha_blend | `Overwrite ]
-
let to_int : t -> int = function
-
| `Alpha_blend -> 0
-
| `Overwrite -> 1
+
let to_int : t -> int = function `Alpha_blend -> 0 | `Overwrite -> 1
+12 -7
lib/kgp_composition.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Pixel Composition Mode
Controls how pixels are blended when compositing images or animation frames.
{2 Protocol Details}
-
The composition mode is specified via the [X] key in the control data
-
(for animation frames) or the [C] key (for frame composition operations):
+
The composition mode is specified via the [X] key in the control data (for
+
animation frames) or the [C] key (for frame composition operations):
- Value 0 or omitted: alpha blending (default)
- Value 1: simple overwrite/replacement
···
- [`Alpha_blend] - Full alpha blending (default). Source pixels are
composited onto the destination using standard Porter-Duff "over"
compositing based on the source alpha channel.
-
- [`Overwrite] - Simple pixel replacement. Source pixels completely
-
replace destination pixels, ignoring alpha values. Faster but no
-
transparency support. *)
+
- [`Overwrite] - Simple pixel replacement. Source pixels completely replace
+
destination pixels, ignoring alpha values. Faster but no transparency
+
support. *)
val to_int : t -> int
(** Convert to protocol integer.
-
Returns 0 for [`Alpha_blend] or 1 for [`Overwrite].
-
These values are used in the [X=] or [C=] control data keys. *)
+
Returns 0 for [`Alpha_blend] or 1 for [`Overwrite]. These values are used in
+
the [X=] or [C=] control data keys. *)
+6 -3
lib/kgp_compression.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
type t = [ `None | `Zlib ]
-
let to_char : t -> char option = function
-
| `None -> None
-
| `Zlib -> Some 'z'
+
let to_char : t -> char option = function `None -> None | `Zlib -> Some 'z'
+13 -8
lib/kgp_compression.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Data Compression
Specifies compression applied to image data before transmission.
···
- No [o] key means no compression
- [o=z] means zlib (RFC 1950 DEFLATE) compression
-
Compression is applied to the raw pixel/PNG data {i before} base64
-
encoding. The terminal decompresses after base64 decoding.
+
Compression is applied to the raw pixel/PNG data {i before} base64 encoding.
+
The terminal decompresses after base64 decoding.
{2 When to Use Compression}
···
{2 PNG with Compression}
When using both [{`Png}] format and [{`Zlib}] compression, the [size]
-
parameter must be specified with the original (uncompressed) PNG size.
-
The terminal needs this to allocate the correct buffer for decompression. *)
+
parameter must be specified with the original (uncompressed) PNG size. The
+
terminal needs this to allocate the correct buffer for decompression. *)
type t = [ `None | `Zlib ]
(** Compression options.
- [`None] - Raw uncompressed data. No [o=] key is sent.
-
- [`Zlib] - RFC 1950 zlib/DEFLATE compression. Data is compressed
-
before base64 encoding and decompressed by the terminal. *)
+
- [`Zlib] - RFC 1950 zlib/DEFLATE compression. Data is compressed before
+
base64 encoding and decompressed by the terminal. *)
val to_char : t -> char option
(** Convert to protocol character.
-
Returns [None] for [`None] (no key sent), or [Some 'z'] for [`Zlib].
-
When [Some c] is returned, [o=c] is added to the control data. *)
+
Returns [None] for [`None] (no key sent), or [Some 'z'] for [`Zlib]. When
+
[Some c] is returned, [o=c] is added to the control data. *)
+6 -3
lib/kgp_cursor.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
type t = [ `Move | `Static ]
-
let to_int : t -> int = function
-
| `Move -> 0
-
| `Static -> 1
+
let to_int : t -> int = function `Move -> 0 | `Static -> 1
+16 -11
lib/kgp_cursor.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Cursor Movement Behavior
Controls cursor position after displaying an image.
···
- Right by the number of columns the image occupies
- Down by the number of rows the image occupies
-
This matches how the cursor moves after printing text, allowing images
-
to flow naturally with text content.
+
This matches how the cursor moves after printing text, allowing images to
+
flow naturally with text content.
{2 Static Cursor}
···
{2 Relative Placements}
-
Note: When using relative placements (positioning images relative to
-
other placements), the cursor never moves regardless of this setting. *)
+
Note: When using relative placements (positioning images relative to other
+
placements), the cursor never moves regardless of this setting. *)
type t = [ `Move | `Static ]
(** Cursor movement behavior.
-
- [`Move] - Advance cursor past the displayed image (default).
-
Cursor moves right by the number of columns and down by the
-
number of rows occupied by the image.
-
- [`Static] - Keep cursor at its original position. The image
-
is displayed but cursor position is unchanged. *)
+
- [`Move] - Advance cursor past the displayed image (default). Cursor moves
+
right by the number of columns and down by the number of rows occupied by
+
the image.
+
- [`Static] - Keep cursor at its original position. The image is displayed
+
but cursor position is unchanged. *)
val to_int : t -> int
(** Convert to protocol integer.
-
Returns 0 for [`Move] or 1 for [`Static].
-
These values are used in the [C=] control data key. *)
+
Returns 0 for [`Move] or 1 for [`Static]. These values are used in the [C=]
+
control data key. *)
+23 -35
lib/kgp_delete.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
type t =
[ `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 ]
+
| `Frames ]
-
let to_char : t -> 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 to_char ~free : t -> char =
+
let base = function
+
| `All_visible -> 'a'
+
| `By_id _ -> 'i'
+
| `By_number _ -> 'n'
+
| `At_cursor -> 'c'
+
| `At_cell _ -> 'p'
+
| `At_cell_z _ -> 'q'
+
| `By_column _ -> 'x'
+
| `By_row _ -> 'y'
+
| `By_z_index _ -> 'z'
+
| `By_id_range _ -> 'r'
+
| `Frames -> 'f'
+
in
+
fun t ->
+
let c = base t in
+
if free then Char.uppercase_ascii c else c
+34 -40
lib/kgp_delete.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Image Deletion Target
Specifies which images or placements to delete.
{2 Protocol Details}
-
Deletion is performed with action [a=d] and the [d] key specifies
-
the deletion type. The [d] key uses single characters:
+
Deletion is performed with action [a=d] and the [d] key specifies the
+
deletion type. The [d] key uses single characters:
{v
| Char | Meaning |
···
{2 Placements vs Image Data}
-
Each deletion type has two variants:
-
- {b Lowercase}: Removes placements only. The image data remains in
-
memory and can be displayed again later.
-
- {b Uppercase}: Removes placements AND frees the image data. The
-
image cannot be displayed again without retransmitting.
-
-
Example: [{`By_id (42, None)}] removes all placements of image 42 but
-
keeps the data. [{`By_id_and_free (42, None)}] removes placements and
-
frees the image data.
+
Each deletion type can optionally free image data (controlled by the [~free]
+
parameter in the delete command):
+
- {b Without free}: Removes placements only. The image data remains in
+
memory and can be displayed again later. (Protocol: lowercase char)
+
- {b With free}: Removes placements AND frees the image data. The image
+
cannot be displayed again without retransmitting. (Protocol: uppercase
+
char)
{2 Placement IDs}
When deleting by image ID or number, an optional placement ID can be
-
specified to delete only a specific placement. If [None], all placements
-
of that image are deleted.
+
specified to delete only a specific placement. If [None], all placements of
+
that image are deleted.
{2 Coordinate-Based Deletion}
-
For [{`At_cell}] and [{`At_cell_z}], coordinates are 0-based cell
-
positions (not pixel positions). Only placements that intersect the
-
specified cell are deleted.
+
For [{`At_cell}] and [{`At_cell_z}], coordinates are 0-based cell positions
+
(not pixel positions). Only placements that intersect the specified cell are
+
deleted.
{2 Virtual Placements}
-
Virtual placements (used for Unicode placeholder mode) are only affected
-
by: [{`By_id}], [{`By_id_and_free}], [{`By_number}], [{`By_number_and_free}],
-
[{`By_id_range}], and [{`By_id_range_and_free}]. Other deletion commands
-
do not affect virtual placements. *)
+
Virtual placements (used for Unicode placeholder mode) are only affected by:
+
[{`By_id}], [{`By_number}], and [{`By_id_range}]. Other deletion commands do
+
not affect virtual placements. *)
type t =
[ `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 ]
+
| `Frames ]
(** Deletion target specification.
{b Screen-based:}
···
- [`By_z_index z] - All placements with z-index z
{b ID-based:}
-
- [`By_id (id, placement_id)] - By image ID. If [placement_id] is
-
[Some p], only that specific placement; if [None], all placements.
-
- [`By_number (n, placement_id)] - By image number (newest image
-
with that number). Placement ID works as above.
+
- [`By_id (id, placement_id)] - By image ID. If [placement_id] is [Some p],
+
only that specific placement; if [None], all placements.
+
- [`By_number (n, placement_id)] - By image number (newest image with that
+
number). Placement ID works as above.
- [`By_id_range (min, max)] - All images with IDs in range [min..max]
{b Animation:}
- [`Frames] - Animation frames only (not the base image)
-
All variants have an [_and_free] version that also releases image data. *)
+
Use the [~free] parameter in the delete command to also release image data
+
from memory. *)
-
val to_char : t -> char
+
val to_char : free:bool -> t -> char
(** Convert to protocol character for the delete command.
-
Returns the character used in the [d=] control data key. Lowercase
-
for placement-only deletion, uppercase for deletion with data free. *)
+
Returns the character used in the [d=] control data key.
+
@param free
+
If true, returns uppercase (frees data); if false, returns lowercase
+
(keeps data). *)
+5
lib/kgp_detect.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(* Kitty Graphics Protocol Detection - Implementation *)
let make_query () =
+5
lib/kgp_detect.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Kitty Graphics Protocol Detection
Detect terminal graphics support capabilities. *)
+6 -4
lib/kgp_format.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
type t = [ `Rgba32 | `Rgb24 | `Png ]
-
let to_int : t -> int = function
-
| `Rgba32 -> 32
-
| `Rgb24 -> 24
-
| `Png -> 100
+
let to_int : t -> int = function `Rgba32 -> 32 | `Rgb24 -> 24 | `Png -> 100
+18 -13
lib/kgp_format.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Image Data Format
Specifies the pixel format of image data being transmitted to the terminal.
···
{2 Raw Pixel Formats}
For [{`Rgb24}] and [{`Rgba32}], the data consists of raw pixel values in
-
row-major order (left-to-right, top-to-bottom). The image dimensions must
-
be specified via the [width] and [height] parameters.
+
row-major order (left-to-right, top-to-bottom). The image dimensions must be
+
specified via the [width] and [height] parameters.
- [{`Rgb24}]: 3 bytes per pixel in sRGB color space (red, green, blue)
- [{`Rgba32}]: 4 bytes per pixel (red, green, blue, alpha)
···
For [{`Png}], the data is a complete PNG image. The terminal extracts
dimensions from PNG metadata, so [width] and [height] are optional.
-
When using both PNG format and zlib compression, you must also specify
-
the [size] parameter with the uncompressed PNG data size. *)
+
When using both PNG format and zlib compression, you must also specify the
+
[size] parameter with the uncompressed PNG data size. *)
type t = [ `Rgba32 | `Rgb24 | `Png ]
(** Image data formats.
-
- [`Rgba32] - 32-bit RGBA (4 bytes per pixel). Default format.
-
Pixels are ordered red, green, blue, alpha. Alpha of 255 is fully
-
opaque, 0 is fully transparent.
-
- [`Rgb24] - 24-bit RGB (3 bytes per pixel). No alpha channel;
-
pixels are fully opaque. More compact than RGBA for opaque images.
+
- [`Rgba32] - 32-bit RGBA (4 bytes per pixel). Default format. Pixels are
+
ordered red, green, blue, alpha. Alpha of 255 is fully opaque, 0 is fully
+
transparent.
+
- [`Rgb24] - 24-bit RGB (3 bytes per pixel). No alpha channel; pixels are
+
fully opaque. More compact than RGBA for opaque images.
- [`Png] - PNG encoded data. The terminal decodes the PNG internally.
-
Supports all PNG color types and bit depths. Most convenient format
-
as dimensions are embedded in the data. *)
+
Supports all PNG color types and bit depths. Most convenient format as
+
dimensions are embedded in the data. *)
val to_int : t -> int
(** Convert to protocol integer value.
-
Returns 24 for [`Rgb24], 32 for [`Rgba32], or 100 for [`Png].
-
These values are used in the [f=] control data key. *)
+
Returns 24 for [`Rgb24], 32 for [`Rgba32], or 100 for [`Png]. These values
+
are used in the [f=] control data key. *)
+7 -1
lib/kgp_frame.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
type t = {
x : int option;
y : int option;
···
background_color = None;
}
-
let make ?x ?y ?base_frame ?edit_frame ?gap_ms ?composition ?background_color () =
+
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
+51 -41
lib/kgp_frame.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Animation Frame Configuration
-
Configuration for adding or editing animation frames. Frames can be
-
full images or partial updates (rectangles), with options for timing
-
and composition.
+
Configuration for adding or editing animation frames. Frames can be full
+
images or partial updates (rectangles), with options for timing and
+
composition.
{2 Protocol Overview}
-
Animations are created by:
-
1. Transmitting a base image (becomes frame 1)
-
2. Adding frames using the frame action ([a=f])
-
3. Controlling playback with animation commands
+
Animations are created by: 1. Transmitting a base image (becomes frame 1) 2.
+
Adding frames using the frame action ([a=f]) 3. Controlling playback with
+
animation commands
Frame numbers are 1-based:
- Frame 1: The original/base image
···
{2 Frame Positioning}
-
For partial frame updates, [x] and [y] specify where the new pixel
-
data is placed within the frame canvas:
+
For partial frame updates, [x] and [y] specify where the new pixel data is
+
placed within the frame canvas:
- [x]: Left edge position in pixels (default 0)
- [y]: Top edge position in pixels (default 0)
-
The frame data dimensions come from the [width] and [height] parameters
-
of the frame command.
+
The frame data dimensions come from the [width] and [height] parameters of
+
the frame command.
{2 Frame Canvas}
Each frame needs a background canvas to composite onto. Options:
-
{b Solid color background} ([background_color]):
-
Use a 32-bit RGBA color. Format: [0xRRGGBBAA] where AA is alpha.
-
Default is 0 (transparent black).
+
{b Solid color background} ([background_color]): Use a 32-bit RGBA color.
+
Format: [0xRRGGBBAA] where AA is alpha. Default is 0 (transparent black).
-
{b Copy from existing frame} ([base_frame]):
-
Use another frame as the starting canvas. Specified as 1-based frame
-
number. The base frame's pixels are copied, then new data is composited.
+
{b Copy from existing frame} ([base_frame]): Use another frame as the
+
starting canvas. Specified as 1-based frame number. The base frame's pixels
+
are copied, then new data is composited.
{2 Editing Existing Frames}
···
{2 Frame Timing}
-
The [gap_ms] parameter controls the delay before transitioning to
-
the next frame:
+
The [gap_ms] parameter controls the delay before transitioning to the next
+
frame:
- Positive value: Delay in milliseconds
- Zero: Ignored (keeps existing gap)
-
- Negative value: "Gapless" frame - not displayed, used as a base
-
for other frames
+
- Negative value: "Gapless" frame - not displayed, used as a base for other
+
frames
-
Default gap for new frames is 40ms. The root frame (frame 1) has
-
a default gap of 0ms.
+
Default gap for new frames is 40ms. The root frame (frame 1) has a default
+
gap of 0ms.
{2 Composition Mode}
-
The [composition] parameter controls how new pixel data is blended
-
onto the canvas. *)
+
The [composition] parameter controls how new pixel data is blended onto the
+
canvas. *)
type t
(** Animation frame configuration. Opaque type; use {!make} to construct. *)
···
t
(** Create a frame specification.
-
@param x Left edge where frame data is placed in pixels (default 0).
-
Protocol key: [x].
-
@param y Top edge where frame data is placed in pixels (default 0).
-
Protocol key: [y].
-
@param base_frame 1-based frame number to use as background canvas.
-
Frame 1 is the root image. Protocol key: [c].
-
@param edit_frame 1-based frame number to edit instead of creating new.
-
If 0 or unset, a new frame is created. Protocol key: [r].
-
@param gap_ms Delay before next frame in milliseconds. Negative values
-
create gapless frames. Protocol key: [z].
-
@param composition How to blend new pixels onto the canvas.
-
Default is alpha blending. Protocol key: [X].
-
@param background_color 32-bit RGBA background color when not using
-
a base frame. Format: [0xRRGGBBAA]. Protocol key: [Y]. *)
+
@param x
+
Left edge where frame data is placed in pixels (default 0). Protocol key:
+
[x].
+
@param y
+
Top edge where frame data is placed in pixels (default 0). Protocol key:
+
[y].
+
@param base_frame
+
1-based frame number to use as background canvas. Frame 1 is the root
+
image. Protocol key: [c].
+
@param edit_frame
+
1-based frame number to edit instead of creating new. If 0 or unset, a new
+
frame is created. Protocol key: [r].
+
@param gap_ms
+
Delay before next frame in milliseconds. Negative values create gapless
+
frames. Protocol key: [z].
+
@param composition
+
How to blend new pixels onto the canvas. Default is alpha blending.
+
Protocol key: [X].
+
@param background_color
+
32-bit RGBA background color when not using a base frame. Format:
+
[0xRRGGBBAA]. Protocol key: [Y]. *)
val empty : t
(** Empty frame spec with all defaults.
-
Creates a new frame with transparent black background, composited
-
at position (0, 0) with default timing (40ms gap). *)
+
Creates a new frame with transparent black background, composited at
+
position (0, 0) with default timing (40ms gap). *)
(** {1 Field Accessors} *)
+5
lib/kgp_placement.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
type t = {
source_x : int option;
source_y : int option;
+49 -36
lib/kgp_placement.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Image Placement Configuration
Configuration for where and how to display images. Placements control
···
- [source_x], [source_y]: Top-left corner in pixels (default: 0, 0)
- [source_width], [source_height]: Size in pixels (default: full image)
-
The displayed area is the intersection of this rectangle with the
-
actual image bounds. This allows cropping images without modifying
-
the original data.
+
The displayed area is the intersection of this rectangle with the actual
+
image bounds. This allows cropping images without modifying the original
+
data.
{2 Cell-Based Sizing}
···
- [columns]: Number of columns to span (width in cells)
- [rows]: Number of rows to span (height in cells)
-
If both are specified, the source rectangle is scaled to fit.
-
If only one is specified, the other is computed to maintain aspect ratio.
-
If neither is specified, the image is displayed at natural size.
+
If both are specified, the source rectangle is scaled to fit. If only one is
+
specified, the other is computed to maintain aspect ratio. If neither is
+
specified, the image is displayed at natural size.
{2 Pixel Offsets}
···
- Positive values: drawn above text
- Zero: drawn at text level
- Negative values: drawn below text
-
- Values < INT32_MIN/2 (-1,073,741,824): drawn under cells with
-
non-default background colors
+
- Values < INT32_MIN/2 (-1,073,741,824): drawn under cells with non-default
+
background colors
-
Overlapping images with the same z-index are ordered by image ID
-
(lower ID draws first/underneath).
+
Overlapping images with the same z-index are ordered by image ID (lower ID
+
draws first/underneath).
{2 Placement IDs}
···
- Deleting specific placements
- Moving placements by resending with same image_id + placement_id
-
If [placement_id] is 0 or unspecified, each display creates an
-
independent placement. *)
+
If [placement_id] is 0 or unspecified, each display creates an independent
+
placement. *)
type t
(** Placement configuration. Opaque type; use {!make} to construct. *)
···
t
(** Create a placement configuration.
-
@param source_x Left edge of source rectangle in pixels (default 0).
-
Protocol key: [x].
-
@param source_y Top edge of source rectangle in pixels (default 0).
-
Protocol key: [y].
-
@param source_width Width of source rectangle in pixels.
-
Default is the full image width. Protocol key: [w].
-
@param source_height Height of source rectangle in pixels.
-
Default is the full image height. Protocol key: [h].
-
@param cell_x_offset X offset within the first cell in pixels.
-
Must be smaller than cell width. Protocol key: [X].
-
@param cell_y_offset Y offset within the first cell in pixels.
-
Must be smaller than cell height. Protocol key: [Y].
-
@param columns Number of columns to display over. Image is scaled
-
to fit. Protocol key: [c].
-
@param rows Number of rows to display over. Image is scaled to fit.
-
Protocol key: [r].
-
@param z_index Stacking order. Positive = above text, negative = below.
-
Protocol key: [z].
-
@param placement_id Unique ID (1-4294967295) for this placement.
-
Allows updating/deleting specific placements. Protocol key: [p].
+
@param source_x
+
Left edge of source rectangle in pixels (default 0). Protocol key: [x].
+
@param source_y
+
Top edge of source rectangle in pixels (default 0). Protocol key: [y].
+
@param source_width
+
Width of source rectangle in pixels. Default is the full image width.
+
Protocol key: [w].
+
@param source_height
+
Height of source rectangle in pixels. Default is the full image height.
+
Protocol key: [h].
+
@param cell_x_offset
+
X offset within the first cell in pixels. Must be smaller than cell width.
+
Protocol key: [X].
+
@param cell_y_offset
+
Y offset within the first cell in pixels. Must be smaller than cell
+
height. Protocol key: [Y].
+
@param columns
+
Number of columns to display over. Image is scaled to fit. Protocol key:
+
[c].
+
@param rows
+
Number of rows to display over. Image is scaled to fit. Protocol key: [r].
+
@param z_index
+
Stacking order. Positive = above text, negative = below. Protocol key:
+
[z].
+
@param placement_id
+
Unique ID (1-4294967295) for this placement. Allows updating/deleting
+
specific placements. Protocol key: [p].
@param cursor Cursor movement policy after display.
-
@param unicode_placeholder If true, creates a virtual (invisible)
-
placement for Unicode placeholder mode. Protocol key: [U=1]. *)
+
@param unicode_placeholder
+
If true, creates a virtual (invisible) placement for Unicode placeholder
+
mode. Protocol key: [U=1]. *)
val empty : t
(** Empty placement with all defaults.
-
Equivalent to [make ()]. The image displays at natural size at the
-
current cursor position with default z-index (0). *)
+
Equivalent to [make ()]. The image displays at natural size at the current
+
cursor position with default z-index (0). *)
(** {1 Field Accessors} *)
+5
lib/kgp_quiet.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
type t = [ `Noisy | `Errors_only | `Silent ]
let to_int : t -> int = function
+19 -14
lib/kgp_quiet.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Response Suppression Level
Controls which terminal responses are sent back to the application.
···
- On success: [ESC _Gi=ID;OK ESC]
- On failure: [ESC _Gi=ID;ECODE:message ESC]
-
Response processing requires reading from the terminal, which can be
-
complex in some applications.
+
Response processing requires reading from the terminal, which can be complex
+
in some applications.
{2 Use Cases}
-
[{`Noisy}] (default): Use when you need to verify operations succeeded
-
or want to handle errors programmatically.
+
[{`Noisy}] (default): Use when you need to verify operations succeeded or
+
want to handle errors programmatically.
[{`Errors_only}]: Use when you want to detect failures but don't need
confirmation of success. Reduces response traffic.
-
[{`Silent}]: Use in fire-and-forget scenarios like shell scripts or
-
when the application cannot easily read terminal responses. Also useful
-
for high-frequency animation updates where response processing would
-
add latency. *)
+
[{`Silent}]: Use in fire-and-forget scenarios like shell scripts or when the
+
application cannot easily read terminal responses. Also useful for
+
high-frequency animation updates where response processing would add
+
latency. *)
type t = [ `Noisy | `Errors_only | `Silent ]
(** Response suppression levels.
- [`Noisy] - Send all responses including OK confirmations (default).
Required for detecting success and getting assigned image IDs.
-
- [`Errors_only] - Suppress OK responses, only send error messages.
-
Useful when success is expected but errors should be caught.
-
- [`Silent] - Suppress all responses including errors. Useful for
-
shell scripts or when response handling is not possible. *)
+
- [`Errors_only] - Suppress OK responses, only send error messages. Useful
+
when success is expected but errors should be caught.
+
- [`Silent] - Suppress all responses including errors. Useful for shell
+
scripts or when response handling is not possible. *)
val to_int : t -> int
(** Convert to protocol integer.
-
Returns 0 for [`Noisy], 1 for [`Errors_only], or 2 for [`Silent].
-
These values are used in the [q=] control data key. *)
+
Returns 0 for [`Noisy], 1 for [`Errors_only], or 2 for [`Silent]. These
+
values are used in the [q=] control data key. *)
+6 -1
lib/kgp_response.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(* Kitty Graphics Protocol Response - Implementation *)
type t = {
···
else
String.index_opt t.message ':'
|> Option.fold ~none:(Some t.message) ~some:(fun i ->
-
Some (String.sub t.message 0 i))
+
Some (String.sub t.message 0 i))
let image_id t = t.image_id
let image_number t = t.image_number
+5
lib/kgp_response.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Kitty Graphics Protocol Response
Parse and interpret terminal responses to graphics commands. *)
+60
lib/kgp_terminal.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(* Terminal Environment Detection *)
+
+
type graphics_mode = [ `Auto | `Enabled | `Disabled | `Tmux ]
+
+
let is_kitty () =
+
Option.is_some (Sys.getenv_opt "KITTY_WINDOW_ID")
+
|| (match Sys.getenv_opt "TERM" with
+
| Some term -> String.lowercase_ascii term = "xterm-kitty"
+
| None -> false)
+
||
+
match Sys.getenv_opt "TERM_PROGRAM" with
+
| Some prog -> String.lowercase_ascii prog = "kitty"
+
| None -> false
+
+
let is_wezterm () =
+
Option.is_some (Sys.getenv_opt "WEZTERM_PANE")
+
||
+
match Sys.getenv_opt "TERM_PROGRAM" with
+
| Some prog -> String.lowercase_ascii prog = "wezterm"
+
| None -> false
+
+
let is_ghostty () =
+
Option.is_some (Sys.getenv_opt "GHOSTTY_RESOURCES_DIR")
+
||
+
match Sys.getenv_opt "TERM_PROGRAM" with
+
| Some prog -> String.lowercase_ascii prog = "ghostty"
+
| None -> false
+
+
let is_graphics_terminal () = is_kitty () || is_wezterm () || is_ghostty ()
+
let is_tmux () = Kgp_tmux.is_active ()
+
let is_interactive () = Unix.isatty Unix.stdout
+
+
let is_pager () =
+
(* Not interactive = likely piped to pager *)
+
(not (is_interactive ()))
+
||
+
(* PAGER set and not in a known graphics terminal *)
+
(Option.is_some (Sys.getenv_opt "PAGER") && not (is_graphics_terminal ()))
+
+
let resolve_mode = function
+
| `Disabled -> `Placeholder
+
| `Enabled -> `Graphics
+
| `Tmux -> `Tmux
+
| `Auto ->
+
if is_pager () || not (is_interactive ()) then `Placeholder
+
else if is_tmux () then
+
(* Inside tmux - use passthrough if underlying terminal supports graphics *)
+
if is_graphics_terminal () then `Tmux else `Placeholder
+
else if is_graphics_terminal () then `Graphics
+
else `Placeholder
+
+
let supports_graphics mode =
+
match resolve_mode mode with
+
| `Graphics | `Tmux -> true
+
| `Placeholder -> false
+85
lib/kgp_terminal.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Terminal Environment Detection
+
+
Detect terminal capabilities and environment for graphics protocol support.
+
+
{2 Supported Terminals}
+
+
The following terminals support the Kitty Graphics Protocol:
+
- Kitty (the original implementation)
+
- WezTerm
+
- Ghostty
+
- Konsole (partial support)
+
+
{2 Environment Detection}
+
+
Detection is based on environment variables:
+
- [KITTY_WINDOW_ID] - set by Kitty
+
- [WEZTERM_PANE] - set by WezTerm
+
- [GHOSTTY_RESOURCES_DIR] - set by Ghostty
+
- [TERM_PROGRAM] - may contain terminal name
+
- [TERM] - may contain "kitty"
+
- [TMUX] - set when inside tmux
+
- [PAGER] / output to non-tty - indicates pager mode *)
+
+
(** {1 Graphics Mode} *)
+
+
type graphics_mode = [ `Auto | `Enabled | `Disabled | `Tmux ]
+
(** Graphics output mode.
+
+
- [`Auto] - Auto-detect based on environment
+
- [`Enabled] - Force graphics enabled
+
- [`Disabled] - Force graphics disabled (use placeholders)
+
- [`Tmux] - Force tmux passthrough mode *)
+
+
(** {1 Detection} *)
+
+
val is_kitty : unit -> bool
+
(** Detect if running in Kitty terminal. *)
+
+
val is_wezterm : unit -> bool
+
(** Detect if running in WezTerm terminal. *)
+
+
val is_ghostty : unit -> bool
+
(** Detect if running in Ghostty terminal. *)
+
+
val is_graphics_terminal : unit -> bool
+
(** Detect if running in any terminal that supports the graphics protocol. *)
+
+
val is_tmux : unit -> bool
+
(** Detect if running inside tmux. *)
+
+
val is_pager : unit -> bool
+
(** Detect if output is likely going to a pager.
+
+
Returns [true] if:
+
- stdout is not a tty, or
+
- [PAGER] environment variable is set and we're not in a known
+
graphics-capable terminal *)
+
+
val is_interactive : unit -> bool
+
(** Detect if running interactively (stdout is a tty). *)
+
+
(** {1 Mode Resolution} *)
+
+
val resolve_mode : graphics_mode -> [ `Graphics | `Tmux | `Placeholder ]
+
(** Resolve a graphics mode to the actual output method.
+
+
- [`Graphics] - use direct graphics protocol
+
- [`Tmux] - use graphics protocol with tmux passthrough
+
- [`Placeholder] - use text placeholders (block characters)
+
+
For [Auto] mode:
+
- If in a pager or non-interactive: [`Placeholder]
+
- If in tmux with graphics terminal: [`Tmux]
+
- If in graphics terminal: [`Graphics]
+
- Otherwise: [`Placeholder] *)
+
+
val supports_graphics : graphics_mode -> bool
+
(** Check if the resolved mode supports graphics output.
+
+
Returns [true] for [`Graphics] and [`Tmux], [false] for [`Placeholder]. *)
+27
lib/kgp_tmux.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(* Tmux Passthrough Support - Implementation *)
+
+
let is_active () = Option.is_some (Sys.getenv_opt "TMUX")
+
+
let write_wrapped buf s =
+
(* DCS passthrough prefix: ESC P tmux ; *)
+
Buffer.add_string buf "\027Ptmux;";
+
(* Double all ESC characters in the content *)
+
String.iter
+
(fun c ->
+
if c = '\027' then Buffer.add_string buf "\027\027"
+
else Buffer.add_char buf c)
+
s;
+
(* DCS terminator: ESC \ *)
+
Buffer.add_string buf "\027\\"
+
+
let wrap_always s =
+
let buf = Buffer.create ((String.length s * 2) + 10) in
+
write_wrapped buf s;
+
Buffer.contents buf
+
+
let wrap s = if is_active () then wrap_always s else s
+69
lib/kgp_tmux.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Tmux Passthrough Support
+
+
Support for passing graphics protocol escape sequences through tmux to the
+
underlying terminal emulator.
+
+
{2 Background}
+
+
When running inside tmux, graphics protocol escape sequences need to be
+
wrapped in a DCS (Device Control String) passthrough sequence so that tmux
+
forwards them to the actual terminal (kitty, wezterm, ghostty, etc.) rather
+
than interpreting them itself.
+
+
The passthrough format is:
+
- Prefix: [ESC P tmux ;]
+
- Content with all ESC characters doubled
+
- Suffix: [ESC]
+
+
{2 Requirements}
+
+
For tmux passthrough to work:
+
{ul
+
{- tmux version 3.3 or later }
+
{- [allow-passthrough] must be enabled in tmux.conf:
+
{v set -g allow-passthrough on v}
+
}
+
}
+
+
{2 Usage}
+
+
{[
+
if Kgp.Tmux.is_active () then
+
let wrapped = Kgp.Tmux.wrap graphics_command in
+
print_string wrapped
+
else print_string graphics_command
+
]} *)
+
+
val is_active : unit -> bool
+
(** Detect if we are running inside tmux.
+
+
Returns [true] if the [TMUX] environment variable is set, indicating the
+
process is running inside a tmux session. *)
+
+
val wrap : string -> string
+
(** Wrap an escape sequence for tmux passthrough.
+
+
Takes a graphics protocol escape sequence and wraps it in the tmux DCS
+
passthrough format:
+
- Adds [ESC P tmux ;] prefix
+
- Doubles all ESC characters in the content
+
- Adds [ESC] suffix
+
+
If not running inside tmux, returns the input unchanged. *)
+
+
val wrap_always : string -> string
+
(** Wrap an escape sequence for tmux passthrough unconditionally.
+
+
Like {!wrap} but always applies the wrapping, regardless of whether we are
+
inside tmux. Useful when you want to pre-generate tmux-compatible output. *)
+
+
val write_wrapped : Buffer.t -> string -> unit
+
(** Write a wrapped escape sequence directly to a buffer.
+
+
More efficient than {!wrap_always} when building output in a buffer, as it
+
avoids allocating an intermediate string. *)
+5
lib/kgp_transmission.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
type t = [ `Direct | `File | `Tempfile ]
let to_char : t -> char = function
+16 -11
lib/kgp_transmission.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Data Transmission Method
Specifies how image data is transmitted to the terminal.
···
{2 Direct Transmission}
-
[{`Direct}] sends data inline within the escape sequence itself. The data
-
is base64-encoded in the payload section. This is the simplest method and
-
works over any connection (including SSH).
+
[{`Direct}] sends data inline within the escape sequence itself. The data is
+
base64-encoded in the payload section. This is the simplest method and works
+
over any connection (including SSH).
For images larger than 4096 bytes (after base64 encoding), the data is
automatically split into chunks using the [m=] key:
···
{2 File Transmission}
-
[{`File}] tells the terminal to read data from a file path. The path is
-
sent base64-encoded in the payload. Additional parameters:
+
[{`File}] tells the terminal to read data from a file path. The path is sent
+
base64-encoded in the payload. Additional parameters:
- [S=] specifies the number of bytes to read
- [O=] specifies the byte offset to start reading from
-
File transmission only works when the terminal and client share a
-
filesystem (i.e., local terminals, not SSH).
+
File transmission only works when the terminal and client share a filesystem
+
(i.e., local terminals, not SSH).
-
Security: The terminal will refuse to read device files, sockets, or
-
files in sensitive locations like [/proc], [/sys], or [/dev].
+
Security: The terminal will refuse to read device files, sockets, or files
+
in sensitive locations like [/proc], [/sys], or [/dev].
{2 Temporary File Transmission}
···
- [`Direct] - Data is sent inline in the escape sequence (base64-encoded).
Works over any connection including SSH. Automatic chunking for large
images.
-
- [`File] - Terminal reads from a file path sent in the payload.
-
Only works when terminal and client share a filesystem.
+
- [`File] - Terminal reads from a file path sent in the payload. Only works
+
when terminal and client share a filesystem.
- [`Tempfile] - Like [`File] but terminal deletes the file after reading.
File must be in a recognized temporary directory. *)
+284 -44
lib/kgp_unicode.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(* 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;
+
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 next_image_id () =
+
let rec gen () =
+
let id = Random.int32 Int32.max_int |> Int32.to_int in
+
(* Ensure high byte and middle bytes are non-zero *)
+
if id land 0xFF000000 = 0 || id land 0x00FFFF00 = 0 then gen () else id
+
in
+
gen ()
let add_uchar buf u =
let code = Uchar.to_int u in
···
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"
+
(* Set foreground color using colon subparameter format *)
+
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 *)
+
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 - always written, even when 0 *)
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
+
let id_diac = id_high_byte_diacritic high_byte 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)
+
add_uchar buf id_diac
done;
if row < rows - 1 then Buffer.add_string buf "\n\r"
done;
+34 -3
lib/kgp_unicode.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
(** Kitty Graphics Protocol Unicode Placeholders
-
Support for invisible Unicode placeholder characters that encode
-
image position metadata for accessibility and compatibility. *)
+
Support for invisible Unicode placeholder characters that encode image
+
position metadata for accessibility and compatibility.
+
+
{2 Image ID Requirements}
+
+
When using unicode placeholders, image IDs must have non-zero bytes in
+
specific positions for correct rendering:
+
- High byte (bits 24-31): encoded as the third combining diacritic
+
- Middle bytes (bits 8-23): encoded in the foreground RGB color
+
+
Use {!next_image_id} to generate IDs that satisfy these requirements. *)
val placeholder_char : Uchar.t
(** The Unicode placeholder character U+10EEEE. *)
+
val next_image_id : unit -> int
+
(** Generate a random image ID suitable for unicode placeholders.
+
+
The returned ID has non-zero bytes in all required positions:
+
- High byte (bits 24-31) is non-zero
+
- Middle bytes (bits 8-23) are non-zero
+
+
This ensures the foreground color encoding and diacritic encoding work
+
correctly. Uses [Random] internally. *)
+
val write :
Buffer.t ->
image_id:int ->
···
cols:int ->
unit ->
unit
-
(** Write placeholder characters to a buffer. *)
+
(** Write placeholder characters to a buffer.
+
+
@param image_id
+
Should be generated with {!next_image_id} for correct rendering.
+
@param placement_id
+
Optional placement ID for multiple placements of same image.
+
@param rows Number of rows in the placeholder grid.
+
@param cols Number of columns in the placeholder grid. *)
val row_diacritic : int -> Uchar.t
(** Get the combining diacritic for a row number (0-based). *)
+4
lib-cli/dune
···
+
(library
+
(name kgp_cli)
+
(public_name kgp.cli)
+
(libraries kgp cmdliner))
+29
lib-cli/kgp_cli.ml
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(* Cmdliner Support for Kitty Graphics Protocol *)
+
+
open Cmdliner
+
+
let graphics_docs = "GRAPHICS OPTIONS"
+
+
let graphics_term =
+
let doc = "Force graphics output enabled, ignoring terminal detection." in
+
let enable = Arg.info [ "g"; "graphics" ] ~doc ~docs:graphics_docs in
+
let doc = "Disable graphics output, use text placeholders instead." in
+
let disable = Arg.info [ "no-graphics" ] ~doc ~docs:graphics_docs in
+
let doc = "Force tmux passthrough mode for graphics." in
+
let tmux = Arg.info [ "tmux" ] ~doc ~docs:graphics_docs in
+
let choose enable disable tmux : Kgp.Terminal.graphics_mode =
+
if enable then `Enabled
+
else if disable then `Disabled
+
else if tmux then `Tmux
+
else `Auto
+
in
+
Term.(
+
const choose
+
$ Arg.(value & flag enable)
+
$ Arg.(value & flag disable)
+
$ Arg.(value & flag tmux))
+52
lib-cli/kgp_cli.mli
···
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Cmdliner Support for Kitty Graphics Protocol
+
+
This module provides Cmdliner terms for configuring graphics output mode in
+
CLI applications. It allows users to override auto-detection with
+
command-line flags.
+
+
{2 Usage}
+
+
Add the graphics term to your command:
+
+
{[
+
let cmd =
+
let graphics = Kgp_cli.graphics_term in
+
let my_args = ... in
+
Cmd.v info Term.(const my_run $ graphics $ my_args)
+
]}
+
+
Then use the resolved mode in your application:
+
+
{[
+
let my_run graphics_mode args =
+
if Kgp.Terminal.supports_graphics graphics_mode then
+
(* render with graphics *)
+
else
+
(* use text fallback *)
+
]} *)
+
+
(** {1 Terms} *)
+
+
val graphics_term : Kgp.Terminal.graphics_mode Cmdliner.Term.t
+
(** Cmdliner term for graphics mode selection.
+
+
Provides the following command-line options:
+
+
- [--graphics] / [-g]: Force graphics output enabled
+
- [--no-graphics]: Force graphics output disabled (use placeholders)
+
- [--tmux]: Force tmux passthrough mode
+
- (default): Auto-detect based on terminal environment
+
+
The term evaluates to a {!Kgp.Terminal.graphics_mode} value which can be
+
passed to {!Kgp.Terminal.supports_graphics} or {!Kgp.Terminal.resolve_mode}.
+
*)
+
+
val graphics_docs : string
+
(** Section name for graphics options in help output ("GRAPHICS OPTIONS").
+
+
Use this when grouping graphics options in a separate help section. *)