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