this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

sync

+643 -867
+204 -93
stack/kitty_graphics/example/example.ml
··· 1 - (* Example usage of the Kitty Graphics Protocol library *) 1 + (* Kitty Graphics Protocol Demo - Matching kgp/examples/demo workflow *) 2 2 3 - (* Create a 64x64 colorful gradient image in RGBA format *) 4 - let test_rgba_image () = 5 - let size = 64 in 6 - let pixels = Bytes.create (size * size * 4) in 7 - for y = 0 to size - 1 do 8 - for x = 0 to size - 1 do 9 - let offset = (y * size + x) * 4 in 10 - (* Red gradient left to right *) 11 - Bytes.set pixels offset (Char.chr (x * 4 land 0xFF)); 12 - (* Green gradient top to bottom *) 13 - Bytes.set pixels (offset + 1) (Char.chr (y * 4 land 0xFF)); 14 - (* Blue diagonal gradient *) 15 - Bytes.set pixels (offset + 2) (Char.chr ((x + y) * 2 land 0xFF)); 16 - (* Fully opaque *) 17 - Bytes.set pixels (offset + 3) '\xff' 18 - done 3 + module K = Kitty_graphics 4 + 5 + (* Generate a solid color RGBA frame *) 6 + let make_solid_frame ~width ~height ~r ~g ~b = 7 + let pixels = Bytes.create (width * height * 4) in 8 + for i = 0 to (width * height) - 1 do 9 + let idx = i * 4 in 10 + Bytes.set pixels idx (Char.chr r); 11 + Bytes.set pixels (idx + 1) (Char.chr g); 12 + Bytes.set pixels (idx + 2) (Char.chr b); 13 + Bytes.set pixels (idx + 3) '\xff' 19 14 done; 20 - (size, Bytes.to_string pixels) 15 + Bytes.to_string pixels 16 + 17 + let send cmd ~data = 18 + print_string (K.Command.to_string cmd ~data); 19 + flush stdout 20 + 21 + let wait_for_enter () = 22 + print_string "Press Enter to continue..."; 23 + flush stdout; 24 + let _ = read_line () in 25 + print_newline () 26 + 27 + let clear_screen () = 28 + print_string "\x1b[2J\x1b[H"; 29 + for _ = 1 to 5 do print_newline () done; 30 + flush stdout 21 31 22 32 let () = 23 - print_endline "Kitty Graphics Protocol Example"; 24 - print_endline "================================"; 33 + clear_screen (); 34 + print_endline "Kitty Graphics Protocol - OCaml Demo"; 35 + print_endline "====================================="; 36 + print_newline (); 37 + print_endline "Press Enter to proceed through each demo..."; 25 38 print_newline (); 39 + wait_for_enter (); 26 40 27 - (* Example 1: Display a simple RGBA image *) 28 - print_endline "1. Displaying a 64x64 RGBA gradient image:"; 41 + (* Demo 1: Basic RGBA format *) 42 + clear_screen (); 43 + print_endline "Demo 1: Image Format - RGBA (32-bit)"; 44 + let blue_data = make_solid_frame ~width:100 ~height:100 ~r:0 ~g:0 ~b:255 in 45 + send 46 + (K.Command.transmit_and_display 47 + ~image_id:1 48 + ~format:`Rgba32 49 + ~width:100 ~height:100 50 + ~quiet:`Errors_only 51 + ()) 52 + ~data:blue_data; 53 + print_endline "Blue square displayed using raw RGBA format"; 29 54 print_newline (); 30 - flush stdout; 55 + wait_for_enter (); 31 56 32 - let (size, image_data) = test_rgba_image () in 33 - let cmd = 34 - Kitty_graphics.Command.transmit_and_display 35 - ~format:Kitty_graphics.Format.Rgba32 36 - ~width:size ~height:size 37 - () 57 + (* Demo 2: Basic RGB format *) 58 + clear_screen (); 59 + print_endline "Demo 2: Image Format - RGB (24-bit)"; 60 + (* RGB is 3 bytes per pixel *) 61 + let green_rgb = 62 + let pixels = Bytes.create (100 * 100 * 3) in 63 + for i = 0 to (100 * 100) - 1 do 64 + let idx = i * 3 in 65 + Bytes.set pixels idx '\x00'; (* R *) 66 + Bytes.set pixels (idx + 1) '\xff'; (* G *) 67 + Bytes.set pixels (idx + 2) '\x00' (* B *) 68 + done; 69 + Bytes.to_string pixels 38 70 in 39 - let buf = Buffer.create 4096 in 40 - Kitty_graphics.Command.write buf cmd ~data:image_data; 41 - print_string (Buffer.contents buf); 42 - flush stdout; 71 + send 72 + (K.Command.transmit_and_display 73 + ~image_id:2 74 + ~format:`Rgb24 75 + ~width:100 ~height:100 76 + ~quiet:`Errors_only 77 + ()) 78 + ~data:green_rgb; 79 + print_endline "Green square displayed using raw RGB format (no alpha)"; 43 80 print_newline (); 81 + wait_for_enter (); 82 + 83 + (* Demo 3: Multiple placements - transmit once, display multiple times *) 84 + clear_screen (); 85 + print_endline "Demo 3: Multiple Placements"; 86 + let cyan_data = make_solid_frame ~width:80 ~height:80 ~r:0 ~g:255 ~b:255 in 87 + (* Transmit only (a=t) *) 88 + send 89 + (K.Command.transmit 90 + ~image_id:100 91 + ~format:`Rgba32 92 + ~width:80 ~height:80 93 + ~quiet:`Errors_only 94 + ()) 95 + ~data:cyan_data; 96 + (* Display first placement *) 97 + send 98 + (K.Command.display 99 + ~image_id:100 100 + ~placement:(K.Placement.make ~columns:10 ~rows:5 ()) 101 + ~quiet:`Errors_only 102 + ()) 103 + ~data:""; 104 + print_string " "; 105 + (* Display second placement *) 106 + send 107 + (K.Command.display 108 + ~image_id:100 109 + ~placement:(K.Placement.make ~columns:5 ~rows:3 ()) 110 + ~quiet:`Errors_only 111 + ()) 112 + ~data:""; 44 113 print_newline (); 114 + print_endline "Same image displayed twice at different sizes"; 115 + print_newline (); 116 + wait_for_enter (); 45 117 46 - (* Example 2: Display scaled to specific cell size *) 47 - print_endline "2. Same image scaled to 20 columns x 10 rows:"; 118 + (* Demo 4: Z-index layering *) 119 + clear_screen (); 120 + print_endline "Demo 4: Z-Index Layering"; 121 + let orange_data = make_solid_frame ~width:200 ~height:100 ~r:255 ~g:165 ~b:0 in 122 + send 123 + (K.Command.transmit_and_display 124 + ~image_id:200 125 + ~format:`Rgba32 126 + ~width:200 ~height:100 127 + ~placement:(K.Placement.make ~z_index:(-1) ~cursor:`Static ()) 128 + ~quiet:`Errors_only 129 + ()) 130 + ~data:orange_data; 131 + print_endline "This orange square should appear behind the text!"; 48 132 print_newline (); 49 - flush stdout; 133 + wait_for_enter (); 50 134 51 - let placement = 52 - Kitty_graphics.Placement.make ~columns:20 ~rows:10 () 53 - in 54 - let cmd = 55 - Kitty_graphics.Command.transmit_and_display 56 - ~format:Kitty_graphics.Format.Rgba32 57 - ~width:size ~height:size 58 - ~placement 59 - () 60 - in 61 - Buffer.clear buf; 62 - Kitty_graphics.Command.write buf cmd ~data:image_data; 63 - print_string (Buffer.contents buf); 135 + (* Demo 5: Animation - matching kgp demo exactly *) 136 + clear_screen (); 137 + print_endline "Demo 5: Animation - Color-changing square"; 138 + print_endline "Creating animated sequence..."; 64 139 flush stdout; 65 - print_newline (); 66 - print_newline (); 67 140 68 - (* Example 3: Query terminal support *) 69 - print_endline "3. Query command (to test graphics support):"; 70 - let query = Kitty_graphics.Detect.make_query () in 71 - Printf.printf " Query escape sequence: %S\n" query; 72 - print_newline (); 141 + (* Using small size to avoid chunking - 10x10 = 400 bytes raw *) 142 + let width, height = 10, 10 in 143 + let image_id = 300 in 73 144 74 - (* Example 4: Delete command *) 75 - print_endline "4. Delete all visible images:"; 76 - let del_cmd = 77 - Kitty_graphics.Command.delete Kitty_graphics.Delete.All_visible 78 - in 79 - Buffer.clear buf; 80 - Kitty_graphics.Command.write buf del_cmd ~data:""; 81 - Printf.printf " Delete escape sequence: %S\n" (Buffer.contents buf); 82 - print_newline (); 145 + (* Step 1: Create base frame (red) - transmit only, don't display yet *) 146 + let red_frame = make_solid_frame ~width ~height ~r:255 ~g:0 ~b:0 in 147 + send 148 + (K.Command.transmit 149 + ~image_id 150 + ~format:`Rgba32 151 + ~width ~height 152 + ~quiet:`Errors_only 153 + ()) 154 + ~data:red_frame; 155 + 156 + (* Step 2: Add frame 2 (orange) with gap and composition replace *) 157 + let orange_frame = make_solid_frame ~width ~height ~r:255 ~g:165 ~b:0 in 158 + send 159 + (K.Command.frame 160 + ~image_id 161 + ~format:`Rgba32 162 + ~width ~height 163 + ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 164 + ~quiet:`Errors_only 165 + ()) 166 + ~data:orange_frame; 83 167 84 - (* Example 5: Unicode placeholder *) 85 - print_endline "5. Unicode placeholder (for tmux compatibility):"; 86 - print_newline (); 87 - Buffer.clear buf; 88 - Kitty_graphics.Unicode_placeholder.write buf ~image_id:42 ~rows:2 ~cols:4 (); 89 - print_string (Buffer.contents buf); 168 + (* Step 3: Add frame 3 (yellow) *) 169 + let yellow_frame = make_solid_frame ~width ~height ~r:255 ~g:255 ~b:0 in 170 + send 171 + (K.Command.frame 172 + ~image_id 173 + ~format:`Rgba32 174 + ~width ~height 175 + ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 176 + ~quiet:`Errors_only 177 + ()) 178 + ~data:yellow_frame; 179 + 180 + (* Step 4: Add frame 4 (green) *) 181 + let green_frame = make_solid_frame ~width ~height ~r:0 ~g:255 ~b:0 in 182 + send 183 + (K.Command.frame 184 + ~image_id 185 + ~format:`Rgba32 186 + ~width ~height 187 + ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ()) 188 + ~quiet:`Errors_only 189 + ()) 190 + ~data:green_frame; 191 + 192 + (* Step 5: Create placement to display the animation *) 193 + (* Add columns/rows to scale up the small image for visibility *) 194 + send 195 + (K.Command.display 196 + ~image_id 197 + ~placement:(K.Placement.make 198 + ~placement_id:1 199 + ~columns:10 200 + ~rows:5 201 + ~cursor:`Static 202 + ()) 203 + ~quiet:`Errors_only 204 + ()) 205 + ~data:""; 206 + 207 + (* Step 6: Start animation with infinite looping (s=3, v=1) *) 208 + send 209 + (K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run)) 210 + ~data:""; 211 + 90 212 print_newline (); 213 + print_endline "Animation playing: Red -> Orange -> Yellow -> Green"; 91 214 print_newline (); 215 + wait_for_enter (); 92 216 93 - (* Example 6: Parse a response *) 94 - print_endline "6. Parsing terminal responses:"; 95 - let test_response = "\027_Gi=123;OK\027\\" in 96 - (match Kitty_graphics.Response.parse test_response with 97 - | Some r -> 98 - Printf.printf " Parsed response: is_ok=%b, image_id=%s\n" 99 - (Kitty_graphics.Response.is_ok r) 100 - (match Kitty_graphics.Response.image_id r with 101 - | Some id -> string_of_int id 102 - | None -> "none") 103 - | None -> print_endline " Failed to parse"); 217 + (* Stop the animation *) 218 + send 219 + (K.Command.animate ~image_id (K.Animation.set_state `Stop)) 220 + ~data:""; 104 221 105 - let error_response = "\027_Gi=456;ENOENT:Image not found\027\\" in 106 - (match Kitty_graphics.Response.parse error_response with 107 - | Some r -> 108 - Printf.printf " Error response: code=%s, message=%s\n" 109 - (match Kitty_graphics.Response.error_code r with 110 - | Some c -> c 111 - | None -> "none") 112 - (Kitty_graphics.Response.message r) 113 - | None -> print_endline " Failed to parse"); 222 + print_endline "Animation stopped."; 223 + print_newline (); 114 224 115 - print_newline (); 116 - print_endline "Done!" 225 + (* Cleanup *) 226 + print_endline "Demo complete!"; 227 + ()
+327 -544
stack/kitty_graphics/lib/kitty_graphics.ml
··· 1 1 (* Kitty Terminal Graphics Protocol - Implementation *) 2 2 3 + (* Polymorphic variant types *) 4 + type format = [ `Rgba32 | `Rgb24 | `Png ] 5 + type transmission = [ `Direct | `File | `Tempfile ] 6 + type compression = [ `None | `Zlib ] 7 + type quiet = [ `Noisy | `Errors_only | `Silent ] 8 + type cursor = [ `Move | `Static ] 9 + type composition = [ `Alpha_blend | `Overwrite ] 10 + 11 + type delete = 12 + [ `All_visible 13 + | `All_visible_and_free 14 + | `By_id of int * int option 15 + | `By_id_and_free of int * int option 16 + | `By_number of int * int option 17 + | `By_number_and_free of int * int option 18 + | `At_cursor 19 + | `At_cursor_and_free 20 + | `At_cell of int * int 21 + | `At_cell_and_free of int * int 22 + | `At_cell_z of int * int * int 23 + | `At_cell_z_and_free of int * int * int 24 + | `By_column of int 25 + | `By_column_and_free of int 26 + | `By_row of int 27 + | `By_row_and_free of int 28 + | `By_z_index of int 29 + | `By_z_index_and_free of int 30 + | `By_id_range of int * int 31 + | `By_id_range_and_free of int * int 32 + | `Frames 33 + | `Frames_and_free ] 34 + 35 + type animation_state = [ `Stop | `Loading | `Run ] 36 + 37 + (* Modules re-export the types with conversion functions *) 3 38 module Format = struct 4 - type t = Rgba32 | Rgb24 | Png 39 + type t = format 5 40 6 - let to_int = function Rgba32 -> 32 | Rgb24 -> 24 | Png -> 100 41 + let to_int : t -> int = function 42 + | `Rgba32 -> 32 43 + | `Rgb24 -> 24 44 + | `Png -> 100 7 45 end 8 46 9 47 module Transmission = struct 10 - type t = Direct | File | Tempfile 48 + type t = transmission 11 49 12 - let to_char = function Direct -> 'd' | File -> 'f' | Tempfile -> 't' 50 + let to_char : t -> char = function 51 + | `Direct -> 'd' 52 + | `File -> 'f' 53 + | `Tempfile -> 't' 13 54 end 14 55 15 56 module Compression = struct 16 - type t = None | Zlib 57 + type t = compression 17 58 18 - let to_char = function None -> Option.none | Zlib -> Some 'z' 59 + let to_char : t -> char option = function 60 + | `None -> None 61 + | `Zlib -> Some 'z' 19 62 end 20 63 21 64 module Quiet = struct 22 - type t = Noisy | Errors_only | Silent 65 + type t = quiet 23 66 24 - let to_int = function Noisy -> 0 | Errors_only -> 1 | Silent -> 2 67 + let to_int : t -> int = function 68 + | `Noisy -> 0 69 + | `Errors_only -> 1 70 + | `Silent -> 2 25 71 end 26 72 27 73 module Cursor = struct 28 - type t = Move | Static 74 + type t = cursor 29 75 30 - let to_int = function Move -> 0 | Static -> 1 76 + let to_int : t -> int = function 77 + | `Move -> 0 78 + | `Static -> 1 31 79 end 32 80 33 81 module Composition = struct 34 - type t = Alpha_blend | Overwrite 82 + type t = composition 35 83 36 - let to_int = function Alpha_blend -> 0 | Overwrite -> 1 84 + let to_int : t -> int = function 85 + | `Alpha_blend -> 0 86 + | `Overwrite -> 1 37 87 end 38 88 39 89 module Delete = struct 40 - type t = 41 - | All_visible 42 - | All_visible_and_free 43 - | By_id of { image_id : int; placement_id : int option } 44 - | By_id_and_free of { image_id : int; placement_id : int option } 45 - | By_number of { image_number : int; placement_id : int option } 46 - | By_number_and_free of { image_number : int; placement_id : int option } 47 - | At_cursor 48 - | At_cursor_and_free 49 - | At_cell of { x : int; y : int } 50 - | At_cell_and_free of { x : int; y : int } 51 - | At_cell_z of { x : int; y : int; z : int } 52 - | At_cell_z_and_free of { x : int; y : int; z : int } 53 - | By_column of int 54 - | By_column_and_free of int 55 - | By_row of int 56 - | By_row_and_free of int 57 - | By_z_index of int 58 - | By_z_index_and_free of int 59 - | By_id_range of { min_id : int; max_id : int } 60 - | By_id_range_and_free of { min_id : int; max_id : int } 61 - | Frames 62 - | Frames_and_free 90 + type t = delete 63 91 end 64 92 65 93 module Placement = struct ··· 74 102 rows : int option; 75 103 z_index : int option; 76 104 placement_id : int option; 77 - cursor : Cursor.t option; 105 + cursor : cursor option; 78 106 unicode_placeholder : bool; 79 107 } 80 108 ··· 120 148 base_frame : int option; 121 149 edit_frame : int option; 122 150 gap_ms : int option; 123 - composition : Composition.t option; 151 + composition : composition option; 124 152 background_color : int32 option; 125 153 } 126 154 ··· 141 169 end 142 170 143 171 module Animation = struct 144 - type state = Stop | Loading | Run 172 + type state = animation_state 145 173 146 174 type t = 147 - | Set_state of { state : state; loops : int option } 148 - | Set_gap of { frame : int; gap_ms : int } 149 - | Set_current of int 175 + [ `Set_state of state * int option 176 + | `Set_gap of int * int 177 + | `Set_current of int ] 150 178 151 - let set_state ?loops state = Set_state { state; loops } 152 - let set_gap ~frame ~gap_ms = Set_gap { frame; gap_ms } 153 - let set_current_frame frame = Set_current frame 179 + let set_state ?loops state = `Set_state (state, loops) 180 + let set_gap ~frame ~gap_ms = `Set_gap (frame, gap_ms) 181 + let set_current_frame frame = `Set_current frame 154 182 end 155 183 156 184 module Compose = struct ··· 163 191 source_y : int option; 164 192 dest_x : int option; 165 193 dest_y : int option; 166 - composition : Composition.t option; 194 + composition : composition option; 167 195 } 168 196 169 197 let make ~source_frame ~dest_frame ?width ?height ?source_x ?source_y ?dest_x ··· 183 211 184 212 module Command = struct 185 213 type action = 186 - | Transmit 187 - | Transmit_and_display 188 - | Query 189 - | Display 190 - | Delete 191 - | Frame 192 - | Animate 193 - | Compose 214 + [ `Transmit 215 + | `Transmit_and_display 216 + | `Query 217 + | `Display 218 + | `Delete 219 + | `Frame 220 + | `Animate 221 + | `Compose ] 194 222 195 223 type t = { 196 224 action : action; 197 - format : Format.t option; 198 - transmission : Transmission.t option; 199 - compression : Compression.t option; 225 + format : format option; 226 + transmission : transmission option; 227 + compression : compression option; 200 228 width : int option; 201 229 height : int option; 202 230 size : int option; 203 231 offset : int option; 204 - quiet : Quiet.t option; 232 + quiet : quiet option; 205 233 image_id : int option; 206 234 image_number : int option; 207 235 placement : Placement.t option; 208 - delete : Delete.t option; 236 + delete : delete option; 209 237 frame : Frame.t option; 210 238 animation : Animation.t option; 211 239 compose : Compose.t option; 212 240 } 213 241 214 - let make_base action = 242 + let make action = 215 243 { 216 244 action; 217 245 format = None; ··· 234 262 let transmit ?image_id ?image_number ?format ?transmission ?compression ?width 235 263 ?height ?size ?offset ?quiet () = 236 264 { 237 - (make_base Transmit) with 265 + (make `Transmit) with 238 266 image_id; 239 267 image_number; 240 268 format; ··· 250 278 let transmit_and_display ?image_id ?image_number ?format ?transmission 251 279 ?compression ?width ?height ?size ?offset ?quiet ?placement () = 252 280 { 253 - (make_base Transmit_and_display) with 281 + (make `Transmit_and_display) with 254 282 image_id; 255 283 image_number; 256 284 format; ··· 265 293 } 266 294 267 295 let query ?format ?transmission ?width ?height ?quiet () = 268 - { (make_base Query) with format; transmission; width; height; quiet } 296 + { (make `Query) with format; transmission; width; height; quiet } 269 297 270 298 let display ?image_id ?image_number ?placement ?quiet () = 271 - { (make_base Display) with image_id; image_number; placement; quiet } 299 + { (make `Display) with image_id; image_number; placement; quiet } 272 300 273 - let delete ?quiet del = 274 - { (make_base Delete) with quiet; delete = Some del } 301 + let delete ?quiet del = { (make `Delete) with quiet; delete = Some del } 275 302 276 303 let frame ?image_id ?image_number ?format ?transmission ?compression ?width 277 304 ?height ?quiet ~frame () = 278 305 { 279 - (make_base Frame) with 306 + (make `Frame) with 280 307 image_id; 281 308 image_number; 282 309 format; ··· 289 316 } 290 317 291 318 let animate ?image_id ?image_number ?quiet anim = 292 - { (make_base Animate) with image_id; image_number; quiet; animation = Some anim } 319 + { (make `Animate) with image_id; image_number; quiet; animation = Some anim } 293 320 294 321 let compose ?image_id ?image_number ?quiet comp = 295 - { (make_base Compose) with image_id; image_number; quiet; compose = Some comp } 322 + { (make `Compose) with image_id; image_number; quiet; compose = Some comp } 296 323 297 - (* APC escape sequences *) 324 + (* Serialization helpers *) 298 325 let apc_start = "\027_G" 299 326 let apc_end = "\027\\" 300 327 301 - (* Helper to add key=value pairs *) 302 - let add_kv buf key value = 303 - Buffer.add_char buf key; 304 - Buffer.add_char buf '='; 305 - Buffer.add_string buf value 328 + (* Key-value writer with separator handling *) 329 + type kv_writer = { mutable first : bool; buf : Buffer.t } 306 330 307 - let add_kv_int buf key value = 308 - Buffer.add_char buf key; 309 - Buffer.add_char buf '='; 310 - Buffer.add_string buf (string_of_int value) 331 + let kv_writer buf = { first = true; buf } 311 332 312 - let add_kv_int32 buf key value = 313 - Buffer.add_char buf key; 314 - Buffer.add_char buf '='; 315 - Buffer.add_string buf (Int32.to_string value) 333 + let kv w key value = 334 + if not w.first then Buffer.add_char w.buf ','; 335 + w.first <- false; 336 + Buffer.add_char w.buf key; 337 + Buffer.add_char w.buf '='; 338 + Buffer.add_string w.buf value 316 339 317 - let add_comma buf = Buffer.add_char buf ',' 340 + let kv_int w key value = kv w key (string_of_int value) 341 + let kv_int32 w key value = kv w key (Int32.to_string value) 342 + let kv_char w key value = kv w key (String.make 1 value) 318 343 319 - let action_char = function 320 - | Transmit -> 't' 321 - | Transmit_and_display -> 'T' 322 - | Query -> 'q' 323 - | Display -> 'p' 324 - | Delete -> 'd' 325 - | Frame -> 'f' 326 - | Animate -> 'a' 327 - | Compose -> 'c' 344 + (* Conditional writers using Option.iter *) 345 + let kv_int_opt w key = Option.iter (kv_int w key) 346 + let kv_int32_opt w key = Option.iter (kv_int32 w key) 328 347 329 - let delete_char = function 330 - | Delete.All_visible -> 'a' 331 - | All_visible_and_free -> 'A' 332 - | By_id _ -> 'i' 333 - | By_id_and_free _ -> 'I' 334 - | By_number _ -> 'n' 335 - | By_number_and_free _ -> 'N' 336 - | At_cursor -> 'c' 337 - | At_cursor_and_free -> 'C' 338 - | At_cell _ -> 'p' 339 - | At_cell_and_free _ -> 'P' 340 - | At_cell_z _ -> 'q' 341 - | At_cell_z_and_free _ -> 'Q' 342 - | By_column _ -> 'x' 343 - | By_column_and_free _ -> 'X' 344 - | By_row _ -> 'y' 345 - | By_row_and_free _ -> 'Y' 346 - | By_z_index _ -> 'z' 347 - | By_z_index_and_free _ -> 'Z' 348 - | By_id_range _ -> 'r' 349 - | By_id_range_and_free _ -> 'R' 350 - | Frames -> 'f' 351 - | Frames_and_free -> 'F' 348 + let kv_int_if w key ~default opt = 349 + Option.iter (fun v -> if v <> default then kv_int w key v) opt 350 + 351 + let action_char : action -> char = function 352 + | `Transmit -> 't' 353 + | `Transmit_and_display -> 'T' 354 + | `Query -> 'q' 355 + | `Display -> 'p' 356 + | `Delete -> 'd' 357 + | `Frame -> 'f' 358 + | `Animate -> 'a' 359 + | `Compose -> 'c' 360 + 361 + let delete_char : delete -> char = function 362 + | `All_visible -> 'a' 363 + | `All_visible_and_free -> 'A' 364 + | `By_id _ -> 'i' 365 + | `By_id_and_free _ -> 'I' 366 + | `By_number _ -> 'n' 367 + | `By_number_and_free _ -> 'N' 368 + | `At_cursor -> 'c' 369 + | `At_cursor_and_free -> 'C' 370 + | `At_cell _ -> 'p' 371 + | `At_cell_and_free _ -> 'P' 372 + | `At_cell_z _ -> 'q' 373 + | `At_cell_z_and_free _ -> 'Q' 374 + | `By_column _ -> 'x' 375 + | `By_column_and_free _ -> 'X' 376 + | `By_row _ -> 'y' 377 + | `By_row_and_free _ -> 'Y' 378 + | `By_z_index _ -> 'z' 379 + | `By_z_index_and_free _ -> 'Z' 380 + | `By_id_range _ -> 'r' 381 + | `By_id_range_and_free _ -> 'R' 382 + | `Frames -> 'f' 383 + | `Frames_and_free -> 'F' 384 + 385 + let write_placement w (p : Placement.t) = 386 + kv_int_opt w 'x' p.source_x; 387 + kv_int_opt w 'y' p.source_y; 388 + kv_int_opt w 'w' p.source_width; 389 + kv_int_opt w 'h' p.source_height; 390 + kv_int_opt w 'X' p.cell_x_offset; 391 + kv_int_opt w 'Y' p.cell_y_offset; 392 + kv_int_opt w 'c' p.columns; 393 + kv_int_opt w 'r' p.rows; 394 + kv_int_opt w 'z' p.z_index; 395 + kv_int_opt w 'p' p.placement_id; 396 + p.cursor |> Option.iter (fun c -> kv_int_if w 'C' ~default:0 (Some (Cursor.to_int c))); 397 + if p.unicode_placeholder then kv_int w 'U' 1 398 + 399 + let write_delete w (d : delete) = 400 + kv_char w 'd' (delete_char d); 401 + match d with 402 + | `By_id (id, pid) | `By_id_and_free (id, pid) -> 403 + kv_int w 'i' id; 404 + kv_int_opt w 'p' pid 405 + | `By_number (n, pid) | `By_number_and_free (n, pid) -> 406 + kv_int w 'I' n; 407 + kv_int_opt w 'p' pid 408 + | `At_cell (x, y) | `At_cell_and_free (x, y) -> 409 + kv_int w 'x' x; 410 + kv_int w 'y' y 411 + | `At_cell_z (x, y, z) | `At_cell_z_and_free (x, y, z) -> 412 + kv_int w 'x' x; 413 + kv_int w 'y' y; 414 + kv_int w 'z' z 415 + | `By_column c | `By_column_and_free c -> kv_int w 'x' c 416 + | `By_row r | `By_row_and_free r -> kv_int w 'y' r 417 + | `By_z_index z | `By_z_index_and_free z -> kv_int w 'z' z 418 + | `By_id_range (min_id, max_id) | `By_id_range_and_free (min_id, max_id) -> 419 + kv_int w 'x' min_id; 420 + kv_int w 'y' max_id 421 + | `All_visible | `All_visible_and_free | `At_cursor | `At_cursor_and_free 422 + | `Frames | `Frames_and_free -> 423 + () 424 + 425 + let write_frame w (f : Frame.t) = 426 + kv_int_opt w 'x' f.x; 427 + kv_int_opt w 'y' f.y; 428 + kv_int_opt w 'c' f.base_frame; 429 + kv_int_opt w 'r' f.edit_frame; 430 + kv_int_opt w 'z' f.gap_ms; 431 + f.composition 432 + |> Option.iter (fun c -> kv_int_if w 'X' ~default:0 (Some (Composition.to_int c))); 433 + kv_int32_opt w 'Y' f.background_color 434 + 435 + let write_animation w : Animation.t -> unit = function 436 + | `Set_state (state, loops) -> 437 + let s = match state with `Stop -> 1 | `Loading -> 2 | `Run -> 3 in 438 + kv_int w 's' s; 439 + kv_int_opt w 'v' loops 440 + | `Set_gap (frame, gap_ms) -> 441 + kv_int w 'r' frame; 442 + kv_int w 'z' gap_ms 443 + | `Set_current frame -> kv_int w 'c' frame 444 + 445 + let write_compose w (c : Compose.t) = 446 + kv_int w 'r' c.source_frame; 447 + kv_int w 'c' c.dest_frame; 448 + kv_int_opt w 'w' c.width; 449 + kv_int_opt w 'h' c.height; 450 + kv_int_opt w 'x' c.dest_x; 451 + kv_int_opt w 'y' c.dest_y; 452 + kv_int_opt w 'X' c.source_x; 453 + kv_int_opt w 'Y' c.source_y; 454 + c.composition 455 + |> Option.iter (fun comp -> kv_int_if w 'C' ~default:0 (Some (Composition.to_int comp))) 352 456 353 457 let write_control_data buf cmd = 354 - let first = ref true in 355 - let sep () = 356 - if !first then first := false else add_comma buf 357 - in 458 + let w = kv_writer buf in 358 459 (* Action *) 359 - sep (); 360 - add_kv buf 'a' (String.make 1 (action_char cmd.action)); 361 - (* Quiet *) 362 - Option.iter 363 - (fun q -> 364 - let v = Quiet.to_int q in 365 - if v <> 0 then ( 366 - sep (); 367 - add_kv_int buf 'q' v)) 368 - cmd.quiet; 460 + kv_char w 'a' (action_char cmd.action); 461 + (* Quiet - only if non-default *) 462 + cmd.quiet |> Option.iter (fun q -> kv_int_if w 'q' ~default:0 (Some (Quiet.to_int q))); 369 463 (* Format *) 370 - Option.iter 371 - (fun f -> 372 - sep (); 373 - add_kv_int buf 'f' (Format.to_int f)) 374 - cmd.format; 375 - (* Transmission *) 376 - Option.iter 377 - (fun t -> 378 - let c = Transmission.to_char t in 379 - if c <> 'd' then ( 380 - sep (); 381 - add_kv buf 't' (String.make 1 c))) 382 - cmd.transmission; 464 + cmd.format |> Option.iter (fun f -> kv_int w 'f' (Format.to_int f)); 465 + (* Transmission - only if non-default *) 466 + cmd.transmission 467 + |> Option.iter (fun t -> 468 + let c = Transmission.to_char t in 469 + if c <> 'd' then kv_char w 't' c); 383 470 (* Compression *) 384 - Option.iter 385 - (fun c -> 386 - match Compression.to_char c with 387 - | Some ch -> 388 - sep (); 389 - add_kv buf 'o' (String.make 1 ch) 390 - | None -> ()) 391 - cmd.compression; 471 + cmd.compression |> Option.iter (fun c -> Compression.to_char c |> Option.iter (kv_char w 'o')); 392 472 (* Dimensions *) 393 - Option.iter 394 - (fun w -> 395 - sep (); 396 - add_kv_int buf 's' w) 397 - cmd.width; 398 - Option.iter 399 - (fun h -> 400 - sep (); 401 - add_kv_int buf 'v' h) 402 - cmd.height; 473 + kv_int_opt w 's' cmd.width; 474 + kv_int_opt w 'v' cmd.height; 403 475 (* File size/offset *) 404 - Option.iter 405 - (fun s -> 406 - sep (); 407 - add_kv_int buf 'S' s) 408 - cmd.size; 409 - Option.iter 410 - (fun o -> 411 - sep (); 412 - add_kv_int buf 'O' o) 413 - cmd.offset; 414 - (* Image ID *) 415 - Option.iter 416 - (fun id -> 417 - sep (); 418 - add_kv_int buf 'i' id) 419 - cmd.image_id; 420 - (* Image number *) 421 - Option.iter 422 - (fun n -> 423 - sep (); 424 - add_kv_int buf 'I' n) 425 - cmd.image_number; 426 - (* Placement options *) 427 - Option.iter 428 - (fun (p : Placement.t) -> 429 - Option.iter 430 - (fun v -> 431 - sep (); 432 - add_kv_int buf 'x' v) 433 - p.source_x; 434 - Option.iter 435 - (fun v -> 436 - sep (); 437 - add_kv_int buf 'y' v) 438 - p.source_y; 439 - Option.iter 440 - (fun v -> 441 - sep (); 442 - add_kv_int buf 'w' v) 443 - p.source_width; 444 - Option.iter 445 - (fun v -> 446 - sep (); 447 - add_kv_int buf 'h' v) 448 - p.source_height; 449 - Option.iter 450 - (fun v -> 451 - sep (); 452 - add_kv_int buf 'X' v) 453 - p.cell_x_offset; 454 - Option.iter 455 - (fun v -> 456 - sep (); 457 - add_kv_int buf 'Y' v) 458 - p.cell_y_offset; 459 - Option.iter 460 - (fun v -> 461 - sep (); 462 - add_kv_int buf 'c' v) 463 - p.columns; 464 - Option.iter 465 - (fun v -> 466 - sep (); 467 - add_kv_int buf 'r' v) 468 - p.rows; 469 - Option.iter 470 - (fun v -> 471 - sep (); 472 - add_kv_int buf 'z' v) 473 - p.z_index; 474 - Option.iter 475 - (fun v -> 476 - sep (); 477 - add_kv_int buf 'p' v) 478 - p.placement_id; 479 - Option.iter 480 - (fun c -> 481 - let v = Cursor.to_int c in 482 - if v <> 0 then ( 483 - sep (); 484 - add_kv_int buf 'C' v)) 485 - p.cursor; 486 - if p.unicode_placeholder then ( 487 - sep (); 488 - add_kv_int buf 'U' 1)) 489 - cmd.placement; 490 - (* Delete options *) 491 - Option.iter 492 - (fun d -> 493 - sep (); 494 - add_kv buf 'd' (String.make 1 (delete_char d)); 495 - match d with 496 - | Delete.By_id { image_id; placement_id } 497 - | Delete.By_id_and_free { image_id; placement_id } -> 498 - sep (); 499 - add_kv_int buf 'i' image_id; 500 - Option.iter 501 - (fun p -> 502 - sep (); 503 - add_kv_int buf 'p' p) 504 - placement_id 505 - | Delete.By_number { image_number; placement_id } 506 - | Delete.By_number_and_free { image_number; placement_id } -> 507 - sep (); 508 - add_kv_int buf 'I' image_number; 509 - Option.iter 510 - (fun p -> 511 - sep (); 512 - add_kv_int buf 'p' p) 513 - placement_id 514 - | Delete.At_cell { x; y } | Delete.At_cell_and_free { x; y } -> 515 - sep (); 516 - add_kv_int buf 'x' x; 517 - sep (); 518 - add_kv_int buf 'y' y 519 - | Delete.At_cell_z { x; y; z } 520 - | Delete.At_cell_z_and_free { x; y; z } -> 521 - sep (); 522 - add_kv_int buf 'x' x; 523 - sep (); 524 - add_kv_int buf 'y' y; 525 - sep (); 526 - add_kv_int buf 'z' z 527 - | Delete.By_column c | Delete.By_column_and_free c -> 528 - sep (); 529 - add_kv_int buf 'x' c 530 - | Delete.By_row r | Delete.By_row_and_free r -> 531 - sep (); 532 - add_kv_int buf 'y' r 533 - | Delete.By_z_index z | Delete.By_z_index_and_free z -> 534 - sep (); 535 - add_kv_int buf 'z' z 536 - | Delete.By_id_range { min_id; max_id } 537 - | Delete.By_id_range_and_free { min_id; max_id } -> 538 - sep (); 539 - add_kv_int buf 'x' min_id; 540 - sep (); 541 - add_kv_int buf 'y' max_id 542 - | _ -> ()) 543 - cmd.delete; 544 - (* Frame options *) 545 - Option.iter 546 - (fun (f : Frame.t) -> 547 - Option.iter 548 - (fun v -> 549 - sep (); 550 - add_kv_int buf 'x' v) 551 - f.x; 552 - Option.iter 553 - (fun v -> 554 - sep (); 555 - add_kv_int buf 'y' v) 556 - f.y; 557 - Option.iter 558 - (fun v -> 559 - sep (); 560 - add_kv_int buf 'c' v) 561 - f.base_frame; 562 - Option.iter 563 - (fun v -> 564 - sep (); 565 - add_kv_int buf 'r' v) 566 - f.edit_frame; 567 - Option.iter 568 - (fun v -> 569 - sep (); 570 - add_kv_int buf 'z' v) 571 - f.gap_ms; 572 - Option.iter 573 - (fun c -> 574 - let v = Composition.to_int c in 575 - if v <> 0 then ( 576 - sep (); 577 - add_kv_int buf 'X' v)) 578 - f.composition; 579 - Option.iter 580 - (fun v -> 581 - sep (); 582 - add_kv_int32 buf 'Y' v) 583 - f.background_color) 584 - cmd.frame; 585 - (* Animation options *) 586 - Option.iter 587 - (fun a -> 588 - match a with 589 - | Animation.Set_state { state; loops } -> 590 - let s = 591 - match state with 592 - | Animation.Stop -> 1 593 - | Animation.Loading -> 2 594 - | Animation.Run -> 3 595 - in 596 - sep (); 597 - add_kv_int buf 's' s; 598 - Option.iter 599 - (fun v -> 600 - sep (); 601 - add_kv_int buf 'v' v) 602 - loops 603 - | Animation.Set_gap { frame; gap_ms } -> 604 - sep (); 605 - add_kv_int buf 'r' frame; 606 - sep (); 607 - add_kv_int buf 'z' gap_ms 608 - | Animation.Set_current frame -> 609 - sep (); 610 - add_kv_int buf 'c' frame) 611 - cmd.animation; 612 - (* Compose options *) 613 - Option.iter 614 - (fun (c : Compose.t) -> 615 - sep (); 616 - add_kv_int buf 'r' c.source_frame; 617 - sep (); 618 - add_kv_int buf 'c' c.dest_frame; 619 - Option.iter 620 - (fun v -> 621 - sep (); 622 - add_kv_int buf 'w' v) 623 - c.width; 624 - Option.iter 625 - (fun v -> 626 - sep (); 627 - add_kv_int buf 'h' v) 628 - c.height; 629 - Option.iter 630 - (fun v -> 631 - sep (); 632 - add_kv_int buf 'x' v) 633 - c.dest_x; 634 - Option.iter 635 - (fun v -> 636 - sep (); 637 - add_kv_int buf 'y' v) 638 - c.dest_y; 639 - Option.iter 640 - (fun v -> 641 - sep (); 642 - add_kv_int buf 'X' v) 643 - c.source_x; 644 - Option.iter 645 - (fun v -> 646 - sep (); 647 - add_kv_int buf 'Y' v) 648 - c.source_y; 649 - Option.iter 650 - (fun comp -> 651 - let v = Composition.to_int comp in 652 - if v <> 0 then ( 653 - sep (); 654 - add_kv_int buf 'C' v)) 655 - c.composition) 656 - cmd.compose 476 + kv_int_opt w 'S' cmd.size; 477 + kv_int_opt w 'O' cmd.offset; 478 + (* Image ID/number *) 479 + kv_int_opt w 'i' cmd.image_id; 480 + kv_int_opt w 'I' cmd.image_number; 481 + (* Complex options *) 482 + cmd.placement |> Option.iter (write_placement w); 483 + cmd.delete |> Option.iter (write_delete w); 484 + cmd.frame |> Option.iter (write_frame w); 485 + cmd.animation |> Option.iter (write_animation w); 486 + cmd.compose |> Option.iter (write_compose w); 487 + w 657 488 658 489 let chunk_size = 4096 659 490 660 491 let write buf cmd ~data = 661 492 Buffer.add_string buf apc_start; 662 - write_control_data buf cmd; 493 + let w = write_control_data buf cmd in 663 494 if String.length data > 0 then begin 664 495 let encoded = Base64.encode_string data in 665 496 let len = String.length encoded in ··· 669 500 Buffer.add_string buf apc_end) 670 501 else begin 671 502 (* Multiple chunks *) 672 - let pos = ref 0 in 673 - let first = ref true in 674 - while !pos < len do 675 - let remaining = len - !pos in 676 - let this_chunk = min chunk_size remaining in 677 - let is_last = !pos + this_chunk >= len in 678 - if !first then ( 679 - (* First chunk *) 680 - first := false; 681 - add_comma buf; 682 - add_kv_int buf 'm' 1; 683 - Buffer.add_char buf ';'; 684 - Buffer.add_substring buf encoded !pos this_chunk; 685 - Buffer.add_string buf apc_end) 686 - else ( 687 - (* Continuation chunk *) 688 - Buffer.add_string buf apc_start; 689 - add_kv_int buf 'm' (if is_last then 0 else 1); 690 - Buffer.add_char buf ';'; 691 - Buffer.add_substring buf encoded !pos this_chunk; 692 - Buffer.add_string buf apc_end); 693 - pos := !pos + this_chunk 694 - done 503 + let rec write_chunks pos first = 504 + if pos < len then begin 505 + let remaining = len - pos in 506 + let this_chunk = min chunk_size remaining in 507 + let is_last = pos + this_chunk >= len in 508 + if first then ( 509 + kv_int w 'm' 1; 510 + Buffer.add_char buf ';'; 511 + Buffer.add_substring buf encoded pos this_chunk; 512 + Buffer.add_string buf apc_end) 513 + else ( 514 + Buffer.add_string buf apc_start; 515 + Buffer.add_string buf (if is_last then "m=0" else "m=1"); 516 + Buffer.add_char buf ';'; 517 + Buffer.add_substring buf encoded pos this_chunk; 518 + Buffer.add_string buf apc_end); 519 + write_chunks (pos + this_chunk) false 520 + end 521 + in 522 + write_chunks 0 true 695 523 end 696 524 end 697 525 else Buffer.add_string buf apc_end ··· 715 543 716 544 let error_code t = 717 545 if is_ok t then None 718 - else 719 - match String.index_opt t.message ':' with 720 - | Some i -> Some (String.sub t.message 0 i) 721 - | None -> Some t.message 546 + else String.index_opt t.message ':' |> Option.fold ~none:(Some t.message) ~some:(fun i -> Some (String.sub t.message 0 i)) 722 547 723 548 let image_id t = t.image_id 724 549 let image_number t = t.image_number 725 550 let placement_id t = t.placement_id 726 551 727 552 let parse s = 728 - (* Format: <ESC>_G<keys>;message<ESC>\ *) 553 + let ( let* ) = Option.bind in 729 554 let esc = '\027' in 730 555 let len = String.length s in 731 - if len < 5 then None 732 - else if s.[0] <> esc || s.[1] <> '_' || s.[2] <> 'G' then None 733 - else 734 - (* Find the semicolon and end *) 735 - match String.index_from_opt s 3 ';' with 736 - | None -> None 737 - | Some semi_pos -> ( 738 - (* Find the APC terminator *) 739 - let rec find_end pos = 740 - if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then 741 - Some pos 742 - else if pos + 1 < len then find_end (pos + 1) 743 - else None 744 - in 745 - match find_end (semi_pos + 1) with 746 - | None -> None 747 - | Some end_pos -> 748 - let keys_str = String.sub s 3 (semi_pos - 3) in 749 - let message = 750 - String.sub s (semi_pos + 1) (end_pos - semi_pos - 1) 751 - in 752 - (* Parse keys *) 753 - let image_id = ref None in 754 - let image_number = ref None in 755 - let placement_id = ref None in 756 - let parts = String.split_on_char ',' keys_str in 757 - List.iter 758 - (fun part -> 759 - if String.length part >= 3 && part.[1] = '=' then 760 - let key = part.[0] in 761 - let value = String.sub part 2 (String.length part - 2) in 762 - match key with 763 - | 'i' -> image_id := int_of_string_opt value 764 - | 'I' -> image_number := int_of_string_opt value 765 - | 'p' -> placement_id := int_of_string_opt value 766 - | _ -> ()) 767 - parts; 768 - Some 769 - { 770 - message; 771 - image_id = !image_id; 772 - image_number = !image_number; 773 - placement_id = !placement_id; 774 - }) 556 + let* () = if len >= 5 && s.[0] = esc && s.[1] = '_' && s.[2] = 'G' then Some () else None in 557 + let* semi_pos = String.index_from_opt s 3 ';' in 558 + let rec find_end pos = 559 + if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then Some pos 560 + else if pos + 1 < len then find_end (pos + 1) 561 + else None 562 + in 563 + let* end_pos = find_end (semi_pos + 1) in 564 + let keys_str = String.sub s 3 (semi_pos - 3) in 565 + let message = String.sub s (semi_pos + 1) (end_pos - semi_pos - 1) in 566 + let parse_kv part = 567 + if String.length part >= 3 && part.[1] = '=' then 568 + Some (part.[0], String.sub part 2 (String.length part - 2)) 569 + else None 570 + in 571 + let keys = String.split_on_char ',' keys_str |> List.filter_map parse_kv in 572 + let find_int key = List.assoc_opt key keys |> Fun.flip Option.bind int_of_string_opt in 573 + Some 574 + { 575 + message; 576 + image_id = find_int 'i'; 577 + image_number = find_int 'I'; 578 + placement_id = find_int 'p'; 579 + } 775 580 end 776 581 777 582 module Unicode_placeholder = struct 778 583 let placeholder_char = Uchar.of_int 0x10EEEE 779 584 780 - (* Row/column diacritics from the protocol spec *) 781 585 let diacritics = 782 586 [| 783 587 0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F; ··· 815 619 0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD; 0x1D242; 0x1D243; 0x1D244; 816 620 |] 817 621 818 - let row_diacritic n = 819 - if n >= 0 && n < Array.length diacritics then 820 - Uchar.of_int diacritics.(n) 821 - else Uchar.of_int diacritics.(0) 622 + let diacritic n = 623 + Uchar.of_int diacritics.(n mod Array.length diacritics) 822 624 823 - let column_diacritic = row_diacritic 824 - let id_high_byte_diacritic = row_diacritic 625 + let row_diacritic = diacritic 626 + let column_diacritic = diacritic 627 + let id_high_byte_diacritic = diacritic 825 628 826 629 let add_uchar buf u = 827 - let b = Bytes.create 4 in 828 - let len = Uchar.utf_8_byte_length u in 829 - let _ = Uchar.unsafe_to_char u in 830 - (* Encode UTF-8 manually *) 831 630 let code = Uchar.to_int u in 832 - if code < 0x80 then ( 833 - Bytes.set b 0 (Char.chr code); 834 - Buffer.add_subbytes buf b 0 1) 631 + let put = Buffer.add_char buf in 632 + if code < 0x80 then put (Char.chr code) 835 633 else if code < 0x800 then ( 836 - Bytes.set b 0 (Char.chr (0xC0 lor (code lsr 6))); 837 - Bytes.set b 1 (Char.chr (0x80 lor (code land 0x3F))); 838 - Buffer.add_subbytes buf b 0 2) 634 + put (Char.chr (0xC0 lor (code lsr 6))); 635 + put (Char.chr (0x80 lor (code land 0x3F)))) 839 636 else if code < 0x10000 then ( 840 - Bytes.set b 0 (Char.chr (0xE0 lor (code lsr 12))); 841 - Bytes.set b 1 (Char.chr (0x80 lor ((code lsr 6) land 0x3F))); 842 - Bytes.set b 2 (Char.chr (0x80 lor (code land 0x3F))); 843 - Buffer.add_subbytes buf b 0 3) 637 + put (Char.chr (0xE0 lor (code lsr 12))); 638 + put (Char.chr (0x80 lor ((code lsr 6) land 0x3F))); 639 + put (Char.chr (0x80 lor (code land 0x3F)))) 844 640 else ( 845 - Bytes.set b 0 (Char.chr (0xF0 lor (code lsr 18))); 846 - Bytes.set b 1 (Char.chr (0x80 lor ((code lsr 12) land 0x3F))); 847 - Bytes.set b 2 (Char.chr (0x80 lor ((code lsr 6) land 0x3F))); 848 - Bytes.set b 3 (Char.chr (0x80 lor (code land 0x3F))); 849 - Buffer.add_subbytes buf b 0 len) 641 + put (Char.chr (0xF0 lor (code lsr 18))); 642 + put (Char.chr (0x80 lor ((code lsr 12) land 0x3F))); 643 + put (Char.chr (0x80 lor ((code lsr 6) land 0x3F))); 644 + put (Char.chr (0x80 lor (code land 0x3F)))) 850 645 851 646 let write buf ~image_id ?placement_id ~rows ~cols () = 852 - (* Set foreground color using 24-bit mode *) 853 - let r = (image_id lsr 16) land 0xFF in 854 - let g = (image_id lsr 8) land 0xFF in 855 - let b = image_id land 0xFF in 856 - Buffer.add_string buf (Printf.sprintf "\027[38;2;%d;%d;%dm" r g b); 857 - (* Optionally set underline color for placement ID *) 858 - (match placement_id with 859 - | Some pid -> 860 - let pr = (pid lsr 16) land 0xFF in 861 - let pg = (pid lsr 8) land 0xFF in 862 - let pb = pid land 0xFF in 863 - Buffer.add_string buf (Printf.sprintf "\027[58;2;%d;%d;%dm" pr pg pb) 864 - | None -> ()); 865 - (* High byte diacritic if needed *) 647 + (* Set foreground color *) 648 + Printf.bprintf buf "\027[38;2;%d;%d;%dm" 649 + ((image_id lsr 16) land 0xFF) 650 + ((image_id lsr 8) land 0xFF) 651 + (image_id land 0xFF); 652 + (* Optional placement ID in underline color *) 653 + placement_id 654 + |> Option.iter (fun pid -> 655 + Printf.bprintf buf "\027[58;2;%d;%d;%dm" 656 + ((pid lsr 16) land 0xFF) 657 + ((pid lsr 8) land 0xFF) 658 + (pid land 0xFF)); 659 + (* High byte diacritic *) 866 660 let high_byte = (image_id lsr 24) land 0xFF in 867 - let high_diac = 868 - if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None 869 - in 870 - (* Write placeholder grid *) 661 + let high_diac = if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None in 662 + (* Write grid *) 871 663 for row = 0 to rows - 1 do 872 664 for col = 0 to cols - 1 do 873 665 add_uchar buf placeholder_char; 874 666 add_uchar buf (row_diacritic row); 875 667 add_uchar buf (column_diacritic col); 876 - Option.iter (add_uchar buf) high_diac 668 + high_diac |> Option.iter (add_uchar buf) 877 669 done; 878 670 if row < rows - 1 then Buffer.add_string buf "\n\r" 879 671 done; 880 672 (* Reset colors *) 881 673 Buffer.add_string buf "\027[39m"; 882 - match placement_id with Some _ -> Buffer.add_string buf "\027[59m" | None -> () 674 + if Option.is_some placement_id then Buffer.add_string buf "\027[59m" 883 675 end 884 676 885 677 module Detect = struct 886 678 let make_query () = 887 - (* Send a 1x1 transparent pixel query *) 888 - let cmd = 889 - Command.query ~format:Format.Rgb24 ~transmission:Transmission.Direct 890 - ~width:1 ~height:1 () 891 - in 892 - let data = "\x00\x00\x00" in 893 - let query = Command.to_string cmd ~data in 894 - (* Add DA1 query to detect non-supporting terminals *) 895 - query ^ "\027[c" 679 + let cmd = Command.query ~format:`Rgb24 ~transmission:`Direct ~width:1 ~height:1 () in 680 + Command.to_string cmd ~data:"\x00\x00\x00" ^ "\027[c" 896 681 897 682 let supports_graphics response ~da1_received = 898 - match response with 899 - | Some r -> Response.is_ok r 900 - | None -> not da1_received 683 + response |> Option.map Response.is_ok |> Option.value ~default:(not da1_received) 901 684 end
+112 -230
stack/kitty_graphics/lib/kitty_graphics.mli
··· 14 14 {[ 15 15 (* Display a PNG image *) 16 16 let png_data = read_file "image.png" in 17 - let cmd = Kitty_graphics.Command.transmit_and_display 18 - ~format:Kitty_graphics.Format.Png 19 - () 20 - in 17 + let cmd = Kitty_graphics.Command.transmit_and_display ~format:`Png () in 21 18 let buf = Buffer.create 1024 in 22 19 Kitty_graphics.Command.write buf cmd ~data:png_data; 23 20 print_string (Buffer.contents buf) ··· 28 25 See {{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics Protocol} 29 26 for the full specification. *) 30 27 31 - (** {1 Core Types} *) 28 + (** {1 Polymorphic Variant Types} *) 29 + 30 + type format = [ `Rgba32 | `Rgb24 | `Png ] 31 + (** Image data formats. [`Rgba32] is 32-bit RGBA (4 bytes per pixel), 32 + [`Rgb24] is 24-bit RGB (3 bytes per pixel), [`Png] is PNG encoded data. *) 33 + 34 + type transmission = [ `Direct | `File | `Tempfile ] 35 + (** Transmission methods. [`Direct] sends data inline, [`File] reads from a path, 36 + [`Tempfile] reads from a temp file that the terminal deletes after reading. *) 37 + 38 + type compression = [ `None | `Zlib ] 39 + (** Compression options. [`None] for raw data, [`Zlib] for RFC 1950 compression. *) 40 + 41 + type quiet = [ `Noisy | `Errors_only | `Silent ] 42 + (** Response suppression. [`Noisy] sends all responses (default), 43 + [`Errors_only] suppresses OK responses, [`Silent] suppresses all. *) 44 + 45 + type cursor = [ `Move | `Static ] 46 + (** Cursor movement after displaying. [`Move] advances cursor (default), 47 + [`Static] keeps cursor in place. *) 48 + 49 + type composition = [ `Alpha_blend | `Overwrite ] 50 + (** Composition modes. [`Alpha_blend] for full blending (default), 51 + [`Overwrite] for simple pixel replacement. *) 32 52 33 - (** Image data formats. *) 53 + type delete = 54 + [ `All_visible 55 + | `All_visible_and_free 56 + | `By_id of int * int option 57 + | `By_id_and_free of int * int option 58 + | `By_number of int * int option 59 + | `By_number_and_free of int * int option 60 + | `At_cursor 61 + | `At_cursor_and_free 62 + | `At_cell of int * int 63 + | `At_cell_and_free of int * int 64 + | `At_cell_z of int * int * int 65 + | `At_cell_z_and_free of int * int * int 66 + | `By_column of int 67 + | `By_column_and_free of int 68 + | `By_row of int 69 + | `By_row_and_free of int 70 + | `By_z_index of int 71 + | `By_z_index_and_free of int 72 + | `By_id_range of int * int 73 + | `By_id_range_and_free of int * int 74 + | `Frames 75 + | `Frames_and_free ] 76 + (** Delete target specification. Each variant has two forms: one that only 77 + removes placements (e.g., [`All_visible]) and one that also frees the 78 + image data (e.g., [`All_visible_and_free]). Tuple variants contain 79 + (image_id, optional_placement_id) or (x, y) coordinates. *) 80 + 81 + type animation_state = [ `Stop | `Loading | `Run ] 82 + (** Animation playback state. [`Stop] halts animation, [`Loading] runs but 83 + waits for new frames at end, [`Run] runs normally and loops. *) 84 + 85 + (** {1 Type Modules} *) 86 + 34 87 module Format : sig 35 - type t = 36 - | Rgba32 (** 32-bit RGBA, 4 bytes per pixel *) 37 - | Rgb24 (** 24-bit RGB, 3 bytes per pixel *) 38 - | Png (** PNG encoded data *) 88 + type t = format 39 89 40 90 val to_int : t -> int 41 91 (** Convert to protocol integer value (32, 24, or 100). *) 42 92 end 43 93 44 - (** Transmission methods for image data. *) 45 94 module Transmission : sig 46 - type t = 47 - | Direct (** Data transmitted inline in the escape sequence *) 48 - | File (** Data read from a file path *) 49 - | Tempfile (** Data read from a temp file, deleted after reading *) 95 + type t = transmission 50 96 51 97 val to_char : t -> char 52 98 (** Convert to protocol character ('d', 'f', or 't'). *) 53 99 end 54 100 55 - (** Compression options for transmitted data. *) 56 101 module Compression : sig 57 - type t = 58 - | None (** No compression *) 59 - | Zlib (** RFC 1950 zlib compression *) 102 + type t = compression 60 103 61 104 val to_char : t -> char option 62 - (** Convert to protocol character (None or Some 'z'). *) 105 + (** Convert to protocol character ([None] or [Some 'z']). *) 63 106 end 64 107 65 - (** Response suppression modes. *) 66 108 module Quiet : sig 67 - type t = 68 - | Noisy (** Terminal sends all responses (default) *) 69 - | Errors_only (** Terminal only sends error responses *) 70 - | Silent (** Terminal sends no responses *) 109 + type t = quiet 71 110 72 111 val to_int : t -> int 73 112 (** Convert to protocol integer (0, 1, or 2). *) 74 113 end 75 114 76 - (** Cursor movement policy after displaying an image. *) 77 115 module Cursor : sig 78 - type t = 79 - | Move (** Move cursor after image (default) *) 80 - | Static (** Keep cursor in place *) 116 + type t = cursor 81 117 82 118 val to_int : t -> int 83 119 (** Convert to protocol integer (0 or 1). *) 84 120 end 85 121 86 - (** Composition modes for blending. *) 87 122 module Composition : sig 88 - type t = 89 - | Alpha_blend (** Full alpha blending (default) *) 90 - | Overwrite (** Simple pixel replacement *) 123 + type t = composition 91 124 92 125 val to_int : t -> int 93 126 (** Convert to protocol integer (0 or 1). *) 94 127 end 95 128 96 - (** {1 Delete Operations} *) 97 - 98 - (** Specifies what to delete when using delete commands. *) 99 129 module Delete : sig 100 - (** Delete target specification. 101 - 102 - Each variant has two forms: one that only removes placements (keeping 103 - image data for potential reuse) and one that also frees the image data. *) 104 - type t = 105 - | All_visible 106 - (** Delete all visible placements. *) 107 - | All_visible_and_free 108 - (** Delete all visible placements and free their image data. *) 109 - | By_id of { image_id : int; placement_id : int option } 110 - (** Delete placements for a specific image ID, optionally filtered 111 - by placement ID. *) 112 - | By_id_and_free of { image_id : int; placement_id : int option } 113 - (** Delete and free by image ID. *) 114 - | By_number of { image_number : int; placement_id : int option } 115 - (** Delete by image number (newest with that number). *) 116 - | By_number_and_free of { image_number : int; placement_id : int option } 117 - (** Delete and free by image number. *) 118 - | At_cursor 119 - (** Delete placements intersecting cursor position. *) 120 - | At_cursor_and_free 121 - (** Delete and free at cursor position. *) 122 - | At_cell of { x : int; y : int } 123 - (** Delete placements intersecting a specific cell (1-based). *) 124 - | At_cell_and_free of { x : int; y : int } 125 - (** Delete and free at specific cell. *) 126 - | At_cell_z of { x : int; y : int; z : int } 127 - (** Delete at cell with specific z-index. *) 128 - | At_cell_z_and_free of { x : int; y : int; z : int } 129 - (** Delete and free at cell with z-index. *) 130 - | By_column of int 131 - (** Delete all placements intersecting a column (1-based). *) 132 - | By_column_and_free of int 133 - (** Delete and free by column. *) 134 - | By_row of int 135 - (** Delete all placements intersecting a row (1-based). *) 136 - | By_row_and_free of int 137 - (** Delete and free by row. *) 138 - | By_z_index of int 139 - (** Delete all placements with a specific z-index. *) 140 - | By_z_index_and_free of int 141 - (** Delete and free by z-index. *) 142 - | By_id_range of { min_id : int; max_id : int } 143 - (** Delete images with IDs in range [min_id, max_id]. *) 144 - | By_id_range_and_free of { min_id : int; max_id : int } 145 - (** Delete and free by ID range. *) 146 - | Frames 147 - (** Delete animation frames. *) 148 - | Frames_and_free 149 - (** Delete animation frames and free if no frames remain. *) 130 + type t = delete 150 131 end 151 132 152 133 (** {1 Placement Options} *) 153 134 154 - (** Image placement configuration. 155 - 156 - Controls how an image is positioned and scaled when displayed. *) 157 135 module Placement : sig 158 136 type t 159 137 (** Placement configuration. *) ··· 169 147 ?rows:int -> 170 148 ?z_index:int -> 171 149 ?placement_id:int -> 172 - ?cursor:Cursor.t -> 150 + ?cursor:cursor -> 173 151 ?unicode_placeholder:bool -> 174 152 unit -> 175 153 t ··· 194 172 195 173 (** {1 Animation} *) 196 174 197 - (** Animation frame specification. *) 198 175 module Frame : sig 199 176 type t 200 177 (** Animation frame configuration. *) ··· 205 182 ?base_frame:int -> 206 183 ?edit_frame:int -> 207 184 ?gap_ms:int -> 208 - ?composition:Composition.t -> 185 + ?composition:composition -> 209 186 ?background_color:int32 -> 210 187 unit -> 211 188 t ··· 223 200 (** Empty frame spec with defaults. *) 224 201 end 225 202 226 - (** Animation control operations. *) 227 203 module Animation : sig 228 - type state = 229 - | Stop (** Stop the animation *) 230 - | Loading (** Run but wait for new frames at end *) 231 - | Run (** Run normally, loop at end *) 204 + type state = animation_state 232 205 233 - type t 234 - (** Animation control configuration. *) 206 + type t = 207 + [ `Set_state of state * int option 208 + | `Set_gap of int * int 209 + | `Set_current of int ] 210 + (** Animation control operations. *) 235 211 236 212 val set_state : ?loops:int -> state -> t 237 213 (** Set animation state. 238 - 239 214 @param loops Number of loops: 0 = ignored, 1 = infinite, n = n-1 loops *) 240 215 241 216 val set_gap : frame:int -> gap_ms:int -> t 242 217 (** Set the gap (delay) for a specific frame. 243 - 244 218 @param frame 1-based frame number 245 219 @param gap_ms Delay in milliseconds (negative = gapless) *) 246 220 ··· 248 222 (** Make a specific frame (1-based) the current displayed frame. *) 249 223 end 250 224 251 - (** Frame composition for combining frame regions. *) 252 225 module Compose : sig 253 226 type t 254 227 (** Composition operation. *) ··· 262 235 ?source_y:int -> 263 236 ?dest_x:int -> 264 237 ?dest_y:int -> 265 - ?composition:Composition.t -> 238 + ?composition:composition -> 266 239 unit -> 267 240 t 268 - (** Compose a rectangle from one frame onto another. 269 - 270 - @param source_frame 1-based source frame number 271 - @param dest_frame 1-based destination frame number 272 - @param width Rectangle width in pixels (default: full width) 273 - @param height Rectangle height in pixels (default: full height) 274 - @param source_x Left edge of source rectangle 275 - @param source_y Top edge of source rectangle 276 - @param dest_x Left edge of destination rectangle 277 - @param dest_y Top edge of destination rectangle 278 - @param composition Blend mode *) 241 + (** Compose a rectangle from one frame onto another. *) 279 242 end 280 243 281 244 (** {1 Commands} *) 282 245 283 - (** Graphics command builder. 284 - 285 - This is the main API for constructing graphics protocol commands. 286 - Commands are built using the various constructors, then written to 287 - a buffer with {!write}. *) 288 246 module Command : sig 289 247 type t 290 248 (** A graphics protocol command. *) ··· 294 252 val transmit : 295 253 ?image_id:int -> 296 254 ?image_number:int -> 297 - ?format:Format.t -> 298 - ?transmission:Transmission.t -> 299 - ?compression:Compression.t -> 255 + ?format:format -> 256 + ?transmission:transmission -> 257 + ?compression:compression -> 300 258 ?width:int -> 301 259 ?height:int -> 302 260 ?size:int -> 303 261 ?offset:int -> 304 - ?quiet:Quiet.t -> 262 + ?quiet:quiet -> 305 263 unit -> 306 264 t 307 - (** Transmit image data without displaying. 308 - 309 - @param image_id Unique ID for the image (1-4294967295) 310 - @param image_number Image number (terminal assigns ID) 311 - @param format Pixel format of the data 312 - @param transmission How data is transmitted 313 - @param compression Compression applied to data 314 - @param width Image width in pixels (required for RGB/RGBA) 315 - @param height Image height in pixels (required for RGB/RGBA) 316 - @param size Number of bytes to read (for file transmission) 317 - @param offset Byte offset to start reading (for file transmission) 318 - @param quiet Response suppression mode *) 265 + (** Transmit image data without displaying. *) 319 266 320 267 val transmit_and_display : 321 268 ?image_id:int -> 322 269 ?image_number:int -> 323 - ?format:Format.t -> 324 - ?transmission:Transmission.t -> 325 - ?compression:Compression.t -> 270 + ?format:format -> 271 + ?transmission:transmission -> 272 + ?compression:compression -> 326 273 ?width:int -> 327 274 ?height:int -> 328 275 ?size:int -> 329 276 ?offset:int -> 330 - ?quiet:Quiet.t -> 277 + ?quiet:quiet -> 331 278 ?placement:Placement.t -> 332 279 unit -> 333 280 t 334 - (** Transmit image data and display it immediately. 335 - 336 - This is the most common operation for displaying images. 337 - See {!transmit} for transmission parameters and {!Placement} 338 - for display options. *) 281 + (** Transmit image data and display it immediately. *) 339 282 340 283 val query : 341 - ?format:Format.t -> 342 - ?transmission:Transmission.t -> 284 + ?format:format -> 285 + ?transmission:transmission -> 343 286 ?width:int -> 344 287 ?height:int -> 345 - ?quiet:Quiet.t -> 288 + ?quiet:quiet -> 346 289 unit -> 347 290 t 348 - (** Query terminal support without storing the image. 349 - 350 - Send a small test image to check if the terminal supports 351 - the graphics protocol. The terminal responds with OK or 352 - an error without storing the image. *) 291 + (** Query terminal support without storing the image. *) 353 292 354 293 (** {2 Display} *) 355 294 ··· 357 296 ?image_id:int -> 358 297 ?image_number:int -> 359 298 ?placement:Placement.t -> 360 - ?quiet:Quiet.t -> 299 + ?quiet:quiet -> 361 300 unit -> 362 301 t 363 - (** Display a previously transmitted image. 364 - 365 - @param image_id ID of a previously transmitted image 366 - @param image_number Number of the image to display 367 - @param placement Display placement options 368 - @param quiet Response suppression *) 302 + (** Display a previously transmitted image. *) 369 303 370 304 (** {2 Deletion} *) 371 305 372 - val delete : ?quiet:Quiet.t -> Delete.t -> t 373 - (** Delete images or placements. 374 - 375 - See {!Delete} for the various deletion modes. *) 306 + val delete : ?quiet:quiet -> delete -> t 307 + (** Delete images or placements. *) 376 308 377 309 (** {2 Animation} *) 378 310 379 311 val frame : 380 312 ?image_id:int -> 381 313 ?image_number:int -> 382 - ?format:Format.t -> 383 - ?transmission:Transmission.t -> 384 - ?compression:Compression.t -> 314 + ?format:format -> 315 + ?transmission:transmission -> 316 + ?compression:compression -> 385 317 ?width:int -> 386 318 ?height:int -> 387 - ?quiet:Quiet.t -> 319 + ?quiet:quiet -> 388 320 frame:Frame.t -> 389 321 unit -> 390 322 t 391 - (** Transmit animation frame data. 392 - 393 - Similar to {!transmit} but adds frame-specific parameters. *) 323 + (** Transmit animation frame data. *) 394 324 395 - val animate : 396 - ?image_id:int -> 397 - ?image_number:int -> 398 - ?quiet:Quiet.t -> 399 - Animation.t -> 400 - t 325 + val animate : ?image_id:int -> ?image_number:int -> ?quiet:quiet -> Animation.t -> t 401 326 (** Control animation playback. *) 402 327 403 - val compose : 404 - ?image_id:int -> 405 - ?image_number:int -> 406 - ?quiet:Quiet.t -> 407 - Compose.t -> 408 - t 328 + val compose : ?image_id:int -> ?image_number:int -> ?quiet:quiet -> Compose.t -> t 409 329 (** Compose animation frames. *) 410 330 411 331 (** {2 Output} *) 412 332 413 333 val write : Buffer.t -> t -> data:string -> unit 414 - (** Write the command to a buffer. 415 - 416 - @param data The payload data (image bytes, file path, etc.). 417 - For {!display}, {!delete}, {!animate}, pass empty string. *) 334 + (** Write the command to a buffer. *) 418 335 419 336 val to_string : t -> data:string -> string 420 337 (** Convert command to a string. *) ··· 422 339 423 340 (** {1 Response Parsing} *) 424 341 425 - (** Terminal response parsing. 426 - 427 - When the terminal processes a graphics command, it may send back 428 - a response indicating success or failure. *) 429 342 module Response : sig 430 343 type t 431 344 (** A parsed terminal response. *) 432 345 433 346 val parse : string -> t option 434 - (** Parse a response from terminal output. 435 - 436 - Expects the format: [<ESC>_G...;message<ESC>\] 437 - Returns [None] if the string is not a valid graphics response. *) 347 + (** Parse a response from terminal output. *) 438 348 439 349 val is_ok : t -> bool 440 350 (** Check if the response indicates success. *) 441 351 442 352 val message : t -> string 443 - (** Get the response message ("OK" or error description). *) 353 + (** Get the response message. *) 444 354 445 355 val error_code : t -> string option 446 - (** Extract the error code if this is an error response. 447 - 448 - Error codes include: ENOENT, EINVAL, ENOSPC, EBADPNG, etc. *) 356 + (** Extract the error code if this is an error response. *) 449 357 450 358 val image_id : t -> int option 451 - (** Get the image ID from the response, if present. *) 359 + (** Get the image ID from the response. *) 452 360 453 361 val image_number : t -> int option 454 - (** Get the image number from the response, if present. *) 362 + (** Get the image number from the response. *) 455 363 456 364 val placement_id : t -> int option 457 - (** Get the placement ID from the response, if present. *) 365 + (** Get the placement ID from the response. *) 458 366 end 459 367 460 368 (** {1 Unicode Placeholders} *) 461 369 462 - (** Unicode placeholder generation for tmux/vim compatibility. 463 - 464 - Unicode placeholders allow images to work with applications that 465 - don't understand the graphics protocol but support Unicode and 466 - foreground colors. The image is transmitted with a virtual placement, 467 - then placeholder characters are written to the terminal. *) 468 370 module Unicode_placeholder : sig 469 371 val placeholder_char : Uchar.t 470 372 (** The Unicode placeholder character U+10EEEE. *) ··· 477 379 cols:int -> 478 380 unit -> 479 381 unit 480 - (** Write placeholder characters to a buffer. 481 - 482 - The image ID is encoded in the foreground color (24-bit mode). 483 - Row and column positions are encoded using combining diacritics. 484 - 485 - @param image_id The image ID (should have non-zero bytes for 24-bit) 486 - @param placement_id Optional placement ID (encoded in underline color) 487 - @param rows Number of rows to fill 488 - @param cols Number of columns per row *) 382 + (** Write placeholder characters to a buffer. *) 489 383 490 384 val row_diacritic : int -> Uchar.t 491 385 (** Get the combining diacritic for a row number (0-based). *) ··· 499 393 500 394 (** {1 Terminal Detection} *) 501 395 502 - (** Helpers for detecting terminal graphics support. *) 503 396 module Detect : sig 504 397 val make_query : unit -> string 505 - (** Generate a query command to test graphics support. 506 - 507 - Send this to stdout and read the terminal's response. 508 - Follow with a DA1 query ([<ESC>[c]) to detect terminals 509 - that don't support graphics (they'll answer DA1 but not 510 - the graphics query). *) 398 + (** Generate a query command to test graphics support. *) 511 399 512 400 val supports_graphics : Response.t option -> da1_received:bool -> bool 513 - (** Determine if graphics are supported based on query results. 514 - 515 - @param response The parsed graphics response, if any 516 - @param da1_received Whether a DA1 response was received 517 - 518 - Returns [true] if a graphics OK response was received, 519 - or [false] if only DA1 was received (no graphics support). *) 401 + (** Determine if graphics are supported based on query results. *) 520 402 end