Kitty Graphics Protocol in OCaml
terminal graphics ocaml

break out libraries

+1289 -1103
+2 -2
dune-project
··· 1 1 (lang dune 3.20) 2 - (name kitty_graphics) 2 + (name kgp) 3 3 4 4 (package 5 - (name kitty_graphics) 5 + (name kgp) 6 6 (synopsis "OCaml implementation of the Kitty terminal graphics protocol") 7 7 (description 8 8 "A standalone library for rendering images in terminals that support the Kitty graphics protocol. Supports image transmission, display, animation, Unicode placeholders, and terminal capability detection.")
+1 -1
example/anim_test.ml
··· 1 1 (* Minimal animation test - shows exact bytes sent *) 2 2 3 - module K = Kitty_graphics 3 + module K = Kgp 4 4 5 5 let solid_color_rgba ~width ~height ~r ~g ~b ~a = 6 6 let pixels = Bytes.create (width * height * 4) in
+1 -1
example/debug_anim.ml
··· 1 1 (* Debug: Output animation escape sequences for comparison with Go *) 2 2 3 - module K = Kitty_graphics 3 + module K = Kgp 4 4 5 5 let solid_color_rgba ~width ~height ~r ~g ~b ~a = 6 6 let pixels = Bytes.create (width * height * 4) in
+5 -5
example/dune
··· 1 1 (executable 2 2 (name example) 3 - (libraries kitty_graphics unix)) 3 + (libraries kgp unix)) 4 4 5 5 (executable 6 6 (name debug_anim) 7 - (libraries kitty_graphics)) 7 + (libraries kgp)) 8 8 9 9 (executable 10 10 (name test_output) 11 - (libraries kitty_graphics)) 11 + (libraries kgp)) 12 12 13 13 (executable 14 14 (name anim_test) 15 - (libraries kitty_graphics)) 15 + (libraries kgp)) 16 16 17 17 (executable 18 18 (name tiny_anim) 19 - (libraries kitty_graphics)) 19 + (libraries kgp))
+1 -1
example/example.ml
··· 1 1 (* Kitty Graphics Protocol Demo - Matching kgp/examples/demo *) 2 2 3 - module K = Kitty_graphics 3 + module K = Kgp 4 4 5 5 (* Helper: Generate a solid color RGBA image *) 6 6 let solid_color_rgba ~width ~height ~r ~g ~b ~a =
+1 -1
example/test_output.ml
··· 1 1 (* Simple test to show exact escape sequences without data *) 2 2 3 - module K = Kitty_graphics 3 + module K = Kgp 4 4 5 5 let print_escaped s = 6 6 String.iter (fun c ->
+1 -1
example/tiny_anim.ml
··· 1 1 (* Tiny animation test - no chunking needed *) 2 2 (* Uses 20x20 images which are ~1067 bytes base64 (well under 4096) *) 3 3 4 - module K = Kitty_graphics 4 + module K = Kgp 5 5 6 6 let solid_color_rgba ~width ~height ~r ~g ~b ~a = 7 7 let pixels = Bytes.create (width * height * 4) in
+2 -2
lib/dune
··· 1 1 (library 2 - (name kitty_graphics) 3 - (public_name kitty_graphics) 2 + (name kgp) 3 + (public_name kgp) 4 4 (libraries base64))
+34
lib/kgp.ml
··· 1 + (* Kitty Terminal Graphics Protocol - Main Module *) 2 + 3 + (* Re-export polymorphic variant types *) 4 + type format = Kgp_types.format 5 + type transmission = Kgp_types.transmission 6 + type compression = Kgp_types.compression 7 + type quiet = Kgp_types.quiet 8 + type cursor = Kgp_types.cursor 9 + type composition = Kgp_types.composition 10 + type delete = Kgp_types.delete 11 + type animation_state = Kgp_types.animation_state 12 + 13 + (* Type conversion modules *) 14 + module Format = Kgp_types.Format 15 + module Transmission = Kgp_types.Transmission 16 + module Compression = Kgp_types.Compression 17 + module Quiet = Kgp_types.Quiet 18 + module Cursor = Kgp_types.Cursor 19 + module Composition = Kgp_types.Composition 20 + module Delete = Kgp_types.Delete 21 + 22 + (* Configuration modules *) 23 + module Placement = Kgp_placement 24 + module Frame = Kgp_frame 25 + module Animation = Kgp_animation 26 + module Compose = Kgp_compose 27 + 28 + (* Core modules *) 29 + module Command = Kgp_command 30 + module Response = Kgp_response 31 + 32 + (* Utility modules *) 33 + module Unicode_placeholder = Kgp_unicode 34 + module Detect = Kgp_detect
+87
lib/kgp.mli
··· 1 + (** Kitty Terminal Graphics Protocol 2 + 3 + This library implements the Kitty terminal graphics protocol, allowing 4 + OCaml programs to display images in terminals that support the protocol 5 + (Kitty, WezTerm, Konsole, Ghostty, etc.). 6 + 7 + The protocol uses APC (Application Programming Command) escape sequences 8 + to transmit and display pixel graphics. Images can be transmitted as raw 9 + RGB/RGBA data or PNG, and displayed at specific positions with various 10 + placement options. 11 + 12 + {2 Basic Usage} 13 + 14 + {[ 15 + (* Display a PNG image *) 16 + let png_data = read_file "image.png" in 17 + let cmd = Kgp.Command.transmit_and_display ~format:`Png () in 18 + let buf = Buffer.create 1024 in 19 + Kgp.Command.write buf cmd ~data:png_data; 20 + print_string (Buffer.contents buf) 21 + ]} 22 + 23 + {2 Protocol Reference} 24 + 25 + See {{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics Protocol} 26 + for the full specification. *) 27 + 28 + (** {1 Polymorphic Variant Types} *) 29 + 30 + type format = Kgp_types.format 31 + (** Image data formats. [`Rgba32] is 32-bit RGBA (4 bytes per pixel), 32 + [`Rgb24] is 24-bit RGB (3 bytes per pixel), [`Png] is PNG encoded data. *) 33 + 34 + type transmission = Kgp_types.transmission 35 + (** Transmission methods. [`Direct] sends data inline, [`File] reads from a path, 36 + [`Tempfile] reads from a temp file that the terminal deletes after reading. *) 37 + 38 + type compression = Kgp_types.compression 39 + (** Compression options. [`None] for raw data, [`Zlib] for RFC 1950 compression. *) 40 + 41 + type quiet = Kgp_types.quiet 42 + (** Response suppression. [`Noisy] sends all responses (default), 43 + [`Errors_only] suppresses OK responses, [`Silent] suppresses all. *) 44 + 45 + type cursor = Kgp_types.cursor 46 + (** Cursor movement after displaying. [`Move] advances cursor (default), 47 + [`Static] keeps cursor in place. *) 48 + 49 + type composition = Kgp_types.composition 50 + (** Composition modes. [`Alpha_blend] for full blending (default), 51 + [`Overwrite] for simple pixel replacement. *) 52 + 53 + type delete = Kgp_types.delete 54 + (** Delete target specification. Each variant has two forms: one that only 55 + removes placements (e.g., [`All_visible]) and one that also frees the 56 + image data (e.g., [`All_visible_and_free]). *) 57 + 58 + type animation_state = Kgp_types.animation_state 59 + (** Animation playback state. [`Stop] halts animation, [`Loading] runs but 60 + waits for new frames at end, [`Run] runs normally and loops. *) 61 + 62 + (** {1 Type Modules} *) 63 + 64 + module Format = Kgp_types.Format 65 + module Transmission = Kgp_types.Transmission 66 + module Compression = Kgp_types.Compression 67 + module Quiet = Kgp_types.Quiet 68 + module Cursor = Kgp_types.Cursor 69 + module Composition = Kgp_types.Composition 70 + module Delete = Kgp_types.Delete 71 + 72 + (** {1 Configuration Modules} *) 73 + 74 + module Placement = Kgp_placement 75 + module Frame = Kgp_frame 76 + module Animation = Kgp_animation 77 + module Compose = Kgp_compose 78 + 79 + (** {1 Command and Response} *) 80 + 81 + module Command = Kgp_command 82 + module Response = Kgp_response 83 + 84 + (** {1 Utilities} *) 85 + 86 + module Unicode_placeholder = Kgp_unicode 87 + module Detect = Kgp_detect
+12
lib/kgp_animation.ml
··· 1 + (* Kitty Graphics Protocol Animation - Implementation *) 2 + 3 + type state = Kgp_types.animation_state 4 + 5 + type t = 6 + [ `Set_state of state * int option 7 + | `Set_gap of int * int 8 + | `Set_current of int ] 9 + 10 + let set_state ?loops state = `Set_state (state, loops) 11 + let set_gap ~frame ~gap_ms = `Set_gap (frame, gap_ms) 12 + let set_current_frame frame = `Set_current frame
+24
lib/kgp_animation.mli
··· 1 + (** Kitty Graphics Protocol Animation 2 + 3 + Animation control operations. *) 4 + 5 + type state = Kgp_types.animation_state 6 + (** Animation playback state. *) 7 + 8 + type t = 9 + [ `Set_state of state * int option 10 + | `Set_gap of int * int 11 + | `Set_current of int ] 12 + (** Animation control operations. *) 13 + 14 + val set_state : ?loops:int -> state -> t 15 + (** Set animation state. 16 + @param loops Number of loops: 0 = ignored, 1 = infinite, n = n-1 loops *) 17 + 18 + val set_gap : frame:int -> gap_ms:int -> t 19 + (** Set the gap (delay) for a specific frame. 20 + @param frame 1-based frame number 21 + @param gap_ms Delay in milliseconds (negative = gapless) *) 22 + 23 + val set_current_frame : int -> t 24 + (** Make a specific frame (1-based) the current displayed frame. *)
+332
lib/kgp_command.ml
··· 1 + (* Kitty Graphics Protocol Command - Implementation *) 2 + 3 + type action = 4 + [ `Transmit 5 + | `Transmit_and_display 6 + | `Query 7 + | `Display 8 + | `Delete 9 + | `Frame 10 + | `Animate 11 + | `Compose ] 12 + 13 + type t = { 14 + action : action; 15 + format : Kgp_types.format option; 16 + transmission : Kgp_types.transmission option; 17 + compression : Kgp_types.compression option; 18 + width : int option; 19 + height : int option; 20 + size : int option; 21 + offset : int option; 22 + quiet : Kgp_types.quiet option; 23 + image_id : int option; 24 + image_number : int option; 25 + placement : Kgp_placement.t option; 26 + delete : Kgp_types.delete option; 27 + frame : Kgp_frame.t option; 28 + animation : Kgp_animation.t option; 29 + compose : Kgp_compose.t option; 30 + } 31 + 32 + let make action = 33 + { 34 + action; 35 + format = None; 36 + transmission = None; 37 + compression = None; 38 + width = None; 39 + height = None; 40 + size = None; 41 + offset = None; 42 + quiet = None; 43 + image_id = None; 44 + image_number = None; 45 + placement = None; 46 + delete = None; 47 + frame = None; 48 + animation = None; 49 + compose = None; 50 + } 51 + 52 + let transmit ?image_id ?image_number ?format ?transmission ?compression ?width 53 + ?height ?size ?offset ?quiet () = 54 + { 55 + (make `Transmit) with 56 + image_id; 57 + image_number; 58 + format; 59 + transmission; 60 + compression; 61 + width; 62 + height; 63 + size; 64 + offset; 65 + quiet; 66 + } 67 + 68 + let transmit_and_display ?image_id ?image_number ?format ?transmission 69 + ?compression ?width ?height ?size ?offset ?quiet ?placement () = 70 + { 71 + (make `Transmit_and_display) with 72 + image_id; 73 + image_number; 74 + format; 75 + transmission; 76 + compression; 77 + width; 78 + height; 79 + size; 80 + offset; 81 + quiet; 82 + placement; 83 + } 84 + 85 + let query ?format ?transmission ?width ?height ?quiet () = 86 + { (make `Query) with format; transmission; width; height; quiet } 87 + 88 + let display ?image_id ?image_number ?placement ?quiet () = 89 + { (make `Display) with image_id; image_number; placement; quiet } 90 + 91 + let delete ?quiet del = { (make `Delete) with quiet; delete = Some del } 92 + 93 + let frame ?image_id ?image_number ?format ?transmission ?compression ?width 94 + ?height ?quiet ~frame () = 95 + { 96 + (make `Frame) with 97 + image_id; 98 + image_number; 99 + format; 100 + transmission; 101 + compression; 102 + width; 103 + height; 104 + quiet; 105 + frame = Some frame; 106 + } 107 + 108 + let animate ?image_id ?image_number ?quiet anim = 109 + { (make `Animate) with image_id; image_number; quiet; animation = Some anim } 110 + 111 + let compose ?image_id ?image_number ?quiet comp = 112 + { (make `Compose) with image_id; image_number; quiet; compose = Some comp } 113 + 114 + (* Serialization helpers *) 115 + let apc_start = "\027_G" 116 + let apc_end = "\027\\" 117 + 118 + (* Key-value writer with separator handling *) 119 + type kv_writer = { mutable first : bool; buf : Buffer.t } 120 + 121 + let kv_writer buf = { first = true; buf } 122 + 123 + let kv w key value = 124 + if not w.first then Buffer.add_char w.buf ','; 125 + w.first <- false; 126 + Buffer.add_char w.buf key; 127 + Buffer.add_char w.buf '='; 128 + Buffer.add_string w.buf value 129 + 130 + let kv_int w key value = kv w key (string_of_int value) 131 + let kv_int32 w key value = kv w key (Int32.to_string value) 132 + let kv_char w key value = kv w key (String.make 1 value) 133 + 134 + (* Conditional writers using Option.iter *) 135 + let kv_int_opt w key = Option.iter (kv_int w key) 136 + let kv_int32_opt w key = Option.iter (kv_int32 w key) 137 + 138 + let kv_int_if w key ~default opt = 139 + Option.iter (fun v -> if v <> default then kv_int w key v) opt 140 + 141 + let action_char : action -> char = function 142 + | `Transmit -> 't' 143 + | `Transmit_and_display -> 'T' 144 + | `Query -> 'q' 145 + | `Display -> 'p' 146 + | `Delete -> 'd' 147 + | `Frame -> 'f' 148 + | `Animate -> 'a' 149 + | `Compose -> 'c' 150 + 151 + let delete_char : Kgp_types.delete -> char = function 152 + | `All_visible -> 'a' 153 + | `All_visible_and_free -> 'A' 154 + | `By_id _ -> 'i' 155 + | `By_id_and_free _ -> 'I' 156 + | `By_number _ -> 'n' 157 + | `By_number_and_free _ -> 'N' 158 + | `At_cursor -> 'c' 159 + | `At_cursor_and_free -> 'C' 160 + | `At_cell _ -> 'p' 161 + | `At_cell_and_free _ -> 'P' 162 + | `At_cell_z _ -> 'q' 163 + | `At_cell_z_and_free _ -> 'Q' 164 + | `By_column _ -> 'x' 165 + | `By_column_and_free _ -> 'X' 166 + | `By_row _ -> 'y' 167 + | `By_row_and_free _ -> 'Y' 168 + | `By_z_index _ -> 'z' 169 + | `By_z_index_and_free _ -> 'Z' 170 + | `By_id_range _ -> 'r' 171 + | `By_id_range_and_free _ -> 'R' 172 + | `Frames -> 'f' 173 + | `Frames_and_free -> 'F' 174 + 175 + let write_placement w (p : Kgp_placement.t) = 176 + kv_int_opt w 'x' (Kgp_placement.source_x p); 177 + kv_int_opt w 'y' (Kgp_placement.source_y p); 178 + kv_int_opt w 'w' (Kgp_placement.source_width p); 179 + kv_int_opt w 'h' (Kgp_placement.source_height p); 180 + kv_int_opt w 'X' (Kgp_placement.cell_x_offset p); 181 + kv_int_opt w 'Y' (Kgp_placement.cell_y_offset p); 182 + kv_int_opt w 'c' (Kgp_placement.columns p); 183 + kv_int_opt w 'r' (Kgp_placement.rows p); 184 + kv_int_opt w 'z' (Kgp_placement.z_index p); 185 + kv_int_opt w 'p' (Kgp_placement.placement_id p); 186 + Kgp_placement.cursor p 187 + |> Option.iter (fun c -> 188 + kv_int_if w 'C' ~default:0 (Some (Kgp_types.Cursor.to_int c))); 189 + if Kgp_placement.unicode_placeholder p then kv_int w 'U' 1 190 + 191 + let write_delete w (d : Kgp_types.delete) = 192 + kv_char w 'd' (delete_char d); 193 + match d with 194 + | `By_id (id, pid) | `By_id_and_free (id, pid) -> 195 + kv_int w 'i' id; 196 + kv_int_opt w 'p' pid 197 + | `By_number (n, pid) | `By_number_and_free (n, pid) -> 198 + kv_int w 'I' n; 199 + kv_int_opt w 'p' pid 200 + | `At_cell (x, y) | `At_cell_and_free (x, y) -> 201 + kv_int w 'x' x; 202 + kv_int w 'y' y 203 + | `At_cell_z (x, y, z) | `At_cell_z_and_free (x, y, z) -> 204 + kv_int w 'x' x; 205 + kv_int w 'y' y; 206 + kv_int w 'z' z 207 + | `By_column c | `By_column_and_free c -> kv_int w 'x' c 208 + | `By_row r | `By_row_and_free r -> kv_int w 'y' r 209 + | `By_z_index z | `By_z_index_and_free z -> kv_int w 'z' z 210 + | `By_id_range (min_id, max_id) | `By_id_range_and_free (min_id, max_id) -> 211 + kv_int w 'x' min_id; 212 + kv_int w 'y' max_id 213 + | `All_visible | `All_visible_and_free | `At_cursor | `At_cursor_and_free 214 + | `Frames | `Frames_and_free -> 215 + () 216 + 217 + let write_frame w (f : Kgp_frame.t) = 218 + kv_int_opt w 'x' (Kgp_frame.x f); 219 + kv_int_opt w 'y' (Kgp_frame.y f); 220 + kv_int_opt w 'c' (Kgp_frame.base_frame f); 221 + kv_int_opt w 'r' (Kgp_frame.edit_frame f); 222 + kv_int_opt w 'z' (Kgp_frame.gap_ms f); 223 + Kgp_frame.composition f 224 + |> Option.iter (fun c -> 225 + kv_int_if w 'X' ~default:0 (Some (Kgp_types.Composition.to_int c))); 226 + kv_int32_opt w 'Y' (Kgp_frame.background_color f) 227 + 228 + let write_animation w : Kgp_animation.t -> unit = function 229 + | `Set_state (state, loops) -> 230 + let s = match state with `Stop -> 1 | `Loading -> 2 | `Run -> 3 in 231 + kv_int w 's' s; 232 + kv_int_opt w 'v' loops 233 + | `Set_gap (frame, gap_ms) -> 234 + kv_int w 'r' frame; 235 + kv_int w 'z' gap_ms 236 + | `Set_current frame -> kv_int w 'c' frame 237 + 238 + let write_compose w (c : Kgp_compose.t) = 239 + kv_int w 'r' (Kgp_compose.source_frame c); 240 + kv_int w 'c' (Kgp_compose.dest_frame c); 241 + kv_int_opt w 'w' (Kgp_compose.width c); 242 + kv_int_opt w 'h' (Kgp_compose.height c); 243 + kv_int_opt w 'x' (Kgp_compose.dest_x c); 244 + kv_int_opt w 'y' (Kgp_compose.dest_y c); 245 + kv_int_opt w 'X' (Kgp_compose.source_x c); 246 + kv_int_opt w 'Y' (Kgp_compose.source_y c); 247 + Kgp_compose.composition c 248 + |> Option.iter (fun comp -> 249 + kv_int_if w 'C' ~default:0 (Some (Kgp_types.Composition.to_int comp))) 250 + 251 + let write_control_data buf cmd = 252 + let w = kv_writer buf in 253 + (* Action *) 254 + kv_char w 'a' (action_char cmd.action); 255 + (* Quiet - only if non-default *) 256 + cmd.quiet 257 + |> Option.iter (fun q -> 258 + kv_int_if w 'q' ~default:0 (Some (Kgp_types.Quiet.to_int q))); 259 + (* Format *) 260 + cmd.format 261 + |> Option.iter (fun f -> kv_int w 'f' (Kgp_types.Format.to_int f)); 262 + (* Transmission - only for transmit/frame actions, always include t=d for compatibility *) 263 + (match cmd.action with 264 + | `Transmit | `Transmit_and_display | `Frame -> ( 265 + match cmd.transmission with 266 + | Some t -> kv_char w 't' (Kgp_types.Transmission.to_char t) 267 + | None -> kv_char w 't' 'd') 268 + | _ -> ()); 269 + (* Compression *) 270 + cmd.compression 271 + |> Option.iter (fun c -> 272 + Kgp_types.Compression.to_char c |> Option.iter (kv_char w 'o')); 273 + (* Dimensions *) 274 + kv_int_opt w 's' cmd.width; 275 + kv_int_opt w 'v' cmd.height; 276 + (* File size/offset *) 277 + kv_int_opt w 'S' cmd.size; 278 + kv_int_opt w 'O' cmd.offset; 279 + (* Image ID/number *) 280 + kv_int_opt w 'i' cmd.image_id; 281 + kv_int_opt w 'I' cmd.image_number; 282 + (* Complex options *) 283 + cmd.placement |> Option.iter (write_placement w); 284 + cmd.delete |> Option.iter (write_delete w); 285 + cmd.frame |> Option.iter (write_frame w); 286 + cmd.animation |> Option.iter (write_animation w); 287 + cmd.compose |> Option.iter (write_compose w); 288 + w 289 + 290 + (* Use large chunk size to avoid chunking - Kitty animation doesn't handle chunks well *) 291 + let chunk_size = 1024 * 1024 (* 1MB - effectively no chunking *) 292 + 293 + let write buf cmd ~data = 294 + Buffer.add_string buf apc_start; 295 + let w = write_control_data buf cmd in 296 + if String.length data > 0 then begin 297 + let encoded = Base64.encode_string data in 298 + let len = String.length encoded in 299 + if len <= chunk_size then ( 300 + Buffer.add_char buf ';'; 301 + Buffer.add_string buf encoded; 302 + Buffer.add_string buf apc_end) 303 + else begin 304 + (* Multiple chunks *) 305 + let rec write_chunks pos first = 306 + if pos < len then begin 307 + let remaining = len - pos in 308 + let this_chunk = min chunk_size remaining in 309 + let is_last = pos + this_chunk >= len in 310 + if first then ( 311 + kv_int w 'm' 1; 312 + Buffer.add_char buf ';'; 313 + Buffer.add_substring buf encoded pos this_chunk; 314 + Buffer.add_string buf apc_end) 315 + else ( 316 + Buffer.add_string buf apc_start; 317 + Buffer.add_string buf (if is_last then "m=0" else "m=1"); 318 + Buffer.add_char buf ';'; 319 + Buffer.add_substring buf encoded pos this_chunk; 320 + Buffer.add_string buf apc_end); 321 + write_chunks (pos + this_chunk) false 322 + end 323 + in 324 + write_chunks 0 true 325 + end 326 + end 327 + else Buffer.add_string buf apc_end 328 + 329 + let to_string cmd ~data = 330 + let buf = Buffer.create 1024 in 331 + write buf cmd ~data; 332 + Buffer.contents buf
+106
lib/kgp_command.mli
··· 1 + (** Kitty Graphics Protocol Commands 2 + 3 + This module provides functions for building and serializing graphics 4 + protocol commands. *) 5 + 6 + type t 7 + (** A graphics protocol command. *) 8 + 9 + (** {1 Image Transmission} *) 10 + 11 + val transmit : 12 + ?image_id:int -> 13 + ?image_number:int -> 14 + ?format:Kgp_types.format -> 15 + ?transmission:Kgp_types.transmission -> 16 + ?compression:Kgp_types.compression -> 17 + ?width:int -> 18 + ?height:int -> 19 + ?size:int -> 20 + ?offset:int -> 21 + ?quiet:Kgp_types.quiet -> 22 + unit -> 23 + t 24 + (** Transmit image data without displaying. *) 25 + 26 + val transmit_and_display : 27 + ?image_id:int -> 28 + ?image_number:int -> 29 + ?format:Kgp_types.format -> 30 + ?transmission:Kgp_types.transmission -> 31 + ?compression:Kgp_types.compression -> 32 + ?width:int -> 33 + ?height:int -> 34 + ?size:int -> 35 + ?offset:int -> 36 + ?quiet:Kgp_types.quiet -> 37 + ?placement:Kgp_placement.t -> 38 + unit -> 39 + t 40 + (** Transmit image data and display it immediately. *) 41 + 42 + val query : 43 + ?format:Kgp_types.format -> 44 + ?transmission:Kgp_types.transmission -> 45 + ?width:int -> 46 + ?height:int -> 47 + ?quiet:Kgp_types.quiet -> 48 + unit -> 49 + t 50 + (** Query terminal support without storing the image. *) 51 + 52 + (** {1 Display} *) 53 + 54 + val display : 55 + ?image_id:int -> 56 + ?image_number:int -> 57 + ?placement:Kgp_placement.t -> 58 + ?quiet:Kgp_types.quiet -> 59 + unit -> 60 + t 61 + (** Display a previously transmitted image. *) 62 + 63 + (** {1 Deletion} *) 64 + 65 + val delete : ?quiet:Kgp_types.quiet -> Kgp_types.delete -> t 66 + (** Delete images or placements. *) 67 + 68 + (** {1 Animation} *) 69 + 70 + val frame : 71 + ?image_id:int -> 72 + ?image_number:int -> 73 + ?format:Kgp_types.format -> 74 + ?transmission:Kgp_types.transmission -> 75 + ?compression:Kgp_types.compression -> 76 + ?width:int -> 77 + ?height:int -> 78 + ?quiet:Kgp_types.quiet -> 79 + frame:Kgp_frame.t -> 80 + unit -> 81 + t 82 + (** Transmit animation frame data. *) 83 + 84 + val animate : 85 + ?image_id:int -> 86 + ?image_number:int -> 87 + ?quiet:Kgp_types.quiet -> 88 + Kgp_animation.t -> 89 + t 90 + (** Control animation playback. *) 91 + 92 + val compose : 93 + ?image_id:int -> 94 + ?image_number:int -> 95 + ?quiet:Kgp_types.quiet -> 96 + Kgp_compose.t -> 97 + t 98 + (** Compose animation frames. *) 99 + 100 + (** {1 Output} *) 101 + 102 + val write : Buffer.t -> t -> data:string -> unit 103 + (** Write the command to a buffer. *) 104 + 105 + val to_string : t -> data:string -> string 106 + (** Convert command to a string. *)
+37
lib/kgp_compose.ml
··· 1 + (* Kitty Graphics Protocol Compose - Implementation *) 2 + 3 + type t = { 4 + source_frame : int; 5 + dest_frame : int; 6 + width : int option; 7 + height : int option; 8 + source_x : int option; 9 + source_y : int option; 10 + dest_x : int option; 11 + dest_y : int option; 12 + composition : Kgp_types.composition option; 13 + } 14 + 15 + let make ~source_frame ~dest_frame ?width ?height ?source_x ?source_y ?dest_x 16 + ?dest_y ?composition () = 17 + { 18 + source_frame; 19 + dest_frame; 20 + width; 21 + height; 22 + source_x; 23 + source_y; 24 + dest_x; 25 + dest_y; 26 + composition; 27 + } 28 + 29 + let source_frame t = t.source_frame 30 + let dest_frame t = t.dest_frame 31 + let width t = t.width 32 + let height t = t.height 33 + let source_x t = t.source_x 34 + let source_y t = t.source_y 35 + let dest_x t = t.dest_x 36 + let dest_y t = t.dest_y 37 + let composition t = t.composition
+32
lib/kgp_compose.mli
··· 1 + (** Kitty Graphics Protocol Compose 2 + 3 + Frame composition operations. *) 4 + 5 + type t 6 + (** Composition operation. *) 7 + 8 + val make : 9 + source_frame:int -> 10 + dest_frame:int -> 11 + ?width:int -> 12 + ?height:int -> 13 + ?source_x:int -> 14 + ?source_y:int -> 15 + ?dest_x:int -> 16 + ?dest_y:int -> 17 + ?composition:Kgp_types.composition -> 18 + unit -> 19 + t 20 + (** Compose a rectangle from one frame onto another. *) 21 + 22 + (** {1 Field Accessors} *) 23 + 24 + val source_frame : t -> int 25 + val dest_frame : t -> int 26 + val width : t -> int option 27 + val height : t -> int option 28 + val source_x : t -> int option 29 + val source_y : t -> int option 30 + val dest_x : t -> int option 31 + val dest_y : t -> int option 32 + val composition : t -> Kgp_types.composition option
+12
lib/kgp_detect.ml
··· 1 + (* Kitty Graphics Protocol Detection - Implementation *) 2 + 3 + let make_query () = 4 + let cmd = 5 + Kgp_command.query ~format:`Rgb24 ~transmission:`Direct ~width:1 ~height:1 () 6 + in 7 + Kgp_command.to_string cmd ~data:"\x00\x00\x00" 8 + 9 + let supports_graphics response ~da1_received = 10 + response 11 + |> Option.map Kgp_response.is_ok 12 + |> Option.value ~default:(not da1_received)
+9
lib/kgp_detect.mli
··· 1 + (** Kitty Graphics Protocol Detection 2 + 3 + Detect terminal graphics support capabilities. *) 4 + 5 + val make_query : unit -> string 6 + (** Generate a query command to test graphics support. *) 7 + 8 + val supports_graphics : Kgp_response.t option -> da1_received:bool -> bool 9 + (** Determine if graphics are supported based on query results. *)
+33
lib/kgp_frame.ml
··· 1 + (* Kitty Graphics Protocol Frame - Implementation *) 2 + 3 + type t = { 4 + x : int option; 5 + y : int option; 6 + base_frame : int option; 7 + edit_frame : int option; 8 + gap_ms : int option; 9 + composition : Kgp_types.composition option; 10 + background_color : int32 option; 11 + } 12 + 13 + let empty = 14 + { 15 + x = None; 16 + y = None; 17 + base_frame = None; 18 + edit_frame = None; 19 + gap_ms = None; 20 + composition = None; 21 + background_color = None; 22 + } 23 + 24 + let make ?x ?y ?base_frame ?edit_frame ?gap_ms ?composition ?background_color () = 25 + { x; y; base_frame; edit_frame; gap_ms; composition; background_color } 26 + 27 + let x t = t.x 28 + let y t = t.y 29 + let base_frame t = t.base_frame 30 + let edit_frame t = t.edit_frame 31 + let gap_ms t = t.gap_ms 32 + let composition t = t.composition 33 + let background_color t = t.background_color
+39
lib/kgp_frame.mli
··· 1 + (** Kitty Graphics Protocol Frame 2 + 3 + Animation frame configuration. *) 4 + 5 + type t 6 + (** Animation frame configuration. *) 7 + 8 + val make : 9 + ?x:int -> 10 + ?y:int -> 11 + ?base_frame:int -> 12 + ?edit_frame:int -> 13 + ?gap_ms:int -> 14 + ?composition:Kgp_types.composition -> 15 + ?background_color:int32 -> 16 + unit -> 17 + t 18 + (** Create a frame specification. 19 + 20 + @param x Left edge where frame data is placed (pixels) 21 + @param y Top edge where frame data is placed (pixels) 22 + @param base_frame 1-based frame number to use as background canvas 23 + @param edit_frame 1-based frame number to edit (0 = new frame) 24 + @param gap_ms Delay before next frame in milliseconds 25 + @param composition How to blend pixels onto the canvas 26 + @param background_color 32-bit RGBA background when no base frame *) 27 + 28 + val empty : t 29 + (** Empty frame spec with defaults. *) 30 + 31 + (** {1 Field Accessors} *) 32 + 33 + val x : t -> int option 34 + val y : t -> int option 35 + val base_frame : t -> int option 36 + val edit_frame : t -> int option 37 + val gap_ms : t -> int option 38 + val composition : t -> Kgp_types.composition option 39 + val background_color : t -> int32 option
+63
lib/kgp_placement.ml
··· 1 + (* Kitty Graphics Protocol Placement - Implementation *) 2 + 3 + type t = { 4 + source_x : int option; 5 + source_y : int option; 6 + source_width : int option; 7 + source_height : int option; 8 + cell_x_offset : int option; 9 + cell_y_offset : int option; 10 + columns : int option; 11 + rows : int option; 12 + z_index : int option; 13 + placement_id : int option; 14 + cursor : Kgp_types.cursor option; 15 + unicode_placeholder : bool; 16 + } 17 + 18 + let empty = 19 + { 20 + source_x = None; 21 + source_y = None; 22 + source_width = None; 23 + source_height = None; 24 + cell_x_offset = None; 25 + cell_y_offset = None; 26 + columns = None; 27 + rows = None; 28 + z_index = None; 29 + placement_id = None; 30 + cursor = None; 31 + unicode_placeholder = false; 32 + } 33 + 34 + let make ?source_x ?source_y ?source_width ?source_height ?cell_x_offset 35 + ?cell_y_offset ?columns ?rows ?z_index ?placement_id ?cursor 36 + ?(unicode_placeholder = false) () = 37 + { 38 + source_x; 39 + source_y; 40 + source_width; 41 + source_height; 42 + cell_x_offset; 43 + cell_y_offset; 44 + columns; 45 + rows; 46 + z_index; 47 + placement_id; 48 + cursor; 49 + unicode_placeholder; 50 + } 51 + 52 + let source_x t = t.source_x 53 + let source_y t = t.source_y 54 + let source_width t = t.source_width 55 + let source_height t = t.source_height 56 + let cell_x_offset t = t.cell_x_offset 57 + let cell_y_offset t = t.cell_y_offset 58 + let columns t = t.columns 59 + let rows t = t.rows 60 + let z_index t = t.z_index 61 + let placement_id t = t.placement_id 62 + let cursor t = t.cursor 63 + let unicode_placeholder t = t.unicode_placeholder
+54
lib/kgp_placement.mli
··· 1 + (** Kitty Graphics Protocol Placement 2 + 3 + Configuration for where and how to display images. *) 4 + 5 + type t 6 + (** Placement configuration. *) 7 + 8 + val make : 9 + ?source_x:int -> 10 + ?source_y:int -> 11 + ?source_width:int -> 12 + ?source_height:int -> 13 + ?cell_x_offset:int -> 14 + ?cell_y_offset:int -> 15 + ?columns:int -> 16 + ?rows:int -> 17 + ?z_index:int -> 18 + ?placement_id:int -> 19 + ?cursor:Kgp_types.cursor -> 20 + ?unicode_placeholder:bool -> 21 + unit -> 22 + t 23 + (** Create a placement configuration. 24 + 25 + @param source_x Left edge of source rectangle in pixels (default 0) 26 + @param source_y Top edge of source rectangle in pixels (default 0) 27 + @param source_width Width of source rectangle (default: full width) 28 + @param source_height Height of source rectangle (default: full height) 29 + @param cell_x_offset X offset within the first cell in pixels 30 + @param cell_y_offset Y offset within the first cell in pixels 31 + @param columns Number of columns to display over (scales image) 32 + @param rows Number of rows to display over (scales image) 33 + @param z_index Stacking order (negative = under text) 34 + @param placement_id Unique ID for this placement 35 + @param cursor Cursor movement policy after display 36 + @param unicode_placeholder Create virtual placement for Unicode mode *) 37 + 38 + val empty : t 39 + (** Empty placement with all defaults. *) 40 + 41 + (** {1 Field Accessors} *) 42 + 43 + val source_x : t -> int option 44 + val source_y : t -> int option 45 + val source_width : t -> int option 46 + val source_height : t -> int option 47 + val cell_x_offset : t -> int option 48 + val cell_y_offset : t -> int option 49 + val columns : t -> int option 50 + val rows : t -> int option 51 + val z_index : t -> int option 52 + val placement_id : t -> int option 53 + val cursor : t -> Kgp_types.cursor option 54 + val unicode_placeholder : t -> bool
+56
lib/kgp_response.ml
··· 1 + (* Kitty Graphics Protocol Response - Implementation *) 2 + 3 + type t = { 4 + message : string; 5 + image_id : int option; 6 + image_number : int option; 7 + placement_id : int option; 8 + } 9 + 10 + let is_ok t = t.message = "OK" 11 + let message t = t.message 12 + 13 + let error_code t = 14 + if is_ok t then None 15 + else 16 + String.index_opt t.message ':' 17 + |> Option.fold ~none:(Some t.message) ~some:(fun i -> 18 + Some (String.sub t.message 0 i)) 19 + 20 + let image_id t = t.image_id 21 + let image_number t = t.image_number 22 + let placement_id t = t.placement_id 23 + 24 + let parse s = 25 + let ( let* ) = Option.bind in 26 + let esc = '\027' in 27 + let len = String.length s in 28 + let* () = 29 + if len >= 5 && s.[0] = esc && s.[1] = '_' && s.[2] = 'G' then Some () 30 + else None 31 + in 32 + let* semi_pos = String.index_from_opt s 3 ';' in 33 + let rec find_end pos = 34 + if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then Some pos 35 + else if pos + 1 < len then find_end (pos + 1) 36 + else None 37 + in 38 + let* end_pos = find_end (semi_pos + 1) in 39 + let keys_str = String.sub s 3 (semi_pos - 3) in 40 + let message = String.sub s (semi_pos + 1) (end_pos - semi_pos - 1) in 41 + let parse_kv part = 42 + if String.length part >= 3 && part.[1] = '=' then 43 + Some (part.[0], String.sub part 2 (String.length part - 2)) 44 + else None 45 + in 46 + let keys = String.split_on_char ',' keys_str |> List.filter_map parse_kv in 47 + let find_int key = 48 + List.assoc_opt key keys |> Fun.flip Option.bind int_of_string_opt 49 + in 50 + Some 51 + { 52 + message; 53 + image_id = find_int 'i'; 54 + image_number = find_int 'I'; 55 + placement_id = find_int 'p'; 56 + }
+27
lib/kgp_response.mli
··· 1 + (** Kitty Graphics Protocol Response 2 + 3 + Parse and interpret terminal responses to graphics commands. *) 4 + 5 + type t 6 + (** A parsed terminal response. *) 7 + 8 + val parse : string -> t option 9 + (** Parse a response from terminal output. *) 10 + 11 + val is_ok : t -> bool 12 + (** Check if the response indicates success. *) 13 + 14 + val message : t -> string 15 + (** Get the response message. *) 16 + 17 + val error_code : t -> string option 18 + (** Extract the error code if this is an error response. *) 19 + 20 + val image_id : t -> int option 21 + (** Get the image ID from the response. *) 22 + 23 + val image_number : t -> int option 24 + (** Get the image number from the response. *) 25 + 26 + val placement_id : t -> int option 27 + (** Get the placement ID from the response. *)
+89
lib/kgp_types.ml
··· 1 + (* Kitty Graphics Protocol Types - Implementation *) 2 + 3 + type format = [ `Rgba32 | `Rgb24 | `Png ] 4 + type transmission = [ `Direct | `File | `Tempfile ] 5 + type compression = [ `None | `Zlib ] 6 + type quiet = [ `Noisy | `Errors_only | `Silent ] 7 + type cursor = [ `Move | `Static ] 8 + type composition = [ `Alpha_blend | `Overwrite ] 9 + 10 + type delete = 11 + [ `All_visible 12 + | `All_visible_and_free 13 + | `By_id of int * int option 14 + | `By_id_and_free of int * int option 15 + | `By_number of int * int option 16 + | `By_number_and_free of int * int option 17 + | `At_cursor 18 + | `At_cursor_and_free 19 + | `At_cell of int * int 20 + | `At_cell_and_free of int * int 21 + | `At_cell_z of int * int * int 22 + | `At_cell_z_and_free of int * int * int 23 + | `By_column of int 24 + | `By_column_and_free of int 25 + | `By_row of int 26 + | `By_row_and_free of int 27 + | `By_z_index of int 28 + | `By_z_index_and_free of int 29 + | `By_id_range of int * int 30 + | `By_id_range_and_free of int * int 31 + | `Frames 32 + | `Frames_and_free ] 33 + 34 + type animation_state = [ `Stop | `Loading | `Run ] 35 + 36 + module Format = struct 37 + type t = format 38 + 39 + let to_int : t -> int = function 40 + | `Rgba32 -> 32 41 + | `Rgb24 -> 24 42 + | `Png -> 100 43 + end 44 + 45 + module Transmission = struct 46 + type t = transmission 47 + 48 + let to_char : t -> char = function 49 + | `Direct -> 'd' 50 + | `File -> 'f' 51 + | `Tempfile -> 't' 52 + end 53 + 54 + module Compression = struct 55 + type t = compression 56 + 57 + let to_char : t -> char option = function 58 + | `None -> None 59 + | `Zlib -> Some 'z' 60 + end 61 + 62 + module Quiet = struct 63 + type t = quiet 64 + 65 + let to_int : t -> int = function 66 + | `Noisy -> 0 67 + | `Errors_only -> 1 68 + | `Silent -> 2 69 + end 70 + 71 + module Cursor = struct 72 + type t = cursor 73 + 74 + let to_int : t -> int = function 75 + | `Move -> 0 76 + | `Static -> 1 77 + end 78 + 79 + module Composition = struct 80 + type t = composition 81 + 82 + let to_int : t -> int = function 83 + | `Alpha_blend -> 0 84 + | `Overwrite -> 1 85 + end 86 + 87 + module Delete = struct 88 + type t = delete 89 + end
+109
lib/kgp_types.mli
··· 1 + (** Kitty Graphics Protocol Types 2 + 3 + This module defines the base polymorphic variant types used throughout 4 + the Kitty graphics protocol implementation. *) 5 + 6 + (** {1 Polymorphic Variant Types} *) 7 + 8 + type format = [ `Rgba32 | `Rgb24 | `Png ] 9 + (** Image data formats. [`Rgba32] is 32-bit RGBA (4 bytes per pixel), 10 + [`Rgb24] is 24-bit RGB (3 bytes per pixel), [`Png] is PNG encoded data. *) 11 + 12 + type transmission = [ `Direct | `File | `Tempfile ] 13 + (** Transmission methods. [`Direct] sends data inline, [`File] reads from a path, 14 + [`Tempfile] reads from a temp file that the terminal deletes after reading. *) 15 + 16 + type compression = [ `None | `Zlib ] 17 + (** Compression options. [`None] for raw data, [`Zlib] for RFC 1950 compression. *) 18 + 19 + type quiet = [ `Noisy | `Errors_only | `Silent ] 20 + (** Response suppression. [`Noisy] sends all responses (default), 21 + [`Errors_only] suppresses OK responses, [`Silent] suppresses all. *) 22 + 23 + type cursor = [ `Move | `Static ] 24 + (** Cursor movement after displaying. [`Move] advances cursor (default), 25 + [`Static] keeps cursor in place. *) 26 + 27 + type composition = [ `Alpha_blend | `Overwrite ] 28 + (** Composition modes. [`Alpha_blend] for full blending (default), 29 + [`Overwrite] for simple pixel replacement. *) 30 + 31 + type delete = 32 + [ `All_visible 33 + | `All_visible_and_free 34 + | `By_id of int * int option 35 + | `By_id_and_free of int * int option 36 + | `By_number of int * int option 37 + | `By_number_and_free of int * int option 38 + | `At_cursor 39 + | `At_cursor_and_free 40 + | `At_cell of int * int 41 + | `At_cell_and_free of int * int 42 + | `At_cell_z of int * int * int 43 + | `At_cell_z_and_free of int * int * int 44 + | `By_column of int 45 + | `By_column_and_free of int 46 + | `By_row of int 47 + | `By_row_and_free of int 48 + | `By_z_index of int 49 + | `By_z_index_and_free of int 50 + | `By_id_range of int * int 51 + | `By_id_range_and_free of int * int 52 + | `Frames 53 + | `Frames_and_free ] 54 + (** Delete target specification. Each variant has two forms: one that only 55 + removes placements (e.g., [`All_visible]) and one that also frees the 56 + image data (e.g., [`All_visible_and_free]). Tuple variants contain 57 + (image_id, optional_placement_id) or (x, y) coordinates. *) 58 + 59 + type animation_state = [ `Stop | `Loading | `Run ] 60 + (** Animation playback state. [`Stop] halts animation, [`Loading] runs but 61 + waits for new frames at end, [`Run] runs normally and loops. *) 62 + 63 + (** {1 Type Modules} *) 64 + 65 + module Format : sig 66 + type t = format 67 + 68 + val to_int : t -> int 69 + (** Convert to protocol integer value (32, 24, or 100). *) 70 + end 71 + 72 + module Transmission : sig 73 + type t = transmission 74 + 75 + val to_char : t -> char 76 + (** Convert to protocol character ('d', 'f', or 't'). *) 77 + end 78 + 79 + module Compression : sig 80 + type t = compression 81 + 82 + val to_char : t -> char option 83 + (** Convert to protocol character ([None] or [Some 'z']). *) 84 + end 85 + 86 + module Quiet : sig 87 + type t = quiet 88 + 89 + val to_int : t -> int 90 + (** Convert to protocol integer (0, 1, or 2). *) 91 + end 92 + 93 + module Cursor : sig 94 + type t = cursor 95 + 96 + val to_int : t -> int 97 + (** Convert to protocol integer (0 or 1). *) 98 + end 99 + 100 + module Composition : sig 101 + type t = composition 102 + 103 + val to_int : t -> int 104 + (** Convert to protocol integer (0 or 1). *) 105 + end 106 + 107 + module Delete : sig 108 + type t = delete 109 + end
+94
lib/kgp_unicode.ml
··· 1 + (* Kitty Graphics Protocol Unicode Placeholders - Implementation *) 2 + 3 + let placeholder_char = Uchar.of_int 0x10EEEE 4 + 5 + let diacritics = 6 + [| 7 + 0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F; 8 + 0x0346; 0x034A; 0x034B; 0x034C; 0x0350; 0x0351; 0x0352; 0x0357; 9 + 0x035B; 0x0363; 0x0364; 0x0365; 0x0366; 0x0367; 0x0368; 0x0369; 10 + 0x036A; 0x036B; 0x036C; 0x036D; 0x036E; 0x036F; 0x0483; 0x0484; 11 + 0x0485; 0x0486; 0x0487; 0x0592; 0x0593; 0x0594; 0x0595; 0x0597; 12 + 0x0598; 0x0599; 0x059C; 0x059D; 0x059E; 0x059F; 0x05A0; 0x05A1; 13 + 0x05A8; 0x05A9; 0x05AB; 0x05AC; 0x05AF; 0x05C4; 0x0610; 0x0611; 14 + 0x0612; 0x0613; 0x0614; 0x0615; 0x0616; 0x0617; 0x0657; 0x0658; 15 + 0x0659; 0x065A; 0x065B; 0x065D; 0x065E; 0x06D6; 0x06D7; 0x06D8; 16 + 0x06D9; 0x06DA; 0x06DB; 0x06DC; 0x06DF; 0x06E0; 0x06E1; 0x06E2; 17 + 0x06E4; 0x06E7; 0x06E8; 0x06EB; 0x06EC; 0x0730; 0x0732; 0x0733; 18 + 0x0735; 0x0736; 0x073A; 0x073D; 0x073F; 0x0740; 0x0741; 0x0743; 19 + 0x0745; 0x0747; 0x0749; 0x074A; 0x07EB; 0x07EC; 0x07ED; 0x07EE; 20 + 0x07EF; 0x07F0; 0x07F1; 0x07F3; 0x0816; 0x0817; 0x0818; 0x0819; 21 + 0x081B; 0x081C; 0x081D; 0x081E; 0x081F; 0x0820; 0x0821; 0x0822; 22 + 0x0823; 0x0825; 0x0826; 0x0827; 0x0829; 0x082A; 0x082B; 0x082C; 23 + 0x082D; 0x0951; 0x0953; 0x0954; 0x0F82; 0x0F83; 0x0F86; 0x0F87; 24 + 0x135D; 0x135E; 0x135F; 0x17DD; 0x193A; 0x1A17; 0x1A75; 0x1A76; 25 + 0x1A77; 0x1A78; 0x1A79; 0x1A7A; 0x1A7B; 0x1A7C; 0x1B6B; 0x1B6D; 26 + 0x1B6E; 0x1B6F; 0x1B70; 0x1B71; 0x1B72; 0x1B73; 0x1CD0; 0x1CD1; 27 + 0x1CD2; 0x1CDA; 0x1CDB; 0x1CE0; 0x1DC0; 0x1DC1; 0x1DC3; 0x1DC4; 28 + 0x1DC5; 0x1DC6; 0x1DC7; 0x1DC8; 0x1DC9; 0x1DCB; 0x1DCC; 0x1DD1; 29 + 0x1DD2; 0x1DD3; 0x1DD4; 0x1DD5; 0x1DD6; 0x1DD7; 0x1DD8; 0x1DD9; 30 + 0x1DDA; 0x1DDB; 0x1DDC; 0x1DDD; 0x1DDE; 0x1DDF; 0x1DE0; 0x1DE1; 31 + 0x1DE2; 0x1DE3; 0x1DE4; 0x1DE5; 0x1DE6; 0x1DFE; 0x20D0; 0x20D1; 32 + 0x20D4; 0x20D5; 0x20D6; 0x20D7; 0x20DB; 0x20DC; 0x20E1; 0x20E7; 33 + 0x20E9; 0x20F0; 0xA66F; 0xA67C; 0xA67D; 0xA6F0; 0xA6F1; 0xA8E0; 34 + 0xA8E1; 0xA8E2; 0xA8E3; 0xA8E4; 0xA8E5; 0xA8E6; 0xA8E7; 0xA8E8; 35 + 0xA8E9; 0xA8EA; 0xA8EB; 0xA8EC; 0xA8ED; 0xA8EE; 0xA8EF; 0xA8F0; 36 + 0xA8F1; 0xAAB0; 0xAAB2; 0xAAB3; 0xAAB7; 0xAAB8; 0xAABE; 0xAABF; 37 + 0xAAC1; 0xFE20; 0xFE21; 0xFE22; 0xFE23; 0xFE24; 0xFE25; 0xFE26; 38 + 0x10A0F; 0x10A38; 0x1D185; 0x1D186; 0x1D187; 0x1D188; 0x1D189; 39 + 0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD; 0x1D242; 0x1D243; 0x1D244; 40 + |] 41 + 42 + let diacritic n = Uchar.of_int diacritics.(n mod Array.length diacritics) 43 + let row_diacritic = diacritic 44 + let column_diacritic = diacritic 45 + let id_high_byte_diacritic = diacritic 46 + 47 + let add_uchar buf u = 48 + let code = Uchar.to_int u in 49 + let put = Buffer.add_char buf in 50 + if code < 0x80 then put (Char.chr code) 51 + else if code < 0x800 then ( 52 + put (Char.chr (0xC0 lor (code lsr 6))); 53 + put (Char.chr (0x80 lor (code land 0x3F)))) 54 + else if code < 0x10000 then ( 55 + put (Char.chr (0xE0 lor (code lsr 12))); 56 + put (Char.chr (0x80 lor ((code lsr 6) land 0x3F))); 57 + put (Char.chr (0x80 lor (code land 0x3F)))) 58 + else ( 59 + put (Char.chr (0xF0 lor (code lsr 18))); 60 + put (Char.chr (0x80 lor ((code lsr 12) land 0x3F))); 61 + put (Char.chr (0x80 lor ((code lsr 6) land 0x3F))); 62 + put (Char.chr (0x80 lor (code land 0x3F)))) 63 + 64 + let write buf ~image_id ?placement_id ~rows ~cols () = 65 + (* Set foreground color *) 66 + Printf.bprintf buf "\027[38;2;%d;%d;%dm" 67 + ((image_id lsr 16) land 0xFF) 68 + ((image_id lsr 8) land 0xFF) 69 + (image_id land 0xFF); 70 + (* Optional placement ID in underline color *) 71 + placement_id 72 + |> Option.iter (fun pid -> 73 + Printf.bprintf buf "\027[58;2;%d;%d;%dm" 74 + ((pid lsr 16) land 0xFF) 75 + ((pid lsr 8) land 0xFF) 76 + (pid land 0xFF)); 77 + (* High byte diacritic *) 78 + let high_byte = (image_id lsr 24) land 0xFF in 79 + let high_diac = 80 + if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None 81 + in 82 + (* Write grid *) 83 + for row = 0 to rows - 1 do 84 + for col = 0 to cols - 1 do 85 + add_uchar buf placeholder_char; 86 + add_uchar buf (row_diacritic row); 87 + add_uchar buf (column_diacritic col); 88 + high_diac |> Option.iter (add_uchar buf) 89 + done; 90 + if row < rows - 1 then Buffer.add_string buf "\n\r" 91 + done; 92 + (* Reset colors *) 93 + Buffer.add_string buf "\027[39m"; 94 + if Option.is_some placement_id then Buffer.add_string buf "\027[59m"
+26
lib/kgp_unicode.mli
··· 1 + (** Kitty Graphics Protocol Unicode Placeholders 2 + 3 + Support for invisible Unicode placeholder characters that encode 4 + image position metadata for accessibility and compatibility. *) 5 + 6 + val placeholder_char : Uchar.t 7 + (** The Unicode placeholder character U+10EEEE. *) 8 + 9 + val write : 10 + Buffer.t -> 11 + image_id:int -> 12 + ?placement_id:int -> 13 + rows:int -> 14 + cols:int -> 15 + unit -> 16 + unit 17 + (** Write placeholder characters to a buffer. *) 18 + 19 + val row_diacritic : int -> Uchar.t 20 + (** Get the combining diacritic for a row number (0-based). *) 21 + 22 + val column_diacritic : int -> Uchar.t 23 + (** Get the combining diacritic for a column number (0-based). *) 24 + 25 + val id_high_byte_diacritic : int -> Uchar.t 26 + (** Get the diacritic for the high byte of a 32-bit image ID. *)
-687
lib/kitty_graphics.ml
··· 1 - (* Kitty Terminal Graphics Protocol - Implementation *) 2 - 3 - (* Polymorphic variant types *) 4 - type format = [ `Rgba32 | `Rgb24 | `Png ] 5 - type transmission = [ `Direct | `File | `Tempfile ] 6 - type compression = [ `None | `Zlib ] 7 - type quiet = [ `Noisy | `Errors_only | `Silent ] 8 - type cursor = [ `Move | `Static ] 9 - type composition = [ `Alpha_blend | `Overwrite ] 10 - 11 - type delete = 12 - [ `All_visible 13 - | `All_visible_and_free 14 - | `By_id of int * int option 15 - | `By_id_and_free of int * int option 16 - | `By_number of int * int option 17 - | `By_number_and_free of int * int option 18 - | `At_cursor 19 - | `At_cursor_and_free 20 - | `At_cell of int * int 21 - | `At_cell_and_free of int * int 22 - | `At_cell_z of int * int * int 23 - | `At_cell_z_and_free of int * int * int 24 - | `By_column of int 25 - | `By_column_and_free of int 26 - | `By_row of int 27 - | `By_row_and_free of int 28 - | `By_z_index of int 29 - | `By_z_index_and_free of int 30 - | `By_id_range of int * int 31 - | `By_id_range_and_free of int * int 32 - | `Frames 33 - | `Frames_and_free ] 34 - 35 - type animation_state = [ `Stop | `Loading | `Run ] 36 - 37 - (* Modules re-export the types with conversion functions *) 38 - module Format = struct 39 - type t = format 40 - 41 - let to_int : t -> int = function 42 - | `Rgba32 -> 32 43 - | `Rgb24 -> 24 44 - | `Png -> 100 45 - end 46 - 47 - module Transmission = struct 48 - type t = transmission 49 - 50 - let to_char : t -> char = function 51 - | `Direct -> 'd' 52 - | `File -> 'f' 53 - | `Tempfile -> 't' 54 - end 55 - 56 - module Compression = struct 57 - type t = compression 58 - 59 - let to_char : t -> char option = function 60 - | `None -> None 61 - | `Zlib -> Some 'z' 62 - end 63 - 64 - module Quiet = struct 65 - type t = quiet 66 - 67 - let to_int : t -> int = function 68 - | `Noisy -> 0 69 - | `Errors_only -> 1 70 - | `Silent -> 2 71 - end 72 - 73 - module Cursor = struct 74 - type t = cursor 75 - 76 - let to_int : t -> int = function 77 - | `Move -> 0 78 - | `Static -> 1 79 - end 80 - 81 - module Composition = struct 82 - type t = composition 83 - 84 - let to_int : t -> int = function 85 - | `Alpha_blend -> 0 86 - | `Overwrite -> 1 87 - end 88 - 89 - module Delete = struct 90 - type t = delete 91 - end 92 - 93 - module Placement = struct 94 - type t = { 95 - source_x : int option; 96 - source_y : int option; 97 - source_width : int option; 98 - source_height : int option; 99 - cell_x_offset : int option; 100 - cell_y_offset : int option; 101 - columns : int option; 102 - rows : int option; 103 - z_index : int option; 104 - placement_id : int option; 105 - cursor : cursor option; 106 - unicode_placeholder : bool; 107 - } 108 - 109 - let empty = 110 - { 111 - source_x = None; 112 - source_y = None; 113 - source_width = None; 114 - source_height = None; 115 - cell_x_offset = None; 116 - cell_y_offset = None; 117 - columns = None; 118 - rows = None; 119 - z_index = None; 120 - placement_id = None; 121 - cursor = None; 122 - unicode_placeholder = false; 123 - } 124 - 125 - let make ?source_x ?source_y ?source_width ?source_height ?cell_x_offset 126 - ?cell_y_offset ?columns ?rows ?z_index ?placement_id ?cursor 127 - ?(unicode_placeholder = false) () = 128 - { 129 - source_x; 130 - source_y; 131 - source_width; 132 - source_height; 133 - cell_x_offset; 134 - cell_y_offset; 135 - columns; 136 - rows; 137 - z_index; 138 - placement_id; 139 - cursor; 140 - unicode_placeholder; 141 - } 142 - end 143 - 144 - module Frame = struct 145 - type t = { 146 - x : int option; 147 - y : int option; 148 - base_frame : int option; 149 - edit_frame : int option; 150 - gap_ms : int option; 151 - composition : composition option; 152 - background_color : int32 option; 153 - } 154 - 155 - let empty = 156 - { 157 - x = None; 158 - y = None; 159 - base_frame = None; 160 - edit_frame = None; 161 - gap_ms = None; 162 - composition = None; 163 - background_color = None; 164 - } 165 - 166 - let make ?x ?y ?base_frame ?edit_frame ?gap_ms ?composition ?background_color 167 - () = 168 - { x; y; base_frame; edit_frame; gap_ms; composition; background_color } 169 - end 170 - 171 - module Animation = struct 172 - type state = animation_state 173 - 174 - type t = 175 - [ `Set_state of state * int option 176 - | `Set_gap of int * int 177 - | `Set_current of int ] 178 - 179 - let set_state ?loops state = `Set_state (state, loops) 180 - let set_gap ~frame ~gap_ms = `Set_gap (frame, gap_ms) 181 - let set_current_frame frame = `Set_current frame 182 - end 183 - 184 - module Compose = struct 185 - type t = { 186 - source_frame : int; 187 - dest_frame : int; 188 - width : int option; 189 - height : int option; 190 - source_x : int option; 191 - source_y : int option; 192 - dest_x : int option; 193 - dest_y : int option; 194 - composition : composition option; 195 - } 196 - 197 - let make ~source_frame ~dest_frame ?width ?height ?source_x ?source_y ?dest_x 198 - ?dest_y ?composition () = 199 - { 200 - source_frame; 201 - dest_frame; 202 - width; 203 - height; 204 - source_x; 205 - source_y; 206 - dest_x; 207 - dest_y; 208 - composition; 209 - } 210 - end 211 - 212 - module Command = struct 213 - type action = 214 - [ `Transmit 215 - | `Transmit_and_display 216 - | `Query 217 - | `Display 218 - | `Delete 219 - | `Frame 220 - | `Animate 221 - | `Compose ] 222 - 223 - type t = { 224 - action : action; 225 - format : format option; 226 - transmission : transmission option; 227 - compression : compression option; 228 - width : int option; 229 - height : int option; 230 - size : int option; 231 - offset : int option; 232 - quiet : quiet option; 233 - image_id : int option; 234 - image_number : int option; 235 - placement : Placement.t option; 236 - delete : delete option; 237 - frame : Frame.t option; 238 - animation : Animation.t option; 239 - compose : Compose.t option; 240 - } 241 - 242 - let make action = 243 - { 244 - action; 245 - format = None; 246 - transmission = None; 247 - compression = None; 248 - width = None; 249 - height = None; 250 - size = None; 251 - offset = None; 252 - quiet = None; 253 - image_id = None; 254 - image_number = None; 255 - placement = None; 256 - delete = None; 257 - frame = None; 258 - animation = None; 259 - compose = None; 260 - } 261 - 262 - let transmit ?image_id ?image_number ?format ?transmission ?compression ?width 263 - ?height ?size ?offset ?quiet () = 264 - { 265 - (make `Transmit) with 266 - image_id; 267 - image_number; 268 - format; 269 - transmission; 270 - compression; 271 - width; 272 - height; 273 - size; 274 - offset; 275 - quiet; 276 - } 277 - 278 - let transmit_and_display ?image_id ?image_number ?format ?transmission 279 - ?compression ?width ?height ?size ?offset ?quiet ?placement () = 280 - { 281 - (make `Transmit_and_display) with 282 - image_id; 283 - image_number; 284 - format; 285 - transmission; 286 - compression; 287 - width; 288 - height; 289 - size; 290 - offset; 291 - quiet; 292 - placement; 293 - } 294 - 295 - let query ?format ?transmission ?width ?height ?quiet () = 296 - { (make `Query) with format; transmission; width; height; quiet } 297 - 298 - let display ?image_id ?image_number ?placement ?quiet () = 299 - { (make `Display) with image_id; image_number; placement; quiet } 300 - 301 - let delete ?quiet del = { (make `Delete) with quiet; delete = Some del } 302 - 303 - let frame ?image_id ?image_number ?format ?transmission ?compression ?width 304 - ?height ?quiet ~frame () = 305 - { 306 - (make `Frame) with 307 - image_id; 308 - image_number; 309 - format; 310 - transmission; 311 - compression; 312 - width; 313 - height; 314 - quiet; 315 - frame = Some frame; 316 - } 317 - 318 - let animate ?image_id ?image_number ?quiet anim = 319 - { (make `Animate) with image_id; image_number; quiet; animation = Some anim } 320 - 321 - let compose ?image_id ?image_number ?quiet comp = 322 - { (make `Compose) with image_id; image_number; quiet; compose = Some comp } 323 - 324 - (* Serialization helpers *) 325 - let apc_start = "\027_G" 326 - let apc_end = "\027\\" 327 - 328 - (* Key-value writer with separator handling *) 329 - type kv_writer = { mutable first : bool; buf : Buffer.t } 330 - 331 - let kv_writer buf = { first = true; buf } 332 - 333 - let kv w key value = 334 - if not w.first then Buffer.add_char w.buf ','; 335 - w.first <- false; 336 - Buffer.add_char w.buf key; 337 - Buffer.add_char w.buf '='; 338 - Buffer.add_string w.buf value 339 - 340 - let kv_int w key value = kv w key (string_of_int value) 341 - let kv_int32 w key value = kv w key (Int32.to_string value) 342 - let kv_char w key value = kv w key (String.make 1 value) 343 - 344 - (* Conditional writers using Option.iter *) 345 - let kv_int_opt w key = Option.iter (kv_int w key) 346 - let kv_int32_opt w key = Option.iter (kv_int32 w key) 347 - 348 - let kv_int_if w key ~default opt = 349 - Option.iter (fun v -> if v <> default then kv_int w key v) opt 350 - 351 - let action_char : action -> char = function 352 - | `Transmit -> 't' 353 - | `Transmit_and_display -> 'T' 354 - | `Query -> 'q' 355 - | `Display -> 'p' 356 - | `Delete -> 'd' 357 - | `Frame -> 'f' 358 - | `Animate -> 'a' 359 - | `Compose -> 'c' 360 - 361 - let delete_char : delete -> char = function 362 - | `All_visible -> 'a' 363 - | `All_visible_and_free -> 'A' 364 - | `By_id _ -> 'i' 365 - | `By_id_and_free _ -> 'I' 366 - | `By_number _ -> 'n' 367 - | `By_number_and_free _ -> 'N' 368 - | `At_cursor -> 'c' 369 - | `At_cursor_and_free -> 'C' 370 - | `At_cell _ -> 'p' 371 - | `At_cell_and_free _ -> 'P' 372 - | `At_cell_z _ -> 'q' 373 - | `At_cell_z_and_free _ -> 'Q' 374 - | `By_column _ -> 'x' 375 - | `By_column_and_free _ -> 'X' 376 - | `By_row _ -> 'y' 377 - | `By_row_and_free _ -> 'Y' 378 - | `By_z_index _ -> 'z' 379 - | `By_z_index_and_free _ -> 'Z' 380 - | `By_id_range _ -> 'r' 381 - | `By_id_range_and_free _ -> 'R' 382 - | `Frames -> 'f' 383 - | `Frames_and_free -> 'F' 384 - 385 - let write_placement w (p : Placement.t) = 386 - kv_int_opt w 'x' p.source_x; 387 - kv_int_opt w 'y' p.source_y; 388 - kv_int_opt w 'w' p.source_width; 389 - kv_int_opt w 'h' p.source_height; 390 - kv_int_opt w 'X' p.cell_x_offset; 391 - kv_int_opt w 'Y' p.cell_y_offset; 392 - kv_int_opt w 'c' p.columns; 393 - kv_int_opt w 'r' p.rows; 394 - kv_int_opt w 'z' p.z_index; 395 - kv_int_opt w 'p' p.placement_id; 396 - p.cursor |> Option.iter (fun c -> kv_int_if w 'C' ~default:0 (Some (Cursor.to_int c))); 397 - if p.unicode_placeholder then kv_int w 'U' 1 398 - 399 - let write_delete w (d : delete) = 400 - kv_char w 'd' (delete_char d); 401 - match d with 402 - | `By_id (id, pid) | `By_id_and_free (id, pid) -> 403 - kv_int w 'i' id; 404 - kv_int_opt w 'p' pid 405 - | `By_number (n, pid) | `By_number_and_free (n, pid) -> 406 - kv_int w 'I' n; 407 - kv_int_opt w 'p' pid 408 - | `At_cell (x, y) | `At_cell_and_free (x, y) -> 409 - kv_int w 'x' x; 410 - kv_int w 'y' y 411 - | `At_cell_z (x, y, z) | `At_cell_z_and_free (x, y, z) -> 412 - kv_int w 'x' x; 413 - kv_int w 'y' y; 414 - kv_int w 'z' z 415 - | `By_column c | `By_column_and_free c -> kv_int w 'x' c 416 - | `By_row r | `By_row_and_free r -> kv_int w 'y' r 417 - | `By_z_index z | `By_z_index_and_free z -> kv_int w 'z' z 418 - | `By_id_range (min_id, max_id) | `By_id_range_and_free (min_id, max_id) -> 419 - kv_int w 'x' min_id; 420 - kv_int w 'y' max_id 421 - | `All_visible | `All_visible_and_free | `At_cursor | `At_cursor_and_free 422 - | `Frames | `Frames_and_free -> 423 - () 424 - 425 - let write_frame w (f : Frame.t) = 426 - kv_int_opt w 'x' f.x; 427 - kv_int_opt w 'y' f.y; 428 - kv_int_opt w 'c' f.base_frame; 429 - kv_int_opt w 'r' f.edit_frame; 430 - kv_int_opt w 'z' f.gap_ms; 431 - f.composition 432 - |> Option.iter (fun c -> kv_int_if w 'X' ~default:0 (Some (Composition.to_int c))); 433 - kv_int32_opt w 'Y' f.background_color 434 - 435 - let write_animation w : Animation.t -> unit = function 436 - | `Set_state (state, loops) -> 437 - let s = match state with `Stop -> 1 | `Loading -> 2 | `Run -> 3 in 438 - kv_int w 's' s; 439 - kv_int_opt w 'v' loops 440 - | `Set_gap (frame, gap_ms) -> 441 - kv_int w 'r' frame; 442 - kv_int w 'z' gap_ms 443 - | `Set_current frame -> kv_int w 'c' frame 444 - 445 - let write_compose w (c : Compose.t) = 446 - kv_int w 'r' c.source_frame; 447 - kv_int w 'c' c.dest_frame; 448 - kv_int_opt w 'w' c.width; 449 - kv_int_opt w 'h' c.height; 450 - kv_int_opt w 'x' c.dest_x; 451 - kv_int_opt w 'y' c.dest_y; 452 - kv_int_opt w 'X' c.source_x; 453 - kv_int_opt w 'Y' c.source_y; 454 - c.composition 455 - |> Option.iter (fun comp -> kv_int_if w 'C' ~default:0 (Some (Composition.to_int comp))) 456 - 457 - let write_control_data buf cmd = 458 - let w = kv_writer buf in 459 - (* Action *) 460 - kv_char w 'a' (action_char cmd.action); 461 - (* Quiet - only if non-default *) 462 - cmd.quiet |> Option.iter (fun q -> kv_int_if w 'q' ~default:0 (Some (Quiet.to_int q))); 463 - (* Format *) 464 - cmd.format |> Option.iter (fun f -> kv_int w 'f' (Format.to_int f)); 465 - (* Transmission - only for transmit/frame actions, always include t=d for compatibility *) 466 - (match cmd.action with 467 - | `Transmit | `Transmit_and_display | `Frame -> 468 - (match cmd.transmission with 469 - | Some t -> kv_char w 't' (Transmission.to_char t) 470 - | None -> kv_char w 't' 'd') 471 - | _ -> ()); 472 - (* Compression *) 473 - cmd.compression |> Option.iter (fun c -> Compression.to_char c |> Option.iter (kv_char w 'o')); 474 - (* Dimensions *) 475 - kv_int_opt w 's' cmd.width; 476 - kv_int_opt w 'v' cmd.height; 477 - (* File size/offset *) 478 - kv_int_opt w 'S' cmd.size; 479 - kv_int_opt w 'O' cmd.offset; 480 - (* Image ID/number *) 481 - kv_int_opt w 'i' cmd.image_id; 482 - kv_int_opt w 'I' cmd.image_number; 483 - (* Complex options *) 484 - cmd.placement |> Option.iter (write_placement w); 485 - cmd.delete |> Option.iter (write_delete w); 486 - cmd.frame |> Option.iter (write_frame w); 487 - cmd.animation |> Option.iter (write_animation w); 488 - cmd.compose |> Option.iter (write_compose w); 489 - w 490 - 491 - (* Use large chunk size to avoid chunking - Kitty animation doesn't handle chunks well *) 492 - let chunk_size = 1024 * 1024 (* 1MB - effectively no chunking *) 493 - 494 - let write buf cmd ~data = 495 - Buffer.add_string buf apc_start; 496 - let w = write_control_data buf cmd in 497 - if String.length data > 0 then begin 498 - let encoded = Base64.encode_string data in 499 - let len = String.length encoded in 500 - if len <= chunk_size then ( 501 - Buffer.add_char buf ';'; 502 - Buffer.add_string buf encoded; 503 - Buffer.add_string buf apc_end) 504 - else begin 505 - (* Multiple chunks *) 506 - let rec write_chunks pos first = 507 - if pos < len then begin 508 - let remaining = len - pos in 509 - let this_chunk = min chunk_size remaining in 510 - let is_last = pos + this_chunk >= len in 511 - if first then ( 512 - kv_int w 'm' 1; 513 - Buffer.add_char buf ';'; 514 - Buffer.add_substring buf encoded pos this_chunk; 515 - Buffer.add_string buf apc_end) 516 - else ( 517 - Buffer.add_string buf apc_start; 518 - Buffer.add_string buf (if is_last then "m=0" else "m=1"); 519 - Buffer.add_char buf ';'; 520 - Buffer.add_substring buf encoded pos this_chunk; 521 - Buffer.add_string buf apc_end); 522 - write_chunks (pos + this_chunk) false 523 - end 524 - in 525 - write_chunks 0 true 526 - end 527 - end 528 - else Buffer.add_string buf apc_end 529 - 530 - let to_string cmd ~data = 531 - let buf = Buffer.create 1024 in 532 - write buf cmd ~data; 533 - Buffer.contents buf 534 - end 535 - 536 - module Response = struct 537 - type t = { 538 - message : string; 539 - image_id : int option; 540 - image_number : int option; 541 - placement_id : int option; 542 - } 543 - 544 - let is_ok t = t.message = "OK" 545 - let message t = t.message 546 - 547 - let error_code t = 548 - if is_ok t then None 549 - else String.index_opt t.message ':' |> Option.fold ~none:(Some t.message) ~some:(fun i -> Some (String.sub t.message 0 i)) 550 - 551 - let image_id t = t.image_id 552 - let image_number t = t.image_number 553 - let placement_id t = t.placement_id 554 - 555 - let parse s = 556 - let ( let* ) = Option.bind in 557 - let esc = '\027' in 558 - let len = String.length s in 559 - let* () = if len >= 5 && s.[0] = esc && s.[1] = '_' && s.[2] = 'G' then Some () else None in 560 - let* semi_pos = String.index_from_opt s 3 ';' in 561 - let rec find_end pos = 562 - if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then Some pos 563 - else if pos + 1 < len then find_end (pos + 1) 564 - else None 565 - in 566 - let* end_pos = find_end (semi_pos + 1) in 567 - let keys_str = String.sub s 3 (semi_pos - 3) in 568 - let message = String.sub s (semi_pos + 1) (end_pos - semi_pos - 1) in 569 - let parse_kv part = 570 - if String.length part >= 3 && part.[1] = '=' then 571 - Some (part.[0], String.sub part 2 (String.length part - 2)) 572 - else None 573 - in 574 - let keys = String.split_on_char ',' keys_str |> List.filter_map parse_kv in 575 - let find_int key = List.assoc_opt key keys |> Fun.flip Option.bind int_of_string_opt in 576 - Some 577 - { 578 - message; 579 - image_id = find_int 'i'; 580 - image_number = find_int 'I'; 581 - placement_id = find_int 'p'; 582 - } 583 - end 584 - 585 - module Unicode_placeholder = struct 586 - let placeholder_char = Uchar.of_int 0x10EEEE 587 - 588 - let diacritics = 589 - [| 590 - 0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F; 591 - 0x0346; 0x034A; 0x034B; 0x034C; 0x0350; 0x0351; 0x0352; 0x0357; 592 - 0x035B; 0x0363; 0x0364; 0x0365; 0x0366; 0x0367; 0x0368; 0x0369; 593 - 0x036A; 0x036B; 0x036C; 0x036D; 0x036E; 0x036F; 0x0483; 0x0484; 594 - 0x0485; 0x0486; 0x0487; 0x0592; 0x0593; 0x0594; 0x0595; 0x0597; 595 - 0x0598; 0x0599; 0x059C; 0x059D; 0x059E; 0x059F; 0x05A0; 0x05A1; 596 - 0x05A8; 0x05A9; 0x05AB; 0x05AC; 0x05AF; 0x05C4; 0x0610; 0x0611; 597 - 0x0612; 0x0613; 0x0614; 0x0615; 0x0616; 0x0617; 0x0657; 0x0658; 598 - 0x0659; 0x065A; 0x065B; 0x065D; 0x065E; 0x06D6; 0x06D7; 0x06D8; 599 - 0x06D9; 0x06DA; 0x06DB; 0x06DC; 0x06DF; 0x06E0; 0x06E1; 0x06E2; 600 - 0x06E4; 0x06E7; 0x06E8; 0x06EB; 0x06EC; 0x0730; 0x0732; 0x0733; 601 - 0x0735; 0x0736; 0x073A; 0x073D; 0x073F; 0x0740; 0x0741; 0x0743; 602 - 0x0745; 0x0747; 0x0749; 0x074A; 0x07EB; 0x07EC; 0x07ED; 0x07EE; 603 - 0x07EF; 0x07F0; 0x07F1; 0x07F3; 0x0816; 0x0817; 0x0818; 0x0819; 604 - 0x081B; 0x081C; 0x081D; 0x081E; 0x081F; 0x0820; 0x0821; 0x0822; 605 - 0x0823; 0x0825; 0x0826; 0x0827; 0x0829; 0x082A; 0x082B; 0x082C; 606 - 0x082D; 0x0951; 0x0953; 0x0954; 0x0F82; 0x0F83; 0x0F86; 0x0F87; 607 - 0x135D; 0x135E; 0x135F; 0x17DD; 0x193A; 0x1A17; 0x1A75; 0x1A76; 608 - 0x1A77; 0x1A78; 0x1A79; 0x1A7A; 0x1A7B; 0x1A7C; 0x1B6B; 0x1B6D; 609 - 0x1B6E; 0x1B6F; 0x1B70; 0x1B71; 0x1B72; 0x1B73; 0x1CD0; 0x1CD1; 610 - 0x1CD2; 0x1CDA; 0x1CDB; 0x1CE0; 0x1DC0; 0x1DC1; 0x1DC3; 0x1DC4; 611 - 0x1DC5; 0x1DC6; 0x1DC7; 0x1DC8; 0x1DC9; 0x1DCB; 0x1DCC; 0x1DD1; 612 - 0x1DD2; 0x1DD3; 0x1DD4; 0x1DD5; 0x1DD6; 0x1DD7; 0x1DD8; 0x1DD9; 613 - 0x1DDA; 0x1DDB; 0x1DDC; 0x1DDD; 0x1DDE; 0x1DDF; 0x1DE0; 0x1DE1; 614 - 0x1DE2; 0x1DE3; 0x1DE4; 0x1DE5; 0x1DE6; 0x1DFE; 0x20D0; 0x20D1; 615 - 0x20D4; 0x20D5; 0x20D6; 0x20D7; 0x20DB; 0x20DC; 0x20E1; 0x20E7; 616 - 0x20E9; 0x20F0; 0xA66F; 0xA67C; 0xA67D; 0xA6F0; 0xA6F1; 0xA8E0; 617 - 0xA8E1; 0xA8E2; 0xA8E3; 0xA8E4; 0xA8E5; 0xA8E6; 0xA8E7; 0xA8E8; 618 - 0xA8E9; 0xA8EA; 0xA8EB; 0xA8EC; 0xA8ED; 0xA8EE; 0xA8EF; 0xA8F0; 619 - 0xA8F1; 0xAAB0; 0xAAB2; 0xAAB3; 0xAAB7; 0xAAB8; 0xAABE; 0xAABF; 620 - 0xAAC1; 0xFE20; 0xFE21; 0xFE22; 0xFE23; 0xFE24; 0xFE25; 0xFE26; 621 - 0x10A0F; 0x10A38; 0x1D185; 0x1D186; 0x1D187; 0x1D188; 0x1D189; 622 - 0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD; 0x1D242; 0x1D243; 0x1D244; 623 - |] 624 - 625 - let diacritic n = 626 - Uchar.of_int diacritics.(n mod Array.length diacritics) 627 - 628 - let row_diacritic = diacritic 629 - let column_diacritic = diacritic 630 - let id_high_byte_diacritic = diacritic 631 - 632 - let add_uchar buf u = 633 - let code = Uchar.to_int u in 634 - let put = Buffer.add_char buf in 635 - if code < 0x80 then put (Char.chr code) 636 - else if code < 0x800 then ( 637 - put (Char.chr (0xC0 lor (code lsr 6))); 638 - put (Char.chr (0x80 lor (code land 0x3F)))) 639 - else if code < 0x10000 then ( 640 - put (Char.chr (0xE0 lor (code lsr 12))); 641 - put (Char.chr (0x80 lor ((code lsr 6) land 0x3F))); 642 - put (Char.chr (0x80 lor (code land 0x3F)))) 643 - else ( 644 - put (Char.chr (0xF0 lor (code lsr 18))); 645 - put (Char.chr (0x80 lor ((code lsr 12) land 0x3F))); 646 - put (Char.chr (0x80 lor ((code lsr 6) land 0x3F))); 647 - put (Char.chr (0x80 lor (code land 0x3F)))) 648 - 649 - let write buf ~image_id ?placement_id ~rows ~cols () = 650 - (* Set foreground color *) 651 - Printf.bprintf buf "\027[38;2;%d;%d;%dm" 652 - ((image_id lsr 16) land 0xFF) 653 - ((image_id lsr 8) land 0xFF) 654 - (image_id land 0xFF); 655 - (* Optional placement ID in underline color *) 656 - placement_id 657 - |> Option.iter (fun pid -> 658 - Printf.bprintf buf "\027[58;2;%d;%d;%dm" 659 - ((pid lsr 16) land 0xFF) 660 - ((pid lsr 8) land 0xFF) 661 - (pid land 0xFF)); 662 - (* High byte diacritic *) 663 - let high_byte = (image_id lsr 24) land 0xFF in 664 - let high_diac = if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None in 665 - (* Write grid *) 666 - for row = 0 to rows - 1 do 667 - for col = 0 to cols - 1 do 668 - add_uchar buf placeholder_char; 669 - add_uchar buf (row_diacritic row); 670 - add_uchar buf (column_diacritic col); 671 - high_diac |> Option.iter (add_uchar buf) 672 - done; 673 - if row < rows - 1 then Buffer.add_string buf "\n\r" 674 - done; 675 - (* Reset colors *) 676 - Buffer.add_string buf "\027[39m"; 677 - if Option.is_some placement_id then Buffer.add_string buf "\027[59m" 678 - end 679 - 680 - module Detect = struct 681 - let make_query () = 682 - let cmd = Command.query ~format:`Rgb24 ~transmission:`Direct ~width:1 ~height:1 () in 683 - Command.to_string cmd ~data:"\x00\x00\x00" 684 - 685 - let supports_graphics response ~da1_received = 686 - response |> Option.map Response.is_ok |> Option.value ~default:(not da1_received) 687 - end
-402
lib/kitty_graphics.mli
··· 1 - (** Kitty Terminal Graphics Protocol 2 - 3 - This library implements the Kitty terminal graphics protocol, allowing 4 - OCaml programs to display images in terminals that support the protocol 5 - (Kitty, WezTerm, Konsole, Ghostty, etc.). 6 - 7 - The protocol uses APC (Application Programming Command) escape sequences 8 - to transmit and display pixel graphics. Images can be transmitted as raw 9 - RGB/RGBA data or PNG, and displayed at specific positions with various 10 - placement options. 11 - 12 - {2 Basic Usage} 13 - 14 - {[ 15 - (* Display a PNG image *) 16 - let png_data = read_file "image.png" in 17 - let cmd = Kitty_graphics.Command.transmit_and_display ~format:`Png () in 18 - let buf = Buffer.create 1024 in 19 - Kitty_graphics.Command.write buf cmd ~data:png_data; 20 - print_string (Buffer.contents buf) 21 - ]} 22 - 23 - {2 Protocol Reference} 24 - 25 - See {{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics Protocol} 26 - for the full specification. *) 27 - 28 - (** {1 Polymorphic Variant Types} *) 29 - 30 - type format = [ `Rgba32 | `Rgb24 | `Png ] 31 - (** Image data formats. [`Rgba32] is 32-bit RGBA (4 bytes per pixel), 32 - [`Rgb24] is 24-bit RGB (3 bytes per pixel), [`Png] is PNG encoded data. *) 33 - 34 - type transmission = [ `Direct | `File | `Tempfile ] 35 - (** Transmission methods. [`Direct] sends data inline, [`File] reads from a path, 36 - [`Tempfile] reads from a temp file that the terminal deletes after reading. *) 37 - 38 - type compression = [ `None | `Zlib ] 39 - (** Compression options. [`None] for raw data, [`Zlib] for RFC 1950 compression. *) 40 - 41 - type quiet = [ `Noisy | `Errors_only | `Silent ] 42 - (** Response suppression. [`Noisy] sends all responses (default), 43 - [`Errors_only] suppresses OK responses, [`Silent] suppresses all. *) 44 - 45 - type cursor = [ `Move | `Static ] 46 - (** Cursor movement after displaying. [`Move] advances cursor (default), 47 - [`Static] keeps cursor in place. *) 48 - 49 - type composition = [ `Alpha_blend | `Overwrite ] 50 - (** Composition modes. [`Alpha_blend] for full blending (default), 51 - [`Overwrite] for simple pixel replacement. *) 52 - 53 - type delete = 54 - [ `All_visible 55 - | `All_visible_and_free 56 - | `By_id of int * int option 57 - | `By_id_and_free of int * int option 58 - | `By_number of int * int option 59 - | `By_number_and_free of int * int option 60 - | `At_cursor 61 - | `At_cursor_and_free 62 - | `At_cell of int * int 63 - | `At_cell_and_free of int * int 64 - | `At_cell_z of int * int * int 65 - | `At_cell_z_and_free of int * int * int 66 - | `By_column of int 67 - | `By_column_and_free of int 68 - | `By_row of int 69 - | `By_row_and_free of int 70 - | `By_z_index of int 71 - | `By_z_index_and_free of int 72 - | `By_id_range of int * int 73 - | `By_id_range_and_free of int * int 74 - | `Frames 75 - | `Frames_and_free ] 76 - (** Delete target specification. Each variant has two forms: one that only 77 - removes placements (e.g., [`All_visible]) and one that also frees the 78 - image data (e.g., [`All_visible_and_free]). Tuple variants contain 79 - (image_id, optional_placement_id) or (x, y) coordinates. *) 80 - 81 - type animation_state = [ `Stop | `Loading | `Run ] 82 - (** Animation playback state. [`Stop] halts animation, [`Loading] runs but 83 - waits for new frames at end, [`Run] runs normally and loops. *) 84 - 85 - (** {1 Type Modules} *) 86 - 87 - module Format : sig 88 - type t = format 89 - 90 - val to_int : t -> int 91 - (** Convert to protocol integer value (32, 24, or 100). *) 92 - end 93 - 94 - module Transmission : sig 95 - type t = transmission 96 - 97 - val to_char : t -> char 98 - (** Convert to protocol character ('d', 'f', or 't'). *) 99 - end 100 - 101 - module Compression : sig 102 - type t = compression 103 - 104 - val to_char : t -> char option 105 - (** Convert to protocol character ([None] or [Some 'z']). *) 106 - end 107 - 108 - module Quiet : sig 109 - type t = quiet 110 - 111 - val to_int : t -> int 112 - (** Convert to protocol integer (0, 1, or 2). *) 113 - end 114 - 115 - module Cursor : sig 116 - type t = cursor 117 - 118 - val to_int : t -> int 119 - (** Convert to protocol integer (0 or 1). *) 120 - end 121 - 122 - module Composition : sig 123 - type t = composition 124 - 125 - val to_int : t -> int 126 - (** Convert to protocol integer (0 or 1). *) 127 - end 128 - 129 - module Delete : sig 130 - type t = delete 131 - end 132 - 133 - (** {1 Placement Options} *) 134 - 135 - module Placement : sig 136 - type t 137 - (** Placement configuration. *) 138 - 139 - val make : 140 - ?source_x:int -> 141 - ?source_y:int -> 142 - ?source_width:int -> 143 - ?source_height:int -> 144 - ?cell_x_offset:int -> 145 - ?cell_y_offset:int -> 146 - ?columns:int -> 147 - ?rows:int -> 148 - ?z_index:int -> 149 - ?placement_id:int -> 150 - ?cursor:cursor -> 151 - ?unicode_placeholder:bool -> 152 - unit -> 153 - t 154 - (** Create a placement configuration. 155 - 156 - @param source_x Left edge of source rectangle in pixels (default 0) 157 - @param source_y Top edge of source rectangle in pixels (default 0) 158 - @param source_width Width of source rectangle (default: full width) 159 - @param source_height Height of source rectangle (default: full height) 160 - @param cell_x_offset X offset within the first cell in pixels 161 - @param cell_y_offset Y offset within the first cell in pixels 162 - @param columns Number of columns to display over (scales image) 163 - @param rows Number of rows to display over (scales image) 164 - @param z_index Stacking order (negative = under text) 165 - @param placement_id Unique ID for this placement 166 - @param cursor Cursor movement policy after display 167 - @param unicode_placeholder Create virtual placement for Unicode mode *) 168 - 169 - val empty : t 170 - (** Empty placement with all defaults. *) 171 - end 172 - 173 - (** {1 Animation} *) 174 - 175 - module Frame : sig 176 - type t 177 - (** Animation frame configuration. *) 178 - 179 - val make : 180 - ?x:int -> 181 - ?y:int -> 182 - ?base_frame:int -> 183 - ?edit_frame:int -> 184 - ?gap_ms:int -> 185 - ?composition:composition -> 186 - ?background_color:int32 -> 187 - unit -> 188 - t 189 - (** Create a frame specification. 190 - 191 - @param x Left edge where frame data is placed (pixels) 192 - @param y Top edge where frame data is placed (pixels) 193 - @param base_frame 1-based frame number to use as background canvas 194 - @param edit_frame 1-based frame number to edit (0 = new frame) 195 - @param gap_ms Delay before next frame in milliseconds 196 - @param composition How to blend pixels onto the canvas 197 - @param background_color 32-bit RGBA background when no base frame *) 198 - 199 - val empty : t 200 - (** Empty frame spec with defaults. *) 201 - end 202 - 203 - module Animation : sig 204 - type state = animation_state 205 - 206 - type t = 207 - [ `Set_state of state * int option 208 - | `Set_gap of int * int 209 - | `Set_current of int ] 210 - (** Animation control operations. *) 211 - 212 - val set_state : ?loops:int -> state -> t 213 - (** Set animation state. 214 - @param loops Number of loops: 0 = ignored, 1 = infinite, n = n-1 loops *) 215 - 216 - val set_gap : frame:int -> gap_ms:int -> t 217 - (** Set the gap (delay) for a specific frame. 218 - @param frame 1-based frame number 219 - @param gap_ms Delay in milliseconds (negative = gapless) *) 220 - 221 - val set_current_frame : int -> t 222 - (** Make a specific frame (1-based) the current displayed frame. *) 223 - end 224 - 225 - module Compose : sig 226 - type t 227 - (** Composition operation. *) 228 - 229 - val make : 230 - source_frame:int -> 231 - dest_frame:int -> 232 - ?width:int -> 233 - ?height:int -> 234 - ?source_x:int -> 235 - ?source_y:int -> 236 - ?dest_x:int -> 237 - ?dest_y:int -> 238 - ?composition:composition -> 239 - unit -> 240 - t 241 - (** Compose a rectangle from one frame onto another. *) 242 - end 243 - 244 - (** {1 Commands} *) 245 - 246 - module Command : sig 247 - type t 248 - (** A graphics protocol command. *) 249 - 250 - (** {2 Image Transmission} *) 251 - 252 - val transmit : 253 - ?image_id:int -> 254 - ?image_number:int -> 255 - ?format:format -> 256 - ?transmission:transmission -> 257 - ?compression:compression -> 258 - ?width:int -> 259 - ?height:int -> 260 - ?size:int -> 261 - ?offset:int -> 262 - ?quiet:quiet -> 263 - unit -> 264 - t 265 - (** Transmit image data without displaying. *) 266 - 267 - val transmit_and_display : 268 - ?image_id:int -> 269 - ?image_number:int -> 270 - ?format:format -> 271 - ?transmission:transmission -> 272 - ?compression:compression -> 273 - ?width:int -> 274 - ?height:int -> 275 - ?size:int -> 276 - ?offset:int -> 277 - ?quiet:quiet -> 278 - ?placement:Placement.t -> 279 - unit -> 280 - t 281 - (** Transmit image data and display it immediately. *) 282 - 283 - val query : 284 - ?format:format -> 285 - ?transmission:transmission -> 286 - ?width:int -> 287 - ?height:int -> 288 - ?quiet:quiet -> 289 - unit -> 290 - t 291 - (** Query terminal support without storing the image. *) 292 - 293 - (** {2 Display} *) 294 - 295 - val display : 296 - ?image_id:int -> 297 - ?image_number:int -> 298 - ?placement:Placement.t -> 299 - ?quiet:quiet -> 300 - unit -> 301 - t 302 - (** Display a previously transmitted image. *) 303 - 304 - (** {2 Deletion} *) 305 - 306 - val delete : ?quiet:quiet -> delete -> t 307 - (** Delete images or placements. *) 308 - 309 - (** {2 Animation} *) 310 - 311 - val frame : 312 - ?image_id:int -> 313 - ?image_number:int -> 314 - ?format:format -> 315 - ?transmission:transmission -> 316 - ?compression:compression -> 317 - ?width:int -> 318 - ?height:int -> 319 - ?quiet:quiet -> 320 - frame:Frame.t -> 321 - unit -> 322 - t 323 - (** Transmit animation frame data. *) 324 - 325 - val animate : ?image_id:int -> ?image_number:int -> ?quiet:quiet -> Animation.t -> t 326 - (** Control animation playback. *) 327 - 328 - val compose : ?image_id:int -> ?image_number:int -> ?quiet:quiet -> Compose.t -> t 329 - (** Compose animation frames. *) 330 - 331 - (** {2 Output} *) 332 - 333 - val write : Buffer.t -> t -> data:string -> unit 334 - (** Write the command to a buffer. *) 335 - 336 - val to_string : t -> data:string -> string 337 - (** Convert command to a string. *) 338 - end 339 - 340 - (** {1 Response Parsing} *) 341 - 342 - module Response : sig 343 - type t 344 - (** A parsed terminal response. *) 345 - 346 - val parse : string -> t option 347 - (** Parse a response from terminal output. *) 348 - 349 - val is_ok : t -> bool 350 - (** Check if the response indicates success. *) 351 - 352 - val message : t -> string 353 - (** Get the response message. *) 354 - 355 - val error_code : t -> string option 356 - (** Extract the error code if this is an error response. *) 357 - 358 - val image_id : t -> int option 359 - (** Get the image ID from the response. *) 360 - 361 - val image_number : t -> int option 362 - (** Get the image number from the response. *) 363 - 364 - val placement_id : t -> int option 365 - (** Get the placement ID from the response. *) 366 - end 367 - 368 - (** {1 Unicode Placeholders} *) 369 - 370 - module Unicode_placeholder : sig 371 - val placeholder_char : Uchar.t 372 - (** The Unicode placeholder character U+10EEEE. *) 373 - 374 - val write : 375 - Buffer.t -> 376 - image_id:int -> 377 - ?placement_id:int -> 378 - rows:int -> 379 - cols:int -> 380 - unit -> 381 - unit 382 - (** Write placeholder characters to a buffer. *) 383 - 384 - val row_diacritic : int -> Uchar.t 385 - (** Get the combining diacritic for a row number (0-based). *) 386 - 387 - val column_diacritic : int -> Uchar.t 388 - (** Get the combining diacritic for a column number (0-based). *) 389 - 390 - val id_high_byte_diacritic : int -> Uchar.t 391 - (** Get the diacritic for the high byte of a 32-bit image ID. *) 392 - end 393 - 394 - (** {1 Terminal Detection} *) 395 - 396 - module Detect : sig 397 - val make_query : unit -> string 398 - (** Generate a query command to test graphics support. *) 399 - 400 - val supports_graphics : Response.t option -> da1_received:bool -> bool 401 - (** Determine if graphics are supported based on query results. *) 402 - end