My agentic slop goes here. Not intended for anyone else!
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