···11+(* Cmdliner Support for Kitty Graphics Protocol *)
22+33+open Cmdliner
44+55+let graphics_docs = "GRAPHICS OPTIONS"
66+77+let graphics_term =
88+ let doc = "Force graphics output enabled, ignoring terminal detection." in
99+ let enable = Arg.info ["g"; "graphics"] ~doc ~docs:graphics_docs in
1010+ let doc = "Disable graphics output, use text placeholders instead." in
1111+ let disable = Arg.info ["no-graphics"] ~doc ~docs:graphics_docs in
1212+ let doc = "Force tmux passthrough mode for graphics." in
1313+ let tmux = Arg.info ["tmux"] ~doc ~docs:graphics_docs in
1414+ let choose enable disable tmux : Kgp.Terminal.graphics_mode =
1515+ if enable then `Enabled
1616+ else if disable then `Disabled
1717+ else if tmux then `Tmux
1818+ else `Auto
1919+ in
2020+ Term.(const choose
2121+ $ Arg.(value & flag enable)
2222+ $ Arg.(value & flag disable)
2323+ $ Arg.(value & flag tmux))
+46
lib-cli/kgp_cli.mli
···11+(** Cmdliner Support for Kitty Graphics Protocol
22+33+ This module provides Cmdliner terms for configuring graphics output mode
44+ in CLI applications. It allows users to override auto-detection with
55+ command-line flags.
66+77+ {2 Usage}
88+99+ Add the graphics term to your command:
1010+1111+ {[
1212+ let cmd =
1313+ let graphics = Kgp_cli.graphics_term in
1414+ let my_args = ... in
1515+ Cmd.v info Term.(const my_run $ graphics $ my_args)
1616+ ]}
1717+1818+ Then use the resolved mode in your application:
1919+2020+ {[
2121+ let my_run graphics_mode args =
2222+ if Kgp.Terminal.supports_graphics graphics_mode then
2323+ (* render with graphics *)
2424+ else
2525+ (* use text fallback *)
2626+ ]} *)
2727+2828+(** {1 Terms} *)
2929+3030+val graphics_term : Kgp.Terminal.graphics_mode Cmdliner.Term.t
3131+(** Cmdliner term for graphics mode selection.
3232+3333+ Provides the following command-line options:
3434+3535+ - [--graphics] / [-g]: Force graphics output enabled
3636+ - [--no-graphics]: Force graphics output disabled (use placeholders)
3737+ - [--tmux]: Force tmux passthrough mode
3838+ - (default): Auto-detect based on terminal environment
3939+4040+ The term evaluates to a {!Kgp.Terminal.graphics_mode} value which can
4141+ be passed to {!Kgp.Terminal.supports_graphics} or {!Kgp.Terminal.resolve_mode}. *)
4242+4343+val graphics_docs : string
4444+(** Section name for graphics options in help output ("GRAPHICS OPTIONS").
4545+4646+ Use this when grouping graphics options in a separate help section. *)
···19192020 All graphics commands use the Application Programming Command (APC) format:
21212222- {v <ESC>_G<control data>;<payload><ESC>\ v}
2222+ {v <ESC>_G<control data>;<payload><ESC> v}
23232424 Where:
2525 - [ESC _G] is the APC start sequence (bytes 0x1B 0x5F 0x47)
···336336 Convenience wrapper around {!write} that returns the serialized
337337 command as a string. *)
338338339339+val write_tmux : Buffer.t -> command -> data:string -> unit
340340+(** Write the command to a buffer with tmux passthrough support.
341341+342342+ If running inside tmux (detected via [TMUX] environment variable),
343343+ wraps the graphics command in a DCS passthrough sequence so it
344344+ reaches the underlying terminal. Otherwise, behaves like {!write}.
345345+346346+ Requires tmux 3.3+ with [allow-passthrough] enabled. *)
347347+348348+val to_string_tmux : command -> data:string -> string
349349+(** Convert command to a string with tmux passthrough support.
350350+351351+ Convenience wrapper around {!write_tmux}. If running inside tmux,
352352+ wraps the output for passthrough. Otherwise, behaves like {!to_string}. *)
353353+339354(** {1 Response} *)
340355341356module Response = Kgp_response
···345360module Unicode_placeholder = Kgp_unicode
346361module Detect = Kgp_detect
347362348348-(** {1 Low-level Access} *)
363363+module Tmux = Kgp_tmux
364364+(** Tmux passthrough support. Provides functions to detect if running
365365+ inside tmux and to wrap escape sequences for passthrough. *)
366366+367367+module Terminal = Kgp_terminal
368368+(** Terminal environment detection. Provides functions to detect terminal
369369+ capabilities, pager mode, and resolve graphics output mode. *)
349370350350-module Command = Kgp_command
351351-(** Low-level command module. The command functions are also available
352352- at the top level of this module for convenience. *)
+13
lib/kgp_command.ml
···305305 let buf = Buffer.create 1024 in
306306 write buf cmd ~data;
307307 Buffer.contents buf
308308+309309+let write_tmux buf cmd ~data =
310310+ if Kgp_tmux.is_active () then begin
311311+ let inner_buf = Buffer.create 1024 in
312312+ write inner_buf cmd ~data;
313313+ Kgp_tmux.write_wrapped buf (Buffer.contents inner_buf)
314314+ end else
315315+ write buf cmd ~data
316316+317317+let to_string_tmux cmd ~data =
318318+ let buf = Buffer.create 1024 in
319319+ write_tmux buf cmd ~data;
320320+ Buffer.contents buf
+13
lib/kgp_command.mli
···108108109109val to_string : t -> data:string -> string
110110(** Convert command to a string. *)
111111+112112+val write_tmux : Buffer.t -> t -> data:string -> unit
113113+(** Write the command to a buffer with tmux passthrough wrapping.
114114+115115+ If running inside tmux (detected via [TMUX] environment variable),
116116+ wraps the graphics command in a DCS passthrough sequence. Otherwise,
117117+ behaves like {!write}. *)
118118+119119+val to_string_tmux : t -> data:string -> string
120120+(** Convert command to a string with tmux passthrough wrapping.
121121+122122+ If running inside tmux, wraps the output for passthrough.
123123+ Otherwise, behaves like {!to_string}. *)
+59
lib/kgp_terminal.ml
···11+(* Terminal Environment Detection *)
22+33+type graphics_mode = [ `Auto | `Enabled | `Disabled | `Tmux ]
44+55+let is_kitty () =
66+ Option.is_some (Sys.getenv_opt "KITTY_WINDOW_ID") ||
77+ (match Sys.getenv_opt "TERM" with
88+ | Some term -> String.lowercase_ascii term = "xterm-kitty"
99+ | None -> false) ||
1010+ (match Sys.getenv_opt "TERM_PROGRAM" with
1111+ | Some prog -> String.lowercase_ascii prog = "kitty"
1212+ | None -> false)
1313+1414+let is_wezterm () =
1515+ Option.is_some (Sys.getenv_opt "WEZTERM_PANE") ||
1616+ (match Sys.getenv_opt "TERM_PROGRAM" with
1717+ | Some prog -> String.lowercase_ascii prog = "wezterm"
1818+ | None -> false)
1919+2020+let is_ghostty () =
2121+ Option.is_some (Sys.getenv_opt "GHOSTTY_RESOURCES_DIR") ||
2222+ (match Sys.getenv_opt "TERM_PROGRAM" with
2323+ | Some prog -> String.lowercase_ascii prog = "ghostty"
2424+ | None -> false)
2525+2626+let is_graphics_terminal () =
2727+ is_kitty () || is_wezterm () || is_ghostty ()
2828+2929+let is_tmux () = Kgp_tmux.is_active ()
3030+3131+let is_interactive () =
3232+ Unix.isatty Unix.stdout
3333+3434+let is_pager () =
3535+ (* Not interactive = likely piped to pager *)
3636+ not (is_interactive ()) ||
3737+ (* PAGER set and not in a known graphics terminal *)
3838+ (Option.is_some (Sys.getenv_opt "PAGER") && not (is_graphics_terminal ()))
3939+4040+let resolve_mode = function
4141+ | `Disabled -> `Placeholder
4242+ | `Enabled -> `Graphics
4343+ | `Tmux -> `Tmux
4444+ | `Auto ->
4545+ if is_pager () || not (is_interactive ()) then
4646+ `Placeholder
4747+ else if is_tmux () then
4848+ (* Inside tmux - use passthrough if underlying terminal supports graphics *)
4949+ if is_graphics_terminal () then `Tmux
5050+ else `Placeholder
5151+ else if is_graphics_terminal () then
5252+ `Graphics
5353+ else
5454+ `Placeholder
5555+5656+let supports_graphics mode =
5757+ match resolve_mode mode with
5858+ | `Graphics | `Tmux -> true
5959+ | `Placeholder -> false
+80
lib/kgp_terminal.mli
···11+(** Terminal Environment Detection
22+33+ Detect terminal capabilities and environment for graphics protocol support.
44+55+ {2 Supported Terminals}
66+77+ The following terminals support the Kitty Graphics Protocol:
88+ - Kitty (the original implementation)
99+ - WezTerm
1010+ - Ghostty
1111+ - Konsole (partial support)
1212+1313+ {2 Environment Detection}
1414+1515+ Detection is based on environment variables:
1616+ - [KITTY_WINDOW_ID] - set by Kitty
1717+ - [WEZTERM_PANE] - set by WezTerm
1818+ - [GHOSTTY_RESOURCES_DIR] - set by Ghostty
1919+ - [TERM_PROGRAM] - may contain terminal name
2020+ - [TERM] - may contain "kitty"
2121+ - [TMUX] - set when inside tmux
2222+ - [PAGER] / output to non-tty - indicates pager mode *)
2323+2424+(** {1 Graphics Mode} *)
2525+2626+type graphics_mode = [ `Auto | `Enabled | `Disabled | `Tmux ]
2727+(** Graphics output mode.
2828+2929+ - [`Auto] - Auto-detect based on environment
3030+ - [`Enabled] - Force graphics enabled
3131+ - [`Disabled] - Force graphics disabled (use placeholders)
3232+ - [`Tmux] - Force tmux passthrough mode *)
3333+3434+(** {1 Detection} *)
3535+3636+val is_kitty : unit -> bool
3737+(** Detect if running in Kitty terminal. *)
3838+3939+val is_wezterm : unit -> bool
4040+(** Detect if running in WezTerm terminal. *)
4141+4242+val is_ghostty : unit -> bool
4343+(** Detect if running in Ghostty terminal. *)
4444+4545+val is_graphics_terminal : unit -> bool
4646+(** Detect if running in any terminal that supports the graphics protocol. *)
4747+4848+val is_tmux : unit -> bool
4949+(** Detect if running inside tmux. *)
5050+5151+val is_pager : unit -> bool
5252+(** Detect if output is likely going to a pager.
5353+5454+ Returns [true] if:
5555+ - stdout is not a tty, or
5656+ - [PAGER] environment variable is set and we're not in a known
5757+ graphics-capable terminal *)
5858+5959+val is_interactive : unit -> bool
6060+(** Detect if running interactively (stdout is a tty). *)
6161+6262+(** {1 Mode Resolution} *)
6363+6464+val resolve_mode : graphics_mode -> [ `Graphics | `Tmux | `Placeholder ]
6565+(** Resolve a graphics mode to the actual output method.
6666+6767+ - [`Graphics] - use direct graphics protocol
6868+ - [`Tmux] - use graphics protocol with tmux passthrough
6969+ - [`Placeholder] - use text placeholders (block characters)
7070+7171+ For [Auto] mode:
7272+ - If in a pager or non-interactive: [`Placeholder]
7373+ - If in tmux with graphics terminal: [`Tmux]
7474+ - If in graphics terminal: [`Graphics]
7575+ - Otherwise: [`Placeholder] *)
7676+7777+val supports_graphics : graphics_mode -> bool
7878+(** Check if the resolved mode supports graphics output.
7979+8080+ Returns [true] for [`Graphics] and [`Tmux], [false] for [`Placeholder]. *)
+23
lib/kgp_tmux.ml
···11+(* Tmux Passthrough Support - Implementation *)
22+33+let is_active () =
44+ Option.is_some (Sys.getenv_opt "TMUX")
55+66+let write_wrapped buf s =
77+ (* DCS passthrough prefix: ESC P tmux ; *)
88+ Buffer.add_string buf "\027Ptmux;";
99+ (* Double all ESC characters in the content *)
1010+ String.iter (fun c ->
1111+ if c = '\027' then Buffer.add_string buf "\027\027"
1212+ else Buffer.add_char buf c
1313+ ) s;
1414+ (* DCS terminator: ESC \ *)
1515+ Buffer.add_string buf "\027\\"
1616+1717+let wrap_always s =
1818+ let buf = Buffer.create (String.length s * 2 + 10) in
1919+ write_wrapped buf s;
2020+ Buffer.contents buf
2121+2222+let wrap s =
2323+ if is_active () then wrap_always s else s
+63
lib/kgp_tmux.mli
···11+(** Tmux Passthrough Support
22+33+ Support for passing graphics protocol escape sequences through tmux
44+ to the underlying terminal emulator.
55+66+ {2 Background}
77+88+ When running inside tmux, graphics protocol escape sequences need to be
99+ wrapped in a DCS (Device Control String) passthrough sequence so that
1010+ tmux forwards them to the actual terminal (kitty, wezterm, ghostty, etc.)
1111+ rather than interpreting them itself.
1212+1313+ The passthrough format is:
1414+ - Prefix: [ESC P tmux ;]
1515+ - Content with all ESC characters doubled
1616+ - Suffix: [ESC]
1717+1818+ {2 Requirements}
1919+2020+ For tmux passthrough to work:
2121+ - tmux version 3.3 or later
2222+ - [allow-passthrough] must be enabled in tmux.conf:
2323+ {v set -g allow-passthrough on v}
2424+2525+ {2 Usage}
2626+2727+ {[
2828+ if Kgp.Tmux.is_active () then
2929+ let wrapped = Kgp.Tmux.wrap graphics_command in
3030+ print_string wrapped
3131+ else
3232+ print_string graphics_command
3333+ ]} *)
3434+3535+val is_active : unit -> bool
3636+(** Detect if we are running inside tmux.
3737+3838+ Returns [true] if the [TMUX] environment variable is set,
3939+ indicating the process is running inside a tmux session. *)
4040+4141+val wrap : string -> string
4242+(** Wrap an escape sequence for tmux passthrough.
4343+4444+ Takes a graphics protocol escape sequence and wraps it in the
4545+ tmux DCS passthrough format:
4646+ - Adds [ESC P tmux ;] prefix
4747+ - Doubles all ESC characters in the content
4848+ - Adds [ESC] suffix
4949+5050+ If not running inside tmux, returns the input unchanged. *)
5151+5252+val wrap_always : string -> string
5353+(** Wrap an escape sequence for tmux passthrough unconditionally.
5454+5555+ Like {!wrap} but always applies the wrapping, regardless of
5656+ whether we are inside tmux. Useful when you want to pre-generate
5757+ tmux-compatible output. *)
5858+5959+val write_wrapped : Buffer.t -> string -> unit
6060+(** Write a wrapped escape sequence directly to a buffer.
6161+6262+ More efficient than {!wrap_always} when building output in a buffer,
6363+ as it avoids allocating an intermediate string. *)
+15-8
lib/kgp_unicode.ml
···4444let column_diacritic = diacritic
4545let id_high_byte_diacritic = diacritic
46464747+let next_image_id () =
4848+ let rec gen () =
4949+ let id = Random.int32 Int32.max_int |> Int32.to_int in
5050+ (* Ensure high byte and middle bytes are non-zero *)
5151+ if id land 0xFF000000 = 0 || id land 0x00FFFF00 = 0 then gen ()
5252+ else id
5353+ in
5454+ gen ()
5555+4756let add_uchar buf u =
4857 let code = Uchar.to_int u in
4958 let put = Buffer.add_char buf in
···6271 put (Char.chr (0x80 lor (code land 0x3F))))
63726473let write buf ~image_id ?placement_id ~rows ~cols () =
6565- (* Set foreground color *)
6666- Printf.bprintf buf "\027[38;2;%d;%d;%dm"
7474+ (* Set foreground color using colon subparameter format *)
7575+ Printf.bprintf buf "\027[38:2:%d:%d:%dm"
6776 ((image_id lsr 16) land 0xFF)
6877 ((image_id lsr 8) land 0xFF)
6978 (image_id land 0xFF);
7079 (* Optional placement ID in underline color *)
7180 placement_id
7281 |> Option.iter (fun pid ->
7373- Printf.bprintf buf "\027[58;2;%d;%d;%dm"
8282+ Printf.bprintf buf "\027[58:2:%d:%d:%dm"
7483 ((pid lsr 16) land 0xFF)
7584 ((pid lsr 8) land 0xFF)
7685 (pid land 0xFF));
7777- (* High byte diacritic *)
8686+ (* High byte diacritic - always written, even when 0 *)
7887 let high_byte = (image_id lsr 24) land 0xFF in
7979- let high_diac =
8080- if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None
8181- in
8888+ let id_diac = id_high_byte_diacritic high_byte in
8289 (* Write grid *)
8390 for row = 0 to rows - 1 do
8491 for col = 0 to cols - 1 do
8592 add_uchar buf placeholder_char;
8693 add_uchar buf (row_diacritic row);
8794 add_uchar buf (column_diacritic col);
8888- high_diac |> Option.iter (add_uchar buf)
9595+ add_uchar buf id_diac
8996 done;
9097 if row < rows - 1 then Buffer.add_string buf "\n\r"
9198 done;
+26-2
lib/kgp_unicode.mli
···11(** Kitty Graphics Protocol Unicode Placeholders
2233 Support for invisible Unicode placeholder characters that encode
44- image position metadata for accessibility and compatibility. *)
44+ image position metadata for accessibility and compatibility.
55+66+ {2 Image ID Requirements}
77+88+ When using unicode placeholders, image IDs must have non-zero bytes in
99+ specific positions for correct rendering:
1010+ - High byte (bits 24-31): encoded as the third combining diacritic
1111+ - Middle bytes (bits 8-23): encoded in the foreground RGB color
1212+1313+ Use {!next_image_id} to generate IDs that satisfy these requirements. *)
514615val placeholder_char : Uchar.t
716(** The Unicode placeholder character U+10EEEE. *)
8171818+val next_image_id : unit -> int
1919+(** Generate a random image ID suitable for unicode placeholders.
2020+2121+ The returned ID has non-zero bytes in all required positions:
2222+ - High byte (bits 24-31) is non-zero
2323+ - Middle bytes (bits 8-23) are non-zero
2424+2525+ This ensures the foreground color encoding and diacritic encoding
2626+ work correctly. Uses [Random] internally. *)
2727+928val write :
1029 Buffer.t ->
1130 image_id:int ->
···1433 cols:int ->
1534 unit ->
1635 unit
1717-(** Write placeholder characters to a buffer. *)
3636+(** Write placeholder characters to a buffer.
3737+3838+ @param image_id Should be generated with {!next_image_id} for correct rendering.
3939+ @param placement_id Optional placement ID for multiple placements of same image.
4040+ @param rows Number of rows in the placeholder grid.
4141+ @param cols Number of columns in the placeholder grid. *)
18421943val row_diacritic : int -> Uchar.t
2044(** Get the combining diacritic for a row number (0-based). *)