Kitty Graphics Protocol in OCaml
terminal graphics ocaml

initial import

+1899
+1
.gitignore
··· 1 + _build
+11
dune-project
··· 1 + (lang dune 3.20) 2 + (name kitty_graphics) 3 + 4 + (package 5 + (name kitty_graphics) 6 + (synopsis "OCaml implementation of the Kitty terminal graphics protocol") 7 + (description 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.") 9 + (depends 10 + (ocaml (>= 4.14.0)) 11 + base64))
+97
example/anim_test.ml
··· 1 + (* Minimal animation test - shows exact bytes sent *) 2 + 3 + module K = Kitty_graphics 4 + 5 + let solid_color_rgba ~width ~height ~r ~g ~b ~a = 6 + let pixels = Bytes.create (width * height * 4) in 7 + for i = 0 to (width * height) - 1 do 8 + let idx = i * 4 in 9 + Bytes.set pixels idx (Char.chr r); 10 + Bytes.set pixels (idx + 1) (Char.chr g); 11 + Bytes.set pixels (idx + 2) (Char.chr b); 12 + Bytes.set pixels (idx + 3) (Char.chr a) 13 + done; 14 + Bytes.to_string pixels 15 + 16 + let send cmd ~data = 17 + print_string (K.Command.to_string cmd ~data); 18 + flush stdout 19 + 20 + let () = 21 + let width, height = 40, 40 in (* Smaller for faster testing *) 22 + let image_id = 500 in 23 + 24 + (* Clear any existing image *) 25 + send (K.Command.delete ~quiet:`Errors_only (`All_visible_and_free)) ~data:""; 26 + 27 + (* Step 1: Transmit base frame (red) *) 28 + let red_frame = solid_color_rgba ~width ~height ~r:255 ~g:0 ~b:0 ~a:255 in 29 + send 30 + (K.Command.transmit 31 + ~image_id 32 + ~format:`Rgba32 33 + ~width ~height 34 + ~quiet:`Errors_only 35 + ()) 36 + ~data:red_frame; 37 + 38 + (* Step 2: Add frame (blue) *) 39 + let blue_frame = solid_color_rgba ~width ~height ~r:0 ~g:0 ~b:255 ~a:255 in 40 + send 41 + (K.Command.frame 42 + ~image_id 43 + ~format:`Rgba32 44 + ~width ~height 45 + ~frame:(K.Frame.make ~gap_ms:500 ~composition:`Overwrite ()) 46 + ~quiet:`Errors_only 47 + ()) 48 + ~data:blue_frame; 49 + 50 + (* Step 3: Add frame (green) *) 51 + let green_frame = solid_color_rgba ~width ~height ~r:0 ~g:255 ~b:0 ~a:255 in 52 + send 53 + (K.Command.frame 54 + ~image_id 55 + ~format:`Rgba32 56 + ~width ~height 57 + ~frame:(K.Frame.make ~gap_ms:500 ~composition:`Overwrite ()) 58 + ~quiet:`Errors_only 59 + ()) 60 + ~data:green_frame; 61 + 62 + (* Step 4: Create placement *) 63 + send 64 + (K.Command.display 65 + ~image_id 66 + ~placement:(K.Placement.make 67 + ~placement_id:1 68 + ~cursor:`Static 69 + ()) 70 + ~quiet:`Errors_only 71 + ()) 72 + ~data:""; 73 + 74 + (* Step 5: Set root frame gap - IMPORTANT: root frame has no gap by default *) 75 + send 76 + (K.Command.animate ~image_id (K.Animation.set_gap ~frame:1 ~gap_ms:500)) 77 + ~data:""; 78 + 79 + (* Step 6: Start animation *) 80 + send 81 + (K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run)) 82 + ~data:""; 83 + 84 + print_endline ""; 85 + print_endline "Animation should be playing (red -> blue -> green)."; 86 + print_endline "Press Enter to stop..."; 87 + flush stdout; 88 + let _ = read_line () in 89 + 90 + (* Stop animation *) 91 + send 92 + (K.Command.animate ~image_id (K.Animation.set_state `Stop)) 93 + ~data:""; 94 + 95 + (* Clean up *) 96 + send (K.Command.delete ~quiet:`Errors_only (`All_visible_and_free)) ~data:""; 97 + print_endline "Done."
+94
example/debug_anim.ml
··· 1 + (* Debug: Output animation escape sequences for comparison with Go *) 2 + 3 + module K = Kitty_graphics 4 + 5 + let solid_color_rgba ~width ~height ~r ~g ~b ~a = 6 + let pixels = Bytes.create (width * height * 4) in 7 + for i = 0 to (width * height) - 1 do 8 + let idx = i * 4 in 9 + Bytes.set pixels idx (Char.chr r); 10 + Bytes.set pixels (idx + 1) (Char.chr g); 11 + Bytes.set pixels (idx + 2) (Char.chr b); 12 + Bytes.set pixels (idx + 3) (Char.chr a) 13 + done; 14 + Bytes.to_string pixels 15 + 16 + let send cmd ~data = 17 + let s = K.Command.to_string cmd ~data in 18 + (* Print escaped version for debugging *) 19 + String.iter (fun c -> 20 + let code = Char.code c in 21 + if code = 27 then print_string "\\x1b" 22 + else if code < 32 || code > 126 then Printf.printf "\\x%02x" code 23 + else print_char c 24 + ) s; 25 + print_newline () 26 + 27 + let () = 28 + let width, height = 80, 80 in 29 + let image_id = 300 in 30 + 31 + print_endline "=== OCaml Animation Debug ===\n"; 32 + 33 + (* Step 1: Transmit base frame *) 34 + print_endline "1. Transmit base frame (a=t):"; 35 + let red_frame = solid_color_rgba ~width ~height ~r:255 ~g:0 ~b:0 ~a:255 in 36 + send 37 + (K.Command.transmit 38 + ~image_id 39 + ~format:`Rgba32 40 + ~width ~height 41 + ~quiet:`Errors_only 42 + ()) 43 + ~data:red_frame; 44 + print_newline (); 45 + 46 + (* Step 2: Add frame *) 47 + print_endline "2. Add frame (a=f):"; 48 + let orange_frame = solid_color_rgba ~width ~height ~r:255 ~g:165 ~b:0 ~a:255 in 49 + send 50 + (K.Command.frame 51 + ~image_id 52 + ~format:`Rgba32 53 + ~width ~height 54 + ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 55 + ~quiet:`Errors_only 56 + ()) 57 + ~data:orange_frame; 58 + print_newline (); 59 + 60 + (* Step 3: Put/display placement *) 61 + print_endline "3. Create placement (a=p):"; 62 + send 63 + (K.Command.display 64 + ~image_id 65 + ~placement:(K.Placement.make 66 + ~placement_id:1 67 + ~cell_x_offset:0 68 + ~cell_y_offset:0 69 + ~cursor:`Static 70 + ()) 71 + ~quiet:`Errors_only 72 + ()) 73 + ~data:""; 74 + print_newline (); 75 + 76 + (* Step 4: Set root frame gap *) 77 + print_endline "4. Set root frame gap (a=a,r=1,z=100):"; 78 + send 79 + (K.Command.animate ~image_id (K.Animation.set_gap ~frame:1 ~gap_ms:100)) 80 + ~data:""; 81 + print_newline (); 82 + 83 + (* Step 5: Animate *) 84 + print_endline "5. Start animation (a=a,s=3,v=1):"; 85 + send 86 + (K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run)) 87 + ~data:""; 88 + print_newline (); 89 + 90 + (* Step 6: Stop animation *) 91 + print_endline "6. Stop animation:"; 92 + send 93 + (K.Command.animate ~image_id (K.Animation.set_state `Stop)) 94 + ~data:""
+19
example/dune
··· 1 + (executable 2 + (name example) 3 + (libraries kitty_graphics unix)) 4 + 5 + (executable 6 + (name debug_anim) 7 + (libraries kitty_graphics)) 8 + 9 + (executable 10 + (name test_output) 11 + (libraries kitty_graphics)) 12 + 13 + (executable 14 + (name anim_test) 15 + (libraries kitty_graphics)) 16 + 17 + (executable 18 + (name tiny_anim) 19 + (libraries kitty_graphics))
+417
example/example.ml
··· 1 + (* Kitty Graphics Protocol Demo - Matching kgp/examples/demo *) 2 + 3 + module K = Kitty_graphics 4 + 5 + (* Helper: Generate a solid color RGBA image *) 6 + let solid_color_rgba ~width ~height ~r ~g ~b ~a = 7 + let pixels = Bytes.create (width * height * 4) in 8 + for i = 0 to (width * height) - 1 do 9 + let idx = i * 4 in 10 + Bytes.set pixels idx (Char.chr r); 11 + Bytes.set pixels (idx + 1) (Char.chr g); 12 + Bytes.set pixels (idx + 2) (Char.chr b); 13 + Bytes.set pixels (idx + 3) (Char.chr a) 14 + done; 15 + Bytes.to_string pixels 16 + 17 + (* Helper: Generate a solid color RGB image (no alpha) *) 18 + let solid_color_rgb ~width ~height ~r ~g ~b = 19 + let pixels = Bytes.create (width * height * 3) in 20 + for i = 0 to (width * height) - 1 do 21 + let idx = i * 3 in 22 + Bytes.set pixels idx (Char.chr r); 23 + Bytes.set pixels (idx + 1) (Char.chr g); 24 + Bytes.set pixels (idx + 2) (Char.chr b) 25 + done; 26 + Bytes.to_string pixels 27 + 28 + (* Helper: Generate a gradient RGBA image *) 29 + let gradient_rgba ~width ~height = 30 + let pixels = Bytes.create (width * height * 4) in 31 + for y = 0 to height - 1 do 32 + for x = 0 to width - 1 do 33 + let idx = (y * width + x) * 4 in 34 + let r = 255 * x / width in 35 + let b = 255 * (width - x) / width in 36 + Bytes.set pixels idx (Char.chr r); 37 + Bytes.set pixels (idx + 1) (Char.chr 128); 38 + Bytes.set pixels (idx + 2) (Char.chr b); 39 + Bytes.set pixels (idx + 3) '\xff' 40 + done 41 + done; 42 + Bytes.to_string pixels 43 + 44 + (* Helper: Read a file *) 45 + let read_file filename = 46 + let ic = open_in_bin filename in 47 + let n = in_channel_length ic in 48 + let s = really_input_string ic n in 49 + close_in ic; 50 + s 51 + 52 + let send cmd ~data = 53 + print_string (K.Command.to_string cmd ~data); 54 + flush stdout 55 + 56 + let wait_for_enter () = 57 + print_string "Press Enter to continue..."; 58 + flush stdout; 59 + let _ = read_line () in 60 + print_newline () 61 + 62 + let clear_screen () = 63 + print_string "\x1b[2J\x1b[H"; 64 + for _ = 1 to 5 do print_newline () done; 65 + flush stdout 66 + 67 + let () = 68 + let reader = stdin in 69 + ignore reader; 70 + 71 + clear_screen (); 72 + print_endline "Kitty Graphics Protocol - OCaml Demo"; 73 + print_endline "====================================="; 74 + print_newline (); 75 + print_endline "Press Enter to proceed through each demo..."; 76 + print_newline (); 77 + 78 + (* Demo 1: Basic formats - PNG *) 79 + clear_screen (); 80 + print_endline "Demo 1: Image Formats - PNG format"; 81 + (* Read sf.png and display a small portion as demo *) 82 + (try 83 + let png_data = read_file "sf.png" in 84 + send 85 + (K.Command.transmit_and_display 86 + ~image_id:1 87 + ~format:`Png 88 + ~quiet:`Errors_only 89 + ~placement:(K.Placement.make ~columns:15 ~rows:8 ()) 90 + ()) 91 + ~data:png_data; 92 + print_endline "sf.png displayed using PNG format" 93 + with _ -> 94 + (* Fallback: red square as RGBA *) 95 + let red_data = solid_color_rgba ~width:100 ~height:100 ~r:255 ~g:0 ~b:0 ~a:255 in 96 + send 97 + (K.Command.transmit_and_display 98 + ~image_id:1 99 + ~format:`Rgba32 100 + ~width:100 ~height:100 101 + ~quiet:`Errors_only 102 + ()) 103 + ~data:red_data; 104 + print_endline "Red square displayed (sf.png not found)"); 105 + print_newline (); 106 + wait_for_enter (); 107 + 108 + (* Demo 2: Basic formats - RGBA *) 109 + clear_screen (); 110 + print_endline "Demo 2: Image Formats - RGBA format (32-bit)"; 111 + let blue_data = solid_color_rgba ~width:100 ~height:100 ~r:0 ~g:0 ~b:255 ~a:255 in 112 + send 113 + (K.Command.transmit_and_display 114 + ~image_id:2 115 + ~format:`Rgba32 116 + ~width:100 ~height:100 117 + ~quiet:`Errors_only 118 + ()) 119 + ~data:blue_data; 120 + print_endline "Blue square displayed using raw RGBA format"; 121 + print_newline (); 122 + wait_for_enter (); 123 + 124 + (* Demo 3: Basic formats - RGB *) 125 + clear_screen (); 126 + print_endline "Demo 3: Image Formats - RGB format (24-bit)"; 127 + let green_data = solid_color_rgb ~width:100 ~height:100 ~r:0 ~g:255 ~b:0 in 128 + send 129 + (K.Command.transmit_and_display 130 + ~image_id:3 131 + ~format:`Rgb24 132 + ~width:100 ~height:100 133 + ~quiet:`Errors_only 134 + ()) 135 + ~data:green_data; 136 + print_endline "Green square displayed using raw RGB format (no alpha channel)"; 137 + print_newline (); 138 + wait_for_enter (); 139 + 140 + (* Demo 4: Compression - Note: would need zlib library for actual compression *) 141 + clear_screen (); 142 + print_endline "Demo 4: Large Image (compression requires zlib library)"; 143 + let orange_data = solid_color_rgba ~width:200 ~height:200 ~r:255 ~g:165 ~b:0 ~a:255 in 144 + send 145 + (K.Command.transmit_and_display 146 + ~image_id:4 147 + ~format:`Rgba32 148 + ~width:200 ~height:200 149 + ~quiet:`Errors_only 150 + ()) 151 + ~data:orange_data; 152 + Printf.printf "Orange square (200x200) - %d bytes uncompressed\n" (String.length orange_data); 153 + print_newline (); 154 + wait_for_enter (); 155 + 156 + (* Demo 5: Load and display external PNG file *) 157 + clear_screen (); 158 + print_endline "Demo 5: Loading external PNG file (sf.png)"; 159 + (try 160 + let png_data = read_file "sf.png" in 161 + send 162 + (K.Command.transmit_and_display 163 + ~image_id:10 164 + ~format:`Png 165 + ~quiet:`Errors_only 166 + ()) 167 + ~data:png_data; 168 + print_endline "sf.png loaded and displayed" 169 + with Sys_error msg -> 170 + Printf.printf "sf.png not found: %s\n" msg); 171 + print_newline (); 172 + wait_for_enter (); 173 + 174 + (* Demo 6: Cropping and scaling *) 175 + clear_screen (); 176 + print_endline "Demo 6: Cropping and Scaling - Display part of an image"; 177 + let gradient = gradient_rgba ~width:200 ~height:200 in 178 + send 179 + (K.Command.transmit_and_display 180 + ~image_id:20 181 + ~format:`Rgba32 182 + ~width:200 ~height:200 183 + ~placement:(K.Placement.make 184 + ~source_x:50 ~source_y:50 185 + ~source_width:100 ~source_height:100 186 + ~columns:10 ~rows:10 187 + ()) 188 + ~quiet:`Errors_only 189 + ()) 190 + ~data:gradient; 191 + print_endline "Cropped to center 100x100 region of a 200x200 gradient"; 192 + print_newline (); 193 + wait_for_enter (); 194 + 195 + (* Demo 7: Multiple placements *) 196 + clear_screen (); 197 + print_endline "Demo 7: Multiple Placements - One image, multiple displays"; 198 + let cyan_data = solid_color_rgba ~width:80 ~height:80 ~r:0 ~g:255 ~b:255 ~a:255 in 199 + (* Transmit once with an ID *) 200 + send 201 + (K.Command.transmit 202 + ~image_id:100 203 + ~format:`Rgba32 204 + ~width:80 ~height:80 205 + ~quiet:`Errors_only 206 + ()) 207 + ~data:cyan_data; 208 + (* Create first placement *) 209 + send 210 + (K.Command.display 211 + ~image_id:100 212 + ~placement:(K.Placement.make ~columns:10 ~rows:5 ()) 213 + ~quiet:`Errors_only 214 + ()) 215 + ~data:""; 216 + (* Create second placement *) 217 + send 218 + (K.Command.display 219 + ~image_id:100 220 + ~placement:(K.Placement.make ~columns:5 ~rows:3 ()) 221 + ~quiet:`Errors_only 222 + ()) 223 + ~data:""; 224 + print_newline (); 225 + wait_for_enter (); 226 + 227 + (* Demo 8: Multiple placements with spacing *) 228 + clear_screen (); 229 + print_endline "Demo 8: Multiple Placements with Different Sizes"; 230 + print_newline (); 231 + print_endline "Showing same image at different sizes:"; 232 + print_newline (); 233 + (* Create a gradient square *) 234 + let grad_small = gradient_rgba ~width:100 ~height:100 in 235 + (* Transmit once *) 236 + send 237 + (K.Command.transmit 238 + ~image_id:160 239 + ~format:`Rgba32 240 + ~width:100 ~height:100 241 + ~quiet:`Errors_only 242 + ()) 243 + ~data:grad_small; 244 + (* Place same image three times at different sizes *) 245 + send 246 + (K.Command.display 247 + ~image_id:160 248 + ~placement:(K.Placement.make ~columns:5 ~rows:5 ()) 249 + ~quiet:`Errors_only 250 + ()) 251 + ~data:""; 252 + print_string " "; 253 + send 254 + (K.Command.display 255 + ~image_id:160 256 + ~placement:(K.Placement.make ~columns:8 ~rows:8 ()) 257 + ~quiet:`Errors_only 258 + ()) 259 + ~data:""; 260 + print_string " "; 261 + send 262 + (K.Command.display 263 + ~image_id:160 264 + ~placement:(K.Placement.make ~columns:12 ~rows:12 ()) 265 + ~quiet:`Errors_only 266 + ()) 267 + ~data:""; 268 + print_newline (); 269 + print_newline (); 270 + print_endline "Small (5x5 cells), Medium (8x8 cells), Large (12x12 cells)"; 271 + print_newline (); 272 + wait_for_enter (); 273 + 274 + (* Demo 9: Z-index layering *) 275 + clear_screen (); 276 + print_endline "Demo 9: Z-Index Layering - Images above/below text"; 277 + let bg_data = solid_color_rgba ~width:200 ~height:100 ~r:255 ~g:165 ~b:0 ~a:128 in 278 + send 279 + (K.Command.transmit_and_display 280 + ~image_id:200 281 + ~format:`Rgba32 282 + ~width:200 ~height:100 283 + ~placement:(K.Placement.make ~z_index:(-1) ~cursor:`Static ()) 284 + ~quiet:`Errors_only 285 + ()) 286 + ~data:bg_data; 287 + print_endline "This orange square should appear behind the text!"; 288 + print_newline (); 289 + wait_for_enter (); 290 + 291 + (* Demo 10: Query support *) 292 + clear_screen (); 293 + print_endline "Demo 10: Query Support - Check terminal capabilities"; 294 + let query_str = K.Detect.make_query () in 295 + print_string query_str; 296 + flush stdout; 297 + print_endline "(Check if your terminal responds with OK)"; 298 + print_newline (); 299 + wait_for_enter (); 300 + 301 + (* Demo 11: Animation - color-changing square *) 302 + clear_screen (); 303 + print_endline "Demo 11: Animation - Color-changing square"; 304 + print_endline "Creating animated sequence with 4 colors..."; 305 + 306 + let width, height = 80, 80 in 307 + let image_id = 300 in 308 + 309 + (* Create base frame (red) - transmit without displaying *) 310 + let red_frame = solid_color_rgba ~width ~height ~r:255 ~g:0 ~b:0 ~a:255 in 311 + send 312 + (K.Command.transmit 313 + ~image_id 314 + ~format:`Rgba32 315 + ~width ~height 316 + ~quiet:`Errors_only 317 + ()) 318 + ~data:red_frame; 319 + 320 + (* Add frames with composition replace *) 321 + let orange_frame = solid_color_rgba ~width ~height ~r:255 ~g:165 ~b:0 ~a:255 in 322 + send 323 + (K.Command.frame 324 + ~image_id 325 + ~format:`Rgba32 326 + ~width ~height 327 + ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 328 + ~quiet:`Errors_only 329 + ()) 330 + ~data:orange_frame; 331 + 332 + let yellow_frame = solid_color_rgba ~width ~height ~r:255 ~g:255 ~b:0 ~a:255 in 333 + send 334 + (K.Command.frame 335 + ~image_id 336 + ~format:`Rgba32 337 + ~width ~height 338 + ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 339 + ~quiet:`Errors_only 340 + ()) 341 + ~data:yellow_frame; 342 + 343 + let green_frame = solid_color_rgba ~width ~height ~r:0 ~g:255 ~b:0 ~a:255 in 344 + send 345 + (K.Command.frame 346 + ~image_id 347 + ~format:`Rgba32 348 + ~width ~height 349 + ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 350 + ~quiet:`Errors_only 351 + ()) 352 + ~data:green_frame; 353 + 354 + (* Create placement and start animation *) 355 + send 356 + (K.Command.display 357 + ~image_id 358 + ~placement:(K.Placement.make 359 + ~placement_id:1 360 + ~cell_x_offset:0 361 + ~cell_y_offset:0 362 + ~cursor:`Static 363 + ()) 364 + ~quiet:`Errors_only 365 + ()) 366 + ~data:""; 367 + 368 + (* Set root frame gap - root frame has no gap by default per Kitty protocol *) 369 + send 370 + (K.Command.animate ~image_id (K.Animation.set_gap ~frame:1 ~gap_ms:100)) 371 + ~data:""; 372 + 373 + (* Start animation with infinite looping *) 374 + send 375 + (K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run)) 376 + ~data:""; 377 + 378 + print_newline (); 379 + print_endline "Animation playing with colors: Red -> Orange -> Yellow -> Green"; 380 + print_newline (); 381 + 382 + (* Simulate movement by deleting and recreating placement at different positions *) 383 + for i = 1 to 7 do 384 + Unix.sleepf 0.4; 385 + 386 + (* Delete the current placement *) 387 + send 388 + (K.Command.delete ~quiet:`Errors_only (`By_id (image_id, Some 1))) 389 + ~data:""; 390 + 391 + (* Create new placement at next position *) 392 + send 393 + (K.Command.display 394 + ~image_id 395 + ~placement:(K.Placement.make 396 + ~placement_id:1 397 + ~cell_x_offset:(i * 5) 398 + ~cell_y_offset:0 399 + ~cursor:`Static 400 + ()) 401 + ~quiet:`Errors_only 402 + ()) 403 + ~data:"" 404 + done; 405 + 406 + (* Stop the animation *) 407 + send 408 + (K.Command.animate ~image_id (K.Animation.set_state `Stop)) 409 + ~data:""; 410 + 411 + print_endline "Animation stopped."; 412 + print_newline (); 413 + print_newline (); 414 + print_endline "Demo complete!"; 415 + print_newline (); 416 + print_endline "For more examples, see the library documentation."; 417 + wait_for_enter ()
+59
example/test_output.ml
··· 1 + (* Simple test to show exact escape sequences without data *) 2 + 3 + module K = Kitty_graphics 4 + 5 + let print_escaped s = 6 + String.iter (fun c -> 7 + let code = Char.code c in 8 + if code = 27 then print_string "\\x1b" 9 + else if code < 32 || code > 126 then Printf.printf "\\x%02x" code 10 + else print_char c 11 + ) s; 12 + print_newline () 13 + 14 + let () = 15 + let image_id = 300 in 16 + let width, height = 80, 80 in 17 + 18 + print_endline "=== Animation Escape Sequences (no data) ===\n"; 19 + 20 + (* 1. Transmit base frame (no data for testing) *) 21 + print_endline "1. Transmit (a=t):"; 22 + let cmd1 = K.Command.transmit 23 + ~image_id ~format:`Rgba32 ~width ~height ~quiet:`Errors_only () in 24 + print_escaped (K.Command.to_string cmd1 ~data:""); 25 + 26 + (* 2. Frame command *) 27 + print_endline "\n2. Frame (a=f):"; 28 + let cmd2 = K.Command.frame 29 + ~image_id ~format:`Rgba32 ~width ~height 30 + ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 31 + ~quiet:`Errors_only () in 32 + print_escaped (K.Command.to_string cmd2 ~data:""); 33 + 34 + (* 3. Put/display command *) 35 + print_endline "\n3. Display/Put (a=p):"; 36 + let cmd3 = K.Command.display 37 + ~image_id 38 + ~placement:(K.Placement.make 39 + ~placement_id:1 40 + ~cell_x_offset:0 41 + ~cell_y_offset:0 42 + ~cursor:`Static ()) 43 + ~quiet:`Errors_only () in 44 + print_escaped (K.Command.to_string cmd3 ~data:""); 45 + 46 + (* 4. Set root frame gap - IMPORTANT for animation! *) 47 + print_endline "\n4. Set root frame gap (a=a, r=1, z=100):"; 48 + let cmd4 = K.Command.animate ~image_id (K.Animation.set_gap ~frame:1 ~gap_ms:100) in 49 + print_escaped (K.Command.to_string cmd4 ~data:""); 50 + 51 + (* 5. Animate - start *) 52 + print_endline "\n5. Animate start (a=a, s=3, v=1):"; 53 + let cmd5 = K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run) in 54 + print_escaped (K.Command.to_string cmd5 ~data:""); 55 + 56 + (* 6. Animate - stop *) 57 + print_endline "\n6. Animate stop (a=a, s=1):"; 58 + let cmd6 = K.Command.animate ~image_id (K.Animation.set_state `Stop) in 59 + print_escaped (K.Command.to_string cmd6 ~data:"")
+108
example/tiny_anim.ml
··· 1 + (* Tiny animation test - no chunking needed *) 2 + (* Uses 20x20 images which are ~1067 bytes base64 (well under 4096) *) 3 + 4 + module K = Kitty_graphics 5 + 6 + let solid_color_rgba ~width ~height ~r ~g ~b ~a = 7 + let pixels = Bytes.create (width * height * 4) in 8 + for i = 0 to (width * height) - 1 do 9 + let idx = i * 4 in 10 + Bytes.set pixels idx (Char.chr r); 11 + Bytes.set pixels (idx + 1) (Char.chr g); 12 + Bytes.set pixels (idx + 2) (Char.chr b); 13 + Bytes.set pixels (idx + 3) (Char.chr a) 14 + done; 15 + Bytes.to_string pixels 16 + 17 + let send cmd ~data = 18 + print_string (K.Command.to_string cmd ~data); 19 + flush stdout 20 + 21 + let () = 22 + (* Use 20x20 to avoid chunking: 20*20*4 = 1600 bytes, base64 ~2134 bytes *) 23 + let width, height = 20, 20 in 24 + let image_id = 999 in 25 + 26 + (* Clear any existing images *) 27 + send (K.Command.delete ~quiet:`Errors_only (`All_visible_and_free)) ~data:""; 28 + 29 + (* Step 1: Transmit base frame (red) - matching Go's sequence *) 30 + let red_frame = solid_color_rgba ~width ~height ~r:255 ~g:0 ~b:0 ~a:255 in 31 + send 32 + (K.Command.transmit 33 + ~image_id 34 + ~format:`Rgba32 35 + ~width ~height 36 + ~quiet:`Errors_only 37 + ()) 38 + ~data:red_frame; 39 + 40 + (* Step 2: Add frame (orange) with 100ms gap - like Go *) 41 + let orange_frame = solid_color_rgba ~width ~height ~r:255 ~g:165 ~b:0 ~a:255 in 42 + send 43 + (K.Command.frame 44 + ~image_id 45 + ~format:`Rgba32 46 + ~width ~height 47 + ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 48 + ~quiet:`Errors_only 49 + ()) 50 + ~data:orange_frame; 51 + 52 + (* Step 3: Add frame (yellow) *) 53 + let yellow_frame = solid_color_rgba ~width ~height ~r:255 ~g:255 ~b:0 ~a:255 in 54 + send 55 + (K.Command.frame 56 + ~image_id 57 + ~format:`Rgba32 58 + ~width ~height 59 + ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 60 + ~quiet:`Errors_only 61 + ()) 62 + ~data:yellow_frame; 63 + 64 + (* Step 4: Add frame (green) *) 65 + let green_frame = solid_color_rgba ~width ~height ~r:0 ~g:255 ~b:0 ~a:255 in 66 + send 67 + (K.Command.frame 68 + ~image_id 69 + ~format:`Rgba32 70 + ~width ~height 71 + ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 72 + ~quiet:`Errors_only 73 + ()) 74 + ~data:green_frame; 75 + 76 + (* Step 5: Create placement - exactly like Go *) 77 + send 78 + (K.Command.display 79 + ~image_id 80 + ~placement:(K.Placement.make 81 + ~placement_id:1 82 + ~cell_x_offset:0 83 + ~cell_y_offset:0 84 + ~cursor:`Static 85 + ()) 86 + ~quiet:`Errors_only 87 + ()) 88 + ~data:""; 89 + 90 + (* Step 6: Start animation - exactly like Go (NO root frame gap) *) 91 + send 92 + (K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run)) 93 + ~data:""; 94 + 95 + print_endline ""; 96 + print_endline "Tiny animation (20x20) - Red -> Orange -> Yellow -> Green"; 97 + print_endline "This uses no chunking. Press Enter to stop..."; 98 + flush stdout; 99 + let _ = read_line () in 100 + 101 + (* Stop animation *) 102 + send 103 + (K.Command.animate ~image_id (K.Animation.set_state `Stop)) 104 + ~data:""; 105 + 106 + (* Clean up *) 107 + send (K.Command.delete ~quiet:`Errors_only (`All_visible_and_free)) ~data:""; 108 + print_endline "Done."
+4
lib/dune
··· 1 + (library 2 + (name kitty_graphics) 3 + (public_name kitty_graphics) 4 + (libraries base64))
+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