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))
+23 -7
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
-
-
(* Configuration modules *)
module Placement = Kgp_placement
module Frame = Kgp_frame
module Animation = Kgp_animation
module Compose = Kgp_compose
-
(* Core modules *)
-
module Command = Kgp_command
+
type command = Kgp_command.t
+
+
let transmit = Kgp_command.transmit
+
let transmit_and_display = Kgp_command.transmit_and_display
+
let query = Kgp_command.query
+
let display = Kgp_command.display
+
let delete = Kgp_command.delete
+
let frame = Kgp_command.frame
+
let animate = Kgp_command.animate
+
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
+
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
+337 -13
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.).
-
The protocol uses APC (Application Programming Command) escape sequences
-
to transmit and display pixel graphics. Images can be transmitted as raw
-
RGB/RGBA data or PNG, and displayed at specific positions with various
-
placement options.
+
{1 Protocol Overview}
+
+
The Kitty Graphics Protocol is a flexible, performant protocol for rendering
+
arbitrary pixel (raster) graphics in terminal emulators. Key features:
+
+
- 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)
+
- Automatic scrolling with text
+
- Animation support with frame deltas for efficiency
+
+
{2 Escape Sequence Format}
+
+
All graphics commands use the Application Programming Command (APC) format:
+
+
{v <ESC>_G<control data>;<payload><ESC> v}
+
+
Where:
+
- [ESC _G] is the APC start sequence (bytes 0x1B 0x5F 0x47)
+
- Control data is comma-separated key=value pairs
+
- Payload is base64-encoded binary data (RFC-4648)
+
- [ESC] is the APC terminator (bytes 0x1B 0x5C)
+
+
Most terminal emulators ignore unrecognized APC sequences, making the
+
protocol safe to use even in unsupported terminals.
+
+
{2 Terminal Responses}
+
+
When an image ID is specified, the terminal responds:
+
- On success: [ESC _Gi=ID;OK ESC]
+
- On failure: [ESC _Gi=ID;error ESC]
+
+
Common error codes include [ENOENT] (image not found), [EINVAL] (invalid
+
parameter), and [ENOSPC] (storage quota exceeded).
+
+
{2 Image Storage}
+
+
Terminal emulators maintain a storage quota for images (typically ~320MB).
+
When the quota is exceeded, older images are deleted to make room for new
+
ones. Images without active placements are preferred for deletion.
+
+
For animations, frame data is stored separately with a larger quota
+
(typically 5x the base quota).
{2 Basic Usage}
{[
(* Display a PNG image *)
let png_data = read_file "image.png" in
-
let cmd = Kgp.Command.transmit_and_display ~format:`Png () in
+
let cmd = Kgp.transmit_and_display ~format:`Png () in
let buf = Buffer.create 1024 in
-
Kgp.Command.write buf cmd ~data:png_data;
+
Kgp.write buf cmd ~data:png_data;
print_string (Buffer.contents buf)
]}
+
{[
+
(* Transmit an image, then display it multiple times *)
+
let png_data = read_file "icon.png" in
+
let cmd = Kgp.transmit ~image_id:1 ~format:`Png () in
+
Kgp.write buf cmd ~data:png_data;
+
+
(* Display at different positions *)
+
let cmd = Kgp.display ~image_id:1 () in
+
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} *)
···
module Animation = Kgp_animation
module Compose = Kgp_compose
-
(** {1 Command and Response} *)
+
(** {1 Commands} *)
+
+
type command = Kgp_command.t
+
(** A graphics protocol command. Commands are built using the functions below
+
and then serialized using {!write} or {!to_string}. *)
+
+
(** {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.
+
+
For large images, the library automatically handles chunked transmission
+
(splitting data into 4096-byte base64-encoded chunks). *)
+
+
val transmit :
+
?image_id:int ->
+
?image_number:int ->
+
?format:Format.t ->
+
?transmission:Transmission.t ->
+
?compression:Compression.t ->
+
?width:int ->
+
?height:int ->
+
?size:int ->
+
?offset:int ->
+
?quiet:Quiet.t ->
+
unit ->
+
command
+
(** Transmit image data without displaying.
+
+
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 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].
+
@param width Image width in pixels (required for raw RGB/RGBA formats).
+
@param height Image height in pixels (required for raw RGB/RGBA formats).
+
@param size Size in bytes when reading from file.
+
@param offset Byte offset when reading from file.
+
@param quiet Response suppression level. *)
+
+
val transmit_and_display :
+
?image_id:int ->
+
?image_number:int ->
+
?format:Format.t ->
+
?transmission:Transmission.t ->
+
?compression:Compression.t ->
+
?width:int ->
+
?height:int ->
+
?size:int ->
+
?offset:int ->
+
?quiet:Quiet.t ->
+
?placement:Placement.t ->
+
unit ->
+
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.
+
+
See {!transmit} for parameter descriptions. The [placement] parameter
+
controls display position and scaling. *)
+
+
val query :
+
?format:Format.t ->
+
?transmission:Transmission.t ->
+
?width:int ->
+
?height:int ->
+
?quiet:Quiet.t ->
+
unit ->
+
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.
+
+
To detect graphics support, send a query and check for a response:
+
{[
+
(* Send query with a tiny 1x1 RGB image *)
+
let cmd = Kgp.query ~format:`Rgb24 ~width:1 ~height:1 () in
+
Kgp.write buf cmd ~data:"\x00\x00\x00"
+
(* If terminal responds, it supports the protocol *)
+
]} *)
+
+
(** {2 Display}
-
module Command = Kgp_command
+
Previously transmitted images can be displayed multiple times at different
+
positions with different cropping and scaling options. *)
+
+
val display :
+
?image_id:int ->
+
?image_number:int ->
+
?placement:Placement.t ->
+
?quiet:Quiet.t ->
+
unit ->
+
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.
+
+
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 placement Display configuration (position, size, z-index, etc.). *)
+
+
(** {2 Deletion}
+
+
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 : ?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, 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 ~free:true
+
(`By_id (42, None))
+
(* Delete all placements at a specific cell *)
+
Kgp.delete
+
(`At_cell (10, 5))
+
]} *)
+
+
(** {2 Animation}
+
+
The protocol supports both client-driven and terminal-driven animations.
+
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. *)
+
+
val frame :
+
?image_id:int ->
+
?image_number:int ->
+
?format:Format.t ->
+
?transmission:Transmission.t ->
+
?compression:Compression.t ->
+
?width:int ->
+
?height:int ->
+
?quiet:Quiet.t ->
+
frame:Frame.t ->
+
unit ->
+
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).
+
+
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
+
(** 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)
+
]}
+
+
For client-driven animation:
+
{[
+
(* Manually advance to specific frame *)
+
Kgp.animate ~image_id:1 (Animation.set_current_frame 5)
+
]} *)
+
+
val compose :
+
?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.
+
+
{[
+
(* 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
+
Kgp.compose ~image_id:1 comp
+
]} *)
+
+
(** {2 Output}
+
+
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}).
+
+
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. *)
+
+
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} *)
+
module Response = Kgp_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
+
+
module Tmux = Kgp_tmux
+
(** Tmux passthrough support. Provides functions to detect if running inside
+
tmux and to wrap escape sequences for passthrough. *)
+
+
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
+100 -8
lib/kgp_animation.mli
···
-
(** Kitty Graphics Protocol Animation
+
(*---------------------------------------------------------------------------
+
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.
+
+
{2 Protocol Overview}
+
+
Animation control uses action [a=a] with various keys:
+
- [s]: Set playback state (1=stop, 2=loading, 3=run)
+
- [c]: Set current frame (1-based frame number)
+
- [r]: Target frame number for gap changes
+
- [z]: Frame gap/delay in milliseconds
+
- [v]: Loop count
+
+
{2 Terminal-Driven Animation}
-
Animation control operations. *)
+
The terminal automatically advances frames based on each frame's gap
+
(delay). To start terminal-driven animation:
+
+
{[
+
(* 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)
+
]}
+
+
{2 Client-Driven Animation}
+
+
The client manually controls which frame is displayed:
+
+
{[
+
(* Display specific frame *)
+
Kgp.animate ~image_id:1 (Animation.set_current_frame 5)
+
+
(* Advance to next frame in application logic *)
+
let next_frame = (current_frame mod total_frames) + 1 in
+
Kgp.animate ~image_id:1 (Animation.set_current_frame next_frame)
+
]}
+
+
{2 Modifying Frame Timing}
+
+
Frame gaps can be changed during playback:
+
+
{[
+
(* 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))
+
]}
+
+
{2 Loop Counting}
+
+
The [loops] parameter in {!set_state}:
+
- 0: Ignored (doesn't change loop setting)
+
- 1: Infinite loop
+
- n > 1: Loop (n-1) times, then stop *)
type t =
[ `Set_state of Kgp_animation_state.t * int option
| `Set_gap of int * int
| `Set_current of int ]
-
(** Animation control operations. *)
+
(** Animation control operations.
+
+
- [`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 state.
-
@param loops Number of loops: 0 = ignored, 1 = infinite, n = n-1 loops *)
+
(** Set animation playback state.
+
+
@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 *)
+
]} *)
val set_gap : frame:int -> gap_ms:int -> t
(** Set the gap (delay) for a specific frame.
-
@param frame 1-based frame number
-
@param gap_ms Delay in milliseconds (negative = gapless) *)
+
+
@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].
+
+
Note: Frame 1 is the root/base image. Use 2+ for added frames. *)
val set_current_frame : int -> t
-
(** Make a specific frame (1-based) the current displayed frame. *)
+
(** Make a specific frame the current displayed frame.
+
+
@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. *)
+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
+60 -5
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. *)
+
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]:
+
- [s=1]: stop animation
+
- [s=2]: run in loading mode
+
- [s=3]: run normally
+
+
{2 Animation Modes}
+
+
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 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:
+
- 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:
+
- 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]. *)
type t = [ `Stop | `Loading | `Run ]
(** Animation playback states.
-
- [`Stop] - Halt animation playback
-
- [`Loading] - Run animation but wait for new frames at end
-
- [`Run] - Run animation normally and loop *)
+
- [`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 (1, 2, or 3). *)
+
(** 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. *)
+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;
+91 -4
lib/kgp_compose.mli
···
-
(** Kitty Graphics Protocol Compose
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
+
+
(** Frame Composition
+
+
Operations for compositing rectangular regions between animation frames.
+
This allows building complex frames from simpler components.
+
+
{2 Protocol Overview}
+
+
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
+
- Creating complex animations efficiently
+
+
{2 Coordinate System}
+
+
All coordinates are in pixels, relative to the top-left corner of the
+
respective frame:
-
Frame composition operations. *)
+
- [source_x], [source_y]: Top-left of rectangle in source frame
+
- [dest_x], [dest_y]: Top-left of destination in target frame
+
- [width], [height]: Size of the rectangle to copy
+
+
If width/height are omitted, the entire source frame is used.
+
+
{2 Composition Mode}
+
+
The [composition] parameter controls blending:
+
- [{`Alpha_blend}]: Standard alpha compositing (default)
+
- [{`Overwrite}]: Direct pixel replacement
+
+
{2 Error Conditions}
+
+
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
+
- [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 *)
+
in
+
Kgp.compose ~image_id:1 comp
+
]} *)
type t
-
(** Composition operation. *)
+
(** Composition operation. Opaque type; use {!make} to construct. *)
val make :
source_frame:int ->
···
?composition:Kgp_composition.t ->
unit ->
t
-
(** Compose a rectangle from one frame onto another. *)
+
(** 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]. *)
(** {1 Field Accessors} *)
val source_frame : t -> int
+
(** 1-based source frame number. *)
+
val dest_frame : t -> int
+
(** 1-based destination frame number. *)
+
val width : t -> int option
+
(** Width of rectangle in pixels. *)
+
val height : t -> int option
+
(** Height of rectangle in pixels. *)
+
val source_x : t -> int option
+
(** Left edge of source rectangle. *)
+
val source_y : t -> int option
+
(** Top edge of source rectangle. *)
+
val dest_x : t -> int option
+
(** Left edge of destination position. *)
+
val dest_y : t -> int option
+
(** Top edge of destination position. *)
+
val composition : t -> Kgp_composition.t option
+
(** Blending mode for composition. *)
+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
+44 -4
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. *)
+
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):
+
- Value 0 or omitted: alpha blending (default)
+
- Value 1: simple overwrite/replacement
+
+
{2 Alpha Blending}
+
+
[{`Alpha_blend}] performs standard alpha compositing using the source
+
pixel's alpha channel. For each pixel:
+
- If source alpha is 255 (opaque), source pixel replaces destination
+
- If source alpha is 0 (transparent), destination pixel is unchanged
+
- Otherwise, colors are blended proportionally
+
+
This mode is essential for:
+
- Transparent PNG images
+
- Overlaying graphics on backgrounds
+
- Anti-aliased edges and text
+
+
{2 Overwrite Mode}
+
+
[{`Overwrite}] simply replaces destination pixels with source pixels,
+
ignoring the alpha channel. This is useful for:
+
- Performance optimization when transparency isn't needed
+
- Replacing rectangular regions entirely
+
- Animation frames that completely replace the previous frame *)
type t = [ `Alpha_blend | `Overwrite ]
(** Composition modes.
-
- [`Alpha_blend] - Full alpha blending (default)
-
- [`Overwrite] - Simple pixel 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. *)
val to_int : t -> int
-
(** Convert to protocol integer (0 or 1). *)
+
(** 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. *)
+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'
+40 -5
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. *)
+
Specifies compression applied to image data before transmission.
+
+
{2 Protocol Details}
+
+
Compression is specified via the [o] key in the control data:
+
- 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.
+
+
{2 When to Use Compression}
+
+
Zlib compression is beneficial for:
+
- Large images with repetitive patterns
+
- Screenshots and UI graphics
+
- Images with large solid color regions
+
+
It may not help (or could increase size) for:
+
- Already-compressed PNG data
+
- Photographic images with high entropy
+
- Very small images (compression overhead)
+
+
{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. *)
type t = [ `None | `Zlib ]
(** Compression options.
-
- [`None] - Raw uncompressed data
-
- [`Zlib] - RFC 1950 zlib compression *)
+
- [`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. *)
val to_char : t -> char option
-
(** Convert to protocol character. Returns [None] for no compression,
-
[Some 'z'] for zlib. *)
+
(** 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. *)
+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
+45 -4
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. *)
+
Controls cursor position after displaying an image.
+
+
{2 Protocol Details}
+
+
Cursor movement is specified via the [C] key in the control data:
+
- [C=0] or no [C] key: move cursor after display (default)
+
- [C=1]: keep cursor in place (static)
+
+
This key was added in Kitty 0.20.0.
+
+
{2 Default Behavior}
+
+
By default ([{`Move}]), after displaying an image the cursor advances:
+
- 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.
+
+
{2 Static Cursor}
+
+
With [{`Static}], the cursor remains at its original position. This is
+
useful when:
+
- Overlaying images on existing content
+
- Positioning multiple images relative to the same starting point
+
- Implementing custom cursor management
+
+
{2 Relative Placements}
+
+
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)
-
- [`Static] - Keep cursor in place *)
+
- [`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 (0 or 1). *)
+
(** Convert to protocol integer.
+
+
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
+82 -28
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. Each deletion type has
-
two variants: one that only removes placements and one that also frees
-
the underlying image data. *)
+
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:
+
+
{v
+
| Char | Meaning |
+
|------|--------------------------------------------------------|
+
| a/A | All placements visible on screen |
+
| i/I | By image ID (with optional placement ID) |
+
| n/N | By image number (newest with that number) |
+
| c/C | At current cursor position |
+
| p/P | At specific cell coordinates (x, y) |
+
| q/Q | At specific cell with z-index (x, y, z) |
+
| x/X | All in specific column |
+
| y/Y | All in specific row |
+
| z/Z | All with specific z-index |
+
| r/R | By image ID range (min_id to max_id) |
+
| f/F | Animation frames only |
+
v}
+
+
{2 Placements vs 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.
+
+
{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.
+
+
{2 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.
-
- [`All_visible] - All visible placements
-
- [`By_id (id, placement_id)] - By image ID and optional placement ID
-
- [`By_number (n, placement_id)] - By image number and optional placement ID
-
- [`At_cursor] - Placement at cursor position
-
- [`At_cell (x, y)] - Placement at cell coordinates
-
- [`At_cell_z (x, y, z)] - Placement at cell with specific z-index
-
- [`By_column c] - All placements in column c
-
- [`By_row r] - All placements in row r
+
{b Screen-based:}
+
- [`All_visible] - All placements currently visible on screen
+
- [`At_cursor] - Placements at current cursor position
+
- [`At_cell (x, y)] - Placements intersecting cell at column x, row y
+
- [`At_cell_z (x, y, z)] - Like [`At_cell] but only with z-index z
+
- [`By_column x] - All placements intersecting column x
+
- [`By_row y] - All placements intersecting row y
- [`By_z_index z] - All placements with z-index z
-
- [`By_id_range (min, max)] - All images with IDs in range
-
- [`Frames] - Animation frames only
+
+
{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_range (min, max)] - All images with IDs in range [min..max]
+
+
{b Animation:}
+
- [`Frames] - Animation frames only (not the base image)
+
+
Use the [~free] parameter in the delete command to also release image data
+
from memory. *)
-
The [_and_free] variants also release the image data from memory. *)
+
val to_char : free:bool -> t -> char
+
(** Convert to protocol character for the delete command.
-
val to_char : t -> char
-
(** Convert to protocol character for the delete command. *)
+
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
+42 -5
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. *)
+
Specifies the pixel format of image data being transmitted to the terminal.
+
+
{2 Protocol Details}
+
+
The format is specified via the [f] key in the control data:
+
- [f=24] for RGB (3 bytes per pixel)
+
- [f=32] for RGBA (4 bytes per pixel, default)
+
- [f=100] for PNG
+
+
{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.
+
+
- [{`Rgb24}]: 3 bytes per pixel in sRGB color space (red, green, blue)
+
- [{`Rgba32}]: 4 bytes per pixel (red, green, blue, alpha)
+
+
{2 PNG Format}
+
+
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. *)
type t = [ `Rgba32 | `Rgb24 | `Png ]
(** Image data formats.
-
- [`Rgba32] - 32-bit RGBA (4 bytes per pixel)
-
- [`Rgb24] - 24-bit RGB (3 bytes per pixel)
-
- [`Png] - PNG encoded data *)
+
- [`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. *)
val to_int : t -> int
-
(** Convert to protocol integer value (32, 24, or 100). *)
+
(** 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. *)
+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
+103 -11
lib/kgp_frame.mli
···
-
(** Kitty Graphics Protocol Frame
+
(*---------------------------------------------------------------------------
+
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.
+
+
{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
+
+
Frame numbers are 1-based:
+
- Frame 1: The original/base image
+
- Frame 2+: Added animation frames
+
+
{2 Frame Positioning}
+
+
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.
+
+
{2 Frame Canvas}
-
Animation frame configuration. *)
+
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 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}
+
+
Instead of creating a new frame, you can edit an existing one:
+
- Set [edit_frame] to the 1-based frame number
+
- The frame itself becomes the canvas
+
- New data is composited onto it
+
+
{2 Frame Timing}
+
+
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
+
+
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. *)
type t
-
(** Animation frame configuration. *)
+
(** Animation frame configuration. Opaque type; use {!make} to construct. *)
val make :
?x:int ->
···
t
(** Create a frame specification.
-
@param x Left edge where frame data is placed (pixels)
-
@param y Top edge where frame data is placed (pixels)
-
@param base_frame 1-based frame number to use as background canvas
-
@param edit_frame 1-based frame number to edit (0 = new frame)
-
@param gap_ms Delay before next frame in milliseconds
-
@param composition How to blend pixels onto the canvas
-
@param background_color 32-bit RGBA background when no base frame *)
+
@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 defaults. *)
+
(** Empty frame spec with all defaults.
+
+
Creates a new frame with transparent black background, composited at
+
position (0, 0) with default timing (40ms gap). *)
(** {1 Field Accessors} *)
val x : t -> int option
+
(** Left edge position for frame data in pixels. *)
+
val y : t -> int option
+
(** Top edge position for frame data in pixels. *)
+
val base_frame : t -> int option
+
(** 1-based frame number to use as background canvas. *)
+
val edit_frame : t -> int option
+
(** 1-based frame number being edited (0 or None = new frame). *)
+
val gap_ms : t -> int option
+
(** Delay before next frame in milliseconds. *)
+
val composition : t -> Kgp_composition.t option
+
(** Pixel composition mode. *)
+
val background_color : t -> int32 option
+
(** 32-bit RGBA background color. *)
+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;
+127 -16
lib/kgp_placement.mli
···
-
(** Kitty Graphics Protocol Placement
+
(*---------------------------------------------------------------------------
+
Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
+
SPDX-License-Identifier: ISC
+
---------------------------------------------------------------------------*)
-
Configuration for where and how to display images. *)
+
(** Image Placement Configuration
+
+
Configuration for where and how to display images. Placements control
+
cropping, scaling, positioning, and layering of images.
+
+
{2 Protocol Overview}
+
+
When displaying an image, the protocol allows specifying:
+
- Which part of the source image to display (source rectangle)
+
- Where to display it (cell position and pixel offsets)
+
- How large to display it (scaling to cell dimensions)
+
- How it layers with other content (z-index)
+
- Whether it can be referenced via Unicode placeholders
+
+
{2 Source Rectangle}
+
+
The source rectangle specifies which portion of the image to display:
+
- [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.
+
+
{2 Cell-Based Sizing}
+
+
Images are sized in terminal cells:
+
- [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.
+
+
{2 Pixel Offsets}
+
+
Fine-grained positioning within the starting cell:
+
- [cell_x_offset]: Horizontal offset in pixels from cell left edge
+
- [cell_y_offset]: Vertical offset in pixels from cell top edge
+
+
These offsets must be smaller than the cell dimensions.
+
+
{2 Z-Index Layering}
+
+
The [z_index] controls vertical stacking:
+
- 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
+
+
Overlapping images with the same z-index are ordered by image ID (lower ID
+
draws first/underneath).
+
+
{2 Placement IDs}
+
+
Each placement can have a unique [placement_id] (1-4294967295). This
+
enables:
+
- Updating a specific placement without affecting others
+
- 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. *)
type t
-
(** Placement configuration. *)
+
(** Placement configuration. Opaque type; use {!make} to construct. *)
val make :
?source_x:int ->
···
t
(** Create a placement configuration.
-
@param source_x Left edge of source rectangle in pixels (default 0)
-
@param source_y Top edge of source rectangle in pixels (default 0)
-
@param source_width Width of source rectangle (default: full width)
-
@param source_height Height of source rectangle (default: full height)
-
@param cell_x_offset X offset within the first cell in pixels
-
@param cell_y_offset Y offset within the first cell in pixels
-
@param columns Number of columns to display over (scales image)
-
@param rows Number of rows to display over (scales image)
-
@param z_index Stacking order (negative = under text)
-
@param placement_id Unique ID for this placement
-
@param cursor Cursor movement policy after display
-
@param unicode_placeholder Create virtual placement for Unicode mode *)
+
@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]. *)
val empty : t
-
(** Empty placement with all defaults. *)
+
(** Empty placement with all defaults.
+
+
Equivalent to [make ()]. The image displays at natural size at the current
+
cursor position with default z-index (0). *)
(** {1 Field Accessors} *)
val source_x : t -> int option
+
(** Left edge of source rectangle in pixels. *)
+
val source_y : t -> int option
+
(** Top edge of source rectangle in pixels. *)
+
val source_width : t -> int option
+
(** Width of source rectangle in pixels. *)
+
val source_height : t -> int option
+
(** Height of source rectangle in pixels. *)
+
val cell_x_offset : t -> int option
+
(** X offset within the first cell in pixels. *)
+
val cell_y_offset : t -> int option
+
(** Y offset within the first cell in pixels. *)
+
val columns : t -> int option
+
(** Number of columns to display over. *)
+
val rows : t -> int option
+
(** Number of rows to display over. *)
+
val z_index : t -> int option
+
(** Stacking order (z-index). *)
+
val placement_id : t -> int option
+
(** Unique placement identifier. *)
+
val cursor : t -> Kgp_cursor.t option
+
(** Cursor movement policy. *)
+
val unicode_placeholder : t -> bool
+
(** Whether this is a virtual placement for Unicode mode. *)
+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
+45 -5
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. *)
+
Controls which terminal responses are sent back to the application.
+
+
{2 Protocol Details}
+
+
The quiet level is specified via the [q] key in the control data:
+
- [q=0] or no [q] key: send all responses (default)
+
- [q=1]: suppress OK responses, only send errors
+
- [q=2]: suppress all responses
+
+
{2 Terminal Responses}
+
+
Normally, when an [image_id] is specified, the terminal responds:
+
- 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.
+
+
{2 Use Cases}
+
+
[{`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. *)
type t = [ `Noisy | `Errors_only | `Silent ]
(** Response suppression levels.
-
- [`Noisy] - Send all responses (default)
-
- [`Errors_only] - Suppress OK responses, only send errors
-
- [`Silent] - Suppress all responses *)
+
- [`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. *)
val to_int : t -> int
-
(** Convert to protocol integer (0, 1, or 2). *)
+
(** 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. *)
+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
+57 -5
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. *)
+
Specifies how image data is transmitted to the terminal.
+
+
{2 Protocol Details}
+
+
The transmission method is specified via the [t] key in the control data:
+
- [t=d] for direct (inline) transmission (default)
+
- [t=f] for regular file
+
- [t=t] for temporary file (deleted after reading)
+
+
{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).
+
+
For images larger than 4096 bytes (after base64 encoding), the data is
+
automatically split into chunks using the [m=] key:
+
- [m=1] indicates more chunks follow
+
- [m=0] indicates the final chunk
+
+
{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:
+
- [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).
+
+
Security: The terminal will refuse to read device files, sockets, or files
+
in sensitive locations like [/proc], [/sys], or [/dev].
+
+
{2 Temporary File Transmission}
+
+
[{`Tempfile}] is like [{`File}] but the terminal deletes the file after
+
reading. The file must be in a recognized temporary directory:
+
- [/tmp]
+
- [/dev/shm]
+
- The [TMPDIR] environment variable location
+
- Platform-specific temp directories containing [tty-graphics-protocol] *)
type t = [ `Direct | `File | `Tempfile ]
(** Transmission methods.
-
- [`Direct] - Data is sent inline in the escape sequence
-
- [`File] - Terminal reads from a file path
-
- [`Tempfile] - Terminal reads and deletes a temporary file *)
+
- [`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.
+
- [`Tempfile] - Like [`File] but terminal deletes the file after reading.
+
File must be in a recognized temporary directory. *)
val to_char : t -> char
-
(** Convert to protocol character ('d', 'f', or 't'). *)
+
(** Convert to protocol character.
+
+
Returns ['d'] for [`Direct], ['f'] for [`File], or ['t'] for [`Tempfile].
+
These values are used in the [t=] control data key. *)
+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. *)