My agentic slop goes here. Not intended for anyone else!
at main 9.6 kB view raw
1(* Kitty Graphics Protocol - Command *) 2 3type action = 4 [ `Transmit 5 | `Transmit_and_display 6 | `Query 7 | `Display 8 | `Delete 9 | `Frame 10 | `Animate 11 | `Compose ] 12 13type t = { 14 action : action; 15 format : Kgp_types.format option; 16 transmission : Kgp_types.transmission option; 17 compression : Kgp_types.compression option; 18 width : int option; 19 height : int option; 20 size : int option; 21 offset : int option; 22 quiet : Kgp_types.quiet option; 23 image_id : int option; 24 image_number : int option; 25 placement : Kgp_placement.t option; 26 delete : Kgp_types.delete option; 27 frame : Kgp_frame.t option; 28 animation : Kgp_animation.t option; 29 compose : Kgp_compose.t option; 30} 31 32let make action = 33 { 34 action; 35 format = None; 36 transmission = None; 37 compression = None; 38 width = None; 39 height = None; 40 size = None; 41 offset = None; 42 quiet = None; 43 image_id = None; 44 image_number = None; 45 placement = None; 46 delete = None; 47 frame = None; 48 animation = None; 49 compose = None; 50 } 51 52let transmit ?image_id ?image_number ?format ?transmission ?compression ?width 53 ?height ?size ?offset ?quiet () = 54 { 55 (make `Transmit) with 56 image_id; 57 image_number; 58 format; 59 transmission; 60 compression; 61 width; 62 height; 63 size; 64 offset; 65 quiet; 66 } 67 68let transmit_and_display ?image_id ?image_number ?format ?transmission 69 ?compression ?width ?height ?size ?offset ?quiet ?placement () = 70 { 71 (make `Transmit_and_display) with 72 image_id; 73 image_number; 74 format; 75 transmission; 76 compression; 77 width; 78 height; 79 size; 80 offset; 81 quiet; 82 placement; 83 } 84 85let query ?format ?transmission ?width ?height ?quiet () = 86 { (make `Query) with format; transmission; width; height; quiet } 87 88let display ?image_id ?image_number ?placement ?quiet () = 89 { (make `Display) with image_id; image_number; placement; quiet } 90 91let delete ?quiet del = { (make `Delete) with quiet; delete = Some del } 92 93let frame ?image_id ?image_number ?format ?transmission ?compression ?width 94 ?height ?quiet ~frame () = 95 { 96 (make `Frame) with 97 image_id; 98 image_number; 99 format; 100 transmission; 101 compression; 102 width; 103 height; 104 quiet; 105 frame = Some frame; 106 } 107 108let animate ?image_id ?image_number ?quiet anim = 109 { (make `Animate) with image_id; image_number; quiet; animation = Some anim } 110 111let compose ?image_id ?image_number ?quiet comp = 112 { (make `Compose) with image_id; image_number; quiet; compose = Some comp } 113 114(* Serialization helpers *) 115let apc_start = "\027_G" 116let apc_end = "\027\\" 117 118(* Key-value writer with separator handling *) 119type kv_writer = { mutable first : bool; buf : Buffer.t } 120 121let kv_writer buf = { first = true; buf } 122 123let kv w key value = 124 if not w.first then Buffer.add_char w.buf ','; 125 w.first <- false; 126 Buffer.add_char w.buf key; 127 Buffer.add_char w.buf '='; 128 Buffer.add_string w.buf value 129 130let kv_int w key value = kv w key (string_of_int value) 131let kv_int32 w key value = kv w key (Int32.to_string value) 132let kv_char w key value = kv w key (String.make 1 value) 133 134(* Conditional writers using Option.iter *) 135let kv_int_opt w key = Option.iter (kv_int w key) 136let kv_int32_opt w key = Option.iter (kv_int32 w key) 137 138let kv_int_if w key ~default opt = 139 Option.iter (fun v -> if v <> default then kv_int w key v) opt 140 141let action_char : action -> char = function 142 | `Transmit -> 't' 143 | `Transmit_and_display -> 'T' 144 | `Query -> 'q' 145 | `Display -> 'p' 146 | `Delete -> 'd' 147 | `Frame -> 'f' 148 | `Animate -> 'a' 149 | `Compose -> 'c' 150 151let delete_char : Kgp_types.delete -> char = function 152 | `All_visible -> 'a' 153 | `All_visible_and_free -> 'A' 154 | `By_id _ -> 'i' 155 | `By_id_and_free _ -> 'I' 156 | `By_number _ -> 'n' 157 | `By_number_and_free _ -> 'N' 158 | `At_cursor -> 'c' 159 | `At_cursor_and_free -> 'C' 160 | `At_cell _ -> 'p' 161 | `At_cell_and_free _ -> 'P' 162 | `At_cell_z _ -> 'q' 163 | `At_cell_z_and_free _ -> 'Q' 164 | `By_column _ -> 'x' 165 | `By_column_and_free _ -> 'X' 166 | `By_row _ -> 'y' 167 | `By_row_and_free _ -> 'Y' 168 | `By_z_index _ -> 'z' 169 | `By_z_index_and_free _ -> 'Z' 170 | `By_id_range _ -> 'r' 171 | `By_id_range_and_free _ -> 'R' 172 | `Frames -> 'f' 173 | `Frames_and_free -> 'F' 174 175let write_placement w (p : Kgp_placement.t) = 176 kv_int_opt w 'x' p.source_x; 177 kv_int_opt w 'y' p.source_y; 178 kv_int_opt w 'w' p.source_width; 179 kv_int_opt w 'h' p.source_height; 180 kv_int_opt w 'X' p.cell_x_offset; 181 kv_int_opt w 'Y' p.cell_y_offset; 182 kv_int_opt w 'c' p.columns; 183 kv_int_opt w 'r' p.rows; 184 kv_int_opt w 'z' p.z_index; 185 kv_int_opt w 'p' p.placement_id; 186 p.cursor 187 |> Option.iter (fun c -> 188 kv_int_if w 'C' ~default:0 (Some (Kgp_types.Cursor.to_int c))); 189 if p.unicode_placeholder then kv_int w 'U' 1 190 191let write_delete w (d : Kgp_types.delete) = 192 kv_char w 'd' (delete_char d); 193 match d with 194 | `By_id (id, pid) | `By_id_and_free (id, pid) -> 195 kv_int w 'i' id; 196 kv_int_opt w 'p' pid 197 | `By_number (n, pid) | `By_number_and_free (n, pid) -> 198 kv_int w 'I' n; 199 kv_int_opt w 'p' pid 200 | `At_cell (x, y) | `At_cell_and_free (x, y) -> 201 kv_int w 'x' x; 202 kv_int w 'y' y 203 | `At_cell_z (x, y, z) | `At_cell_z_and_free (x, y, z) -> 204 kv_int w 'x' x; 205 kv_int w 'y' y; 206 kv_int w 'z' z 207 | `By_column c | `By_column_and_free c -> kv_int w 'x' c 208 | `By_row r | `By_row_and_free r -> kv_int w 'y' r 209 | `By_z_index z | `By_z_index_and_free z -> kv_int w 'z' z 210 | `By_id_range (min_id, max_id) | `By_id_range_and_free (min_id, max_id) -> 211 kv_int w 'x' min_id; 212 kv_int w 'y' max_id 213 | `All_visible | `All_visible_and_free | `At_cursor | `At_cursor_and_free 214 | `Frames | `Frames_and_free -> 215 () 216 217let write_frame w (f : Kgp_frame.t) = 218 kv_int_opt w 'x' f.x; 219 kv_int_opt w 'y' f.y; 220 kv_int_opt w 'c' f.base_frame; 221 kv_int_opt w 'r' f.edit_frame; 222 kv_int_opt w 'z' f.gap_ms; 223 f.composition 224 |> Option.iter (fun c -> 225 kv_int_if w 'X' ~default:0 (Some (Kgp_types.Composition.to_int c))); 226 kv_int32_opt w 'Y' f.background_color 227 228let write_animation w : Kgp_animation.t -> unit = function 229 | `Set_state (state, loops) -> 230 let s = match state with `Stop -> 1 | `Loading -> 2 | `Run -> 3 in 231 kv_int w 's' s; 232 kv_int_opt w 'v' loops 233 | `Set_gap (frame, gap_ms) -> 234 kv_int w 'r' frame; 235 kv_int w 'z' gap_ms 236 | `Set_current frame -> kv_int w 'c' frame 237 238let write_compose w (c : Kgp_compose.t) = 239 kv_int w 'r' c.source_frame; 240 kv_int w 'c' c.dest_frame; 241 kv_int_opt w 'w' c.width; 242 kv_int_opt w 'h' c.height; 243 kv_int_opt w 'x' c.dest_x; 244 kv_int_opt w 'y' c.dest_y; 245 kv_int_opt w 'X' c.source_x; 246 kv_int_opt w 'Y' c.source_y; 247 c.composition 248 |> Option.iter (fun comp -> 249 kv_int_if w 'C' ~default:0 (Some (Kgp_types.Composition.to_int comp))) 250 251let write_control_data buf cmd = 252 let w = kv_writer buf in 253 (* Action *) 254 kv_char w 'a' (action_char cmd.action); 255 (* Quiet - only if non-default *) 256 cmd.quiet 257 |> Option.iter (fun q -> 258 kv_int_if w 'q' ~default:0 (Some (Kgp_types.Quiet.to_int q))); 259 (* Format *) 260 cmd.format 261 |> Option.iter (fun f -> kv_int w 'f' (Kgp_types.Format.to_int f)); 262 (* Transmission - only for transmit/frame actions, always include t=d for compatibility *) 263 (match cmd.action with 264 | `Transmit | `Transmit_and_display | `Frame -> ( 265 match cmd.transmission with 266 | Some t -> kv_char w 't' (Kgp_types.Transmission.to_char t) 267 | None -> kv_char w 't' 'd') 268 | _ -> ()); 269 (* Compression *) 270 cmd.compression 271 |> Option.iter (fun c -> 272 Kgp_types.Compression.to_char c |> Option.iter (kv_char w 'o')); 273 (* Dimensions *) 274 kv_int_opt w 's' cmd.width; 275 kv_int_opt w 'v' cmd.height; 276 (* File size/offset *) 277 kv_int_opt w 'S' cmd.size; 278 kv_int_opt w 'O' cmd.offset; 279 (* Image ID/number *) 280 kv_int_opt w 'i' cmd.image_id; 281 kv_int_opt w 'I' cmd.image_number; 282 (* Complex options *) 283 cmd.placement |> Option.iter (write_placement w); 284 cmd.delete |> Option.iter (write_delete w); 285 cmd.frame |> Option.iter (write_frame w); 286 cmd.animation |> Option.iter (write_animation w); 287 cmd.compose |> Option.iter (write_compose w); 288 w 289 290(* Use large chunk size to avoid chunking - Kitty animation doesn't handle chunks well *) 291let chunk_size = 1024 * 1024 (* 1MB - effectively no chunking *) 292 293let write buf cmd ~data = 294 Buffer.add_string buf apc_start; 295 let w = write_control_data buf cmd in 296 if String.length data > 0 then begin 297 let encoded = Base64.encode_string data in 298 let len = String.length encoded in 299 if len <= chunk_size then ( 300 Buffer.add_char buf ';'; 301 Buffer.add_string buf encoded; 302 Buffer.add_string buf apc_end) 303 else begin 304 (* Multiple chunks *) 305 let rec write_chunks pos first = 306 if pos < len then begin 307 let remaining = len - pos in 308 let this_chunk = min chunk_size remaining in 309 let is_last = pos + this_chunk >= len in 310 if first then ( 311 kv_int w 'm' 1; 312 Buffer.add_char buf ';'; 313 Buffer.add_substring buf encoded pos this_chunk; 314 Buffer.add_string buf apc_end) 315 else ( 316 Buffer.add_string buf apc_start; 317 Buffer.add_string buf (if is_last then "m=0" else "m=1"); 318 Buffer.add_char buf ';'; 319 Buffer.add_substring buf encoded pos this_chunk; 320 Buffer.add_string buf apc_end); 321 write_chunks (pos + this_chunk) false 322 end 323 in 324 write_chunks 0 true 325 end 326 end 327 else Buffer.add_string buf apc_end 328 329let to_string cmd ~data = 330 let buf = Buffer.create 1024 in 331 write buf cmd ~data; 332 Buffer.contents buf