Kitty Graphics Protocol in OCaml
terminal graphics ocaml

ocamlformat

+857 -689
+1
.ocamlformat
··· 1 + version=0.28.1
+10 -1
dune-project
··· 1 1 (lang dune 3.20) 2 2 (name kgp) 3 3 4 + (generate_opam_files true) 5 + 6 + (license ISC) 7 + (authors "Anil Madhavapeddy") 8 + (homepage "https://tangled.org/@anil.recoil.org/ocaml-kgp") 9 + (maintainers "Anil Madhavapeddy <anil@recoil.org>") 10 + (bug_reports "https://tangled.org/@anil.recoil.org/ocaml-kgp/issues") 11 + (maintenance_intent "(latest)") 12 + 4 13 (package 5 14 (name kgp) 6 15 (synopsis "OCaml implementation of the Kitty terminal graphics protocol") 7 16 (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.") 17 + "Library for rendering images in terminals that support the Kitty graphics protocol. Supports image transmission, display, animation, Unicode placeholders, and terminal capability detection.") 9 18 (depends 10 19 (ocaml (>= 4.14.0)) 11 20 cmdliner
+83 -152
example/example.ml
··· 35 35 let pixels = Bytes.create (width * height * 4) in 36 36 for y = 0 to height - 1 do 37 37 for x = 0 to width - 1 do 38 - let idx = (y * width + x) * 4 in 38 + let idx = ((y * width) + x) * 4 in 39 39 let r = 255 * x / width in 40 40 let b = 255 * (width - x) / width in 41 41 Bytes.set pixels idx (Char.chr r); ··· 66 66 67 67 let clear_screen () = 68 68 print_string "\x1b[2J\x1b[H"; 69 - for _ = 1 to 5 do print_newline () done; 69 + for _ = 1 to 5 do 70 + print_newline () 71 + done; 70 72 flush stdout 71 73 72 74 let () = ··· 87 89 (try 88 90 let png_data = read_file "camel.png" in 89 91 send 90 - (K.transmit_and_display 91 - ~image_id:1 92 - ~format:`Png 93 - ~quiet:`Errors_only 92 + (K.transmit_and_display ~image_id:1 ~format:`Png ~quiet:`Errors_only 94 93 ~placement:(K.Placement.make ~columns:15 ~rows:8 ()) 95 94 ()) 96 95 ~data:png_data; 97 96 print_endline "camel.png displayed using PNG format" 98 97 with _ -> 99 98 (* Fallback: red square as RGBA *) 100 - let red_data = solid_color_rgba ~width:100 ~height:100 ~r:255 ~g:0 ~b:0 ~a:255 in 99 + let red_data = 100 + solid_color_rgba ~width:100 ~height:100 ~r:255 ~g:0 ~b:0 ~a:255 101 + in 101 102 send 102 - (K.transmit_and_display 103 - ~image_id:1 104 - ~format:`Rgba32 105 - ~width:100 ~height:100 106 - ~quiet:`Errors_only 107 - ()) 103 + (K.transmit_and_display ~image_id:1 ~format:`Rgba32 ~width:100 104 + ~height:100 ~quiet:`Errors_only ()) 108 105 ~data:red_data; 109 106 print_endline "Red square displayed (camel.png not found)"); 110 107 print_newline (); ··· 113 110 (* Demo 2: Basic formats - RGBA *) 114 111 clear_screen (); 115 112 print_endline "Demo 2: Image Formats - RGBA format (32-bit)"; 116 - let blue_data = solid_color_rgba ~width:100 ~height:100 ~r:0 ~g:0 ~b:255 ~a:255 in 113 + let blue_data = 114 + solid_color_rgba ~width:100 ~height:100 ~r:0 ~g:0 ~b:255 ~a:255 115 + in 117 116 send 118 - (K.transmit_and_display 119 - ~image_id:2 120 - ~format:`Rgba32 121 - ~width:100 ~height:100 122 - ~quiet:`Errors_only 123 - ()) 117 + (K.transmit_and_display ~image_id:2 ~format:`Rgba32 ~width:100 ~height:100 118 + ~quiet:`Errors_only ()) 124 119 ~data:blue_data; 125 120 print_endline "Blue square displayed using raw RGBA format"; 126 121 print_newline (); ··· 131 126 print_endline "Demo 3: Image Formats - RGB format (24-bit)"; 132 127 let green_data = solid_color_rgb ~width:100 ~height:100 ~r:0 ~g:255 ~b:0 in 133 128 send 134 - (K.transmit_and_display 135 - ~image_id:3 136 - ~format:`Rgb24 137 - ~width:100 ~height:100 138 - ~quiet:`Errors_only 139 - ()) 129 + (K.transmit_and_display ~image_id:3 ~format:`Rgb24 ~width:100 ~height:100 130 + ~quiet:`Errors_only ()) 140 131 ~data:green_data; 141 132 print_endline "Green square displayed using raw RGB format (no alpha channel)"; 142 133 print_newline (); ··· 145 136 (* Demo 4: Compression - Note: would need zlib library for actual compression *) 146 137 clear_screen (); 147 138 print_endline "Demo 4: Large Image (compression requires zlib library)"; 148 - let orange_data = solid_color_rgba ~width:200 ~height:200 ~r:255 ~g:165 ~b:0 ~a:255 in 139 + let orange_data = 140 + solid_color_rgba ~width:200 ~height:200 ~r:255 ~g:165 ~b:0 ~a:255 141 + in 149 142 send 150 - (K.transmit_and_display 151 - ~image_id:4 152 - ~format:`Rgba32 153 - ~width:200 ~height:200 154 - ~quiet:`Errors_only 155 - ()) 143 + (K.transmit_and_display ~image_id:4 ~format:`Rgba32 ~width:200 ~height:200 144 + ~quiet:`Errors_only ()) 156 145 ~data:orange_data; 157 - Printf.printf "Orange square (200x200) - %d bytes uncompressed\n" (String.length orange_data); 146 + Printf.printf "Orange square (200x200) - %d bytes uncompressed\n" 147 + (String.length orange_data); 158 148 print_newline (); 159 149 wait_for_enter (); 160 150 ··· 164 154 (try 165 155 let png_data = read_file "camel.png" in 166 156 send 167 - (K.transmit_and_display 168 - ~image_id:10 169 - ~format:`Png 170 - ~quiet:`Errors_only 171 - ()) 157 + (K.transmit_and_display ~image_id:10 ~format:`Png ~quiet:`Errors_only ()) 172 158 ~data:png_data; 173 159 print_endline "camel.png loaded and displayed" 174 - with Sys_error msg -> 175 - Printf.printf "camel.png not found: %s\n" msg); 160 + with Sys_error msg -> Printf.printf "camel.png not found: %s\n" msg); 176 161 print_newline (); 177 162 wait_for_enter (); 178 163 ··· 181 166 print_endline "Demo 6: Cropping and Scaling - Display part of an image"; 182 167 let gradient = gradient_rgba ~width:200 ~height:200 in 183 168 send 184 - (K.transmit_and_display 185 - ~image_id:20 186 - ~format:`Rgba32 187 - ~width:200 ~height:200 188 - ~placement:(K.Placement.make 189 - ~source_x:50 ~source_y:50 190 - ~source_width:100 ~source_height:100 191 - ~columns:10 ~rows:10 192 - ()) 193 - ~quiet:`Errors_only 194 - ()) 169 + (K.transmit_and_display ~image_id:20 ~format:`Rgba32 ~width:200 ~height:200 170 + ~placement: 171 + (K.Placement.make ~source_x:50 ~source_y:50 ~source_width:100 172 + ~source_height:100 ~columns:10 ~rows:10 ()) 173 + ~quiet:`Errors_only ()) 195 174 ~data:gradient; 196 175 print_endline "Cropped to center 100x100 region of a 200x200 gradient"; 197 176 print_newline (); ··· 200 179 (* Demo 7: Multiple placements *) 201 180 clear_screen (); 202 181 print_endline "Demo 7: Multiple Placements - One image, multiple displays"; 203 - let cyan_data = solid_color_rgba ~width:80 ~height:80 ~r:0 ~g:255 ~b:255 ~a:255 in 182 + let cyan_data = 183 + solid_color_rgba ~width:80 ~height:80 ~r:0 ~g:255 ~b:255 ~a:255 184 + in 204 185 (* Transmit once with an ID *) 205 186 send 206 - (K.transmit 207 - ~image_id:100 208 - ~format:`Rgba32 209 - ~width:80 ~height:80 210 - ~quiet:`Errors_only 211 - ()) 187 + (K.transmit ~image_id:100 ~format:`Rgba32 ~width:80 ~height:80 188 + ~quiet:`Errors_only ()) 212 189 ~data:cyan_data; 213 190 (* Create first placement *) 214 191 send 215 - (K.display 216 - ~image_id:100 192 + (K.display ~image_id:100 217 193 ~placement:(K.Placement.make ~columns:10 ~rows:5 ()) 218 - ~quiet:`Errors_only 219 - ()) 194 + ~quiet:`Errors_only ()) 220 195 ~data:""; 221 196 (* Create second placement *) 222 197 send 223 - (K.display 224 - ~image_id:100 198 + (K.display ~image_id:100 225 199 ~placement:(K.Placement.make ~columns:5 ~rows:3 ()) 226 - ~quiet:`Errors_only 227 - ()) 200 + ~quiet:`Errors_only ()) 228 201 ~data:""; 229 202 print_newline (); 230 203 wait_for_enter (); ··· 239 212 let grad_small = gradient_rgba ~width:100 ~height:100 in 240 213 (* Transmit once *) 241 214 send 242 - (K.transmit 243 - ~image_id:160 244 - ~format:`Rgba32 245 - ~width:100 ~height:100 246 - ~quiet:`Errors_only 247 - ()) 215 + (K.transmit ~image_id:160 ~format:`Rgba32 ~width:100 ~height:100 216 + ~quiet:`Errors_only ()) 248 217 ~data:grad_small; 249 218 (* Place same image three times at different sizes *) 250 219 send 251 - (K.display 252 - ~image_id:160 220 + (K.display ~image_id:160 253 221 ~placement:(K.Placement.make ~columns:5 ~rows:5 ()) 254 - ~quiet:`Errors_only 255 - ()) 222 + ~quiet:`Errors_only ()) 256 223 ~data:""; 257 224 print_string " "; 258 225 send 259 - (K.display 260 - ~image_id:160 226 + (K.display ~image_id:160 261 227 ~placement:(K.Placement.make ~columns:8 ~rows:8 ()) 262 - ~quiet:`Errors_only 263 - ()) 228 + ~quiet:`Errors_only ()) 264 229 ~data:""; 265 230 print_string " "; 266 231 send 267 - (K.display 268 - ~image_id:160 232 + (K.display ~image_id:160 269 233 ~placement:(K.Placement.make ~columns:12 ~rows:12 ()) 270 - ~quiet:`Errors_only 271 - ()) 234 + ~quiet:`Errors_only ()) 272 235 ~data:""; 273 236 print_newline (); 274 237 print_newline (); ··· 279 242 (* Demo 9: Z-index layering *) 280 243 clear_screen (); 281 244 print_endline "Demo 9: Z-Index Layering - Images above/below text"; 282 - let bg_data = solid_color_rgba ~width:200 ~height:100 ~r:255 ~g:165 ~b:0 ~a:128 in 245 + let bg_data = 246 + solid_color_rgba ~width:200 ~height:100 ~r:255 ~g:165 ~b:0 ~a:128 247 + in 283 248 send 284 - (K.transmit_and_display 285 - ~image_id:200 286 - ~format:`Rgba32 287 - ~width:200 ~height:100 249 + (K.transmit_and_display ~image_id:200 ~format:`Rgba32 ~width:200 ~height:100 288 250 ~placement:(K.Placement.make ~z_index:(-1) ~cursor:`Static ()) 289 - ~quiet:`Errors_only 290 - ()) 251 + ~quiet:`Errors_only ()) 291 252 ~data:bg_data; 292 253 print_endline "This orange square should appear behind the text!"; 293 254 print_newline (); ··· 308 269 print_endline "Demo 11: Animation - Color-changing square"; 309 270 print_endline "Creating animated sequence with 4 colors..."; 310 271 311 - let width, height = 80, 80 in 272 + let width, height = (80, 80) in 312 273 let image_id = 300 in 313 274 314 275 (* Create base frame (red) - transmit without displaying *) 315 276 let red_frame = solid_color_rgba ~width ~height ~r:255 ~g:0 ~b:0 ~a:255 in 316 277 send 317 - (K.transmit 318 - ~image_id 319 - ~format:`Rgba32 320 - ~width ~height 321 - ~quiet:`Errors_only 322 - ()) 278 + (K.transmit ~image_id ~format:`Rgba32 ~width ~height ~quiet:`Errors_only ()) 323 279 ~data:red_frame; 324 280 325 281 (* Add frames with composition replace *) 326 - let orange_frame = solid_color_rgba ~width ~height ~r:255 ~g:165 ~b:0 ~a:255 in 282 + let orange_frame = 283 + solid_color_rgba ~width ~height ~r:255 ~g:165 ~b:0 ~a:255 284 + in 327 285 send 328 - (K.frame 329 - ~image_id 330 - ~format:`Rgba32 331 - ~width ~height 286 + (K.frame ~image_id ~format:`Rgba32 ~width ~height 332 287 ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 333 - ~quiet:`Errors_only 334 - ()) 288 + ~quiet:`Errors_only ()) 335 289 ~data:orange_frame; 336 290 337 - let yellow_frame = solid_color_rgba ~width ~height ~r:255 ~g:255 ~b:0 ~a:255 in 291 + let yellow_frame = 292 + solid_color_rgba ~width ~height ~r:255 ~g:255 ~b:0 ~a:255 293 + in 338 294 send 339 - (K.frame 340 - ~image_id 341 - ~format:`Rgba32 342 - ~width ~height 295 + (K.frame ~image_id ~format:`Rgba32 ~width ~height 343 296 ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 344 - ~quiet:`Errors_only 345 - ()) 297 + ~quiet:`Errors_only ()) 346 298 ~data:yellow_frame; 347 299 348 300 let green_frame = solid_color_rgba ~width ~height ~r:0 ~g:255 ~b:0 ~a:255 in 349 301 send 350 - (K.frame 351 - ~image_id 352 - ~format:`Rgba32 353 - ~width ~height 302 + (K.frame ~image_id ~format:`Rgba32 ~width ~height 354 303 ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 355 - ~quiet:`Errors_only 356 - ()) 304 + ~quiet:`Errors_only ()) 357 305 ~data:green_frame; 358 306 359 307 (* Create placement and start animation *) 360 308 send 361 - (K.display 362 - ~image_id 363 - ~placement:(K.Placement.make 364 - ~placement_id:1 365 - ~cell_x_offset:0 366 - ~cell_y_offset:0 367 - ~cursor:`Static 368 - ()) 369 - ~quiet:`Errors_only 370 - ()) 309 + (K.display ~image_id 310 + ~placement: 311 + (K.Placement.make ~placement_id:1 ~cell_x_offset:0 ~cell_y_offset:0 312 + ~cursor:`Static ()) 313 + ~quiet:`Errors_only ()) 371 314 ~data:""; 372 315 373 316 (* Set root frame gap - root frame has no gap by default per Kitty protocol *) 374 - send 375 - (K.animate ~image_id (K.Animation.set_gap ~frame:1 ~gap_ms:100)) 376 - ~data:""; 317 + send (K.animate ~image_id (K.Animation.set_gap ~frame:1 ~gap_ms:100)) ~data:""; 377 318 378 319 (* Start animation with infinite looping *) 379 - send 380 - (K.animate ~image_id (K.Animation.set_state ~loops:1 `Run)) 381 - ~data:""; 320 + send (K.animate ~image_id (K.Animation.set_state ~loops:1 `Run)) ~data:""; 382 321 383 322 print_newline (); 384 - print_endline "Animation playing with colors: Red -> Orange -> Yellow -> Green"; 323 + print_endline 324 + "Animation playing with colors: Red -> Orange -> Yellow -> Green"; 385 325 print_newline (); 386 326 387 327 (* Simulate movement by deleting and recreating placement at different positions *) ··· 389 329 Unix.sleepf 0.4; 390 330 391 331 (* Delete the current placement *) 392 - send 393 - (K.delete ~quiet:`Errors_only (`By_id (image_id, Some 1))) 394 - ~data:""; 332 + send (K.delete ~quiet:`Errors_only (`By_id (image_id, Some 1))) ~data:""; 395 333 396 334 (* Create new placement at next position *) 397 335 send 398 - (K.display 399 - ~image_id 400 - ~placement:(K.Placement.make 401 - ~placement_id:1 402 - ~cell_x_offset:(i * 5) 403 - ~cell_y_offset:0 404 - ~cursor:`Static 405 - ()) 406 - ~quiet:`Errors_only 407 - ()) 336 + (K.display ~image_id 337 + ~placement: 338 + (K.Placement.make ~placement_id:1 ~cell_x_offset:(i * 5) 339 + ~cell_y_offset:0 ~cursor:`Static ()) 340 + ~quiet:`Errors_only ()) 408 341 ~data:"" 409 342 done; 410 343 411 344 (* Stop the animation *) 412 - send 413 - (K.animate ~image_id (K.Animation.set_state `Stop)) 414 - ~data:""; 345 + send (K.animate ~image_id (K.Animation.set_state `Stop)) ~data:""; 415 346 416 347 print_endline "Animation stopped."; 417 348 print_newline ();
+21 -43
example/tiny_anim.ml
··· 25 25 26 26 let () = 27 27 (* Use 20x20 to avoid chunking: 20*20*4 = 1600 bytes, base64 ~2134 bytes *) 28 - let width, height = 20, 20 in 28 + let width, height = (20, 20) in 29 29 let image_id = 999 in 30 30 31 31 (* Clear any existing images *) ··· 34 34 (* Step 1: Transmit base frame (red) - matching Go's sequence *) 35 35 let red_frame = solid_color_rgba ~width ~height ~r:255 ~g:0 ~b:0 ~a:255 in 36 36 send 37 - (K.transmit 38 - ~image_id 39 - ~format:`Rgba32 40 - ~width ~height 41 - ~quiet:`Errors_only 42 - ()) 37 + (K.transmit ~image_id ~format:`Rgba32 ~width ~height ~quiet:`Errors_only ()) 43 38 ~data:red_frame; 44 39 45 40 (* Step 2: Add frame (orange) with 100ms gap - like Go *) 46 - let orange_frame = solid_color_rgba ~width ~height ~r:255 ~g:165 ~b:0 ~a:255 in 41 + let orange_frame = 42 + solid_color_rgba ~width ~height ~r:255 ~g:165 ~b:0 ~a:255 43 + in 47 44 send 48 - (K.frame 49 - ~image_id 50 - ~format:`Rgba32 51 - ~width ~height 45 + (K.frame ~image_id ~format:`Rgba32 ~width ~height 52 46 ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 53 - ~quiet:`Errors_only 54 - ()) 47 + ~quiet:`Errors_only ()) 55 48 ~data:orange_frame; 56 49 57 50 (* Step 3: Add frame (yellow) *) 58 - let yellow_frame = solid_color_rgba ~width ~height ~r:255 ~g:255 ~b:0 ~a:255 in 51 + let yellow_frame = 52 + solid_color_rgba ~width ~height ~r:255 ~g:255 ~b:0 ~a:255 53 + in 59 54 send 60 - (K.frame 61 - ~image_id 62 - ~format:`Rgba32 63 - ~width ~height 55 + (K.frame ~image_id ~format:`Rgba32 ~width ~height 64 56 ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 65 - ~quiet:`Errors_only 66 - ()) 57 + ~quiet:`Errors_only ()) 67 58 ~data:yellow_frame; 68 59 69 60 (* Step 4: Add frame (green) *) 70 61 let green_frame = solid_color_rgba ~width ~height ~r:0 ~g:255 ~b:0 ~a:255 in 71 62 send 72 - (K.frame 73 - ~image_id 74 - ~format:`Rgba32 75 - ~width ~height 63 + (K.frame ~image_id ~format:`Rgba32 ~width ~height 76 64 ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 77 - ~quiet:`Errors_only 78 - ()) 65 + ~quiet:`Errors_only ()) 79 66 ~data:green_frame; 80 67 81 68 (* Step 5: Create placement - exactly like Go *) 82 69 send 83 - (K.display 84 - ~image_id 85 - ~placement:(K.Placement.make 86 - ~placement_id:1 87 - ~cell_x_offset:0 88 - ~cell_y_offset:0 89 - ~cursor:`Static 90 - ()) 91 - ~quiet:`Errors_only 92 - ()) 70 + (K.display ~image_id 71 + ~placement: 72 + (K.Placement.make ~placement_id:1 ~cell_x_offset:0 ~cell_y_offset:0 73 + ~cursor:`Static ()) 74 + ~quiet:`Errors_only ()) 93 75 ~data:""; 94 76 95 77 (* Step 6: Start animation - exactly like Go (NO root frame gap) *) 96 - send 97 - (K.animate ~image_id (K.Animation.set_state ~loops:1 `Run)) 98 - ~data:""; 78 + send (K.animate ~image_id (K.Animation.set_state ~loops:1 `Run)) ~data:""; 99 79 100 80 print_endline ""; 101 81 print_endline "Tiny animation (20x20) - Red -> Orange -> Yellow -> Green"; ··· 104 84 let _ = read_line () in 105 85 106 86 (* Stop animation *) 107 - send 108 - (K.animate ~image_id (K.Animation.set_state `Stop)) 109 - ~data:""; 87 + send (K.animate ~image_id (K.Animation.set_state `Stop)) ~data:""; 110 88 111 89 (* Clean up *) 112 90 send (K.delete ~free:true ~quiet:`Errors_only `All_visible) ~data:"";
+32
kgp.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "OCaml implementation of the Kitty terminal graphics protocol" 4 + description: 5 + "Library for rendering images in terminals that support the Kitty graphics protocol. Supports image transmission, display, animation, Unicode placeholders, and terminal capability detection." 6 + maintainer: ["Anil Madhavapeddy <anil@recoil.org>"] 7 + authors: ["Anil Madhavapeddy"] 8 + license: "ISC" 9 + homepage: "https://tangled.org/@anil.recoil.org/ocaml-kgp" 10 + bug-reports: "https://tangled.org/@anil.recoil.org/ocaml-kgp/issues" 11 + depends: [ 12 + "dune" {>= "3.20"} 13 + "ocaml" {>= "4.14.0"} 14 + "cmdliner" 15 + "base64" 16 + "odoc" {with-doc} 17 + ] 18 + build: [ 19 + ["dune" "subst"] {dev} 20 + [ 21 + "dune" 22 + "build" 23 + "-p" 24 + name 25 + "-j" 26 + jobs 27 + "@install" 28 + "@runtest" {with-test} 29 + "@doc" {with-doc} 30 + ] 31 + ] 32 + x-maintenance-intent: ["(latest)"]
+8 -7
lib-cli/kgp_cli.ml
··· 11 11 12 12 let graphics_term = 13 13 let doc = "Force graphics output enabled, ignoring terminal detection." in 14 - let enable = Arg.info ["g"; "graphics"] ~doc ~docs:graphics_docs in 14 + let enable = Arg.info [ "g"; "graphics" ] ~doc ~docs:graphics_docs in 15 15 let doc = "Disable graphics output, use text placeholders instead." in 16 - let disable = Arg.info ["no-graphics"] ~doc ~docs:graphics_docs in 16 + let disable = Arg.info [ "no-graphics" ] ~doc ~docs:graphics_docs in 17 17 let doc = "Force tmux passthrough mode for graphics." in 18 - let tmux = Arg.info ["tmux"] ~doc ~docs:graphics_docs in 18 + let tmux = Arg.info [ "tmux" ] ~doc ~docs:graphics_docs in 19 19 let choose enable disable tmux : Kgp.Terminal.graphics_mode = 20 20 if enable then `Enabled 21 21 else if disable then `Disabled 22 22 else if tmux then `Tmux 23 23 else `Auto 24 24 in 25 - Term.(const choose 26 - $ Arg.(value & flag enable) 27 - $ Arg.(value & flag disable) 28 - $ Arg.(value & flag tmux)) 25 + Term.( 26 + const choose 27 + $ Arg.(value & flag enable) 28 + $ Arg.(value & flag disable) 29 + $ Arg.(value & flag tmux))
+5 -4
lib-cli/kgp_cli.mli
··· 5 5 6 6 (** Cmdliner Support for Kitty Graphics Protocol 7 7 8 - This module provides Cmdliner terms for configuring graphics output mode 9 - in CLI applications. It allows users to override auto-detection with 8 + This module provides Cmdliner terms for configuring graphics output mode in 9 + CLI applications. It allows users to override auto-detection with 10 10 command-line flags. 11 11 12 12 {2 Usage} ··· 42 42 - [--tmux]: Force tmux passthrough mode 43 43 - (default): Auto-detect based on terminal environment 44 44 45 - The term evaluates to a {!Kgp.Terminal.graphics_mode} value which can 46 - be passed to {!Kgp.Terminal.supports_graphics} or {!Kgp.Terminal.resolve_mode}. *) 45 + The term evaluates to a {!Kgp.Terminal.graphics_mode} value which can be 46 + passed to {!Kgp.Terminal.supports_graphics} or {!Kgp.Terminal.resolve_mode}. 47 + *) 47 48 48 49 val graphics_docs : string 49 50 (** Section name for graphics options in help output ("GRAPHICS OPTIONS").
-7
lib/kgp.ml
··· 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 - (* Type modules *) 7 6 module Format = Kgp_format 8 7 module Transmission = Kgp_transmission 9 8 module Compression = Kgp_compression ··· 12 11 module Composition = Kgp_composition 13 12 module Delete = Kgp_delete 14 13 module Animation_state = Kgp_animation_state 15 - 16 - (* Configuration modules *) 17 14 module Placement = Kgp_placement 18 15 module Frame = Kgp_frame 19 16 module Animation = Kgp_animation 20 17 module Compose = Kgp_compose 21 18 22 - (* Command type and functions *) 23 19 type command = Kgp_command.t 24 20 25 21 let transmit = Kgp_command.transmit ··· 35 31 let write_tmux = Kgp_command.write_tmux 36 32 let to_string_tmux = Kgp_command.to_string_tmux 37 33 38 - (* Core modules *) 39 34 module Response = Kgp_response 40 - 41 - (* Utility modules *) 42 35 module Unicode_placeholder = Kgp_unicode 43 36 module Detect = Kgp_detect 44 37 module Tmux = Kgp_tmux
+82 -86
lib/kgp.mli
··· 5 5 6 6 (** Kitty Terminal Graphics Protocol 7 7 8 - This library implements the Kitty terminal graphics protocol, allowing 9 - OCaml programs to display images in terminals that support the protocol 10 - (Kitty, WezTerm, Konsole, Ghostty, etc.). 8 + This library implements the Kitty terminal graphics protocol, allowing OCaml 9 + programs to display images in terminals that support the protocol (Kitty, 10 + WezTerm, Konsole, Ghostty, etc.). 11 11 12 12 {1 Protocol Overview} 13 13 ··· 16 16 17 17 - No requirement for terminal emulators to understand image formats 18 18 - Pixel-level positioning of graphics 19 - - Integration with text (graphics can be drawn below/above text with alpha blending) 19 + - Integration with text (graphics can be drawn below/above text with alpha 20 + blending) 20 21 - Automatic scrolling with text 21 22 - Animation support with frame deltas for efficiency 22 23 ··· 72 73 73 74 (* Display at different positions *) 74 75 let cmd = Kgp.display ~image_id:1 () in 75 - Kgp.write buf cmd ~data:""; 76 + Kgp.write buf cmd ~data:"" 76 77 ]} 77 78 78 79 {2 Protocol Reference} 79 80 80 - See {{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics Protocol} 81 - for the full specification. *) 81 + See 82 + {{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics 83 + Protocol} for the full specification. *) 82 84 83 85 (** {1 Type Modules} *) 84 86 ··· 106 108 107 109 (** {2 Image Transmission} 108 110 109 - Images can be transmitted to the terminal for storage and later display. 110 - The terminal assigns storage and responds with success or failure. 111 + Images can be transmitted to the terminal for storage and later display. The 112 + terminal assigns storage and responds with success or failure. 111 113 112 114 For large images, the library automatically handles chunked transmission 113 115 (splitting data into 4096-byte base64-encoded chunks). *) ··· 130 132 The image is stored by the terminal and can be displayed later using 131 133 {!val:display} with the same [image_id]. 132 134 133 - @param image_id Unique identifier (1-4294967295) for later reference. 134 - If specified, the terminal responds with success/failure. 135 - @param image_number Alternative to [image_id] where the terminal assigns 136 - a unique ID and returns it in the response. Useful when multiple 137 - programs share the terminal. 135 + @param image_id 136 + Unique identifier (1-4294967295) for later reference. If specified, the 137 + terminal responds with success/failure. 138 + @param image_number 139 + Alternative to [image_id] where the terminal assigns a unique ID and 140 + returns it in the response. Useful when multiple programs share the 141 + terminal. 138 142 @param format Pixel format of the data. Default is [`Rgba32]. 139 143 @param transmission How data is sent. Default is [`Direct] (inline). 140 144 @param compression Compression applied to data. Default is [`None]. ··· 160 164 command 161 165 (** Transmit image data and display it immediately. 162 166 163 - Combines transmission and display in a single command. The image is 164 - rendered at the current cursor position unless placement options 165 - specify otherwise. 167 + Combines transmission and display in a single command. The image is rendered 168 + at the current cursor position unless placement options specify otherwise. 166 169 167 170 See {!transmit} for parameter descriptions. The [placement] parameter 168 171 controls display position and scaling. *) ··· 177 180 command 178 181 (** Query terminal support without storing the image. 179 182 180 - Performs the same validation as {!transmit} but does not store the 181 - image. Useful for testing whether the terminal supports the graphics 182 - protocol and specific formats. 183 + Performs the same validation as {!transmit} but does not store the image. 184 + Useful for testing whether the terminal supports the graphics protocol and 185 + specific formats. 183 186 184 187 To detect graphics support, send a query and check for a response: 185 188 {[ ··· 191 194 192 195 (** {2 Display} 193 196 194 - Previously transmitted images can be displayed multiple times at 195 - different positions with different cropping and scaling options. *) 197 + Previously transmitted images can be displayed multiple times at different 198 + positions with different cropping and scaling options. *) 196 199 197 200 val display : 198 201 ?image_id:int -> ··· 203 206 command 204 207 (** Display a previously transmitted image. 205 208 206 - The image is rendered at the current cursor position. Use [placement] 207 - to control cropping, scaling, z-index, and other display options. 209 + The image is rendered at the current cursor position. Use [placement] to 210 + control cropping, scaling, z-index, and other display options. 208 211 209 - Each display creates a "placement" of the image. Multiple placements 210 - of the same image share the underlying image data. 212 + Each display creates a "placement" of the image. Multiple placements of the 213 + same image share the underlying image data. 211 214 212 215 @param image_id ID of a previously transmitted image. 213 - @param image_number Image number (acts on the newest image with this number). 216 + @param image_number 217 + Image number (acts on the newest image with this number). 214 218 @param placement Display configuration (position, size, z-index, etc.). *) 215 219 216 220 (** {2 Deletion} 217 221 218 - Images and placements can be deleted to free terminal resources. 219 - By default, only placements are removed and image data is retained 220 - for potential reuse. Use [~free:true] to also release the image data. *) 222 + Images and placements can be deleted to free terminal resources. By default, 223 + only placements are removed and image data is retained for potential reuse. 224 + Use [~free:true] to also release the image data. *) 221 225 222 226 val delete : ?free:bool -> ?quiet:Quiet.t -> Delete.t -> command 223 227 (** Delete images or placements. 224 228 225 229 See {!Delete} for the full list of deletion targets. 226 230 227 - @param free If true, also free the image data from memory (default: false). 228 - Without [~free:true], only placements are removed and the image data 229 - can be reused for new placements. 231 + @param free 232 + If true, also free the image data from memory (default: false). Without 233 + [~free:true], only placements are removed and the image data can be reused 234 + for new placements. 230 235 231 236 Examples: 232 237 {[ 233 238 (* Delete all visible images, keep data *) 234 239 Kgp.delete `All_visible 235 - 236 - (* Delete specific image, keeping data for reuse *) 237 - Kgp.delete (`By_id (42, None)) 238 - 239 - (* Delete specific image and free its data *) 240 - Kgp.delete ~free:true (`By_id (42, None)) 241 - 242 - (* Delete all placements at a specific cell *) 243 - Kgp.delete (`At_cell (10, 5)) 240 + (* Delete specific image, keeping data for reuse *) 241 + Kgp.delete 242 + (`By_id (42, None)) 243 + (* Delete specific image and free its data *) 244 + Kgp.delete ~free:true 245 + (`By_id (42, None)) 246 + (* Delete all placements at a specific cell *) 247 + Kgp.delete 248 + (`At_cell (10, 5)) 244 249 ]} *) 245 250 246 251 (** {2 Animation} ··· 249 254 Animations are created by first transmitting a base image, then adding 250 255 frames with optional delta encoding for efficiency. 251 256 252 - Frame numbers are 1-based: frame 1 is the root (base) image, frame 2 253 - is the first added frame, etc. *) 257 + Frame numbers are 1-based: frame 1 is the root (base) image, frame 2 is the 258 + first added frame, etc. *) 254 259 255 260 val frame : 256 261 ?image_id:int -> ··· 266 271 command 267 272 (** Transmit animation frame data. 268 273 269 - Adds a new frame to an existing image or edits an existing frame. 270 - The frame can be a full image or a partial update (rectangle). 274 + Adds a new frame to an existing image or edits an existing frame. The frame 275 + can be a full image or a partial update (rectangle). 271 276 272 - Use {!Frame.make} to configure the frame's position, timing, and 273 - composition options. 277 + Use {!Frame.make} to configure the frame's position, timing, and composition 278 + options. 274 279 275 280 @param frame Frame configuration including timing and composition. *) 276 281 277 282 val animate : 278 - ?image_id:int -> 279 - ?image_number:int -> 280 - ?quiet:Quiet.t -> 281 - Animation.t -> 282 - command 283 + ?image_id:int -> ?image_number:int -> ?quiet:Quiet.t -> Animation.t -> command 283 284 (** Control animation playback. 284 285 285 286 For terminal-driven animation: 286 287 {[ 287 288 (* Start infinite loop animation *) 288 - Kgp.animate ~image_id:1 (Animation.set_state ~loops:1 `Run) 289 - 290 - (* Stop animation *) 291 - Kgp.animate ~image_id:1 (Animation.set_state `Stop) 292 - 293 - (* Change frame timing *) 294 - Kgp.animate ~image_id:1 (Animation.set_gap ~frame:3 ~gap_ms:100) 289 + Kgp.animate ~image_id:1 290 + (Animation.set_state ~loops:1 `Run) 291 + (* Stop animation *) 292 + Kgp.animate ~image_id:1 293 + (Animation.set_state `Stop) 294 + (* Change frame timing *) 295 + Kgp.animate ~image_id:1 296 + (Animation.set_gap ~frame:3 ~gap_ms:100) 295 297 ]} 296 298 297 299 For client-driven animation: ··· 301 303 ]} *) 302 304 303 305 val compose : 304 - ?image_id:int -> 305 - ?image_number:int -> 306 - ?quiet:Quiet.t -> 307 - Compose.t -> 308 - command 306 + ?image_id:int -> ?image_number:int -> ?quiet:Quiet.t -> Compose.t -> command 309 307 (** Compose animation frames. 310 308 311 - Copies a rectangular region from one frame onto another. Useful for 312 - building complex frames from simpler components. 309 + Copies a rectangular region from one frame onto another. Useful for building 310 + complex frames from simpler components. 313 311 314 312 {[ 315 313 (* Copy a 50x50 region from frame 2 to frame 5 *) 316 - let comp = Compose.make 317 - ~source_frame:2 ~dest_frame:5 318 - ~width:50 ~height:50 319 - ~source_x:10 ~source_y:10 320 - ~dest_x:20 ~dest_y:20 () in 314 + let comp = 315 + Compose.make ~source_frame:2 ~dest_frame:5 ~width:50 ~height:50 316 + ~source_x:10 ~source_y:10 ~dest_x:20 ~dest_y:20 () 317 + in 321 318 Kgp.compose ~image_id:1 comp 322 319 ]} *) 323 320 324 321 (** {2 Output} 325 322 326 - Commands are serialized to escape sequences that can be written 327 - to the terminal. *) 323 + Commands are serialized to escape sequences that can be written to the 324 + terminal. *) 328 325 329 326 val write : Buffer.t -> command -> data:string -> unit 330 327 (** Write the command to a buffer. 331 328 332 329 The [data] parameter contains the raw image/frame data (before base64 333 - encoding). Pass an empty string for commands that don't include payload 334 - data (like {!val:display}, {!val:delete}, {!val:animate}). 330 + encoding). Pass an empty string for commands that don't include payload data 331 + (like {!val:display}, {!val:delete}, {!val:animate}). 335 332 336 333 The library handles base64 encoding and chunking automatically. *) 337 334 338 335 val to_string : command -> data:string -> string 339 336 (** Convert command to a string. 340 337 341 - Convenience wrapper around {!write} that returns the serialized 342 - command as a string. *) 338 + Convenience wrapper around {!write} that returns the serialized command as a 339 + string. *) 343 340 344 341 val write_tmux : Buffer.t -> command -> data:string -> unit 345 342 (** Write the command to a buffer with tmux passthrough support. 346 343 347 - If running inside tmux (detected via [TMUX] environment variable), 348 - wraps the graphics command in a DCS passthrough sequence so it 349 - reaches the underlying terminal. Otherwise, behaves like {!write}. 344 + If running inside tmux (detected via [TMUX] environment variable), wraps the 345 + graphics command in a DCS passthrough sequence so it reaches the underlying 346 + terminal. Otherwise, behaves like {!write}. 350 347 351 348 Requires tmux 3.3+ with [allow-passthrough] enabled. *) 352 349 353 350 val to_string_tmux : command -> data:string -> string 354 351 (** Convert command to a string with tmux passthrough support. 355 352 356 - Convenience wrapper around {!write_tmux}. If running inside tmux, 357 - wraps the output for passthrough. Otherwise, behaves like {!to_string}. *) 353 + Convenience wrapper around {!write_tmux}. If running inside tmux, wraps the 354 + output for passthrough. Otherwise, behaves like {!to_string}. *) 358 355 359 356 (** {1 Response} *) 360 357 ··· 366 363 module Detect = Kgp_detect 367 364 368 365 module Tmux = Kgp_tmux 369 - (** Tmux passthrough support. Provides functions to detect if running 370 - inside tmux and to wrap escape sequences for passthrough. *) 366 + (** Tmux passthrough support. Provides functions to detect if running inside 367 + tmux and to wrap escape sequences for passthrough. *) 371 368 372 369 module Terminal = Kgp_terminal 373 370 (** Terminal environment detection. Provides functions to detect terminal 374 371 capabilities, pager mode, and resolve graphics output mode. *) 375 -
+29 -26
lib/kgp_animation.mli
··· 5 5 6 6 (** Animation Control 7 7 8 - Operations for controlling animation playback. The protocol supports 9 - both terminal-driven and client-driven animation modes. 8 + Operations for controlling animation playback. The protocol supports both 9 + terminal-driven and client-driven animation modes. 10 10 11 11 {2 Protocol Overview} 12 12 ··· 24 24 25 25 {[ 26 26 (* Start infinite loop *) 27 - Kgp.animate ~image_id:1 (Animation.set_state ~loops:1 `Run) 28 - 29 - (* Run 3 times then stop *) 30 - Kgp.animate ~image_id:1 (Animation.set_state ~loops:4 `Run) 31 - 32 - (* Stop animation *) 33 - Kgp.animate ~image_id:1 (Animation.set_state `Stop) 27 + Kgp.animate ~image_id:1 28 + (Animation.set_state ~loops:1 `Run) 29 + (* Run 3 times then stop *) 30 + Kgp.animate ~image_id:1 31 + (Animation.set_state ~loops:4 `Run) 32 + (* Stop animation *) 33 + Kgp.animate ~image_id:1 34 + (Animation.set_state `Stop) 34 35 ]} 35 36 36 37 {2 Client-Driven Animation} ··· 52 53 53 54 {[ 54 55 (* Slow down frame 3 *) 55 - Kgp.animate ~image_id:1 (Animation.set_gap ~frame:3 ~gap_ms:200) 56 - 57 - (* Make frame 5 instant/gapless *) 58 - Kgp.animate ~image_id:1 (Animation.set_gap ~frame:5 ~gap_ms:(-1)) 56 + Kgp.animate ~image_id:1 57 + (Animation.set_gap ~frame:3 ~gap_ms:200) 58 + (* Make frame 5 instant/gapless *) 59 + Kgp.animate ~image_id:1 60 + (Animation.set_gap ~frame:5 ~gap_ms:(-1)) 59 61 ]} 60 62 61 63 {2 Loop Counting} ··· 71 73 | `Set_current of int ] 72 74 (** Animation control operations. 73 75 74 - - [`Set_state (state, loops)] - Set animation playback state with 75 - optional loop count. 76 + - [`Set_state (state, loops)] - Set animation playback state with optional 77 + loop count. 76 78 - [`Set_gap (frame, gap_ms)] - Set the delay for a specific frame. 77 79 - [`Set_current frame] - Jump to a specific frame (1-based). *) 78 80 79 81 val set_state : ?loops:int -> Kgp_animation_state.t -> t 80 82 (** Set animation playback state. 81 83 82 - @param loops Loop count: 0 = ignored, 1 = infinite, n > 1 = (n-1) loops. 83 - Protocol key: [v]. 84 + @param loops 85 + Loop count: 0 = ignored, 1 = infinite, n > 1 = (n-1) loops. Protocol key: 86 + [v]. 84 87 @param state The target playback state. 85 88 86 89 Examples: 87 90 {[ 88 - set_state `Run (* Run with current loop setting *) 89 - set_state ~loops:1 `Run (* Run infinitely *) 90 - set_state ~loops:3 `Run (* Run twice, then stop *) 91 - set_state `Stop (* Pause animation *) 92 - set_state `Loading (* Run, wait for more frames at end *) 91 + set_state `Run (* Run with current loop setting *) set_state ~loops:1 `Run 92 + (* Run infinitely *) set_state ~loops:3 `Run 93 + (* Run twice, then stop *) set_state `Stop (* Pause animation *) 94 + set_state `Loading (* Run, wait for more frames at end *) 93 95 ]} *) 94 96 95 97 val set_gap : frame:int -> gap_ms:int -> t 96 98 (** Set the gap (delay) for a specific frame. 97 99 98 100 @param frame 1-based frame number to modify. Protocol key: [r]. 99 - @param gap_ms Delay in milliseconds before next frame. Negative values 100 - create gapless frames (not displayed, instant skip). Protocol key: [z]. 101 + @param gap_ms 102 + Delay in milliseconds before next frame. Negative values create gapless 103 + frames (not displayed, instant skip). Protocol key: [z]. 101 104 102 105 Note: Frame 1 is the root/base image. Use 2+ for added frames. *) 103 106 ··· 106 109 107 110 @param frame 1-based frame number to display. Protocol key: [c]. 108 111 109 - Used for client-driven animation where the application controls 110 - frame advancement rather than the terminal. *) 112 + Used for client-driven animation where the application controls frame 113 + advancement rather than the terminal. *)
+1 -4
lib/kgp_animation_state.ml
··· 5 5 6 6 type t = [ `Stop | `Loading | `Run ] 7 7 8 - let to_int : t -> int = function 9 - | `Stop -> 1 10 - | `Loading -> 2 11 - | `Run -> 3 8 + let to_int : t -> int = function `Stop -> 1 | `Loading -> 2 | `Run -> 3
+25 -25
lib/kgp_animation_state.mli
··· 9 9 10 10 {2 Protocol Details} 11 11 12 - The animation state is specified via the [s] key in the control data 13 - when using action [a=a]: 12 + The animation state is specified via the [s] key in the control data when 13 + using action [a=a]: 14 14 - [s=1]: stop animation 15 15 - [s=2]: run in loading mode 16 16 - [s=3]: run normally ··· 19 19 20 20 The protocol supports two animation approaches: 21 21 22 - {b Terminal-driven animation}: The terminal automatically advances 23 - frames based on the gap (delay) specified for each frame. Use 24 - [{`Run}] or [{`Loading}] states. 22 + {b Terminal-driven animation}: The terminal automatically advances frames 23 + based on the gap (delay) specified for each frame. Use [{`Run}] or 24 + [{`Loading}] states. 25 25 26 - {b Client-driven animation}: The client manually sets the current 27 - frame using [Kgp.Animation.set_current_frame]. Use [{`Stop}] state 28 - to prevent automatic advancement. 26 + {b Client-driven animation}: The client manually sets the current frame 27 + using [Kgp.Animation.set_current_frame]. Use [{`Stop}] state to prevent 28 + automatic advancement. 29 29 30 30 {2 Stop State} 31 31 32 - [{`Stop}] halts automatic frame advancement. The animation freezes 33 - on the current frame. Use this when: 32 + [{`Stop}] halts automatic frame advancement. The animation freezes on the 33 + current frame. Use this when: 34 34 - Implementing client-driven animation 35 35 - Pausing an animation 36 36 - Displaying a static frame from an animated image 37 37 38 38 {2 Loading State} 39 39 40 - [{`Loading}] runs the animation but waits for new frames when reaching 41 - the end instead of looping. Use this when: 40 + [{`Loading}] runs the animation but waits for new frames when reaching the 41 + end instead of looping. Use this when: 42 42 - Streaming animation frames progressively 43 43 - Building an animation while displaying it 44 44 - The animation is not yet complete 45 45 46 46 {2 Run State} 47 47 48 - [{`Run}] runs the animation normally, looping back to the first frame 49 - after the last. The loop count can be controlled via the [loops] 50 - parameter in [Kgp.Animation.set_state]. *) 48 + [{`Run}] runs the animation normally, looping back to the first frame after 49 + the last. The loop count can be controlled via the [loops] parameter in 50 + [Kgp.Animation.set_state]. *) 51 51 52 52 type t = [ `Stop | `Loading | `Run ] 53 53 (** Animation playback states. 54 54 55 - - [`Stop] - Halt animation playback. The animation freezes on the 56 - current frame and does not advance automatically. 57 - - [`Loading] - Run animation but wait for new frames at end. When 58 - the last frame is reached, the animation pauses until more frames 59 - are added, then continues. 60 - - [`Run] - Run animation normally and loop. After the last frame, 61 - playback returns to the first frame (or stops after the specified 62 - number of loops). *) 55 + - [`Stop] - Halt animation playback. The animation freezes on the current 56 + frame and does not advance automatically. 57 + - [`Loading] - Run animation but wait for new frames at end. When the last 58 + frame is reached, the animation pauses until more frames are added, then 59 + continues. 60 + - [`Run] - Run animation normally and loop. After the last frame, playback 61 + returns to the first frame (or stops after the specified number of loops). 62 + *) 63 63 64 64 val to_int : t -> int 65 65 (** Convert to protocol integer. 66 66 67 - Returns 1 for [`Stop], 2 for [`Loading], or 3 for [`Run]. 68 - These values are used in the [s=] control data key. *) 67 + Returns 1 for [`Stop], 2 for [`Loading], or 3 for [`Run]. These values are 68 + used in the [s=] control data key. *)
+8 -9
lib/kgp_command.ml
··· 167 167 kv_int_opt w 'p' (Kgp_placement.placement_id p); 168 168 Kgp_placement.cursor p 169 169 |> Option.iter (fun c -> 170 - kv_int_if w 'C' ~default:0 (Some (Kgp_cursor.to_int c))); 170 + kv_int_if w 'C' ~default:0 (Some (Kgp_cursor.to_int c))); 171 171 if Kgp_placement.unicode_placeholder p then kv_int w 'U' 1 172 172 173 173 let write_delete w ~free (d : Kgp_delete.t) = ··· 202 202 kv_int_opt w 'z' (Kgp_frame.gap_ms f); 203 203 Kgp_frame.composition f 204 204 |> Option.iter (fun c -> 205 - kv_int_if w 'X' ~default:0 (Some (Kgp_composition.to_int c))); 205 + kv_int_if w 'X' ~default:0 (Some (Kgp_composition.to_int c))); 206 206 kv_int32_opt w 'Y' (Kgp_frame.background_color f) 207 207 208 208 let write_animation w : Kgp_animation.t -> unit = function ··· 226 226 kv_int_opt w 'Y' (Kgp_compose.source_y c); 227 227 Kgp_compose.composition c 228 228 |> Option.iter (fun comp -> 229 - kv_int_if w 'C' ~default:0 (Some (Kgp_composition.to_int comp))) 229 + kv_int_if w 'C' ~default:0 (Some (Kgp_composition.to_int comp))) 230 230 231 231 let write_control_data buf cmd = 232 232 let w = kv_writer buf in ··· 235 235 (* Quiet - only if non-default *) 236 236 cmd.quiet 237 237 |> Option.iter (fun q -> 238 - kv_int_if w 'q' ~default:0 (Some (Kgp_quiet.to_int q))); 238 + kv_int_if w 'q' ~default:0 (Some (Kgp_quiet.to_int q))); 239 239 (* Format *) 240 - cmd.format 241 - |> Option.iter (fun f -> kv_int w 'f' (Kgp_format.to_int f)); 240 + cmd.format |> Option.iter (fun f -> kv_int w 'f' (Kgp_format.to_int f)); 242 241 (* Transmission - only for transmit/frame actions, always include t=d for compatibility *) 243 242 (match cmd.action with 244 243 | `Transmit | `Transmit_and_display | `Frame -> ( ··· 249 248 (* Compression *) 250 249 cmd.compression 251 250 |> Option.iter (fun c -> 252 - Kgp_compression.to_char c |> Option.iter (kv_char w 'o')); 251 + Kgp_compression.to_char c |> Option.iter (kv_char w 'o')); 253 252 (* Dimensions *) 254 253 kv_int_opt w 's' cmd.width; 255 254 kv_int_opt w 'v' cmd.height; ··· 316 315 let inner_buf = Buffer.create 1024 in 317 316 write inner_buf cmd ~data; 318 317 Kgp_tmux.write_wrapped buf (Buffer.contents inner_buf) 319 - end else 320 - write buf cmd ~data 318 + end 319 + else write buf cmd ~data 321 320 322 321 let to_string_tmux cmd ~data = 323 322 let buf = Buffer.create 1024 in
+10 -13
lib/kgp_command.mli
··· 70 70 val delete : ?free:bool -> ?quiet:Kgp_quiet.t -> Kgp_delete.t -> t 71 71 (** Delete images or placements. 72 72 73 - @param free If true, also free the image data from memory (default: false). 74 - Without [~free:true], only placements are removed and the image data 75 - can be reused for new placements. *) 73 + @param free 74 + If true, also free the image data from memory (default: false). Without 75 + [~free:true], only placements are removed and the image data can be reused 76 + for new placements. *) 76 77 77 78 (** {1 Animation} *) 78 79 ··· 99 100 (** Control animation playback. *) 100 101 101 102 val compose : 102 - ?image_id:int -> 103 - ?image_number:int -> 104 - ?quiet:Kgp_quiet.t -> 105 - Kgp_compose.t -> 106 - t 103 + ?image_id:int -> ?image_number:int -> ?quiet:Kgp_quiet.t -> Kgp_compose.t -> t 107 104 (** Compose animation frames. *) 108 105 109 106 (** {1 Output} *) ··· 117 114 val write_tmux : Buffer.t -> t -> data:string -> unit 118 115 (** Write the command to a buffer with tmux passthrough wrapping. 119 116 120 - If running inside tmux (detected via [TMUX] environment variable), 121 - wraps the graphics command in a DCS passthrough sequence. Otherwise, 122 - behaves like {!write}. *) 117 + If running inside tmux (detected via [TMUX] environment variable), wraps the 118 + graphics command in a DCS passthrough sequence. Otherwise, behaves like 119 + {!write}. *) 123 120 124 121 val to_string_tmux : t -> data:string -> string 125 122 (** Convert command to a string with tmux passthrough wrapping. 126 123 127 - If running inside tmux, wraps the output for passthrough. 128 - Otherwise, behaves like {!to_string}. *) 124 + If running inside tmux, wraps the output for passthrough. Otherwise, behaves 125 + like {!to_string}. *)
+27 -29
lib/kgp_compose.mli
··· 10 10 11 11 {2 Protocol Overview} 12 12 13 - Frame composition uses action [a=c] to copy a rectangular region from 14 - one frame onto another. This is useful for: 13 + Frame composition uses action [a=c] to copy a rectangular region from one 14 + frame onto another. This is useful for: 15 15 16 16 - Building frames from reusable sprite components 17 17 - Applying partial updates to existing frames ··· 19 19 20 20 {2 Coordinate System} 21 21 22 - All coordinates are in pixels, relative to the top-left corner of 23 - the respective frame: 22 + All coordinates are in pixels, relative to the top-left corner of the 23 + respective frame: 24 24 25 25 - [source_x], [source_y]: Top-left of rectangle in source frame 26 26 - [dest_x], [dest_y]: Top-left of destination in target frame ··· 38 38 39 39 The terminal responds with errors for: 40 40 - [ENOENT]: Source or destination frame doesn't exist 41 - - [EINVAL]: Rectangle out of bounds, or source equals destination 42 - with overlapping regions 41 + - [EINVAL]: Rectangle out of bounds, or source equals destination with 42 + overlapping regions 43 43 - [ENOSPC]: Not enough storage after composition 44 44 45 45 {2 Example} 46 46 47 47 {[ 48 48 (* Copy a 32x32 sprite from frame 2 to frame 5 *) 49 - let comp = Compose.make 50 - ~source_frame:2 ~dest_frame:5 51 - ~width:32 ~height:32 52 - ~source_x:0 ~source_y:0 (* From top-left of source *) 53 - ~dest_x:100 ~dest_y:50 () (* To position in dest *) 49 + let comp = 50 + Compose.make ~source_frame:2 ~dest_frame:5 ~width:32 ~height:32 51 + ~source_x:0 ~source_y:0 (* From top-left of source *) 52 + ~dest_x:100 ~dest_y:50 () (* To position in dest *) 54 53 in 55 54 Kgp.compose ~image_id:1 comp 56 55 ]} *) ··· 72 71 t 73 72 (** Create a composition operation. 74 73 75 - @param source_frame 1-based frame number to copy from. Required. 76 - Protocol key: [r]. 77 - @param dest_frame 1-based frame number to copy onto. Required. 78 - Protocol key: [c]. 79 - @param width Width of rectangle in pixels. Default is full frame. 80 - Protocol key: [w]. 81 - @param height Height of rectangle in pixels. Default is full frame. 82 - Protocol key: [h]. 83 - @param source_x Left edge of source rectangle (default 0). 84 - Protocol key: [X]. 85 - @param source_y Top edge of source rectangle (default 0). 86 - Protocol key: [Y]. 87 - @param dest_x Left edge of destination position (default 0). 88 - Protocol key: [x]. 89 - @param dest_y Top edge of destination position (default 0). 90 - Protocol key: [y]. 91 - @param composition Blending mode. Default is alpha blending. 92 - Protocol key: [C]. *) 74 + @param source_frame 75 + 1-based frame number to copy from. Required. Protocol key: [r]. 76 + @param dest_frame 77 + 1-based frame number to copy onto. Required. Protocol key: [c]. 78 + @param width 79 + Width of rectangle in pixels. Default is full frame. Protocol key: [w]. 80 + @param height 81 + Height of rectangle in pixels. Default is full frame. Protocol key: [h]. 82 + @param source_x 83 + Left edge of source rectangle (default 0). Protocol key: [X]. 84 + @param source_y Top edge of source rectangle (default 0). Protocol key: [Y]. 85 + @param dest_x 86 + Left edge of destination position (default 0). Protocol key: [x]. 87 + @param dest_y 88 + Top edge of destination position (default 0). Protocol key: [y]. 89 + @param composition 90 + Blending mode. Default is alpha blending. Protocol key: [C]. *) 93 91 94 92 (** {1 Field Accessors} *) 95 93
+1 -3
lib/kgp_composition.ml
··· 5 5 6 6 type t = [ `Alpha_blend | `Overwrite ] 7 7 8 - let to_int : t -> int = function 9 - | `Alpha_blend -> 0 10 - | `Overwrite -> 1 8 + let to_int : t -> int = function `Alpha_blend -> 0 | `Overwrite -> 1
+7 -7
lib/kgp_composition.mli
··· 9 9 10 10 {2 Protocol Details} 11 11 12 - The composition mode is specified via the [X] key in the control data 13 - (for animation frames) or the [C] key (for frame composition operations): 12 + The composition mode is specified via the [X] key in the control data (for 13 + animation frames) or the [C] key (for frame composition operations): 14 14 - Value 0 or omitted: alpha blending (default) 15 15 - Value 1: simple overwrite/replacement 16 16 ··· 41 41 - [`Alpha_blend] - Full alpha blending (default). Source pixels are 42 42 composited onto the destination using standard Porter-Duff "over" 43 43 compositing based on the source alpha channel. 44 - - [`Overwrite] - Simple pixel replacement. Source pixels completely 45 - replace destination pixels, ignoring alpha values. Faster but no 46 - transparency support. *) 44 + - [`Overwrite] - Simple pixel replacement. Source pixels completely replace 45 + destination pixels, ignoring alpha values. Faster but no transparency 46 + support. *) 47 47 48 48 val to_int : t -> int 49 49 (** Convert to protocol integer. 50 50 51 - Returns 0 for [`Alpha_blend] or 1 for [`Overwrite]. 52 - These values are used in the [X=] or [C=] control data keys. *) 51 + Returns 0 for [`Alpha_blend] or 1 for [`Overwrite]. These values are used in 52 + the [X=] or [C=] control data keys. *)
+1 -3
lib/kgp_compression.ml
··· 5 5 6 6 type t = [ `None | `Zlib ] 7 7 8 - let to_char : t -> char option = function 9 - | `None -> None 10 - | `Zlib -> Some 'z' 8 + let to_char : t -> char option = function `None -> None | `Zlib -> Some 'z'
+8 -8
lib/kgp_compression.mli
··· 13 13 - No [o] key means no compression 14 14 - [o=z] means zlib (RFC 1950 DEFLATE) compression 15 15 16 - Compression is applied to the raw pixel/PNG data {i before} base64 17 - encoding. The terminal decompresses after base64 decoding. 16 + Compression is applied to the raw pixel/PNG data {i before} base64 encoding. 17 + The terminal decompresses after base64 decoding. 18 18 19 19 {2 When to Use Compression} 20 20 ··· 31 31 {2 PNG with Compression} 32 32 33 33 When using both [{`Png}] format and [{`Zlib}] compression, the [size] 34 - parameter must be specified with the original (uncompressed) PNG size. 35 - The terminal needs this to allocate the correct buffer for decompression. *) 34 + parameter must be specified with the original (uncompressed) PNG size. The 35 + terminal needs this to allocate the correct buffer for decompression. *) 36 36 37 37 type t = [ `None | `Zlib ] 38 38 (** Compression options. 39 39 40 40 - [`None] - Raw uncompressed data. No [o=] key is sent. 41 - - [`Zlib] - RFC 1950 zlib/DEFLATE compression. Data is compressed 42 - before base64 encoding and decompressed by the terminal. *) 41 + - [`Zlib] - RFC 1950 zlib/DEFLATE compression. Data is compressed before 42 + base64 encoding and decompressed by the terminal. *) 43 43 44 44 val to_char : t -> char option 45 45 (** Convert to protocol character. 46 46 47 - Returns [None] for [`None] (no key sent), or [Some 'z'] for [`Zlib]. 48 - When [Some c] is returned, [o=c] is added to the control data. *) 47 + Returns [None] for [`None] (no key sent), or [Some 'z'] for [`Zlib]. When 48 + [Some c] is returned, [o=c] is added to the control data. *)
+1 -3
lib/kgp_cursor.ml
··· 5 5 6 6 type t = [ `Move | `Static ] 7 7 8 - let to_int : t -> int = function 9 - | `Move -> 0 10 - | `Static -> 1 8 + let to_int : t -> int = function `Move -> 0 | `Static -> 1
+11 -11
lib/kgp_cursor.mli
··· 21 21 - Right by the number of columns the image occupies 22 22 - Down by the number of rows the image occupies 23 23 24 - This matches how the cursor moves after printing text, allowing images 25 - to flow naturally with text content. 24 + This matches how the cursor moves after printing text, allowing images to 25 + flow naturally with text content. 26 26 27 27 {2 Static Cursor} 28 28 ··· 34 34 35 35 {2 Relative Placements} 36 36 37 - Note: When using relative placements (positioning images relative to 38 - other placements), the cursor never moves regardless of this setting. *) 37 + Note: When using relative placements (positioning images relative to other 38 + placements), the cursor never moves regardless of this setting. *) 39 39 40 40 type t = [ `Move | `Static ] 41 41 (** Cursor movement behavior. 42 42 43 - - [`Move] - Advance cursor past the displayed image (default). 44 - Cursor moves right by the number of columns and down by the 45 - number of rows occupied by the image. 46 - - [`Static] - Keep cursor at its original position. The image 47 - is displayed but cursor position is unchanged. *) 43 + - [`Move] - Advance cursor past the displayed image (default). Cursor moves 44 + right by the number of columns and down by the number of rows occupied by 45 + the image. 46 + - [`Static] - Keep cursor at its original position. The image is displayed 47 + but cursor position is unchanged. *) 48 48 49 49 val to_int : t -> int 50 50 (** Convert to protocol integer. 51 51 52 - Returns 0 for [`Move] or 1 for [`Static]. 53 - These values are used in the [C=] control data key. *) 52 + Returns 0 for [`Move] or 1 for [`Static]. These values are used in the [C=] 53 + control data key. *)
+24 -23
lib/kgp_delete.mli
··· 9 9 10 10 {2 Protocol Details} 11 11 12 - Deletion is performed with action [a=d] and the [d] key specifies 13 - the deletion type. The [d] key uses single characters: 12 + Deletion is performed with action [a=d] and the [d] key specifies the 13 + deletion type. The [d] key uses single characters: 14 14 15 15 {v 16 16 | Char | Meaning | ··· 30 30 31 31 {2 Placements vs Image Data} 32 32 33 - Each deletion type can optionally free image data (controlled by the 34 - [~free] parameter in the delete command): 33 + Each deletion type can optionally free image data (controlled by the [~free] 34 + parameter in the delete command): 35 35 - {b Without free}: Removes placements only. The image data remains in 36 36 memory and can be displayed again later. (Protocol: lowercase char) 37 - - {b With free}: Removes placements AND frees the image data. The 38 - image cannot be displayed again without retransmitting. (Protocol: 39 - uppercase char) 37 + - {b With free}: Removes placements AND frees the image data. The image 38 + cannot be displayed again without retransmitting. (Protocol: uppercase 39 + char) 40 40 41 41 {2 Placement IDs} 42 42 43 43 When deleting by image ID or number, an optional placement ID can be 44 - specified to delete only a specific placement. If [None], all placements 45 - of that image are deleted. 44 + specified to delete only a specific placement. If [None], all placements of 45 + that image are deleted. 46 46 47 47 {2 Coordinate-Based Deletion} 48 48 49 - For [{`At_cell}] and [{`At_cell_z}], coordinates are 0-based cell 50 - positions (not pixel positions). Only placements that intersect the 51 - specified cell are deleted. 49 + For [{`At_cell}] and [{`At_cell_z}], coordinates are 0-based cell positions 50 + (not pixel positions). Only placements that intersect the specified cell are 51 + deleted. 52 52 53 53 {2 Virtual Placements} 54 54 55 - Virtual placements (used for Unicode placeholder mode) are only affected 56 - by: [{`By_id}], [{`By_number}], and [{`By_id_range}]. Other deletion 57 - commands do not affect virtual placements. *) 55 + Virtual placements (used for Unicode placeholder mode) are only affected by: 56 + [{`By_id}], [{`By_number}], and [{`By_id_range}]. Other deletion commands do 57 + not affect virtual placements. *) 58 58 59 59 type t = 60 60 [ `All_visible ··· 80 80 - [`By_z_index z] - All placements with z-index z 81 81 82 82 {b ID-based:} 83 - - [`By_id (id, placement_id)] - By image ID. If [placement_id] is 84 - [Some p], only that specific placement; if [None], all placements. 85 - - [`By_number (n, placement_id)] - By image number (newest image 86 - with that number). Placement ID works as above. 83 + - [`By_id (id, placement_id)] - By image ID. If [placement_id] is [Some p], 84 + only that specific placement; if [None], all placements. 85 + - [`By_number (n, placement_id)] - By image number (newest image with that 86 + number). Placement ID works as above. 87 87 - [`By_id_range (min, max)] - All images with IDs in range [min..max] 88 88 89 89 {b Animation:} 90 90 - [`Frames] - Animation frames only (not the base image) 91 91 92 - Use the [~free] parameter in the delete command to also release 93 - image data from memory. *) 92 + Use the [~free] parameter in the delete command to also release image data 93 + from memory. *) 94 94 95 95 val to_char : free:bool -> t -> char 96 96 (** Convert to protocol character for the delete command. 97 97 98 98 Returns the character used in the [d=] control data key. 99 - @param free If true, returns uppercase (frees data); if false, 100 - returns lowercase (keeps data). *) 99 + @param free 100 + If true, returns uppercase (frees data); if false, returns lowercase 101 + (keeps data). *)
+1 -4
lib/kgp_format.ml
··· 5 5 6 6 type t = [ `Rgba32 | `Rgb24 | `Png ] 7 7 8 - let to_int : t -> int = function 9 - | `Rgba32 -> 32 10 - | `Rgb24 -> 24 11 - | `Png -> 100 8 + let to_int : t -> int = function `Rgba32 -> 32 | `Rgb24 -> 24 | `Png -> 100
+13 -13
lib/kgp_format.mli
··· 17 17 {2 Raw Pixel Formats} 18 18 19 19 For [{`Rgb24}] and [{`Rgba32}], the data consists of raw pixel values in 20 - row-major order (left-to-right, top-to-bottom). The image dimensions must 21 - be specified via the [width] and [height] parameters. 20 + row-major order (left-to-right, top-to-bottom). The image dimensions must be 21 + specified via the [width] and [height] parameters. 22 22 23 23 - [{`Rgb24}]: 3 bytes per pixel in sRGB color space (red, green, blue) 24 24 - [{`Rgba32}]: 4 bytes per pixel (red, green, blue, alpha) ··· 28 28 For [{`Png}], the data is a complete PNG image. The terminal extracts 29 29 dimensions from PNG metadata, so [width] and [height] are optional. 30 30 31 - When using both PNG format and zlib compression, you must also specify 32 - the [size] parameter with the uncompressed PNG data size. *) 31 + When using both PNG format and zlib compression, you must also specify the 32 + [size] parameter with the uncompressed PNG data size. *) 33 33 34 34 type t = [ `Rgba32 | `Rgb24 | `Png ] 35 35 (** Image data formats. 36 36 37 - - [`Rgba32] - 32-bit RGBA (4 bytes per pixel). Default format. 38 - Pixels are ordered red, green, blue, alpha. Alpha of 255 is fully 39 - opaque, 0 is fully transparent. 40 - - [`Rgb24] - 24-bit RGB (3 bytes per pixel). No alpha channel; 41 - pixels are fully opaque. More compact than RGBA for opaque images. 37 + - [`Rgba32] - 32-bit RGBA (4 bytes per pixel). Default format. Pixels are 38 + ordered red, green, blue, alpha. Alpha of 255 is fully opaque, 0 is fully 39 + transparent. 40 + - [`Rgb24] - 24-bit RGB (3 bytes per pixel). No alpha channel; pixels are 41 + fully opaque. More compact than RGBA for opaque images. 42 42 - [`Png] - PNG encoded data. The terminal decodes the PNG internally. 43 - Supports all PNG color types and bit depths. Most convenient format 44 - as dimensions are embedded in the data. *) 43 + Supports all PNG color types and bit depths. Most convenient format as 44 + dimensions are embedded in the data. *) 45 45 46 46 val to_int : t -> int 47 47 (** Convert to protocol integer value. 48 48 49 - Returns 24 for [`Rgb24], 32 for [`Rgba32], or 100 for [`Png]. 50 - These values are used in the [f=] control data key. *) 49 + Returns 24 for [`Rgb24], 32 for [`Rgba32], or 100 for [`Png]. These values 50 + are used in the [f=] control data key. *)
+2 -1
lib/kgp_frame.ml
··· 24 24 background_color = None; 25 25 } 26 26 27 - let make ?x ?y ?base_frame ?edit_frame ?gap_ms ?composition ?background_color () = 27 + let make ?x ?y ?base_frame ?edit_frame ?gap_ms ?composition ?background_color () 28 + = 28 29 { x; y; base_frame; edit_frame; gap_ms; composition; background_color } 29 30 30 31 let x t = t.x
+46 -41
lib/kgp_frame.mli
··· 5 5 6 6 (** Animation Frame Configuration 7 7 8 - Configuration for adding or editing animation frames. Frames can be 9 - full images or partial updates (rectangles), with options for timing 10 - and composition. 8 + Configuration for adding or editing animation frames. Frames can be full 9 + images or partial updates (rectangles), with options for timing and 10 + composition. 11 11 12 12 {2 Protocol Overview} 13 13 14 - Animations are created by: 15 - 1. Transmitting a base image (becomes frame 1) 16 - 2. Adding frames using the frame action ([a=f]) 17 - 3. Controlling playback with animation commands 14 + Animations are created by: 1. Transmitting a base image (becomes frame 1) 2. 15 + Adding frames using the frame action ([a=f]) 3. Controlling playback with 16 + animation commands 18 17 19 18 Frame numbers are 1-based: 20 19 - Frame 1: The original/base image ··· 22 21 23 22 {2 Frame Positioning} 24 23 25 - For partial frame updates, [x] and [y] specify where the new pixel 26 - data is placed within the frame canvas: 24 + For partial frame updates, [x] and [y] specify where the new pixel data is 25 + placed within the frame canvas: 27 26 - [x]: Left edge position in pixels (default 0) 28 27 - [y]: Top edge position in pixels (default 0) 29 28 30 - The frame data dimensions come from the [width] and [height] parameters 31 - of the frame command. 29 + The frame data dimensions come from the [width] and [height] parameters of 30 + the frame command. 32 31 33 32 {2 Frame Canvas} 34 33 35 34 Each frame needs a background canvas to composite onto. Options: 36 35 37 - {b Solid color background} ([background_color]): 38 - Use a 32-bit RGBA color. Format: [0xRRGGBBAA] where AA is alpha. 39 - Default is 0 (transparent black). 36 + {b Solid color background} ([background_color]): Use a 32-bit RGBA color. 37 + Format: [0xRRGGBBAA] where AA is alpha. Default is 0 (transparent black). 40 38 41 - {b Copy from existing frame} ([base_frame]): 42 - Use another frame as the starting canvas. Specified as 1-based frame 43 - number. The base frame's pixels are copied, then new data is composited. 39 + {b Copy from existing frame} ([base_frame]): Use another frame as the 40 + starting canvas. Specified as 1-based frame number. The base frame's pixels 41 + are copied, then new data is composited. 44 42 45 43 {2 Editing Existing Frames} 46 44 ··· 51 49 52 50 {2 Frame Timing} 53 51 54 - The [gap_ms] parameter controls the delay before transitioning to 55 - the next frame: 52 + The [gap_ms] parameter controls the delay before transitioning to the next 53 + frame: 56 54 - Positive value: Delay in milliseconds 57 55 - Zero: Ignored (keeps existing gap) 58 - - Negative value: "Gapless" frame - not displayed, used as a base 59 - for other frames 56 + - Negative value: "Gapless" frame - not displayed, used as a base for other 57 + frames 60 58 61 - Default gap for new frames is 40ms. The root frame (frame 1) has 62 - a default gap of 0ms. 59 + Default gap for new frames is 40ms. The root frame (frame 1) has a default 60 + gap of 0ms. 63 61 64 62 {2 Composition Mode} 65 63 66 - The [composition] parameter controls how new pixel data is blended 67 - onto the canvas. *) 64 + The [composition] parameter controls how new pixel data is blended onto the 65 + canvas. *) 68 66 69 67 type t 70 68 (** Animation frame configuration. Opaque type; use {!make} to construct. *) ··· 81 79 t 82 80 (** Create a frame specification. 83 81 84 - @param x Left edge where frame data is placed in pixels (default 0). 85 - Protocol key: [x]. 86 - @param y Top edge where frame data is placed in pixels (default 0). 87 - Protocol key: [y]. 88 - @param base_frame 1-based frame number to use as background canvas. 89 - Frame 1 is the root image. Protocol key: [c]. 90 - @param edit_frame 1-based frame number to edit instead of creating new. 91 - If 0 or unset, a new frame is created. Protocol key: [r]. 92 - @param gap_ms Delay before next frame in milliseconds. Negative values 93 - create gapless frames. Protocol key: [z]. 94 - @param composition How to blend new pixels onto the canvas. 95 - Default is alpha blending. Protocol key: [X]. 96 - @param background_color 32-bit RGBA background color when not using 97 - a base frame. Format: [0xRRGGBBAA]. Protocol key: [Y]. *) 82 + @param x 83 + Left edge where frame data is placed in pixels (default 0). Protocol key: 84 + [x]. 85 + @param y 86 + Top edge where frame data is placed in pixels (default 0). Protocol key: 87 + [y]. 88 + @param base_frame 89 + 1-based frame number to use as background canvas. Frame 1 is the root 90 + image. Protocol key: [c]. 91 + @param edit_frame 92 + 1-based frame number to edit instead of creating new. If 0 or unset, a new 93 + frame is created. Protocol key: [r]. 94 + @param gap_ms 95 + Delay before next frame in milliseconds. Negative values create gapless 96 + frames. Protocol key: [z]. 97 + @param composition 98 + How to blend new pixels onto the canvas. Default is alpha blending. 99 + Protocol key: [X]. 100 + @param background_color 101 + 32-bit RGBA background color when not using a base frame. Format: 102 + [0xRRGGBBAA]. Protocol key: [Y]. *) 98 103 99 104 val empty : t 100 105 (** Empty frame spec with all defaults. 101 106 102 - Creates a new frame with transparent black background, composited 103 - at position (0, 0) with default timing (40ms gap). *) 107 + Creates a new frame with transparent black background, composited at 108 + position (0, 0) with default timing (40ms gap). *) 104 109 105 110 (** {1 Field Accessors} *) 106 111
+44 -36
lib/kgp_placement.mli
··· 23 23 - [source_x], [source_y]: Top-left corner in pixels (default: 0, 0) 24 24 - [source_width], [source_height]: Size in pixels (default: full image) 25 25 26 - The displayed area is the intersection of this rectangle with the 27 - actual image bounds. This allows cropping images without modifying 28 - the original data. 26 + The displayed area is the intersection of this rectangle with the actual 27 + image bounds. This allows cropping images without modifying the original 28 + data. 29 29 30 30 {2 Cell-Based Sizing} 31 31 ··· 33 33 - [columns]: Number of columns to span (width in cells) 34 34 - [rows]: Number of rows to span (height in cells) 35 35 36 - If both are specified, the source rectangle is scaled to fit. 37 - If only one is specified, the other is computed to maintain aspect ratio. 38 - If neither is specified, the image is displayed at natural size. 36 + If both are specified, the source rectangle is scaled to fit. If only one is 37 + specified, the other is computed to maintain aspect ratio. If neither is 38 + specified, the image is displayed at natural size. 39 39 40 40 {2 Pixel Offsets} 41 41 ··· 51 51 - Positive values: drawn above text 52 52 - Zero: drawn at text level 53 53 - Negative values: drawn below text 54 - - Values < INT32_MIN/2 (-1,073,741,824): drawn under cells with 55 - non-default background colors 54 + - Values < INT32_MIN/2 (-1,073,741,824): drawn under cells with non-default 55 + background colors 56 56 57 - Overlapping images with the same z-index are ordered by image ID 58 - (lower ID draws first/underneath). 57 + Overlapping images with the same z-index are ordered by image ID (lower ID 58 + draws first/underneath). 59 59 60 60 {2 Placement IDs} 61 61 ··· 65 65 - Deleting specific placements 66 66 - Moving placements by resending with same image_id + placement_id 67 67 68 - If [placement_id] is 0 or unspecified, each display creates an 69 - independent placement. *) 68 + If [placement_id] is 0 or unspecified, each display creates an independent 69 + placement. *) 70 70 71 71 type t 72 72 (** Placement configuration. Opaque type; use {!make} to construct. *) ··· 88 88 t 89 89 (** Create a placement configuration. 90 90 91 - @param source_x Left edge of source rectangle in pixels (default 0). 92 - Protocol key: [x]. 93 - @param source_y Top edge of source rectangle in pixels (default 0). 94 - Protocol key: [y]. 95 - @param source_width Width of source rectangle in pixels. 96 - Default is the full image width. Protocol key: [w]. 97 - @param source_height Height of source rectangle in pixels. 98 - Default is the full image height. Protocol key: [h]. 99 - @param cell_x_offset X offset within the first cell in pixels. 100 - Must be smaller than cell width. Protocol key: [X]. 101 - @param cell_y_offset Y offset within the first cell in pixels. 102 - Must be smaller than cell height. Protocol key: [Y]. 103 - @param columns Number of columns to display over. Image is scaled 104 - to fit. Protocol key: [c]. 105 - @param rows Number of rows to display over. Image is scaled to fit. 106 - Protocol key: [r]. 107 - @param z_index Stacking order. Positive = above text, negative = below. 108 - Protocol key: [z]. 109 - @param placement_id Unique ID (1-4294967295) for this placement. 110 - Allows updating/deleting specific placements. Protocol key: [p]. 91 + @param source_x 92 + Left edge of source rectangle in pixels (default 0). Protocol key: [x]. 93 + @param source_y 94 + Top edge of source rectangle in pixels (default 0). Protocol key: [y]. 95 + @param source_width 96 + Width of source rectangle in pixels. Default is the full image width. 97 + Protocol key: [w]. 98 + @param source_height 99 + Height of source rectangle in pixels. Default is the full image height. 100 + Protocol key: [h]. 101 + @param cell_x_offset 102 + X offset within the first cell in pixels. Must be smaller than cell width. 103 + Protocol key: [X]. 104 + @param cell_y_offset 105 + Y offset within the first cell in pixels. Must be smaller than cell 106 + height. Protocol key: [Y]. 107 + @param columns 108 + Number of columns to display over. Image is scaled to fit. Protocol key: 109 + [c]. 110 + @param rows 111 + Number of rows to display over. Image is scaled to fit. Protocol key: [r]. 112 + @param z_index 113 + Stacking order. Positive = above text, negative = below. Protocol key: 114 + [z]. 115 + @param placement_id 116 + Unique ID (1-4294967295) for this placement. Allows updating/deleting 117 + specific placements. Protocol key: [p]. 111 118 @param cursor Cursor movement policy after display. 112 - @param unicode_placeholder If true, creates a virtual (invisible) 113 - placement for Unicode placeholder mode. Protocol key: [U=1]. *) 119 + @param unicode_placeholder 120 + If true, creates a virtual (invisible) placement for Unicode placeholder 121 + mode. Protocol key: [U=1]. *) 114 122 115 123 val empty : t 116 124 (** Empty placement with all defaults. 117 125 118 - Equivalent to [make ()]. The image displays at natural size at the 119 - current cursor position with default z-index (0). *) 126 + Equivalent to [make ()]. The image displays at natural size at the current 127 + cursor position with default z-index (0). *) 120 128 121 129 (** {1 Field Accessors} *) 122 130
+14 -14
lib/kgp_quiet.mli
··· 20 20 - On success: [ESC _Gi=ID;OK ESC] 21 21 - On failure: [ESC _Gi=ID;ECODE:message ESC] 22 22 23 - Response processing requires reading from the terminal, which can be 24 - complex in some applications. 23 + Response processing requires reading from the terminal, which can be complex 24 + in some applications. 25 25 26 26 {2 Use Cases} 27 27 28 - [{`Noisy}] (default): Use when you need to verify operations succeeded 29 - or want to handle errors programmatically. 28 + [{`Noisy}] (default): Use when you need to verify operations succeeded or 29 + want to handle errors programmatically. 30 30 31 31 [{`Errors_only}]: Use when you want to detect failures but don't need 32 32 confirmation of success. Reduces response traffic. 33 33 34 - [{`Silent}]: Use in fire-and-forget scenarios like shell scripts or 35 - when the application cannot easily read terminal responses. Also useful 36 - for high-frequency animation updates where response processing would 37 - add latency. *) 34 + [{`Silent}]: Use in fire-and-forget scenarios like shell scripts or when the 35 + application cannot easily read terminal responses. Also useful for 36 + high-frequency animation updates where response processing would add 37 + latency. *) 38 38 39 39 type t = [ `Noisy | `Errors_only | `Silent ] 40 40 (** Response suppression levels. 41 41 42 42 - [`Noisy] - Send all responses including OK confirmations (default). 43 43 Required for detecting success and getting assigned image IDs. 44 - - [`Errors_only] - Suppress OK responses, only send error messages. 45 - Useful when success is expected but errors should be caught. 46 - - [`Silent] - Suppress all responses including errors. Useful for 47 - shell scripts or when response handling is not possible. *) 44 + - [`Errors_only] - Suppress OK responses, only send error messages. Useful 45 + when success is expected but errors should be caught. 46 + - [`Silent] - Suppress all responses including errors. Useful for shell 47 + scripts or when response handling is not possible. *) 48 48 49 49 val to_int : t -> int 50 50 (** Convert to protocol integer. 51 51 52 - Returns 0 for [`Noisy], 1 for [`Errors_only], or 2 for [`Silent]. 53 - These values are used in the [q=] control data key. *) 52 + Returns 0 for [`Noisy], 1 for [`Errors_only], or 2 for [`Silent]. These 53 + values are used in the [q=] control data key. *)
+1 -1
lib/kgp_response.ml
··· 20 20 else 21 21 String.index_opt t.message ':' 22 22 |> Option.fold ~none:(Some t.message) ~some:(fun i -> 23 - Some (String.sub t.message 0 i)) 23 + Some (String.sub t.message 0 i)) 24 24 25 25 let image_id t = t.image_id 26 26 let image_number t = t.image_number
+27 -31
lib/kgp_terminal.ml
··· 8 8 type graphics_mode = [ `Auto | `Enabled | `Disabled | `Tmux ] 9 9 10 10 let is_kitty () = 11 - Option.is_some (Sys.getenv_opt "KITTY_WINDOW_ID") || 12 - (match Sys.getenv_opt "TERM" with 13 - | Some term -> String.lowercase_ascii term = "xterm-kitty" 14 - | None -> false) || 15 - (match Sys.getenv_opt "TERM_PROGRAM" with 16 - | Some prog -> String.lowercase_ascii prog = "kitty" 17 - | None -> false) 11 + Option.is_some (Sys.getenv_opt "KITTY_WINDOW_ID") 12 + || (match Sys.getenv_opt "TERM" with 13 + | Some term -> String.lowercase_ascii term = "xterm-kitty" 14 + | None -> false) 15 + || 16 + match Sys.getenv_opt "TERM_PROGRAM" with 17 + | Some prog -> String.lowercase_ascii prog = "kitty" 18 + | None -> false 18 19 19 20 let is_wezterm () = 20 - Option.is_some (Sys.getenv_opt "WEZTERM_PANE") || 21 - (match Sys.getenv_opt "TERM_PROGRAM" with 22 - | Some prog -> String.lowercase_ascii prog = "wezterm" 23 - | None -> false) 21 + Option.is_some (Sys.getenv_opt "WEZTERM_PANE") 22 + || 23 + match Sys.getenv_opt "TERM_PROGRAM" with 24 + | Some prog -> String.lowercase_ascii prog = "wezterm" 25 + | None -> false 24 26 25 27 let is_ghostty () = 26 - Option.is_some (Sys.getenv_opt "GHOSTTY_RESOURCES_DIR") || 27 - (match Sys.getenv_opt "TERM_PROGRAM" with 28 - | Some prog -> String.lowercase_ascii prog = "ghostty" 29 - | None -> false) 30 - 31 - let is_graphics_terminal () = 32 - is_kitty () || is_wezterm () || is_ghostty () 28 + Option.is_some (Sys.getenv_opt "GHOSTTY_RESOURCES_DIR") 29 + || 30 + match Sys.getenv_opt "TERM_PROGRAM" with 31 + | Some prog -> String.lowercase_ascii prog = "ghostty" 32 + | None -> false 33 33 34 + let is_graphics_terminal () = is_kitty () || is_wezterm () || is_ghostty () 34 35 let is_tmux () = Kgp_tmux.is_active () 35 - 36 - let is_interactive () = 37 - Unix.isatty Unix.stdout 36 + let is_interactive () = Unix.isatty Unix.stdout 38 37 39 38 let is_pager () = 40 39 (* Not interactive = likely piped to pager *) 41 - not (is_interactive ()) || 40 + (not (is_interactive ())) 41 + || 42 42 (* PAGER set and not in a known graphics terminal *) 43 43 (Option.is_some (Sys.getenv_opt "PAGER") && not (is_graphics_terminal ())) 44 44 ··· 47 47 | `Enabled -> `Graphics 48 48 | `Tmux -> `Tmux 49 49 | `Auto -> 50 - if is_pager () || not (is_interactive ()) then 51 - `Placeholder 52 - else if is_tmux () then 53 - (* Inside tmux - use passthrough if underlying terminal supports graphics *) 54 - if is_graphics_terminal () then `Tmux 50 + if is_pager () || not (is_interactive ()) then `Placeholder 51 + else if is_tmux () then 52 + (* Inside tmux - use passthrough if underlying terminal supports graphics *) 53 + if is_graphics_terminal () then `Tmux else `Placeholder 54 + else if is_graphics_terminal () then `Graphics 55 55 else `Placeholder 56 - else if is_graphics_terminal () then 57 - `Graphics 58 - else 59 - `Placeholder 60 56 61 57 let supports_graphics mode = 62 58 match resolve_mode mode with
+8 -9
lib/kgp_tmux.ml
··· 5 5 6 6 (* Tmux Passthrough Support - Implementation *) 7 7 8 - let is_active () = 9 - Option.is_some (Sys.getenv_opt "TMUX") 8 + let is_active () = Option.is_some (Sys.getenv_opt "TMUX") 10 9 11 10 let write_wrapped buf s = 12 11 (* DCS passthrough prefix: ESC P tmux ; *) 13 12 Buffer.add_string buf "\027Ptmux;"; 14 13 (* Double all ESC characters in the content *) 15 - String.iter (fun c -> 16 - if c = '\027' then Buffer.add_string buf "\027\027" 17 - else Buffer.add_char buf c 18 - ) s; 14 + String.iter 15 + (fun c -> 16 + if c = '\027' then Buffer.add_string buf "\027\027" 17 + else Buffer.add_char buf c) 18 + s; 19 19 (* DCS terminator: ESC \ *) 20 20 Buffer.add_string buf "\027\\" 21 21 22 22 let wrap_always s = 23 - let buf = Buffer.create (String.length s * 2 + 10) in 23 + let buf = Buffer.create ((String.length s * 2) + 10) in 24 24 write_wrapped buf s; 25 25 Buffer.contents buf 26 26 27 - let wrap s = 28 - if is_active () then wrap_always s else s 27 + let wrap s = if is_active () then wrap_always s else s
+20 -19
lib/kgp_tmux.mli
··· 5 5 6 6 (** Tmux Passthrough Support 7 7 8 - Support for passing graphics protocol escape sequences through tmux 9 - to the underlying terminal emulator. 8 + Support for passing graphics protocol escape sequences through tmux to the 9 + underlying terminal emulator. 10 10 11 11 {2 Background} 12 12 13 13 When running inside tmux, graphics protocol escape sequences need to be 14 - wrapped in a DCS (Device Control String) passthrough sequence so that 15 - tmux forwards them to the actual terminal (kitty, wezterm, ghostty, etc.) 16 - rather than interpreting them itself. 14 + wrapped in a DCS (Device Control String) passthrough sequence so that tmux 15 + forwards them to the actual terminal (kitty, wezterm, ghostty, etc.) rather 16 + than interpreting them itself. 17 17 18 18 The passthrough format is: 19 19 - Prefix: [ESC P tmux ;] ··· 23 23 {2 Requirements} 24 24 25 25 For tmux passthrough to work: 26 - - tmux version 3.3 or later 27 - - [allow-passthrough] must be enabled in tmux.conf: 28 - {v set -g allow-passthrough on v} 26 + {ul 27 + {- tmux version 3.3 or later } 28 + {- [allow-passthrough] must be enabled in tmux.conf: 29 + {v set -g allow-passthrough on v} 30 + } 31 + } 29 32 30 33 {2 Usage} 31 34 ··· 33 36 if Kgp.Tmux.is_active () then 34 37 let wrapped = Kgp.Tmux.wrap graphics_command in 35 38 print_string wrapped 36 - else 37 - print_string graphics_command 39 + else print_string graphics_command 38 40 ]} *) 39 41 40 42 val is_active : unit -> bool 41 43 (** Detect if we are running inside tmux. 42 44 43 - Returns [true] if the [TMUX] environment variable is set, 44 - indicating the process is running inside a tmux session. *) 45 + Returns [true] if the [TMUX] environment variable is set, indicating the 46 + process is running inside a tmux session. *) 45 47 46 48 val wrap : string -> string 47 49 (** Wrap an escape sequence for tmux passthrough. 48 50 49 - Takes a graphics protocol escape sequence and wraps it in the 50 - tmux DCS passthrough format: 51 + Takes a graphics protocol escape sequence and wraps it in the tmux DCS 52 + passthrough format: 51 53 - Adds [ESC P tmux ;] prefix 52 54 - Doubles all ESC characters in the content 53 55 - Adds [ESC] suffix ··· 57 59 val wrap_always : string -> string 58 60 (** Wrap an escape sequence for tmux passthrough unconditionally. 59 61 60 - Like {!wrap} but always applies the wrapping, regardless of 61 - whether we are inside tmux. Useful when you want to pre-generate 62 - tmux-compatible output. *) 62 + Like {!wrap} but always applies the wrapping, regardless of whether we are 63 + inside tmux. Useful when you want to pre-generate tmux-compatible output. *) 63 64 64 65 val write_wrapped : Buffer.t -> string -> unit 65 66 (** Write a wrapped escape sequence directly to a buffer. 66 67 67 - More efficient than {!wrap_always} when building output in a buffer, 68 - as it avoids allocating an intermediate string. *) 68 + More efficient than {!wrap_always} when building output in a buffer, as it 69 + avoids allocating an intermediate string. *)
+11 -11
lib/kgp_transmission.mli
··· 16 16 17 17 {2 Direct Transmission} 18 18 19 - [{`Direct}] sends data inline within the escape sequence itself. The data 20 - is base64-encoded in the payload section. This is the simplest method and 21 - works over any connection (including SSH). 19 + [{`Direct}] sends data inline within the escape sequence itself. The data is 20 + base64-encoded in the payload section. This is the simplest method and works 21 + over any connection (including SSH). 22 22 23 23 For images larger than 4096 bytes (after base64 encoding), the data is 24 24 automatically split into chunks using the [m=] key: ··· 27 27 28 28 {2 File Transmission} 29 29 30 - [{`File}] tells the terminal to read data from a file path. The path is 31 - sent base64-encoded in the payload. Additional parameters: 30 + [{`File}] tells the terminal to read data from a file path. The path is sent 31 + base64-encoded in the payload. Additional parameters: 32 32 - [S=] specifies the number of bytes to read 33 33 - [O=] specifies the byte offset to start reading from 34 34 35 - File transmission only works when the terminal and client share a 36 - filesystem (i.e., local terminals, not SSH). 35 + File transmission only works when the terminal and client share a filesystem 36 + (i.e., local terminals, not SSH). 37 37 38 - Security: The terminal will refuse to read device files, sockets, or 39 - files in sensitive locations like [/proc], [/sys], or [/dev]. 38 + Security: The terminal will refuse to read device files, sockets, or files 39 + in sensitive locations like [/proc], [/sys], or [/dev]. 40 40 41 41 {2 Temporary File Transmission} 42 42 ··· 53 53 - [`Direct] - Data is sent inline in the escape sequence (base64-encoded). 54 54 Works over any connection including SSH. Automatic chunking for large 55 55 images. 56 - - [`File] - Terminal reads from a file path sent in the payload. 57 - Only works when terminal and client share a filesystem. 56 + - [`File] - Terminal reads from a file path sent in the payload. Only works 57 + when terminal and client share a filesystem. 58 58 - [`Tempfile] - Like [`File] but terminal deletes the file after reading. 59 59 File must be in a recognized temporary directory. *) 60 60
+267 -39
lib/kgp_unicode.ml
··· 9 9 10 10 let diacritics = 11 11 [| 12 - 0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F; 13 - 0x0346; 0x034A; 0x034B; 0x034C; 0x0350; 0x0351; 0x0352; 0x0357; 14 - 0x035B; 0x0363; 0x0364; 0x0365; 0x0366; 0x0367; 0x0368; 0x0369; 15 - 0x036A; 0x036B; 0x036C; 0x036D; 0x036E; 0x036F; 0x0483; 0x0484; 16 - 0x0485; 0x0486; 0x0487; 0x0592; 0x0593; 0x0594; 0x0595; 0x0597; 17 - 0x0598; 0x0599; 0x059C; 0x059D; 0x059E; 0x059F; 0x05A0; 0x05A1; 18 - 0x05A8; 0x05A9; 0x05AB; 0x05AC; 0x05AF; 0x05C4; 0x0610; 0x0611; 19 - 0x0612; 0x0613; 0x0614; 0x0615; 0x0616; 0x0617; 0x0657; 0x0658; 20 - 0x0659; 0x065A; 0x065B; 0x065D; 0x065E; 0x06D6; 0x06D7; 0x06D8; 21 - 0x06D9; 0x06DA; 0x06DB; 0x06DC; 0x06DF; 0x06E0; 0x06E1; 0x06E2; 22 - 0x06E4; 0x06E7; 0x06E8; 0x06EB; 0x06EC; 0x0730; 0x0732; 0x0733; 23 - 0x0735; 0x0736; 0x073A; 0x073D; 0x073F; 0x0740; 0x0741; 0x0743; 24 - 0x0745; 0x0747; 0x0749; 0x074A; 0x07EB; 0x07EC; 0x07ED; 0x07EE; 25 - 0x07EF; 0x07F0; 0x07F1; 0x07F3; 0x0816; 0x0817; 0x0818; 0x0819; 26 - 0x081B; 0x081C; 0x081D; 0x081E; 0x081F; 0x0820; 0x0821; 0x0822; 27 - 0x0823; 0x0825; 0x0826; 0x0827; 0x0829; 0x082A; 0x082B; 0x082C; 28 - 0x082D; 0x0951; 0x0953; 0x0954; 0x0F82; 0x0F83; 0x0F86; 0x0F87; 29 - 0x135D; 0x135E; 0x135F; 0x17DD; 0x193A; 0x1A17; 0x1A75; 0x1A76; 30 - 0x1A77; 0x1A78; 0x1A79; 0x1A7A; 0x1A7B; 0x1A7C; 0x1B6B; 0x1B6D; 31 - 0x1B6E; 0x1B6F; 0x1B70; 0x1B71; 0x1B72; 0x1B73; 0x1CD0; 0x1CD1; 32 - 0x1CD2; 0x1CDA; 0x1CDB; 0x1CE0; 0x1DC0; 0x1DC1; 0x1DC3; 0x1DC4; 33 - 0x1DC5; 0x1DC6; 0x1DC7; 0x1DC8; 0x1DC9; 0x1DCB; 0x1DCC; 0x1DD1; 34 - 0x1DD2; 0x1DD3; 0x1DD4; 0x1DD5; 0x1DD6; 0x1DD7; 0x1DD8; 0x1DD9; 35 - 0x1DDA; 0x1DDB; 0x1DDC; 0x1DDD; 0x1DDE; 0x1DDF; 0x1DE0; 0x1DE1; 36 - 0x1DE2; 0x1DE3; 0x1DE4; 0x1DE5; 0x1DE6; 0x1DFE; 0x20D0; 0x20D1; 37 - 0x20D4; 0x20D5; 0x20D6; 0x20D7; 0x20DB; 0x20DC; 0x20E1; 0x20E7; 38 - 0x20E9; 0x20F0; 0xA66F; 0xA67C; 0xA67D; 0xA6F0; 0xA6F1; 0xA8E0; 39 - 0xA8E1; 0xA8E2; 0xA8E3; 0xA8E4; 0xA8E5; 0xA8E6; 0xA8E7; 0xA8E8; 40 - 0xA8E9; 0xA8EA; 0xA8EB; 0xA8EC; 0xA8ED; 0xA8EE; 0xA8EF; 0xA8F0; 41 - 0xA8F1; 0xAAB0; 0xAAB2; 0xAAB3; 0xAAB7; 0xAAB8; 0xAABE; 0xAABF; 42 - 0xAAC1; 0xFE20; 0xFE21; 0xFE22; 0xFE23; 0xFE24; 0xFE25; 0xFE26; 43 - 0x10A0F; 0x10A38; 0x1D185; 0x1D186; 0x1D187; 0x1D188; 0x1D189; 44 - 0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD; 0x1D242; 0x1D243; 0x1D244; 12 + 0x0305; 13 + 0x030D; 14 + 0x030E; 15 + 0x0310; 16 + 0x0312; 17 + 0x033D; 18 + 0x033E; 19 + 0x033F; 20 + 0x0346; 21 + 0x034A; 22 + 0x034B; 23 + 0x034C; 24 + 0x0350; 25 + 0x0351; 26 + 0x0352; 27 + 0x0357; 28 + 0x035B; 29 + 0x0363; 30 + 0x0364; 31 + 0x0365; 32 + 0x0366; 33 + 0x0367; 34 + 0x0368; 35 + 0x0369; 36 + 0x036A; 37 + 0x036B; 38 + 0x036C; 39 + 0x036D; 40 + 0x036E; 41 + 0x036F; 42 + 0x0483; 43 + 0x0484; 44 + 0x0485; 45 + 0x0486; 46 + 0x0487; 47 + 0x0592; 48 + 0x0593; 49 + 0x0594; 50 + 0x0595; 51 + 0x0597; 52 + 0x0598; 53 + 0x0599; 54 + 0x059C; 55 + 0x059D; 56 + 0x059E; 57 + 0x059F; 58 + 0x05A0; 59 + 0x05A1; 60 + 0x05A8; 61 + 0x05A9; 62 + 0x05AB; 63 + 0x05AC; 64 + 0x05AF; 65 + 0x05C4; 66 + 0x0610; 67 + 0x0611; 68 + 0x0612; 69 + 0x0613; 70 + 0x0614; 71 + 0x0615; 72 + 0x0616; 73 + 0x0617; 74 + 0x0657; 75 + 0x0658; 76 + 0x0659; 77 + 0x065A; 78 + 0x065B; 79 + 0x065D; 80 + 0x065E; 81 + 0x06D6; 82 + 0x06D7; 83 + 0x06D8; 84 + 0x06D9; 85 + 0x06DA; 86 + 0x06DB; 87 + 0x06DC; 88 + 0x06DF; 89 + 0x06E0; 90 + 0x06E1; 91 + 0x06E2; 92 + 0x06E4; 93 + 0x06E7; 94 + 0x06E8; 95 + 0x06EB; 96 + 0x06EC; 97 + 0x0730; 98 + 0x0732; 99 + 0x0733; 100 + 0x0735; 101 + 0x0736; 102 + 0x073A; 103 + 0x073D; 104 + 0x073F; 105 + 0x0740; 106 + 0x0741; 107 + 0x0743; 108 + 0x0745; 109 + 0x0747; 110 + 0x0749; 111 + 0x074A; 112 + 0x07EB; 113 + 0x07EC; 114 + 0x07ED; 115 + 0x07EE; 116 + 0x07EF; 117 + 0x07F0; 118 + 0x07F1; 119 + 0x07F3; 120 + 0x0816; 121 + 0x0817; 122 + 0x0818; 123 + 0x0819; 124 + 0x081B; 125 + 0x081C; 126 + 0x081D; 127 + 0x081E; 128 + 0x081F; 129 + 0x0820; 130 + 0x0821; 131 + 0x0822; 132 + 0x0823; 133 + 0x0825; 134 + 0x0826; 135 + 0x0827; 136 + 0x0829; 137 + 0x082A; 138 + 0x082B; 139 + 0x082C; 140 + 0x082D; 141 + 0x0951; 142 + 0x0953; 143 + 0x0954; 144 + 0x0F82; 145 + 0x0F83; 146 + 0x0F86; 147 + 0x0F87; 148 + 0x135D; 149 + 0x135E; 150 + 0x135F; 151 + 0x17DD; 152 + 0x193A; 153 + 0x1A17; 154 + 0x1A75; 155 + 0x1A76; 156 + 0x1A77; 157 + 0x1A78; 158 + 0x1A79; 159 + 0x1A7A; 160 + 0x1A7B; 161 + 0x1A7C; 162 + 0x1B6B; 163 + 0x1B6D; 164 + 0x1B6E; 165 + 0x1B6F; 166 + 0x1B70; 167 + 0x1B71; 168 + 0x1B72; 169 + 0x1B73; 170 + 0x1CD0; 171 + 0x1CD1; 172 + 0x1CD2; 173 + 0x1CDA; 174 + 0x1CDB; 175 + 0x1CE0; 176 + 0x1DC0; 177 + 0x1DC1; 178 + 0x1DC3; 179 + 0x1DC4; 180 + 0x1DC5; 181 + 0x1DC6; 182 + 0x1DC7; 183 + 0x1DC8; 184 + 0x1DC9; 185 + 0x1DCB; 186 + 0x1DCC; 187 + 0x1DD1; 188 + 0x1DD2; 189 + 0x1DD3; 190 + 0x1DD4; 191 + 0x1DD5; 192 + 0x1DD6; 193 + 0x1DD7; 194 + 0x1DD8; 195 + 0x1DD9; 196 + 0x1DDA; 197 + 0x1DDB; 198 + 0x1DDC; 199 + 0x1DDD; 200 + 0x1DDE; 201 + 0x1DDF; 202 + 0x1DE0; 203 + 0x1DE1; 204 + 0x1DE2; 205 + 0x1DE3; 206 + 0x1DE4; 207 + 0x1DE5; 208 + 0x1DE6; 209 + 0x1DFE; 210 + 0x20D0; 211 + 0x20D1; 212 + 0x20D4; 213 + 0x20D5; 214 + 0x20D6; 215 + 0x20D7; 216 + 0x20DB; 217 + 0x20DC; 218 + 0x20E1; 219 + 0x20E7; 220 + 0x20E9; 221 + 0x20F0; 222 + 0xA66F; 223 + 0xA67C; 224 + 0xA67D; 225 + 0xA6F0; 226 + 0xA6F1; 227 + 0xA8E0; 228 + 0xA8E1; 229 + 0xA8E2; 230 + 0xA8E3; 231 + 0xA8E4; 232 + 0xA8E5; 233 + 0xA8E6; 234 + 0xA8E7; 235 + 0xA8E8; 236 + 0xA8E9; 237 + 0xA8EA; 238 + 0xA8EB; 239 + 0xA8EC; 240 + 0xA8ED; 241 + 0xA8EE; 242 + 0xA8EF; 243 + 0xA8F0; 244 + 0xA8F1; 245 + 0xAAB0; 246 + 0xAAB2; 247 + 0xAAB3; 248 + 0xAAB7; 249 + 0xAAB8; 250 + 0xAABE; 251 + 0xAABF; 252 + 0xAAC1; 253 + 0xFE20; 254 + 0xFE21; 255 + 0xFE22; 256 + 0xFE23; 257 + 0xFE24; 258 + 0xFE25; 259 + 0xFE26; 260 + 0x10A0F; 261 + 0x10A38; 262 + 0x1D185; 263 + 0x1D186; 264 + 0x1D187; 265 + 0x1D188; 266 + 0x1D189; 267 + 0x1D1AA; 268 + 0x1D1AB; 269 + 0x1D1AC; 270 + 0x1D1AD; 271 + 0x1D242; 272 + 0x1D243; 273 + 0x1D244; 45 274 |] 46 275 47 276 let diacritic n = Uchar.of_int diacritics.(n mod Array.length diacritics) ··· 53 282 let rec gen () = 54 283 let id = Random.int32 Int32.max_int |> Int32.to_int in 55 284 (* Ensure high byte and middle bytes are non-zero *) 56 - if id land 0xFF000000 = 0 || id land 0x00FFFF00 = 0 then gen () 57 - else id 285 + if id land 0xFF000000 = 0 || id land 0x00FFFF00 = 0 then gen () else id 58 286 in 59 287 gen () 60 288 ··· 84 312 (* Optional placement ID in underline color *) 85 313 placement_id 86 314 |> Option.iter (fun pid -> 87 - Printf.bprintf buf "\027[58:2:%d:%d:%dm" 88 - ((pid lsr 16) land 0xFF) 89 - ((pid lsr 8) land 0xFF) 90 - (pid land 0xFF)); 315 + Printf.bprintf buf "\027[58:2:%d:%d:%dm" 316 + ((pid lsr 16) land 0xFF) 317 + ((pid lsr 8) land 0xFF) 318 + (pid land 0xFF)); 91 319 (* High byte diacritic - always written, even when 0 *) 92 320 let high_byte = (image_id lsr 24) land 0xFF in 93 321 let id_diac = id_high_byte_diacritic high_byte in
+8 -6
lib/kgp_unicode.mli
··· 5 5 6 6 (** Kitty Graphics Protocol Unicode Placeholders 7 7 8 - Support for invisible Unicode placeholder characters that encode 9 - image position metadata for accessibility and compatibility. 8 + Support for invisible Unicode placeholder characters that encode image 9 + position metadata for accessibility and compatibility. 10 10 11 11 {2 Image ID Requirements} 12 12 ··· 27 27 - High byte (bits 24-31) is non-zero 28 28 - Middle bytes (bits 8-23) are non-zero 29 29 30 - This ensures the foreground color encoding and diacritic encoding 31 - work correctly. Uses [Random] internally. *) 30 + This ensures the foreground color encoding and diacritic encoding work 31 + correctly. Uses [Random] internally. *) 32 32 33 33 val write : 34 34 Buffer.t -> ··· 40 40 unit 41 41 (** Write placeholder characters to a buffer. 42 42 43 - @param image_id Should be generated with {!next_image_id} for correct rendering. 44 - @param placement_id Optional placement ID for multiple placements of same image. 43 + @param image_id 44 + Should be generated with {!next_image_id} for correct rendering. 45 + @param placement_id 46 + Optional placement ID for multiple placements of same image. 45 47 @param rows Number of rows in the placeholder grid. 46 48 @param cols Number of columns in the placeholder grid. *) 47 49