···11+(* Kitty Terminal Graphics Protocol - Implementation *)
22+33+(* Polymorphic variant types *)
44+type format = [ `Rgba32 | `Rgb24 | `Png ]
55+type transmission = [ `Direct | `File | `Tempfile ]
66+type compression = [ `None | `Zlib ]
77+type quiet = [ `Noisy | `Errors_only | `Silent ]
88+type cursor = [ `Move | `Static ]
99+type composition = [ `Alpha_blend | `Overwrite ]
1010+1111+type delete =
1212+ [ `All_visible
1313+ | `All_visible_and_free
1414+ | `By_id of int * int option
1515+ | `By_id_and_free of int * int option
1616+ | `By_number of int * int option
1717+ | `By_number_and_free of int * int option
1818+ | `At_cursor
1919+ | `At_cursor_and_free
2020+ | `At_cell of int * int
2121+ | `At_cell_and_free of int * int
2222+ | `At_cell_z of int * int * int
2323+ | `At_cell_z_and_free of int * int * int
2424+ | `By_column of int
2525+ | `By_column_and_free of int
2626+ | `By_row of int
2727+ | `By_row_and_free of int
2828+ | `By_z_index of int
2929+ | `By_z_index_and_free of int
3030+ | `By_id_range of int * int
3131+ | `By_id_range_and_free of int * int
3232+ | `Frames
3333+ | `Frames_and_free ]
3434+3535+type animation_state = [ `Stop | `Loading | `Run ]
3636+3737+(* Modules re-export the types with conversion functions *)
3838+module Format = struct
3939+ type t = format
4040+4141+ let to_int : t -> int = function
4242+ | `Rgba32 -> 32
4343+ | `Rgb24 -> 24
4444+ | `Png -> 100
4545+end
4646+4747+module Transmission = struct
4848+ type t = transmission
4949+5050+ let to_char : t -> char = function
5151+ | `Direct -> 'd'
5252+ | `File -> 'f'
5353+ | `Tempfile -> 't'
5454+end
5555+5656+module Compression = struct
5757+ type t = compression
5858+5959+ let to_char : t -> char option = function
6060+ | `None -> None
6161+ | `Zlib -> Some 'z'
6262+end
6363+6464+module Quiet = struct
6565+ type t = quiet
6666+6767+ let to_int : t -> int = function
6868+ | `Noisy -> 0
6969+ | `Errors_only -> 1
7070+ | `Silent -> 2
7171+end
7272+7373+module Cursor = struct
7474+ type t = cursor
7575+7676+ let to_int : t -> int = function
7777+ | `Move -> 0
7878+ | `Static -> 1
7979+end
8080+8181+module Composition = struct
8282+ type t = composition
8383+8484+ let to_int : t -> int = function
8585+ | `Alpha_blend -> 0
8686+ | `Overwrite -> 1
8787+end
8888+8989+module Delete = struct
9090+ type t = delete
9191+end
9292+9393+module Placement = struct
9494+ type t = {
9595+ source_x : int option;
9696+ source_y : int option;
9797+ source_width : int option;
9898+ source_height : int option;
9999+ cell_x_offset : int option;
100100+ cell_y_offset : int option;
101101+ columns : int option;
102102+ rows : int option;
103103+ z_index : int option;
104104+ placement_id : int option;
105105+ cursor : cursor option;
106106+ unicode_placeholder : bool;
107107+ }
108108+109109+ let empty =
110110+ {
111111+ source_x = None;
112112+ source_y = None;
113113+ source_width = None;
114114+ source_height = None;
115115+ cell_x_offset = None;
116116+ cell_y_offset = None;
117117+ columns = None;
118118+ rows = None;
119119+ z_index = None;
120120+ placement_id = None;
121121+ cursor = None;
122122+ unicode_placeholder = false;
123123+ }
124124+125125+ let make ?source_x ?source_y ?source_width ?source_height ?cell_x_offset
126126+ ?cell_y_offset ?columns ?rows ?z_index ?placement_id ?cursor
127127+ ?(unicode_placeholder = false) () =
128128+ {
129129+ source_x;
130130+ source_y;
131131+ source_width;
132132+ source_height;
133133+ cell_x_offset;
134134+ cell_y_offset;
135135+ columns;
136136+ rows;
137137+ z_index;
138138+ placement_id;
139139+ cursor;
140140+ unicode_placeholder;
141141+ }
142142+end
143143+144144+module Frame = struct
145145+ type t = {
146146+ x : int option;
147147+ y : int option;
148148+ base_frame : int option;
149149+ edit_frame : int option;
150150+ gap_ms : int option;
151151+ composition : composition option;
152152+ background_color : int32 option;
153153+ }
154154+155155+ let empty =
156156+ {
157157+ x = None;
158158+ y = None;
159159+ base_frame = None;
160160+ edit_frame = None;
161161+ gap_ms = None;
162162+ composition = None;
163163+ background_color = None;
164164+ }
165165+166166+ let make ?x ?y ?base_frame ?edit_frame ?gap_ms ?composition ?background_color
167167+ () =
168168+ { x; y; base_frame; edit_frame; gap_ms; composition; background_color }
169169+end
170170+171171+module Animation = struct
172172+ type state = animation_state
173173+174174+ type t =
175175+ [ `Set_state of state * int option
176176+ | `Set_gap of int * int
177177+ | `Set_current of int ]
178178+179179+ let set_state ?loops state = `Set_state (state, loops)
180180+ let set_gap ~frame ~gap_ms = `Set_gap (frame, gap_ms)
181181+ let set_current_frame frame = `Set_current frame
182182+end
183183+184184+module Compose = struct
185185+ type t = {
186186+ source_frame : int;
187187+ dest_frame : int;
188188+ width : int option;
189189+ height : int option;
190190+ source_x : int option;
191191+ source_y : int option;
192192+ dest_x : int option;
193193+ dest_y : int option;
194194+ composition : composition option;
195195+ }
196196+197197+ let make ~source_frame ~dest_frame ?width ?height ?source_x ?source_y ?dest_x
198198+ ?dest_y ?composition () =
199199+ {
200200+ source_frame;
201201+ dest_frame;
202202+ width;
203203+ height;
204204+ source_x;
205205+ source_y;
206206+ dest_x;
207207+ dest_y;
208208+ composition;
209209+ }
210210+end
211211+212212+module Command = struct
213213+ type action =
214214+ [ `Transmit
215215+ | `Transmit_and_display
216216+ | `Query
217217+ | `Display
218218+ | `Delete
219219+ | `Frame
220220+ | `Animate
221221+ | `Compose ]
222222+223223+ type t = {
224224+ action : action;
225225+ format : format option;
226226+ transmission : transmission option;
227227+ compression : compression option;
228228+ width : int option;
229229+ height : int option;
230230+ size : int option;
231231+ offset : int option;
232232+ quiet : quiet option;
233233+ image_id : int option;
234234+ image_number : int option;
235235+ placement : Placement.t option;
236236+ delete : delete option;
237237+ frame : Frame.t option;
238238+ animation : Animation.t option;
239239+ compose : Compose.t option;
240240+ }
241241+242242+ let make action =
243243+ {
244244+ action;
245245+ format = None;
246246+ transmission = None;
247247+ compression = None;
248248+ width = None;
249249+ height = None;
250250+ size = None;
251251+ offset = None;
252252+ quiet = None;
253253+ image_id = None;
254254+ image_number = None;
255255+ placement = None;
256256+ delete = None;
257257+ frame = None;
258258+ animation = None;
259259+ compose = None;
260260+ }
261261+262262+ let transmit ?image_id ?image_number ?format ?transmission ?compression ?width
263263+ ?height ?size ?offset ?quiet () =
264264+ {
265265+ (make `Transmit) with
266266+ image_id;
267267+ image_number;
268268+ format;
269269+ transmission;
270270+ compression;
271271+ width;
272272+ height;
273273+ size;
274274+ offset;
275275+ quiet;
276276+ }
277277+278278+ let transmit_and_display ?image_id ?image_number ?format ?transmission
279279+ ?compression ?width ?height ?size ?offset ?quiet ?placement () =
280280+ {
281281+ (make `Transmit_and_display) with
282282+ image_id;
283283+ image_number;
284284+ format;
285285+ transmission;
286286+ compression;
287287+ width;
288288+ height;
289289+ size;
290290+ offset;
291291+ quiet;
292292+ placement;
293293+ }
294294+295295+ let query ?format ?transmission ?width ?height ?quiet () =
296296+ { (make `Query) with format; transmission; width; height; quiet }
297297+298298+ let display ?image_id ?image_number ?placement ?quiet () =
299299+ { (make `Display) with image_id; image_number; placement; quiet }
300300+301301+ let delete ?quiet del = { (make `Delete) with quiet; delete = Some del }
302302+303303+ let frame ?image_id ?image_number ?format ?transmission ?compression ?width
304304+ ?height ?quiet ~frame () =
305305+ {
306306+ (make `Frame) with
307307+ image_id;
308308+ image_number;
309309+ format;
310310+ transmission;
311311+ compression;
312312+ width;
313313+ height;
314314+ quiet;
315315+ frame = Some frame;
316316+ }
317317+318318+ let animate ?image_id ?image_number ?quiet anim =
319319+ { (make `Animate) with image_id; image_number; quiet; animation = Some anim }
320320+321321+ let compose ?image_id ?image_number ?quiet comp =
322322+ { (make `Compose) with image_id; image_number; quiet; compose = Some comp }
323323+324324+ (* Serialization helpers *)
325325+ let apc_start = "\027_G"
326326+ let apc_end = "\027\\"
327327+328328+ (* Key-value writer with separator handling *)
329329+ type kv_writer = { mutable first : bool; buf : Buffer.t }
330330+331331+ let kv_writer buf = { first = true; buf }
332332+333333+ let kv w key value =
334334+ if not w.first then Buffer.add_char w.buf ',';
335335+ w.first <- false;
336336+ Buffer.add_char w.buf key;
337337+ Buffer.add_char w.buf '=';
338338+ Buffer.add_string w.buf value
339339+340340+ let kv_int w key value = kv w key (string_of_int value)
341341+ let kv_int32 w key value = kv w key (Int32.to_string value)
342342+ let kv_char w key value = kv w key (String.make 1 value)
343343+344344+ (* Conditional writers using Option.iter *)
345345+ let kv_int_opt w key = Option.iter (kv_int w key)
346346+ let kv_int32_opt w key = Option.iter (kv_int32 w key)
347347+348348+ let kv_int_if w key ~default opt =
349349+ Option.iter (fun v -> if v <> default then kv_int w key v) opt
350350+351351+ let action_char : action -> char = function
352352+ | `Transmit -> 't'
353353+ | `Transmit_and_display -> 'T'
354354+ | `Query -> 'q'
355355+ | `Display -> 'p'
356356+ | `Delete -> 'd'
357357+ | `Frame -> 'f'
358358+ | `Animate -> 'a'
359359+ | `Compose -> 'c'
360360+361361+ let delete_char : delete -> char = function
362362+ | `All_visible -> 'a'
363363+ | `All_visible_and_free -> 'A'
364364+ | `By_id _ -> 'i'
365365+ | `By_id_and_free _ -> 'I'
366366+ | `By_number _ -> 'n'
367367+ | `By_number_and_free _ -> 'N'
368368+ | `At_cursor -> 'c'
369369+ | `At_cursor_and_free -> 'C'
370370+ | `At_cell _ -> 'p'
371371+ | `At_cell_and_free _ -> 'P'
372372+ | `At_cell_z _ -> 'q'
373373+ | `At_cell_z_and_free _ -> 'Q'
374374+ | `By_column _ -> 'x'
375375+ | `By_column_and_free _ -> 'X'
376376+ | `By_row _ -> 'y'
377377+ | `By_row_and_free _ -> 'Y'
378378+ | `By_z_index _ -> 'z'
379379+ | `By_z_index_and_free _ -> 'Z'
380380+ | `By_id_range _ -> 'r'
381381+ | `By_id_range_and_free _ -> 'R'
382382+ | `Frames -> 'f'
383383+ | `Frames_and_free -> 'F'
384384+385385+ let write_placement w (p : Placement.t) =
386386+ kv_int_opt w 'x' p.source_x;
387387+ kv_int_opt w 'y' p.source_y;
388388+ kv_int_opt w 'w' p.source_width;
389389+ kv_int_opt w 'h' p.source_height;
390390+ kv_int_opt w 'X' p.cell_x_offset;
391391+ kv_int_opt w 'Y' p.cell_y_offset;
392392+ kv_int_opt w 'c' p.columns;
393393+ kv_int_opt w 'r' p.rows;
394394+ kv_int_opt w 'z' p.z_index;
395395+ kv_int_opt w 'p' p.placement_id;
396396+ p.cursor |> Option.iter (fun c -> kv_int_if w 'C' ~default:0 (Some (Cursor.to_int c)));
397397+ if p.unicode_placeholder then kv_int w 'U' 1
398398+399399+ let write_delete w (d : delete) =
400400+ kv_char w 'd' (delete_char d);
401401+ match d with
402402+ | `By_id (id, pid) | `By_id_and_free (id, pid) ->
403403+ kv_int w 'i' id;
404404+ kv_int_opt w 'p' pid
405405+ | `By_number (n, pid) | `By_number_and_free (n, pid) ->
406406+ kv_int w 'I' n;
407407+ kv_int_opt w 'p' pid
408408+ | `At_cell (x, y) | `At_cell_and_free (x, y) ->
409409+ kv_int w 'x' x;
410410+ kv_int w 'y' y
411411+ | `At_cell_z (x, y, z) | `At_cell_z_and_free (x, y, z) ->
412412+ kv_int w 'x' x;
413413+ kv_int w 'y' y;
414414+ kv_int w 'z' z
415415+ | `By_column c | `By_column_and_free c -> kv_int w 'x' c
416416+ | `By_row r | `By_row_and_free r -> kv_int w 'y' r
417417+ | `By_z_index z | `By_z_index_and_free z -> kv_int w 'z' z
418418+ | `By_id_range (min_id, max_id) | `By_id_range_and_free (min_id, max_id) ->
419419+ kv_int w 'x' min_id;
420420+ kv_int w 'y' max_id
421421+ | `All_visible | `All_visible_and_free | `At_cursor | `At_cursor_and_free
422422+ | `Frames | `Frames_and_free ->
423423+ ()
424424+425425+ let write_frame w (f : Frame.t) =
426426+ kv_int_opt w 'x' f.x;
427427+ kv_int_opt w 'y' f.y;
428428+ kv_int_opt w 'c' f.base_frame;
429429+ kv_int_opt w 'r' f.edit_frame;
430430+ kv_int_opt w 'z' f.gap_ms;
431431+ f.composition
432432+ |> Option.iter (fun c -> kv_int_if w 'X' ~default:0 (Some (Composition.to_int c)));
433433+ kv_int32_opt w 'Y' f.background_color
434434+435435+ let write_animation w : Animation.t -> unit = function
436436+ | `Set_state (state, loops) ->
437437+ let s = match state with `Stop -> 1 | `Loading -> 2 | `Run -> 3 in
438438+ kv_int w 's' s;
439439+ kv_int_opt w 'v' loops
440440+ | `Set_gap (frame, gap_ms) ->
441441+ kv_int w 'r' frame;
442442+ kv_int w 'z' gap_ms
443443+ | `Set_current frame -> kv_int w 'c' frame
444444+445445+ let write_compose w (c : Compose.t) =
446446+ kv_int w 'r' c.source_frame;
447447+ kv_int w 'c' c.dest_frame;
448448+ kv_int_opt w 'w' c.width;
449449+ kv_int_opt w 'h' c.height;
450450+ kv_int_opt w 'x' c.dest_x;
451451+ kv_int_opt w 'y' c.dest_y;
452452+ kv_int_opt w 'X' c.source_x;
453453+ kv_int_opt w 'Y' c.source_y;
454454+ c.composition
455455+ |> Option.iter (fun comp -> kv_int_if w 'C' ~default:0 (Some (Composition.to_int comp)))
456456+457457+ let write_control_data buf cmd =
458458+ let w = kv_writer buf in
459459+ (* Action *)
460460+ kv_char w 'a' (action_char cmd.action);
461461+ (* Quiet - only if non-default *)
462462+ cmd.quiet |> Option.iter (fun q -> kv_int_if w 'q' ~default:0 (Some (Quiet.to_int q)));
463463+ (* Format *)
464464+ cmd.format |> Option.iter (fun f -> kv_int w 'f' (Format.to_int f));
465465+ (* Transmission - only for transmit/frame actions, always include t=d for compatibility *)
466466+ (match cmd.action with
467467+ | `Transmit | `Transmit_and_display | `Frame ->
468468+ (match cmd.transmission with
469469+ | Some t -> kv_char w 't' (Transmission.to_char t)
470470+ | None -> kv_char w 't' 'd')
471471+ | _ -> ());
472472+ (* Compression *)
473473+ cmd.compression |> Option.iter (fun c -> Compression.to_char c |> Option.iter (kv_char w 'o'));
474474+ (* Dimensions *)
475475+ kv_int_opt w 's' cmd.width;
476476+ kv_int_opt w 'v' cmd.height;
477477+ (* File size/offset *)
478478+ kv_int_opt w 'S' cmd.size;
479479+ kv_int_opt w 'O' cmd.offset;
480480+ (* Image ID/number *)
481481+ kv_int_opt w 'i' cmd.image_id;
482482+ kv_int_opt w 'I' cmd.image_number;
483483+ (* Complex options *)
484484+ cmd.placement |> Option.iter (write_placement w);
485485+ cmd.delete |> Option.iter (write_delete w);
486486+ cmd.frame |> Option.iter (write_frame w);
487487+ cmd.animation |> Option.iter (write_animation w);
488488+ cmd.compose |> Option.iter (write_compose w);
489489+ w
490490+491491+ (* Use large chunk size to avoid chunking - Kitty animation doesn't handle chunks well *)
492492+ let chunk_size = 1024 * 1024 (* 1MB - effectively no chunking *)
493493+494494+ let write buf cmd ~data =
495495+ Buffer.add_string buf apc_start;
496496+ let w = write_control_data buf cmd in
497497+ if String.length data > 0 then begin
498498+ let encoded = Base64.encode_string data in
499499+ let len = String.length encoded in
500500+ if len <= chunk_size then (
501501+ Buffer.add_char buf ';';
502502+ Buffer.add_string buf encoded;
503503+ Buffer.add_string buf apc_end)
504504+ else begin
505505+ (* Multiple chunks *)
506506+ let rec write_chunks pos first =
507507+ if pos < len then begin
508508+ let remaining = len - pos in
509509+ let this_chunk = min chunk_size remaining in
510510+ let is_last = pos + this_chunk >= len in
511511+ if first then (
512512+ kv_int w 'm' 1;
513513+ Buffer.add_char buf ';';
514514+ Buffer.add_substring buf encoded pos this_chunk;
515515+ Buffer.add_string buf apc_end)
516516+ else (
517517+ Buffer.add_string buf apc_start;
518518+ Buffer.add_string buf (if is_last then "m=0" else "m=1");
519519+ Buffer.add_char buf ';';
520520+ Buffer.add_substring buf encoded pos this_chunk;
521521+ Buffer.add_string buf apc_end);
522522+ write_chunks (pos + this_chunk) false
523523+ end
524524+ in
525525+ write_chunks 0 true
526526+ end
527527+ end
528528+ else Buffer.add_string buf apc_end
529529+530530+ let to_string cmd ~data =
531531+ let buf = Buffer.create 1024 in
532532+ write buf cmd ~data;
533533+ Buffer.contents buf
534534+end
535535+536536+module Response = struct
537537+ type t = {
538538+ message : string;
539539+ image_id : int option;
540540+ image_number : int option;
541541+ placement_id : int option;
542542+ }
543543+544544+ let is_ok t = t.message = "OK"
545545+ let message t = t.message
546546+547547+ let error_code t =
548548+ if is_ok t then None
549549+ else String.index_opt t.message ':' |> Option.fold ~none:(Some t.message) ~some:(fun i -> Some (String.sub t.message 0 i))
550550+551551+ let image_id t = t.image_id
552552+ let image_number t = t.image_number
553553+ let placement_id t = t.placement_id
554554+555555+ let parse s =
556556+ let ( let* ) = Option.bind in
557557+ let esc = '\027' in
558558+ let len = String.length s in
559559+ let* () = if len >= 5 && s.[0] = esc && s.[1] = '_' && s.[2] = 'G' then Some () else None in
560560+ let* semi_pos = String.index_from_opt s 3 ';' in
561561+ let rec find_end pos =
562562+ if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then Some pos
563563+ else if pos + 1 < len then find_end (pos + 1)
564564+ else None
565565+ in
566566+ let* end_pos = find_end (semi_pos + 1) in
567567+ let keys_str = String.sub s 3 (semi_pos - 3) in
568568+ let message = String.sub s (semi_pos + 1) (end_pos - semi_pos - 1) in
569569+ let parse_kv part =
570570+ if String.length part >= 3 && part.[1] = '=' then
571571+ Some (part.[0], String.sub part 2 (String.length part - 2))
572572+ else None
573573+ in
574574+ let keys = String.split_on_char ',' keys_str |> List.filter_map parse_kv in
575575+ let find_int key = List.assoc_opt key keys |> Fun.flip Option.bind int_of_string_opt in
576576+ Some
577577+ {
578578+ message;
579579+ image_id = find_int 'i';
580580+ image_number = find_int 'I';
581581+ placement_id = find_int 'p';
582582+ }
583583+end
584584+585585+module Unicode_placeholder = struct
586586+ let placeholder_char = Uchar.of_int 0x10EEEE
587587+588588+ let diacritics =
589589+ [|
590590+ 0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F;
591591+ 0x0346; 0x034A; 0x034B; 0x034C; 0x0350; 0x0351; 0x0352; 0x0357;
592592+ 0x035B; 0x0363; 0x0364; 0x0365; 0x0366; 0x0367; 0x0368; 0x0369;
593593+ 0x036A; 0x036B; 0x036C; 0x036D; 0x036E; 0x036F; 0x0483; 0x0484;
594594+ 0x0485; 0x0486; 0x0487; 0x0592; 0x0593; 0x0594; 0x0595; 0x0597;
595595+ 0x0598; 0x0599; 0x059C; 0x059D; 0x059E; 0x059F; 0x05A0; 0x05A1;
596596+ 0x05A8; 0x05A9; 0x05AB; 0x05AC; 0x05AF; 0x05C4; 0x0610; 0x0611;
597597+ 0x0612; 0x0613; 0x0614; 0x0615; 0x0616; 0x0617; 0x0657; 0x0658;
598598+ 0x0659; 0x065A; 0x065B; 0x065D; 0x065E; 0x06D6; 0x06D7; 0x06D8;
599599+ 0x06D9; 0x06DA; 0x06DB; 0x06DC; 0x06DF; 0x06E0; 0x06E1; 0x06E2;
600600+ 0x06E4; 0x06E7; 0x06E8; 0x06EB; 0x06EC; 0x0730; 0x0732; 0x0733;
601601+ 0x0735; 0x0736; 0x073A; 0x073D; 0x073F; 0x0740; 0x0741; 0x0743;
602602+ 0x0745; 0x0747; 0x0749; 0x074A; 0x07EB; 0x07EC; 0x07ED; 0x07EE;
603603+ 0x07EF; 0x07F0; 0x07F1; 0x07F3; 0x0816; 0x0817; 0x0818; 0x0819;
604604+ 0x081B; 0x081C; 0x081D; 0x081E; 0x081F; 0x0820; 0x0821; 0x0822;
605605+ 0x0823; 0x0825; 0x0826; 0x0827; 0x0829; 0x082A; 0x082B; 0x082C;
606606+ 0x082D; 0x0951; 0x0953; 0x0954; 0x0F82; 0x0F83; 0x0F86; 0x0F87;
607607+ 0x135D; 0x135E; 0x135F; 0x17DD; 0x193A; 0x1A17; 0x1A75; 0x1A76;
608608+ 0x1A77; 0x1A78; 0x1A79; 0x1A7A; 0x1A7B; 0x1A7C; 0x1B6B; 0x1B6D;
609609+ 0x1B6E; 0x1B6F; 0x1B70; 0x1B71; 0x1B72; 0x1B73; 0x1CD0; 0x1CD1;
610610+ 0x1CD2; 0x1CDA; 0x1CDB; 0x1CE0; 0x1DC0; 0x1DC1; 0x1DC3; 0x1DC4;
611611+ 0x1DC5; 0x1DC6; 0x1DC7; 0x1DC8; 0x1DC9; 0x1DCB; 0x1DCC; 0x1DD1;
612612+ 0x1DD2; 0x1DD3; 0x1DD4; 0x1DD5; 0x1DD6; 0x1DD7; 0x1DD8; 0x1DD9;
613613+ 0x1DDA; 0x1DDB; 0x1DDC; 0x1DDD; 0x1DDE; 0x1DDF; 0x1DE0; 0x1DE1;
614614+ 0x1DE2; 0x1DE3; 0x1DE4; 0x1DE5; 0x1DE6; 0x1DFE; 0x20D0; 0x20D1;
615615+ 0x20D4; 0x20D5; 0x20D6; 0x20D7; 0x20DB; 0x20DC; 0x20E1; 0x20E7;
616616+ 0x20E9; 0x20F0; 0xA66F; 0xA67C; 0xA67D; 0xA6F0; 0xA6F1; 0xA8E0;
617617+ 0xA8E1; 0xA8E2; 0xA8E3; 0xA8E4; 0xA8E5; 0xA8E6; 0xA8E7; 0xA8E8;
618618+ 0xA8E9; 0xA8EA; 0xA8EB; 0xA8EC; 0xA8ED; 0xA8EE; 0xA8EF; 0xA8F0;
619619+ 0xA8F1; 0xAAB0; 0xAAB2; 0xAAB3; 0xAAB7; 0xAAB8; 0xAABE; 0xAABF;
620620+ 0xAAC1; 0xFE20; 0xFE21; 0xFE22; 0xFE23; 0xFE24; 0xFE25; 0xFE26;
621621+ 0x10A0F; 0x10A38; 0x1D185; 0x1D186; 0x1D187; 0x1D188; 0x1D189;
622622+ 0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD; 0x1D242; 0x1D243; 0x1D244;
623623+ |]
624624+625625+ let diacritic n =
626626+ Uchar.of_int diacritics.(n mod Array.length diacritics)
627627+628628+ let row_diacritic = diacritic
629629+ let column_diacritic = diacritic
630630+ let id_high_byte_diacritic = diacritic
631631+632632+ let add_uchar buf u =
633633+ let code = Uchar.to_int u in
634634+ let put = Buffer.add_char buf in
635635+ if code < 0x80 then put (Char.chr code)
636636+ else if code < 0x800 then (
637637+ put (Char.chr (0xC0 lor (code lsr 6)));
638638+ put (Char.chr (0x80 lor (code land 0x3F))))
639639+ else if code < 0x10000 then (
640640+ put (Char.chr (0xE0 lor (code lsr 12)));
641641+ put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
642642+ put (Char.chr (0x80 lor (code land 0x3F))))
643643+ else (
644644+ put (Char.chr (0xF0 lor (code lsr 18)));
645645+ put (Char.chr (0x80 lor ((code lsr 12) land 0x3F)));
646646+ put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
647647+ put (Char.chr (0x80 lor (code land 0x3F))))
648648+649649+ let write buf ~image_id ?placement_id ~rows ~cols () =
650650+ (* Set foreground color *)
651651+ Printf.bprintf buf "\027[38;2;%d;%d;%dm"
652652+ ((image_id lsr 16) land 0xFF)
653653+ ((image_id lsr 8) land 0xFF)
654654+ (image_id land 0xFF);
655655+ (* Optional placement ID in underline color *)
656656+ placement_id
657657+ |> Option.iter (fun pid ->
658658+ Printf.bprintf buf "\027[58;2;%d;%d;%dm"
659659+ ((pid lsr 16) land 0xFF)
660660+ ((pid lsr 8) land 0xFF)
661661+ (pid land 0xFF));
662662+ (* High byte diacritic *)
663663+ let high_byte = (image_id lsr 24) land 0xFF in
664664+ let high_diac = if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None in
665665+ (* Write grid *)
666666+ for row = 0 to rows - 1 do
667667+ for col = 0 to cols - 1 do
668668+ add_uchar buf placeholder_char;
669669+ add_uchar buf (row_diacritic row);
670670+ add_uchar buf (column_diacritic col);
671671+ high_diac |> Option.iter (add_uchar buf)
672672+ done;
673673+ if row < rows - 1 then Buffer.add_string buf "\n\r"
674674+ done;
675675+ (* Reset colors *)
676676+ Buffer.add_string buf "\027[39m";
677677+ if Option.is_some placement_id then Buffer.add_string buf "\027[59m"
678678+end
679679+680680+module Detect = struct
681681+ let make_query () =
682682+ let cmd = Command.query ~format:`Rgb24 ~transmission:`Direct ~width:1 ~height:1 () in
683683+ Command.to_string cmd ~data:"\x00\x00\x00"
684684+685685+ let supports_graphics response ~da1_received =
686686+ response |> Option.map Response.is_ok |> Option.value ~default:(not da1_received)
687687+end
+402
lib/kitty_graphics.mli
···11+(** Kitty Terminal Graphics Protocol
22+33+ This library implements the Kitty terminal graphics protocol, allowing
44+ OCaml programs to display images in terminals that support the protocol
55+ (Kitty, WezTerm, Konsole, Ghostty, etc.).
66+77+ The protocol uses APC (Application Programming Command) escape sequences
88+ to transmit and display pixel graphics. Images can be transmitted as raw
99+ RGB/RGBA data or PNG, and displayed at specific positions with various
1010+ placement options.
1111+1212+ {2 Basic Usage}
1313+1414+ {[
1515+ (* Display a PNG image *)
1616+ let png_data = read_file "image.png" in
1717+ let cmd = Kitty_graphics.Command.transmit_and_display ~format:`Png () in
1818+ let buf = Buffer.create 1024 in
1919+ Kitty_graphics.Command.write buf cmd ~data:png_data;
2020+ print_string (Buffer.contents buf)
2121+ ]}
2222+2323+ {2 Protocol Reference}
2424+2525+ See {{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics Protocol}
2626+ for the full specification. *)
2727+2828+(** {1 Polymorphic Variant Types} *)
2929+3030+type format = [ `Rgba32 | `Rgb24 | `Png ]
3131+(** Image data formats. [`Rgba32] is 32-bit RGBA (4 bytes per pixel),
3232+ [`Rgb24] is 24-bit RGB (3 bytes per pixel), [`Png] is PNG encoded data. *)
3333+3434+type transmission = [ `Direct | `File | `Tempfile ]
3535+(** Transmission methods. [`Direct] sends data inline, [`File] reads from a path,
3636+ [`Tempfile] reads from a temp file that the terminal deletes after reading. *)
3737+3838+type compression = [ `None | `Zlib ]
3939+(** Compression options. [`None] for raw data, [`Zlib] for RFC 1950 compression. *)
4040+4141+type quiet = [ `Noisy | `Errors_only | `Silent ]
4242+(** Response suppression. [`Noisy] sends all responses (default),
4343+ [`Errors_only] suppresses OK responses, [`Silent] suppresses all. *)
4444+4545+type cursor = [ `Move | `Static ]
4646+(** Cursor movement after displaying. [`Move] advances cursor (default),
4747+ [`Static] keeps cursor in place. *)
4848+4949+type composition = [ `Alpha_blend | `Overwrite ]
5050+(** Composition modes. [`Alpha_blend] for full blending (default),
5151+ [`Overwrite] for simple pixel replacement. *)
5252+5353+type delete =
5454+ [ `All_visible
5555+ | `All_visible_and_free
5656+ | `By_id of int * int option
5757+ | `By_id_and_free of int * int option
5858+ | `By_number of int * int option
5959+ | `By_number_and_free of int * int option
6060+ | `At_cursor
6161+ | `At_cursor_and_free
6262+ | `At_cell of int * int
6363+ | `At_cell_and_free of int * int
6464+ | `At_cell_z of int * int * int
6565+ | `At_cell_z_and_free of int * int * int
6666+ | `By_column of int
6767+ | `By_column_and_free of int
6868+ | `By_row of int
6969+ | `By_row_and_free of int
7070+ | `By_z_index of int
7171+ | `By_z_index_and_free of int
7272+ | `By_id_range of int * int
7373+ | `By_id_range_and_free of int * int
7474+ | `Frames
7575+ | `Frames_and_free ]
7676+(** Delete target specification. Each variant has two forms: one that only
7777+ removes placements (e.g., [`All_visible]) and one that also frees the
7878+ image data (e.g., [`All_visible_and_free]). Tuple variants contain
7979+ (image_id, optional_placement_id) or (x, y) coordinates. *)
8080+8181+type animation_state = [ `Stop | `Loading | `Run ]
8282+(** Animation playback state. [`Stop] halts animation, [`Loading] runs but
8383+ waits for new frames at end, [`Run] runs normally and loops. *)
8484+8585+(** {1 Type Modules} *)
8686+8787+module Format : sig
8888+ type t = format
8989+9090+ val to_int : t -> int
9191+ (** Convert to protocol integer value (32, 24, or 100). *)
9292+end
9393+9494+module Transmission : sig
9595+ type t = transmission
9696+9797+ val to_char : t -> char
9898+ (** Convert to protocol character ('d', 'f', or 't'). *)
9999+end
100100+101101+module Compression : sig
102102+ type t = compression
103103+104104+ val to_char : t -> char option
105105+ (** Convert to protocol character ([None] or [Some 'z']). *)
106106+end
107107+108108+module Quiet : sig
109109+ type t = quiet
110110+111111+ val to_int : t -> int
112112+ (** Convert to protocol integer (0, 1, or 2). *)
113113+end
114114+115115+module Cursor : sig
116116+ type t = cursor
117117+118118+ val to_int : t -> int
119119+ (** Convert to protocol integer (0 or 1). *)
120120+end
121121+122122+module Composition : sig
123123+ type t = composition
124124+125125+ val to_int : t -> int
126126+ (** Convert to protocol integer (0 or 1). *)
127127+end
128128+129129+module Delete : sig
130130+ type t = delete
131131+end
132132+133133+(** {1 Placement Options} *)
134134+135135+module Placement : sig
136136+ type t
137137+ (** Placement configuration. *)
138138+139139+ val make :
140140+ ?source_x:int ->
141141+ ?source_y:int ->
142142+ ?source_width:int ->
143143+ ?source_height:int ->
144144+ ?cell_x_offset:int ->
145145+ ?cell_y_offset:int ->
146146+ ?columns:int ->
147147+ ?rows:int ->
148148+ ?z_index:int ->
149149+ ?placement_id:int ->
150150+ ?cursor:cursor ->
151151+ ?unicode_placeholder:bool ->
152152+ unit ->
153153+ t
154154+ (** Create a placement configuration.
155155+156156+ @param source_x Left edge of source rectangle in pixels (default 0)
157157+ @param source_y Top edge of source rectangle in pixels (default 0)
158158+ @param source_width Width of source rectangle (default: full width)
159159+ @param source_height Height of source rectangle (default: full height)
160160+ @param cell_x_offset X offset within the first cell in pixels
161161+ @param cell_y_offset Y offset within the first cell in pixels
162162+ @param columns Number of columns to display over (scales image)
163163+ @param rows Number of rows to display over (scales image)
164164+ @param z_index Stacking order (negative = under text)
165165+ @param placement_id Unique ID for this placement
166166+ @param cursor Cursor movement policy after display
167167+ @param unicode_placeholder Create virtual placement for Unicode mode *)
168168+169169+ val empty : t
170170+ (** Empty placement with all defaults. *)
171171+end
172172+173173+(** {1 Animation} *)
174174+175175+module Frame : sig
176176+ type t
177177+ (** Animation frame configuration. *)
178178+179179+ val make :
180180+ ?x:int ->
181181+ ?y:int ->
182182+ ?base_frame:int ->
183183+ ?edit_frame:int ->
184184+ ?gap_ms:int ->
185185+ ?composition:composition ->
186186+ ?background_color:int32 ->
187187+ unit ->
188188+ t
189189+ (** Create a frame specification.
190190+191191+ @param x Left edge where frame data is placed (pixels)
192192+ @param y Top edge where frame data is placed (pixels)
193193+ @param base_frame 1-based frame number to use as background canvas
194194+ @param edit_frame 1-based frame number to edit (0 = new frame)
195195+ @param gap_ms Delay before next frame in milliseconds
196196+ @param composition How to blend pixels onto the canvas
197197+ @param background_color 32-bit RGBA background when no base frame *)
198198+199199+ val empty : t
200200+ (** Empty frame spec with defaults. *)
201201+end
202202+203203+module Animation : sig
204204+ type state = animation_state
205205+206206+ type t =
207207+ [ `Set_state of state * int option
208208+ | `Set_gap of int * int
209209+ | `Set_current of int ]
210210+ (** Animation control operations. *)
211211+212212+ val set_state : ?loops:int -> state -> t
213213+ (** Set animation state.
214214+ @param loops Number of loops: 0 = ignored, 1 = infinite, n = n-1 loops *)
215215+216216+ val set_gap : frame:int -> gap_ms:int -> t
217217+ (** Set the gap (delay) for a specific frame.
218218+ @param frame 1-based frame number
219219+ @param gap_ms Delay in milliseconds (negative = gapless) *)
220220+221221+ val set_current_frame : int -> t
222222+ (** Make a specific frame (1-based) the current displayed frame. *)
223223+end
224224+225225+module Compose : sig
226226+ type t
227227+ (** Composition operation. *)
228228+229229+ val make :
230230+ source_frame:int ->
231231+ dest_frame:int ->
232232+ ?width:int ->
233233+ ?height:int ->
234234+ ?source_x:int ->
235235+ ?source_y:int ->
236236+ ?dest_x:int ->
237237+ ?dest_y:int ->
238238+ ?composition:composition ->
239239+ unit ->
240240+ t
241241+ (** Compose a rectangle from one frame onto another. *)
242242+end
243243+244244+(** {1 Commands} *)
245245+246246+module Command : sig
247247+ type t
248248+ (** A graphics protocol command. *)
249249+250250+ (** {2 Image Transmission} *)
251251+252252+ val transmit :
253253+ ?image_id:int ->
254254+ ?image_number:int ->
255255+ ?format:format ->
256256+ ?transmission:transmission ->
257257+ ?compression:compression ->
258258+ ?width:int ->
259259+ ?height:int ->
260260+ ?size:int ->
261261+ ?offset:int ->
262262+ ?quiet:quiet ->
263263+ unit ->
264264+ t
265265+ (** Transmit image data without displaying. *)
266266+267267+ val transmit_and_display :
268268+ ?image_id:int ->
269269+ ?image_number:int ->
270270+ ?format:format ->
271271+ ?transmission:transmission ->
272272+ ?compression:compression ->
273273+ ?width:int ->
274274+ ?height:int ->
275275+ ?size:int ->
276276+ ?offset:int ->
277277+ ?quiet:quiet ->
278278+ ?placement:Placement.t ->
279279+ unit ->
280280+ t
281281+ (** Transmit image data and display it immediately. *)
282282+283283+ val query :
284284+ ?format:format ->
285285+ ?transmission:transmission ->
286286+ ?width:int ->
287287+ ?height:int ->
288288+ ?quiet:quiet ->
289289+ unit ->
290290+ t
291291+ (** Query terminal support without storing the image. *)
292292+293293+ (** {2 Display} *)
294294+295295+ val display :
296296+ ?image_id:int ->
297297+ ?image_number:int ->
298298+ ?placement:Placement.t ->
299299+ ?quiet:quiet ->
300300+ unit ->
301301+ t
302302+ (** Display a previously transmitted image. *)
303303+304304+ (** {2 Deletion} *)
305305+306306+ val delete : ?quiet:quiet -> delete -> t
307307+ (** Delete images or placements. *)
308308+309309+ (** {2 Animation} *)
310310+311311+ val frame :
312312+ ?image_id:int ->
313313+ ?image_number:int ->
314314+ ?format:format ->
315315+ ?transmission:transmission ->
316316+ ?compression:compression ->
317317+ ?width:int ->
318318+ ?height:int ->
319319+ ?quiet:quiet ->
320320+ frame:Frame.t ->
321321+ unit ->
322322+ t
323323+ (** Transmit animation frame data. *)
324324+325325+ val animate : ?image_id:int -> ?image_number:int -> ?quiet:quiet -> Animation.t -> t
326326+ (** Control animation playback. *)
327327+328328+ val compose : ?image_id:int -> ?image_number:int -> ?quiet:quiet -> Compose.t -> t
329329+ (** Compose animation frames. *)
330330+331331+ (** {2 Output} *)
332332+333333+ val write : Buffer.t -> t -> data:string -> unit
334334+ (** Write the command to a buffer. *)
335335+336336+ val to_string : t -> data:string -> string
337337+ (** Convert command to a string. *)
338338+end
339339+340340+(** {1 Response Parsing} *)
341341+342342+module Response : sig
343343+ type t
344344+ (** A parsed terminal response. *)
345345+346346+ val parse : string -> t option
347347+ (** Parse a response from terminal output. *)
348348+349349+ val is_ok : t -> bool
350350+ (** Check if the response indicates success. *)
351351+352352+ val message : t -> string
353353+ (** Get the response message. *)
354354+355355+ val error_code : t -> string option
356356+ (** Extract the error code if this is an error response. *)
357357+358358+ val image_id : t -> int option
359359+ (** Get the image ID from the response. *)
360360+361361+ val image_number : t -> int option
362362+ (** Get the image number from the response. *)
363363+364364+ val placement_id : t -> int option
365365+ (** Get the placement ID from the response. *)
366366+end
367367+368368+(** {1 Unicode Placeholders} *)
369369+370370+module Unicode_placeholder : sig
371371+ val placeholder_char : Uchar.t
372372+ (** The Unicode placeholder character U+10EEEE. *)
373373+374374+ val write :
375375+ Buffer.t ->
376376+ image_id:int ->
377377+ ?placement_id:int ->
378378+ rows:int ->
379379+ cols:int ->
380380+ unit ->
381381+ unit
382382+ (** Write placeholder characters to a buffer. *)
383383+384384+ val row_diacritic : int -> Uchar.t
385385+ (** Get the combining diacritic for a row number (0-based). *)
386386+387387+ val column_diacritic : int -> Uchar.t
388388+ (** Get the combining diacritic for a column number (0-based). *)
389389+390390+ val id_high_byte_diacritic : int -> Uchar.t
391391+ (** Get the diacritic for the high byte of a 32-bit image ID. *)
392392+end
393393+394394+(** {1 Terminal Detection} *)
395395+396396+module Detect : sig
397397+ val make_query : unit -> string
398398+ (** Generate a query command to test graphics support. *)
399399+400400+ val supports_graphics : Response.t option -> da1_received:bool -> bool
401401+ (** Determine if graphics are supported based on query results. *)
402402+end