Kitty Graphics Protocol in OCaml
terminal graphics ocaml

ocamlformat

+1
.ocamlformat
···
+
version=0.28.1
+10 -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
+83 -152
example/example.ml
···
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);
···
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 () =
···
(try
let png_data = read_file "camel.png" in
send
-
(K.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 "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.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 (camel.png not found)");
print_newline ();
···
(* 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.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.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.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 ();
···
(try
let png_data = read_file "camel.png" in
send
-
(K.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 "camel.png loaded and displayed"
-
with Sys_error msg ->
-
Printf.printf "camel.png not found: %s\n" msg);
+
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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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 ();
+21 -43
example/tiny_anim.ml
···
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 *)
···
(* Step 1: Transmit base frame (red) - matching Go's sequence *)
let red_frame = solid_color_rgba ~width ~height ~r:255 ~g:0 ~b:0 ~a:255 in
send
-
(K.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
+
let orange_frame =
+
solid_color_rgba ~width ~height ~r:255 ~g:165 ~b:0 ~a:255
+
in
send
-
(K.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.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.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 *)
send
-
(K.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.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_endline "";
print_endline "Tiny animation (20x20) - Red -> Orange -> Yellow -> Green";
···
let _ = read_line () in
(* Stop animation *)
-
send
-
(K.animate ~image_id (K.Animation.set_state `Stop))
-
~data:"";
+
send (K.animate ~image_id (K.Animation.set_state `Stop)) ~data:"";
(* Clean up *)
send (K.delete ~free:true ~quiet:`Errors_only `All_visible) ~data:"";
+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)"]
+8 -7
lib-cli/kgp_cli.ml
···
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 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 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 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))
+
Term.(
+
const choose
+
$ Arg.(value & flag enable)
+
$ Arg.(value & flag disable)
+
$ Arg.(value & flag tmux))
+5 -4
lib-cli/kgp_cli.mli
···
(** 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
+
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}
···
- [--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}. *)
+
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").
-7
lib/kgp.ml
···
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
-
(* Command type and functions *)
type command = Kgp_command.t
let transmit = Kgp_command.transmit
···
let write_tmux = Kgp_command.write_tmux
let to_string_tmux = Kgp_command.to_string_tmux
-
(* Core modules *)
module Response = Kgp_response
-
-
(* Utility modules *)
module Unicode_placeholder = Kgp_unicode
module Detect = Kgp_detect
module Tmux = Kgp_tmux
+82 -86
lib/kgp.mli
···
(** Kitty Terminal Graphics Protocol
-
This library implements the Kitty terminal graphics protocol, allowing
-
OCaml programs to display images in terminals that support the protocol
-
(Kitty, WezTerm, Konsole, Ghostty, etc.).
+
This library implements the Kitty terminal graphics protocol, allowing OCaml
+
programs to display images in terminals that support the protocol (Kitty,
+
WezTerm, Konsole, Ghostty, etc.).
{1 Protocol Overview}
···
- No requirement for terminal emulators to understand image formats
- Pixel-level positioning of graphics
-
- Integration with text (graphics can be drawn below/above text with alpha blending)
+
- Integration with text (graphics can be drawn below/above text with alpha
+
blending)
- Automatic scrolling with text
- Animation support with frame deltas for efficiency
···
(* Display at different positions *)
let cmd = Kgp.display ~image_id:1 () in
-
Kgp.write buf cmd ~data:"";
+
Kgp.write buf cmd ~data:""
]}
{2 Protocol Reference}
-
See {{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics Protocol}
-
for the full specification. *)
+
See
+
{{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics
+
Protocol} for the full specification. *)
(** {1 Type Modules} *)
···
(** {2 Image Transmission}
-
Images can be transmitted to the terminal for storage and later display.
-
The terminal assigns storage and responds with success or failure.
+
Images can be transmitted to the terminal for storage and later display. The
+
terminal assigns storage and responds with success or failure.
For large images, the library automatically handles chunked transmission
(splitting data into 4096-byte base64-encoded chunks). *)
···
The image is stored by the terminal and can be displayed later using
{!val:display} with the same [image_id].
-
@param image_id Unique identifier (1-4294967295) for later reference.
-
If specified, the terminal responds with success/failure.
-
@param image_number Alternative to [image_id] where the terminal assigns
-
a unique ID and returns it in the response. Useful when multiple
-
programs share the terminal.
+
@param image_id
+
Unique identifier (1-4294967295) for later reference. If specified, the
+
terminal responds with success/failure.
+
@param image_number
+
Alternative to [image_id] where the terminal assigns a unique ID and
+
returns it in the response. Useful when multiple programs share the
+
terminal.
@param format Pixel format of the data. Default is [`Rgba32].
@param transmission How data is sent. Default is [`Direct] (inline).
@param compression Compression applied to data. Default is [`None].
···
command
(** Transmit image data and display it immediately.
-
Combines transmission and display in a single command. The image is
-
rendered at the current cursor position unless placement options
-
specify otherwise.
+
Combines transmission and display in a single command. The image is rendered
+
at the current cursor position unless placement options specify otherwise.
See {!transmit} for parameter descriptions. The [placement] parameter
controls display position and scaling. *)
···
command
(** Query terminal support without storing the image.
-
Performs the same validation as {!transmit} but does not store the
-
image. Useful for testing whether the terminal supports the graphics
-
protocol and specific formats.
+
Performs the same validation as {!transmit} but does not store the image.
+
Useful for testing whether the terminal supports the graphics protocol and
+
specific formats.
To detect graphics support, send a query and check for a response:
{[
···
(** {2 Display}
-
Previously transmitted images can be displayed multiple times at
-
different positions with different cropping and scaling options. *)
+
Previously transmitted images can be displayed multiple times at different
+
positions with different cropping and scaling options. *)
val display :
?image_id:int ->
···
command
(** Display a previously transmitted image.
-
The image is rendered at the current cursor position. Use [placement]
-
to control cropping, scaling, z-index, and other display options.
+
The image is rendered at the current cursor position. Use [placement] to
+
control cropping, scaling, z-index, and other display options.
-
Each display creates a "placement" of the image. Multiple placements
-
of the same image share the underlying image data.
+
Each display creates a "placement" of the image. Multiple placements of the
+
same image share the underlying image data.
@param image_id ID of a previously transmitted image.
-
@param image_number Image number (acts on the newest image with this number).
+
@param image_number
+
Image number (acts on the newest image with this number).
@param placement Display configuration (position, size, z-index, etc.). *)
(** {2 Deletion}
-
Images and placements can be deleted to free terminal resources.
-
By default, only placements are removed and image data is retained
-
for potential reuse. Use [~free:true] to also release the image data. *)
+
Images and placements can be deleted to free terminal resources. By default,
+
only placements are removed and image data is retained for potential reuse.
+
Use [~free:true] to also release the image data. *)
val delete : ?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.
+
@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))
+
(* Delete specific image, keeping data for reuse *)
+
Kgp.delete
+
(`By_id (42, None))
+
(* Delete specific image and free its data *)
+
Kgp.delete ~free:true
+
(`By_id (42, None))
+
(* Delete all placements at a specific cell *)
+
Kgp.delete
+
(`At_cell (10, 5))
]} *)
(** {2 Animation}
···
Animations are created by first transmitting a base image, then adding
frames with optional delta encoding for efficiency.
-
Frame numbers are 1-based: frame 1 is the root (base) image, frame 2
-
is the first added frame, etc. *)
+
Frame numbers are 1-based: frame 1 is the root (base) image, frame 2 is the
+
first added frame, etc. *)
val frame :
?image_id:int ->
···
command
(** Transmit animation frame data.
-
Adds a new frame to an existing image or edits an existing frame.
-
The frame can be a full image or a partial update (rectangle).
+
Adds a new frame to an existing image or edits an existing frame. The frame
+
can be a full image or a partial update (rectangle).
-
Use {!Frame.make} to configure the frame's position, timing, and
-
composition options.
+
Use {!Frame.make} to configure the frame's position, timing, and composition
+
options.
@param frame Frame configuration including timing and composition. *)
val animate :
-
?image_id:int ->
-
?image_number:int ->
-
?quiet:Quiet.t ->
-
Animation.t ->
-
command
+
?image_id:int -> ?image_number:int -> ?quiet:Quiet.t -> Animation.t -> command
(** Control animation playback.
For terminal-driven animation:
{[
(* Start infinite loop animation *)
-
Kgp.animate ~image_id:1 (Animation.set_state ~loops:1 `Run)
-
-
(* Stop animation *)
-
Kgp.animate ~image_id:1 (Animation.set_state `Stop)
-
-
(* Change frame timing *)
-
Kgp.animate ~image_id:1 (Animation.set_gap ~frame:3 ~gap_ms:100)
+
Kgp.animate ~image_id:1
+
(Animation.set_state ~loops:1 `Run)
+
(* Stop animation *)
+
Kgp.animate ~image_id:1
+
(Animation.set_state `Stop)
+
(* Change frame timing *)
+
Kgp.animate ~image_id:1
+
(Animation.set_gap ~frame:3 ~gap_ms:100)
]}
For client-driven animation:
···
]} *)
val compose :
-
?image_id:int ->
-
?image_number:int ->
-
?quiet:Quiet.t ->
-
Compose.t ->
-
command
+
?image_id:int -> ?image_number:int -> ?quiet:Quiet.t -> Compose.t -> command
(** Compose animation frames.
-
Copies a rectangular region from one frame onto another. Useful for
-
building complex frames from simpler components.
+
Copies a rectangular region from one frame onto another. Useful for building
+
complex frames from simpler components.
{[
(* Copy a 50x50 region from frame 2 to frame 5 *)
-
let comp = Compose.make
-
~source_frame:2 ~dest_frame:5
-
~width:50 ~height:50
-
~source_x:10 ~source_y:10
-
~dest_x:20 ~dest_y:20 () in
+
let comp =
+
Compose.make ~source_frame:2 ~dest_frame:5 ~width:50 ~height:50
+
~source_x:10 ~source_y:10 ~dest_x:20 ~dest_y:20 ()
+
in
Kgp.compose ~image_id:1 comp
]} *)
(** {2 Output}
-
Commands are serialized to escape sequences that can be written
-
to the terminal. *)
+
Commands are serialized to escape sequences that can be written to the
+
terminal. *)
val write : Buffer.t -> command -> data:string -> unit
(** Write the command to a buffer.
The [data] parameter contains the raw image/frame data (before base64
-
encoding). Pass an empty string for commands that don't include payload
-
data (like {!val:display}, {!val:delete}, {!val:animate}).
+
encoding). Pass an empty string for commands that don't include payload data
+
(like {!val:display}, {!val:delete}, {!val:animate}).
The library handles base64 encoding and chunking automatically. *)
val to_string : command -> data:string -> string
(** Convert command to a string.
-
Convenience wrapper around {!write} that returns the serialized
-
command as a string. *)
+
Convenience wrapper around {!write} that returns the serialized command as a
+
string. *)
val write_tmux : Buffer.t -> command -> data:string -> unit
(** Write the command to a buffer with tmux passthrough support.
-
If running inside tmux (detected via [TMUX] environment variable),
-
wraps the graphics command in a DCS passthrough sequence so it
-
reaches the underlying terminal. Otherwise, behaves like {!write}.
+
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}. *)
+
Convenience wrapper around {!write_tmux}. If running inside tmux, wraps the
+
output for passthrough. Otherwise, behaves like {!to_string}. *)
(** {1 Response} *)
···
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. *)
+
(** 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. *)
-
+29 -26
lib/kgp_animation.mli
···
(** Animation Control
-
Operations for controlling animation playback. The protocol supports
-
both terminal-driven and client-driven animation modes.
+
Operations for controlling animation playback. The protocol supports both
+
terminal-driven and client-driven animation modes.
{2 Protocol Overview}
···
{[
(* Start infinite loop *)
-
Kgp.animate ~image_id:1 (Animation.set_state ~loops:1 `Run)
-
-
(* Run 3 times then stop *)
-
Kgp.animate ~image_id:1 (Animation.set_state ~loops:4 `Run)
-
-
(* Stop animation *)
-
Kgp.animate ~image_id:1 (Animation.set_state `Stop)
+
Kgp.animate ~image_id:1
+
(Animation.set_state ~loops:1 `Run)
+
(* Run 3 times then stop *)
+
Kgp.animate ~image_id:1
+
(Animation.set_state ~loops:4 `Run)
+
(* Stop animation *)
+
Kgp.animate ~image_id:1
+
(Animation.set_state `Stop)
]}
{2 Client-Driven Animation}
···
{[
(* Slow down frame 3 *)
-
Kgp.animate ~image_id:1 (Animation.set_gap ~frame:3 ~gap_ms:200)
-
-
(* Make frame 5 instant/gapless *)
-
Kgp.animate ~image_id:1 (Animation.set_gap ~frame:5 ~gap_ms:(-1))
+
Kgp.animate ~image_id:1
+
(Animation.set_gap ~frame:3 ~gap_ms:200)
+
(* Make frame 5 instant/gapless *)
+
Kgp.animate ~image_id:1
+
(Animation.set_gap ~frame:5 ~gap_ms:(-1))
]}
{2 Loop Counting}
···
| `Set_current of int ]
(** Animation control operations.
-
- [`Set_state (state, loops)] - Set animation playback state with
-
optional loop count.
+
- [`Set_state (state, loops)] - Set animation playback state with optional
+
loop count.
- [`Set_gap (frame, gap_ms)] - Set the delay for a specific frame.
- [`Set_current frame] - Jump to a specific frame (1-based). *)
val set_state : ?loops:int -> Kgp_animation_state.t -> t
(** Set animation playback state.
-
@param loops Loop count: 0 = ignored, 1 = infinite, n > 1 = (n-1) loops.
-
Protocol key: [v].
+
@param loops
+
Loop count: 0 = ignored, 1 = infinite, n > 1 = (n-1) loops. Protocol key:
+
[v].
@param state The target playback state.
Examples:
{[
-
set_state `Run (* Run with current loop setting *)
-
set_state ~loops:1 `Run (* Run infinitely *)
-
set_state ~loops:3 `Run (* Run twice, then stop *)
-
set_state `Stop (* Pause animation *)
-
set_state `Loading (* Run, wait for more frames at end *)
+
set_state `Run (* Run with current loop setting *) set_state ~loops:1 `Run
+
(* Run infinitely *) set_state ~loops:3 `Run
+
(* Run twice, then stop *) set_state `Stop (* Pause animation *)
+
set_state `Loading (* Run, wait for more frames at end *)
]} *)
val set_gap : frame:int -> gap_ms:int -> t
(** Set the gap (delay) for a specific frame.
@param frame 1-based frame number to modify. Protocol key: [r].
-
@param gap_ms Delay in milliseconds before next frame. Negative values
-
create gapless frames (not displayed, instant skip). Protocol key: [z].
+
@param gap_ms
+
Delay in milliseconds before next frame. Negative values create gapless
+
frames (not displayed, instant skip). Protocol key: [z].
Note: Frame 1 is the root/base image. Use 2+ for added frames. *)
···
@param frame 1-based frame number to display. Protocol key: [c].
-
Used for client-driven animation where the application controls
-
frame advancement rather than the terminal. *)
+
Used for client-driven animation where the application controls frame
+
advancement rather than the terminal. *)
+1 -4
lib/kgp_animation_state.ml
···
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
+25 -25
lib/kgp_animation_state.mli
···
{2 Protocol Details}
-
The animation state is specified via the [s] key in the control data
-
when using action [a=a]:
+
The animation state is specified via the [s] key in the control data when
+
using action [a=a]:
- [s=1]: stop animation
- [s=2]: run in loading mode
- [s=3]: run normally
···
The protocol supports two animation approaches:
-
{b Terminal-driven animation}: The terminal automatically advances
-
frames based on the gap (delay) specified for each frame. Use
-
[{`Run}] or [{`Loading}] states.
+
{b Terminal-driven animation}: The terminal automatically advances frames
+
based on the gap (delay) specified for each frame. Use [{`Run}] or
+
[{`Loading}] states.
-
{b Client-driven animation}: The client manually sets the current
-
frame using [Kgp.Animation.set_current_frame]. Use [{`Stop}] state
-
to prevent automatic advancement.
+
{b Client-driven animation}: The client manually sets the current frame
+
using [Kgp.Animation.set_current_frame]. Use [{`Stop}] state to prevent
+
automatic advancement.
{2 Stop State}
-
[{`Stop}] halts automatic frame advancement. The animation freezes
-
on the current frame. Use this when:
+
[{`Stop}] halts automatic frame advancement. The animation freezes on the
+
current frame. Use this when:
- Implementing client-driven animation
- Pausing an animation
- Displaying a static frame from an animated image
{2 Loading State}
-
[{`Loading}] runs the animation but waits for new frames when reaching
-
the end instead of looping. Use this when:
+
[{`Loading}] runs the animation but waits for new frames when reaching the
+
end instead of looping. Use this when:
- Streaming animation frames progressively
- Building an animation while displaying it
- The animation is not yet complete
{2 Run State}
-
[{`Run}] runs the animation normally, looping back to the first frame
-
after the last. The loop count can be controlled via the [loops]
-
parameter in [Kgp.Animation.set_state]. *)
+
[{`Run}] runs the animation normally, looping back to the first frame after
+
the last. The loop count can be controlled via the [loops] parameter in
+
[Kgp.Animation.set_state]. *)
type t = [ `Stop | `Loading | `Run ]
(** Animation playback states.
-
- [`Stop] - Halt animation playback. The animation freezes on the
-
current frame and does not advance automatically.
-
- [`Loading] - Run animation but wait for new frames at end. When
-
the last frame is reached, the animation pauses until more frames
-
are added, then continues.
-
- [`Run] - Run animation normally and loop. After the last frame,
-
playback returns to the first frame (or stops after the specified
-
number of loops). *)
+
- [`Stop] - Halt animation playback. The animation freezes on the current
+
frame and does not advance automatically.
+
- [`Loading] - Run animation but wait for new frames at end. When the last
+
frame is reached, the animation pauses until more frames are added, then
+
continues.
+
- [`Run] - Run animation normally and loop. After the last frame, playback
+
returns to the first frame (or stops after the specified number of loops).
+
*)
val to_int : t -> int
(** Convert to protocol integer.
-
Returns 1 for [`Stop], 2 for [`Loading], or 3 for [`Run].
-
These values are used in the [s=] control data key. *)
+
Returns 1 for [`Stop], 2 for [`Loading], or 3 for [`Run]. These values are
+
used in the [s=] control data key. *)
+8 -9
lib/kgp_command.ml
···
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 ~free (d : Kgp_delete.t) =
···
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;
···
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
+
end
+
else write buf cmd ~data
let to_string_tmux cmd ~data =
let buf = Buffer.create 1024 in
+10 -13
lib/kgp_command.mli
···
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. *)
+
@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 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}. *)
+
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}. *)
+
If running inside tmux, wraps the output for passthrough. Otherwise, behaves
+
like {!to_string}. *)
+27 -29
lib/kgp_compose.mli
···
{2 Protocol Overview}
-
Frame composition uses action [a=c] to copy a rectangular region from
-
one frame onto another. This is useful for:
+
Frame composition uses action [a=c] to copy a rectangular region from one
+
frame onto another. This is useful for:
- Building frames from reusable sprite components
- Applying partial updates to existing frames
···
{2 Coordinate System}
-
All coordinates are in pixels, relative to the top-left corner of
-
the respective frame:
+
All coordinates are in pixels, relative to the top-left corner of the
+
respective frame:
- [source_x], [source_y]: Top-left of rectangle in source frame
- [dest_x], [dest_y]: Top-left of destination in target frame
···
The terminal responds with errors for:
- [ENOENT]: Source or destination frame doesn't exist
-
- [EINVAL]: Rectangle out of bounds, or source equals destination
-
with overlapping regions
+
- [EINVAL]: Rectangle out of bounds, or source equals destination with
+
overlapping regions
- [ENOSPC]: Not enough storage after composition
{2 Example}
{[
(* Copy a 32x32 sprite from frame 2 to frame 5 *)
-
let comp = Compose.make
-
~source_frame:2 ~dest_frame:5
-
~width:32 ~height:32
-
~source_x:0 ~source_y:0 (* From top-left of source *)
-
~dest_x:100 ~dest_y:50 () (* To position in dest *)
+
let comp =
+
Compose.make ~source_frame:2 ~dest_frame:5 ~width:32 ~height:32
+
~source_x:0 ~source_y:0 (* From top-left of source *)
+
~dest_x:100 ~dest_y:50 () (* To position in dest *)
in
Kgp.compose ~image_id:1 comp
]} *)
···
t
(** Create a composition operation.
-
@param source_frame 1-based frame number to copy from. Required.
-
Protocol key: [r].
-
@param dest_frame 1-based frame number to copy onto. Required.
-
Protocol key: [c].
-
@param width Width of rectangle in pixels. Default is full frame.
-
Protocol key: [w].
-
@param height Height of rectangle in pixels. Default is full frame.
-
Protocol key: [h].
-
@param source_x Left edge of source rectangle (default 0).
-
Protocol key: [X].
-
@param source_y Top edge of source rectangle (default 0).
-
Protocol key: [Y].
-
@param dest_x Left edge of destination position (default 0).
-
Protocol key: [x].
-
@param dest_y Top edge of destination position (default 0).
-
Protocol key: [y].
-
@param composition Blending mode. Default is alpha blending.
-
Protocol key: [C]. *)
+
@param source_frame
+
1-based frame number to copy from. Required. Protocol key: [r].
+
@param dest_frame
+
1-based frame number to copy onto. Required. Protocol key: [c].
+
@param width
+
Width of rectangle in pixels. Default is full frame. Protocol key: [w].
+
@param height
+
Height of rectangle in pixels. Default is full frame. Protocol key: [h].
+
@param source_x
+
Left edge of source rectangle (default 0). Protocol key: [X].
+
@param source_y Top edge of source rectangle (default 0). Protocol key: [Y].
+
@param dest_x
+
Left edge of destination position (default 0). Protocol key: [x].
+
@param dest_y
+
Top edge of destination position (default 0). Protocol key: [y].
+
@param composition
+
Blending mode. Default is alpha blending. Protocol key: [C]. *)
(** {1 Field Accessors} *)
+1 -3
lib/kgp_composition.ml
···
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
+7 -7
lib/kgp_composition.mli
···
{2 Protocol Details}
-
The composition mode is specified via the [X] key in the control data
-
(for animation frames) or the [C] key (for frame composition operations):
+
The composition mode is specified via the [X] key in the control data (for
+
animation frames) or the [C] key (for frame composition operations):
- Value 0 or omitted: alpha blending (default)
- Value 1: simple overwrite/replacement
···
- [`Alpha_blend] - Full alpha blending (default). Source pixels are
composited onto the destination using standard Porter-Duff "over"
compositing based on the source alpha channel.
-
- [`Overwrite] - Simple pixel replacement. Source pixels completely
-
replace destination pixels, ignoring alpha values. Faster but no
-
transparency support. *)
+
- [`Overwrite] - Simple pixel replacement. Source pixels completely replace
+
destination pixels, ignoring alpha values. Faster but no transparency
+
support. *)
val to_int : t -> int
(** Convert to protocol integer.
-
Returns 0 for [`Alpha_blend] or 1 for [`Overwrite].
-
These values are used in the [X=] or [C=] control data keys. *)
+
Returns 0 for [`Alpha_blend] or 1 for [`Overwrite]. These values are used in
+
the [X=] or [C=] control data keys. *)
+1 -3
lib/kgp_compression.ml
···
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'
+8 -8
lib/kgp_compression.mli
···
- No [o] key means no compression
- [o=z] means zlib (RFC 1950 DEFLATE) compression
-
Compression is applied to the raw pixel/PNG data {i before} base64
-
encoding. The terminal decompresses after base64 decoding.
+
Compression is applied to the raw pixel/PNG data {i before} base64 encoding.
+
The terminal decompresses after base64 decoding.
{2 When to Use Compression}
···
{2 PNG with Compression}
When using both [{`Png}] format and [{`Zlib}] compression, the [size]
-
parameter must be specified with the original (uncompressed) PNG size.
-
The terminal needs this to allocate the correct buffer for decompression. *)
+
parameter must be specified with the original (uncompressed) PNG size. The
+
terminal needs this to allocate the correct buffer for decompression. *)
type t = [ `None | `Zlib ]
(** Compression options.
- [`None] - Raw uncompressed data. No [o=] key is sent.
-
- [`Zlib] - RFC 1950 zlib/DEFLATE compression. Data is compressed
-
before base64 encoding and decompressed by the terminal. *)
+
- [`Zlib] - RFC 1950 zlib/DEFLATE compression. Data is compressed before
+
base64 encoding and decompressed by the terminal. *)
val to_char : t -> char option
(** Convert to protocol character.
-
Returns [None] for [`None] (no key sent), or [Some 'z'] for [`Zlib].
-
When [Some c] is returned, [o=c] is added to the control data. *)
+
Returns [None] for [`None] (no key sent), or [Some 'z'] for [`Zlib]. When
+
[Some c] is returned, [o=c] is added to the control data. *)
+1 -3
lib/kgp_cursor.ml
···
type t = [ `Move | `Static ]
-
let to_int : t -> int = function
-
| `Move -> 0
-
| `Static -> 1
+
let to_int : t -> int = function `Move -> 0 | `Static -> 1
+11 -11
lib/kgp_cursor.mli
···
- Right by the number of columns the image occupies
- Down by the number of rows the image occupies
-
This matches how the cursor moves after printing text, allowing images
-
to flow naturally with text content.
+
This matches how the cursor moves after printing text, allowing images to
+
flow naturally with text content.
{2 Static Cursor}
···
{2 Relative Placements}
-
Note: When using relative placements (positioning images relative to
-
other placements), the cursor never moves regardless of this setting. *)
+
Note: When using relative placements (positioning images relative to other
+
placements), the cursor never moves regardless of this setting. *)
type t = [ `Move | `Static ]
(** Cursor movement behavior.
-
- [`Move] - Advance cursor past the displayed image (default).
-
Cursor moves right by the number of columns and down by the
-
number of rows occupied by the image.
-
- [`Static] - Keep cursor at its original position. The image
-
is displayed but cursor position is unchanged. *)
+
- [`Move] - Advance cursor past the displayed image (default). Cursor moves
+
right by the number of columns and down by the number of rows occupied by
+
the image.
+
- [`Static] - Keep cursor at its original position. The image is displayed
+
but cursor position is unchanged. *)
val to_int : t -> int
(** Convert to protocol integer.
-
Returns 0 for [`Move] or 1 for [`Static].
-
These values are used in the [C=] control data key. *)
+
Returns 0 for [`Move] or 1 for [`Static]. These values are used in the [C=]
+
control data key. *)
+24 -23
lib/kgp_delete.mli
···
{2 Protocol Details}
-
Deletion is performed with action [a=d] and the [d] key specifies
-
the deletion type. The [d] key uses single characters:
+
Deletion is performed with action [a=d] and the [d] key specifies the
+
deletion type. The [d] key uses single characters:
{v
| Char | Meaning |
···
{2 Placements vs Image Data}
-
Each deletion type can optionally free image data (controlled by the
-
[~free] parameter in the delete command):
+
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)
+
- {b With free}: Removes placements AND frees the image data. The image
+
cannot be displayed again without retransmitting. (Protocol: uppercase
+
char)
{2 Placement IDs}
When deleting by image ID or number, an optional placement ID can be
-
specified to delete only a specific placement. If [None], all placements
-
of that image are deleted.
+
specified to delete only a specific placement. If [None], all placements of
+
that image are deleted.
{2 Coordinate-Based Deletion}
-
For [{`At_cell}] and [{`At_cell_z}], coordinates are 0-based cell
-
positions (not pixel positions). Only placements that intersect the
-
specified cell are deleted.
+
For [{`At_cell}] and [{`At_cell_z}], coordinates are 0-based cell positions
+
(not pixel positions). Only placements that intersect the specified cell are
+
deleted.
{2 Virtual Placements}
-
Virtual placements (used for Unicode placeholder mode) are only affected
-
by: [{`By_id}], [{`By_number}], and [{`By_id_range}]. Other deletion
-
commands do not affect virtual placements. *)
+
Virtual placements (used for Unicode placeholder mode) are only affected by:
+
[{`By_id}], [{`By_number}], and [{`By_id_range}]. Other deletion commands do
+
not affect virtual placements. *)
type t =
[ `All_visible
···
- [`By_z_index z] - All placements with z-index z
{b ID-based:}
-
- [`By_id (id, placement_id)] - By image ID. If [placement_id] is
-
[Some p], only that specific placement; if [None], all placements.
-
- [`By_number (n, placement_id)] - By image number (newest image
-
with that number). Placement ID works as above.
+
- [`By_id (id, placement_id)] - By image ID. If [placement_id] is [Some p],
+
only that specific placement; if [None], all placements.
+
- [`By_number (n, placement_id)] - By image number (newest image with that
+
number). Placement ID works as above.
- [`By_id_range (min, max)] - All images with IDs in range [min..max]
{b Animation:}
- [`Frames] - Animation frames only (not the base image)
-
Use the [~free] parameter in the delete command to also release
-
image data from memory. *)
+
Use the [~free] parameter in the delete command to also release image data
+
from memory. *)
val to_char : free:bool -> t -> char
(** Convert to protocol character for the delete command.
Returns the character used in the [d=] control data key.
-
@param free If true, returns uppercase (frees data); if false,
-
returns lowercase (keeps data). *)
+
@param free
+
If true, returns uppercase (frees data); if false, returns lowercase
+
(keeps data). *)
+1 -4
lib/kgp_format.ml
···
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
+13 -13
lib/kgp_format.mli
···
{2 Raw Pixel Formats}
For [{`Rgb24}] and [{`Rgba32}], the data consists of raw pixel values in
-
row-major order (left-to-right, top-to-bottom). The image dimensions must
-
be specified via the [width] and [height] parameters.
+
row-major order (left-to-right, top-to-bottom). The image dimensions must be
+
specified via the [width] and [height] parameters.
- [{`Rgb24}]: 3 bytes per pixel in sRGB color space (red, green, blue)
- [{`Rgba32}]: 4 bytes per pixel (red, green, blue, alpha)
···
For [{`Png}], the data is a complete PNG image. The terminal extracts
dimensions from PNG metadata, so [width] and [height] are optional.
-
When using both PNG format and zlib compression, you must also specify
-
the [size] parameter with the uncompressed PNG data size. *)
+
When using both PNG format and zlib compression, you must also specify the
+
[size] parameter with the uncompressed PNG data size. *)
type t = [ `Rgba32 | `Rgb24 | `Png ]
(** Image data formats.
-
- [`Rgba32] - 32-bit RGBA (4 bytes per pixel). Default format.
-
Pixels are ordered red, green, blue, alpha. Alpha of 255 is fully
-
opaque, 0 is fully transparent.
-
- [`Rgb24] - 24-bit RGB (3 bytes per pixel). No alpha channel;
-
pixels are fully opaque. More compact than RGBA for opaque images.
+
- [`Rgba32] - 32-bit RGBA (4 bytes per pixel). Default format. Pixels are
+
ordered red, green, blue, alpha. Alpha of 255 is fully opaque, 0 is fully
+
transparent.
+
- [`Rgb24] - 24-bit RGB (3 bytes per pixel). No alpha channel; pixels are
+
fully opaque. More compact than RGBA for opaque images.
- [`Png] - PNG encoded data. The terminal decodes the PNG internally.
-
Supports all PNG color types and bit depths. Most convenient format
-
as dimensions are embedded in the data. *)
+
Supports all PNG color types and bit depths. Most convenient format as
+
dimensions are embedded in the data. *)
val to_int : t -> int
(** Convert to protocol integer value.
-
Returns 24 for [`Rgb24], 32 for [`Rgba32], or 100 for [`Png].
-
These values are used in the [f=] control data key. *)
+
Returns 24 for [`Rgb24], 32 for [`Rgba32], or 100 for [`Png]. These values
+
are used in the [f=] control data key. *)
+2 -1
lib/kgp_frame.ml
···
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
+46 -41
lib/kgp_frame.mli
···
(** Animation Frame Configuration
-
Configuration for adding or editing animation frames. Frames can be
-
full images or partial updates (rectangles), with options for timing
-
and composition.
+
Configuration for adding or editing animation frames. Frames can be full
+
images or partial updates (rectangles), with options for timing and
+
composition.
{2 Protocol Overview}
-
Animations are created by:
-
1. Transmitting a base image (becomes frame 1)
-
2. Adding frames using the frame action ([a=f])
-
3. Controlling playback with animation commands
+
Animations are created by: 1. Transmitting a base image (becomes frame 1) 2.
+
Adding frames using the frame action ([a=f]) 3. Controlling playback with
+
animation commands
Frame numbers are 1-based:
- Frame 1: The original/base image
···
{2 Frame Positioning}
-
For partial frame updates, [x] and [y] specify where the new pixel
-
data is placed within the frame canvas:
+
For partial frame updates, [x] and [y] specify where the new pixel data is
+
placed within the frame canvas:
- [x]: Left edge position in pixels (default 0)
- [y]: Top edge position in pixels (default 0)
-
The frame data dimensions come from the [width] and [height] parameters
-
of the frame command.
+
The frame data dimensions come from the [width] and [height] parameters of
+
the frame command.
{2 Frame Canvas}
Each frame needs a background canvas to composite onto. Options:
-
{b Solid color background} ([background_color]):
-
Use a 32-bit RGBA color. Format: [0xRRGGBBAA] where AA is alpha.
-
Default is 0 (transparent black).
+
{b Solid color background} ([background_color]): Use a 32-bit RGBA color.
+
Format: [0xRRGGBBAA] where AA is alpha. Default is 0 (transparent black).
-
{b Copy from existing frame} ([base_frame]):
-
Use another frame as the starting canvas. Specified as 1-based frame
-
number. The base frame's pixels are copied, then new data is composited.
+
{b Copy from existing frame} ([base_frame]): Use another frame as the
+
starting canvas. Specified as 1-based frame number. The base frame's pixels
+
are copied, then new data is composited.
{2 Editing Existing Frames}
···
{2 Frame Timing}
-
The [gap_ms] parameter controls the delay before transitioning to
-
the next frame:
+
The [gap_ms] parameter controls the delay before transitioning to the next
+
frame:
- Positive value: Delay in milliseconds
- Zero: Ignored (keeps existing gap)
-
- Negative value: "Gapless" frame - not displayed, used as a base
-
for other frames
+
- Negative value: "Gapless" frame - not displayed, used as a base for other
+
frames
-
Default gap for new frames is 40ms. The root frame (frame 1) has
-
a default gap of 0ms.
+
Default gap for new frames is 40ms. The root frame (frame 1) has a default
+
gap of 0ms.
{2 Composition Mode}
-
The [composition] parameter controls how new pixel data is blended
-
onto the canvas. *)
+
The [composition] parameter controls how new pixel data is blended onto the
+
canvas. *)
type t
(** Animation frame configuration. Opaque type; use {!make} to construct. *)
···
t
(** Create a frame specification.
-
@param x Left edge where frame data is placed in pixels (default 0).
-
Protocol key: [x].
-
@param y Top edge where frame data is placed in pixels (default 0).
-
Protocol key: [y].
-
@param base_frame 1-based frame number to use as background canvas.
-
Frame 1 is the root image. Protocol key: [c].
-
@param edit_frame 1-based frame number to edit instead of creating new.
-
If 0 or unset, a new frame is created. Protocol key: [r].
-
@param gap_ms Delay before next frame in milliseconds. Negative values
-
create gapless frames. Protocol key: [z].
-
@param composition How to blend new pixels onto the canvas.
-
Default is alpha blending. Protocol key: [X].
-
@param background_color 32-bit RGBA background color when not using
-
a base frame. Format: [0xRRGGBBAA]. Protocol key: [Y]. *)
+
@param x
+
Left edge where frame data is placed in pixels (default 0). Protocol key:
+
[x].
+
@param y
+
Top edge where frame data is placed in pixels (default 0). Protocol key:
+
[y].
+
@param base_frame
+
1-based frame number to use as background canvas. Frame 1 is the root
+
image. Protocol key: [c].
+
@param edit_frame
+
1-based frame number to edit instead of creating new. If 0 or unset, a new
+
frame is created. Protocol key: [r].
+
@param gap_ms
+
Delay before next frame in milliseconds. Negative values create gapless
+
frames. Protocol key: [z].
+
@param composition
+
How to blend new pixels onto the canvas. Default is alpha blending.
+
Protocol key: [X].
+
@param background_color
+
32-bit RGBA background color when not using a base frame. Format:
+
[0xRRGGBBAA]. Protocol key: [Y]. *)
val empty : t
(** Empty frame spec with all defaults.
-
Creates a new frame with transparent black background, composited
-
at position (0, 0) with default timing (40ms gap). *)
+
Creates a new frame with transparent black background, composited at
+
position (0, 0) with default timing (40ms gap). *)
(** {1 Field Accessors} *)
+44 -36
lib/kgp_placement.mli
···
- [source_x], [source_y]: Top-left corner in pixels (default: 0, 0)
- [source_width], [source_height]: Size in pixels (default: full image)
-
The displayed area is the intersection of this rectangle with the
-
actual image bounds. This allows cropping images without modifying
-
the original data.
+
The displayed area is the intersection of this rectangle with the actual
+
image bounds. This allows cropping images without modifying the original
+
data.
{2 Cell-Based Sizing}
···
- [columns]: Number of columns to span (width in cells)
- [rows]: Number of rows to span (height in cells)
-
If both are specified, the source rectangle is scaled to fit.
-
If only one is specified, the other is computed to maintain aspect ratio.
-
If neither is specified, the image is displayed at natural size.
+
If both are specified, the source rectangle is scaled to fit. If only one is
+
specified, the other is computed to maintain aspect ratio. If neither is
+
specified, the image is displayed at natural size.
{2 Pixel Offsets}
···
- Positive values: drawn above text
- Zero: drawn at text level
- Negative values: drawn below text
-
- Values < INT32_MIN/2 (-1,073,741,824): drawn under cells with
-
non-default background colors
+
- Values < INT32_MIN/2 (-1,073,741,824): drawn under cells with non-default
+
background colors
-
Overlapping images with the same z-index are ordered by image ID
-
(lower ID draws first/underneath).
+
Overlapping images with the same z-index are ordered by image ID (lower ID
+
draws first/underneath).
{2 Placement IDs}
···
- Deleting specific placements
- Moving placements by resending with same image_id + placement_id
-
If [placement_id] is 0 or unspecified, each display creates an
-
independent placement. *)
+
If [placement_id] is 0 or unspecified, each display creates an independent
+
placement. *)
type t
(** Placement configuration. Opaque type; use {!make} to construct. *)
···
t
(** Create a placement configuration.
-
@param source_x Left edge of source rectangle in pixels (default 0).
-
Protocol key: [x].
-
@param source_y Top edge of source rectangle in pixels (default 0).
-
Protocol key: [y].
-
@param source_width Width of source rectangle in pixels.
-
Default is the full image width. Protocol key: [w].
-
@param source_height Height of source rectangle in pixels.
-
Default is the full image height. Protocol key: [h].
-
@param cell_x_offset X offset within the first cell in pixels.
-
Must be smaller than cell width. Protocol key: [X].
-
@param cell_y_offset Y offset within the first cell in pixels.
-
Must be smaller than cell height. Protocol key: [Y].
-
@param columns Number of columns to display over. Image is scaled
-
to fit. Protocol key: [c].
-
@param rows Number of rows to display over. Image is scaled to fit.
-
Protocol key: [r].
-
@param z_index Stacking order. Positive = above text, negative = below.
-
Protocol key: [z].
-
@param placement_id Unique ID (1-4294967295) for this placement.
-
Allows updating/deleting specific placements. Protocol key: [p].
+
@param source_x
+
Left edge of source rectangle in pixels (default 0). Protocol key: [x].
+
@param source_y
+
Top edge of source rectangle in pixels (default 0). Protocol key: [y].
+
@param source_width
+
Width of source rectangle in pixels. Default is the full image width.
+
Protocol key: [w].
+
@param source_height
+
Height of source rectangle in pixels. Default is the full image height.
+
Protocol key: [h].
+
@param cell_x_offset
+
X offset within the first cell in pixels. Must be smaller than cell width.
+
Protocol key: [X].
+
@param cell_y_offset
+
Y offset within the first cell in pixels. Must be smaller than cell
+
height. Protocol key: [Y].
+
@param columns
+
Number of columns to display over. Image is scaled to fit. Protocol key:
+
[c].
+
@param rows
+
Number of rows to display over. Image is scaled to fit. Protocol key: [r].
+
@param z_index
+
Stacking order. Positive = above text, negative = below. Protocol key:
+
[z].
+
@param placement_id
+
Unique ID (1-4294967295) for this placement. Allows updating/deleting
+
specific placements. Protocol key: [p].
@param cursor Cursor movement policy after display.
-
@param unicode_placeholder If true, creates a virtual (invisible)
-
placement for Unicode placeholder mode. Protocol key: [U=1]. *)
+
@param unicode_placeholder
+
If true, creates a virtual (invisible) placement for Unicode placeholder
+
mode. Protocol key: [U=1]. *)
val empty : t
(** Empty placement with all defaults.
-
Equivalent to [make ()]. The image displays at natural size at the
-
current cursor position with default z-index (0). *)
+
Equivalent to [make ()]. The image displays at natural size at the current
+
cursor position with default z-index (0). *)
(** {1 Field Accessors} *)
+14 -14
lib/kgp_quiet.mli
···
- On success: [ESC _Gi=ID;OK ESC]
- On failure: [ESC _Gi=ID;ECODE:message ESC]
-
Response processing requires reading from the terminal, which can be
-
complex in some applications.
+
Response processing requires reading from the terminal, which can be complex
+
in some applications.
{2 Use Cases}
-
[{`Noisy}] (default): Use when you need to verify operations succeeded
-
or want to handle errors programmatically.
+
[{`Noisy}] (default): Use when you need to verify operations succeeded or
+
want to handle errors programmatically.
[{`Errors_only}]: Use when you want to detect failures but don't need
confirmation of success. Reduces response traffic.
-
[{`Silent}]: Use in fire-and-forget scenarios like shell scripts or
-
when the application cannot easily read terminal responses. Also useful
-
for high-frequency animation updates where response processing would
-
add latency. *)
+
[{`Silent}]: Use in fire-and-forget scenarios like shell scripts or when the
+
application cannot easily read terminal responses. Also useful for
+
high-frequency animation updates where response processing would add
+
latency. *)
type t = [ `Noisy | `Errors_only | `Silent ]
(** Response suppression levels.
- [`Noisy] - Send all responses including OK confirmations (default).
Required for detecting success and getting assigned image IDs.
-
- [`Errors_only] - Suppress OK responses, only send error messages.
-
Useful when success is expected but errors should be caught.
-
- [`Silent] - Suppress all responses including errors. Useful for
-
shell scripts or when response handling is not possible. *)
+
- [`Errors_only] - Suppress OK responses, only send error messages. Useful
+
when success is expected but errors should be caught.
+
- [`Silent] - Suppress all responses including errors. Useful for shell
+
scripts or when response handling is not possible. *)
val to_int : t -> int
(** Convert to protocol integer.
-
Returns 0 for [`Noisy], 1 for [`Errors_only], or 2 for [`Silent].
-
These values are used in the [q=] control data key. *)
+
Returns 0 for [`Noisy], 1 for [`Errors_only], or 2 for [`Silent]. These
+
values are used in the [q=] control data key. *)
+1 -1
lib/kgp_response.ml
···
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
+27 -31
lib/kgp_terminal.ml
···
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)
+
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)
+
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 ()
+
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_interactive () = Unix.isatty Unix.stdout
let is_pager () =
(* Not interactive = likely piped to pager *)
-
not (is_interactive ()) ||
+
(not (is_interactive ()))
+
||
(* PAGER set and not in a known graphics terminal *)
(Option.is_some (Sys.getenv_opt "PAGER") && not (is_graphics_terminal ()))
···
| `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
+
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
-
else if is_graphics_terminal () then
-
`Graphics
-
else
-
`Placeholder
let supports_graphics mode =
match resolve_mode mode with
+8 -9
lib/kgp_tmux.ml
···
(* Tmux Passthrough Support - Implementation *)
-
let is_active () =
-
Option.is_some (Sys.getenv_opt "TMUX")
+
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;
+
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
+
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
+
let wrap s = if is_active () then wrap_always s else s
+20 -19
lib/kgp_tmux.mli
···
(** Tmux Passthrough Support
-
Support for passing graphics protocol escape sequences through tmux
-
to the underlying terminal emulator.
+
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.
+
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 ;]
···
{2 Requirements}
For tmux passthrough to work:
-
- tmux version 3.3 or later
-
- [allow-passthrough] must be enabled in tmux.conf:
-
{v set -g allow-passthrough on v}
+
{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
+
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. *)
+
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:
+
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
···
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. *)
+
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. *)
+
More efficient than {!wrap_always} when building output in a buffer, as it
+
avoids allocating an intermediate string. *)
+11 -11
lib/kgp_transmission.mli
···
{2 Direct Transmission}
-
[{`Direct}] sends data inline within the escape sequence itself. The data
-
is base64-encoded in the payload section. This is the simplest method and
-
works over any connection (including SSH).
+
[{`Direct}] sends data inline within the escape sequence itself. The data is
+
base64-encoded in the payload section. This is the simplest method and works
+
over any connection (including SSH).
For images larger than 4096 bytes (after base64 encoding), the data is
automatically split into chunks using the [m=] key:
···
{2 File Transmission}
-
[{`File}] tells the terminal to read data from a file path. The path is
-
sent base64-encoded in the payload. Additional parameters:
+
[{`File}] tells the terminal to read data from a file path. The path is sent
+
base64-encoded in the payload. Additional parameters:
- [S=] specifies the number of bytes to read
- [O=] specifies the byte offset to start reading from
-
File transmission only works when the terminal and client share a
-
filesystem (i.e., local terminals, not SSH).
+
File transmission only works when the terminal and client share a filesystem
+
(i.e., local terminals, not SSH).
-
Security: The terminal will refuse to read device files, sockets, or
-
files in sensitive locations like [/proc], [/sys], or [/dev].
+
Security: The terminal will refuse to read device files, sockets, or files
+
in sensitive locations like [/proc], [/sys], or [/dev].
{2 Temporary File Transmission}
···
- [`Direct] - Data is sent inline in the escape sequence (base64-encoded).
Works over any connection including SSH. Automatic chunking for large
images.
-
- [`File] - Terminal reads from a file path sent in the payload.
-
Only works when terminal and client share a filesystem.
+
- [`File] - Terminal reads from a file path sent in the payload. Only works
+
when terminal and client share a filesystem.
- [`Tempfile] - Like [`File] but terminal deletes the file after reading.
File must be in a recognized temporary directory. *)
+267 -39
lib/kgp_unicode.ml
···
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 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
+
if id land 0xFF000000 = 0 || id land 0x00FFFF00 = 0 then gen () else id
in
gen ()
···
(* 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));
+
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 id_diac = id_high_byte_diacritic high_byte in
+8 -6
lib/kgp_unicode.mli
···
(** 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}
···
- 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. *)
+
This ensures the foreground color encoding and diacritic encoding work
+
correctly. Uses [Random] internally. *)
val write :
Buffer.t ->
···
unit
(** 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 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. *)