···
1
+
(* Kitty Terminal Graphics Protocol - Implementation *)
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 ]
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
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
25
+
| `By_column_and_free 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
33
+
| `Frames_and_free ]
35
+
type animation_state = [ `Stop | `Loading | `Run ]
37
+
(* Modules re-export the types with conversion functions *)
38
+
module Format = struct
41
+
let to_int : t -> int = function
47
+
module Transmission = struct
48
+
type t = transmission
50
+
let to_char : t -> char = function
56
+
module Compression = struct
57
+
type t = compression
59
+
let to_char : t -> char option = function
64
+
module Quiet = struct
67
+
let to_int : t -> int = function
73
+
module Cursor = struct
76
+
let to_int : t -> int = function
81
+
module Composition = struct
82
+
type t = composition
84
+
let to_int : t -> int = function
89
+
module Delete = struct
93
+
module Placement = struct
95
+
source_x : int option;
96
+
source_y : int option;
97
+
source_width : int option;
98
+
source_height : int option;
99
+
cell_x_offset : int option;
100
+
cell_y_offset : int option;
101
+
columns : int option;
103
+
z_index : int option;
104
+
placement_id : int option;
105
+
cursor : cursor option;
106
+
unicode_placeholder : bool;
113
+
source_width = None;
114
+
source_height = None;
115
+
cell_x_offset = None;
116
+
cell_y_offset = None;
120
+
placement_id = None;
122
+
unicode_placeholder = false;
125
+
let make ?source_x ?source_y ?source_width ?source_height ?cell_x_offset
126
+
?cell_y_offset ?columns ?rows ?z_index ?placement_id ?cursor
127
+
?(unicode_placeholder = false) () =
140
+
unicode_placeholder;
144
+
module Frame = struct
148
+
base_frame : int option;
149
+
edit_frame : int option;
150
+
gap_ms : int option;
151
+
composition : composition option;
152
+
background_color : int32 option;
162
+
composition = None;
163
+
background_color = None;
166
+
let make ?x ?y ?base_frame ?edit_frame ?gap_ms ?composition ?background_color
168
+
{ x; y; base_frame; edit_frame; gap_ms; composition; background_color }
171
+
module Animation = struct
172
+
type state = animation_state
175
+
[ `Set_state of state * int option
176
+
| `Set_gap of int * int
177
+
| `Set_current of int ]
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
184
+
module Compose = struct
186
+
source_frame : int;
188
+
width : int option;
189
+
height : int option;
190
+
source_x : int option;
191
+
source_y : int option;
192
+
dest_x : int option;
193
+
dest_y : int option;
194
+
composition : composition option;
197
+
let make ~source_frame ~dest_frame ?width ?height ?source_x ?source_y ?dest_x
198
+
?dest_y ?composition () =
212
+
module Command = struct
215
+
| `Transmit_and_display
225
+
format : format option;
226
+
transmission : transmission option;
227
+
compression : compression option;
228
+
width : int option;
229
+
height : int option;
231
+
offset : int option;
232
+
quiet : quiet option;
233
+
image_id : int option;
234
+
image_number : int option;
235
+
placement : Placement.t option;
236
+
delete : delete option;
237
+
frame : Frame.t option;
238
+
animation : Animation.t option;
239
+
compose : Compose.t option;
246
+
transmission = None;
247
+
compression = None;
254
+
image_number = None;
262
+
let transmit ?image_id ?image_number ?format ?transmission ?compression ?width
263
+
?height ?size ?offset ?quiet () =
265
+
(make `Transmit) with
278
+
let transmit_and_display ?image_id ?image_number ?format ?transmission
279
+
?compression ?width ?height ?size ?offset ?quiet ?placement () =
281
+
(make `Transmit_and_display) with
295
+
let query ?format ?transmission ?width ?height ?quiet () =
296
+
{ (make `Query) with format; transmission; width; height; quiet }
298
+
let display ?image_id ?image_number ?placement ?quiet () =
299
+
{ (make `Display) with image_id; image_number; placement; quiet }
301
+
let delete ?quiet del = { (make `Delete) with quiet; delete = Some del }
303
+
let frame ?image_id ?image_number ?format ?transmission ?compression ?width
304
+
?height ?quiet ~frame () =
315
+
frame = Some frame;
318
+
let animate ?image_id ?image_number ?quiet anim =
319
+
{ (make `Animate) with image_id; image_number; quiet; animation = Some anim }
321
+
let compose ?image_id ?image_number ?quiet comp =
322
+
{ (make `Compose) with image_id; image_number; quiet; compose = Some comp }
324
+
(* Serialization helpers *)
325
+
let apc_start = "\027_G"
326
+
let apc_end = "\027\\"
328
+
(* Key-value writer with separator handling *)
329
+
type kv_writer = { mutable first : bool; buf : Buffer.t }
331
+
let kv_writer buf = { first = true; buf }
333
+
let kv w key value =
334
+
if not w.first then Buffer.add_char w.buf ',';
336
+
Buffer.add_char w.buf key;
337
+
Buffer.add_char w.buf '=';
338
+
Buffer.add_string w.buf value
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)
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)
348
+
let kv_int_if w key ~default opt =
349
+
Option.iter (fun v -> if v <> default then kv_int w key v) opt
351
+
let action_char : action -> char = function
353
+
| `Transmit_and_display -> 'T'
361
+
let delete_char : delete -> char = function
362
+
| `All_visible -> 'a'
363
+
| `All_visible_and_free -> 'A'
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'
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'
383
+
| `Frames_and_free -> 'F'
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
399
+
let write_delete w (d : delete) =
400
+
kv_char w 'd' (delete_char d);
402
+
| `By_id (id, pid) | `By_id_and_free (id, pid) ->
404
+
kv_int_opt w 'p' pid
405
+
| `By_number (n, pid) | `By_number_and_free (n, pid) ->
407
+
kv_int_opt w 'p' pid
408
+
| `At_cell (x, y) | `At_cell_and_free (x, y) ->
411
+
| `At_cell_z (x, y, z) | `At_cell_z_and_free (x, y, 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 ->
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;
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
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
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
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;
455
+
|> Option.iter (fun comp -> kv_int_if w 'C' ~default:0 (Some (Composition.to_int comp)))
457
+
let write_control_data buf cmd =
458
+
let w = kv_writer buf in
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)));
464
+
cmd.format |> Option.iter (fun f -> kv_int w 'f' (Format.to_int f));
465
+
(* Transmission - only for transmit/frame actions, always include t=d for compatibility *)
466
+
(match cmd.action with
467
+
| `Transmit | `Transmit_and_display | `Frame ->
468
+
(match cmd.transmission with
469
+
| Some t -> kv_char w 't' (Transmission.to_char t)
470
+
| None -> kv_char w 't' 'd')
473
+
cmd.compression |> Option.iter (fun c -> Compression.to_char c |> Option.iter (kv_char w 'o'));
475
+
kv_int_opt w 's' cmd.width;
476
+
kv_int_opt w 'v' cmd.height;
477
+
(* File size/offset *)
478
+
kv_int_opt w 'S' cmd.size;
479
+
kv_int_opt w 'O' cmd.offset;
480
+
(* Image ID/number *)
481
+
kv_int_opt w 'i' cmd.image_id;
482
+
kv_int_opt w 'I' cmd.image_number;
483
+
(* Complex options *)
484
+
cmd.placement |> Option.iter (write_placement w);
485
+
cmd.delete |> Option.iter (write_delete w);
486
+
cmd.frame |> Option.iter (write_frame w);
487
+
cmd.animation |> Option.iter (write_animation w);
488
+
cmd.compose |> Option.iter (write_compose w);
491
+
(* Use large chunk size to avoid chunking - Kitty animation doesn't handle chunks well *)
492
+
let chunk_size = 1024 * 1024 (* 1MB - effectively no chunking *)
494
+
let write buf cmd ~data =
495
+
Buffer.add_string buf apc_start;
496
+
let w = write_control_data buf cmd in
497
+
if String.length data > 0 then begin
498
+
let encoded = Base64.encode_string data in
499
+
let len = String.length encoded in
500
+
if len <= chunk_size then (
501
+
Buffer.add_char buf ';';
502
+
Buffer.add_string buf encoded;
503
+
Buffer.add_string buf apc_end)
505
+
(* Multiple chunks *)
506
+
let rec write_chunks pos first =
507
+
if pos < len then begin
508
+
let remaining = len - pos in
509
+
let this_chunk = min chunk_size remaining in
510
+
let is_last = pos + this_chunk >= len in
513
+
Buffer.add_char buf ';';
514
+
Buffer.add_substring buf encoded pos this_chunk;
515
+
Buffer.add_string buf apc_end)
517
+
Buffer.add_string buf apc_start;
518
+
Buffer.add_string buf (if is_last then "m=0" else "m=1");
519
+
Buffer.add_char buf ';';
520
+
Buffer.add_substring buf encoded pos this_chunk;
521
+
Buffer.add_string buf apc_end);
522
+
write_chunks (pos + this_chunk) false
525
+
write_chunks 0 true
528
+
else Buffer.add_string buf apc_end
530
+
let to_string cmd ~data =
531
+
let buf = Buffer.create 1024 in
532
+
write buf cmd ~data;
533
+
Buffer.contents buf
536
+
module Response = struct
539
+
image_id : int option;
540
+
image_number : int option;
541
+
placement_id : int option;
544
+
let is_ok t = t.message = "OK"
545
+
let message t = t.message
548
+
if is_ok t then None
549
+
else String.index_opt t.message ':' |> Option.fold ~none:(Some t.message) ~some:(fun i -> Some (String.sub t.message 0 i))
551
+
let image_id t = t.image_id
552
+
let image_number t = t.image_number
553
+
let placement_id t = t.placement_id
556
+
let ( let* ) = Option.bind in
557
+
let esc = '\027' in
558
+
let len = String.length s in
559
+
let* () = if len >= 5 && s.[0] = esc && s.[1] = '_' && s.[2] = 'G' then Some () else None in
560
+
let* semi_pos = String.index_from_opt s 3 ';' in
561
+
let rec find_end pos =
562
+
if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then Some pos
563
+
else if pos + 1 < len then find_end (pos + 1)
566
+
let* end_pos = find_end (semi_pos + 1) in
567
+
let keys_str = String.sub s 3 (semi_pos - 3) in
568
+
let message = String.sub s (semi_pos + 1) (end_pos - semi_pos - 1) in
569
+
let parse_kv part =
570
+
if String.length part >= 3 && part.[1] = '=' then
571
+
Some (part.[0], String.sub part 2 (String.length part - 2))
574
+
let keys = String.split_on_char ',' keys_str |> List.filter_map parse_kv in
575
+
let find_int key = List.assoc_opt key keys |> Fun.flip Option.bind int_of_string_opt in
579
+
image_id = find_int 'i';
580
+
image_number = find_int 'I';
581
+
placement_id = find_int 'p';
585
+
module Unicode_placeholder = struct
586
+
let placeholder_char = Uchar.of_int 0x10EEEE
590
+
0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F;
591
+
0x0346; 0x034A; 0x034B; 0x034C; 0x0350; 0x0351; 0x0352; 0x0357;
592
+
0x035B; 0x0363; 0x0364; 0x0365; 0x0366; 0x0367; 0x0368; 0x0369;
593
+
0x036A; 0x036B; 0x036C; 0x036D; 0x036E; 0x036F; 0x0483; 0x0484;
594
+
0x0485; 0x0486; 0x0487; 0x0592; 0x0593; 0x0594; 0x0595; 0x0597;
595
+
0x0598; 0x0599; 0x059C; 0x059D; 0x059E; 0x059F; 0x05A0; 0x05A1;
596
+
0x05A8; 0x05A9; 0x05AB; 0x05AC; 0x05AF; 0x05C4; 0x0610; 0x0611;
597
+
0x0612; 0x0613; 0x0614; 0x0615; 0x0616; 0x0617; 0x0657; 0x0658;
598
+
0x0659; 0x065A; 0x065B; 0x065D; 0x065E; 0x06D6; 0x06D7; 0x06D8;
599
+
0x06D9; 0x06DA; 0x06DB; 0x06DC; 0x06DF; 0x06E0; 0x06E1; 0x06E2;
600
+
0x06E4; 0x06E7; 0x06E8; 0x06EB; 0x06EC; 0x0730; 0x0732; 0x0733;
601
+
0x0735; 0x0736; 0x073A; 0x073D; 0x073F; 0x0740; 0x0741; 0x0743;
602
+
0x0745; 0x0747; 0x0749; 0x074A; 0x07EB; 0x07EC; 0x07ED; 0x07EE;
603
+
0x07EF; 0x07F0; 0x07F1; 0x07F3; 0x0816; 0x0817; 0x0818; 0x0819;
604
+
0x081B; 0x081C; 0x081D; 0x081E; 0x081F; 0x0820; 0x0821; 0x0822;
605
+
0x0823; 0x0825; 0x0826; 0x0827; 0x0829; 0x082A; 0x082B; 0x082C;
606
+
0x082D; 0x0951; 0x0953; 0x0954; 0x0F82; 0x0F83; 0x0F86; 0x0F87;
607
+
0x135D; 0x135E; 0x135F; 0x17DD; 0x193A; 0x1A17; 0x1A75; 0x1A76;
608
+
0x1A77; 0x1A78; 0x1A79; 0x1A7A; 0x1A7B; 0x1A7C; 0x1B6B; 0x1B6D;
609
+
0x1B6E; 0x1B6F; 0x1B70; 0x1B71; 0x1B72; 0x1B73; 0x1CD0; 0x1CD1;
610
+
0x1CD2; 0x1CDA; 0x1CDB; 0x1CE0; 0x1DC0; 0x1DC1; 0x1DC3; 0x1DC4;
611
+
0x1DC5; 0x1DC6; 0x1DC7; 0x1DC8; 0x1DC9; 0x1DCB; 0x1DCC; 0x1DD1;
612
+
0x1DD2; 0x1DD3; 0x1DD4; 0x1DD5; 0x1DD6; 0x1DD7; 0x1DD8; 0x1DD9;
613
+
0x1DDA; 0x1DDB; 0x1DDC; 0x1DDD; 0x1DDE; 0x1DDF; 0x1DE0; 0x1DE1;
614
+
0x1DE2; 0x1DE3; 0x1DE4; 0x1DE5; 0x1DE6; 0x1DFE; 0x20D0; 0x20D1;
615
+
0x20D4; 0x20D5; 0x20D6; 0x20D7; 0x20DB; 0x20DC; 0x20E1; 0x20E7;
616
+
0x20E9; 0x20F0; 0xA66F; 0xA67C; 0xA67D; 0xA6F0; 0xA6F1; 0xA8E0;
617
+
0xA8E1; 0xA8E2; 0xA8E3; 0xA8E4; 0xA8E5; 0xA8E6; 0xA8E7; 0xA8E8;
618
+
0xA8E9; 0xA8EA; 0xA8EB; 0xA8EC; 0xA8ED; 0xA8EE; 0xA8EF; 0xA8F0;
619
+
0xA8F1; 0xAAB0; 0xAAB2; 0xAAB3; 0xAAB7; 0xAAB8; 0xAABE; 0xAABF;
620
+
0xAAC1; 0xFE20; 0xFE21; 0xFE22; 0xFE23; 0xFE24; 0xFE25; 0xFE26;
621
+
0x10A0F; 0x10A38; 0x1D185; 0x1D186; 0x1D187; 0x1D188; 0x1D189;
622
+
0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD; 0x1D242; 0x1D243; 0x1D244;
626
+
Uchar.of_int diacritics.(n mod Array.length diacritics)
628
+
let row_diacritic = diacritic
629
+
let column_diacritic = diacritic
630
+
let id_high_byte_diacritic = diacritic
632
+
let add_uchar buf u =
633
+
let code = Uchar.to_int u in
634
+
let put = Buffer.add_char buf in
635
+
if code < 0x80 then put (Char.chr code)
636
+
else if code < 0x800 then (
637
+
put (Char.chr (0xC0 lor (code lsr 6)));
638
+
put (Char.chr (0x80 lor (code land 0x3F))))
639
+
else if code < 0x10000 then (
640
+
put (Char.chr (0xE0 lor (code lsr 12)));
641
+
put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
642
+
put (Char.chr (0x80 lor (code land 0x3F))))
644
+
put (Char.chr (0xF0 lor (code lsr 18)));
645
+
put (Char.chr (0x80 lor ((code lsr 12) land 0x3F)));
646
+
put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
647
+
put (Char.chr (0x80 lor (code land 0x3F))))
649
+
let write buf ~image_id ?placement_id ~rows ~cols () =
650
+
(* Set foreground color *)
651
+
Printf.bprintf buf "\027[38;2;%d;%d;%dm"
652
+
((image_id lsr 16) land 0xFF)
653
+
((image_id lsr 8) land 0xFF)
654
+
(image_id land 0xFF);
655
+
(* Optional placement ID in underline color *)
657
+
|> Option.iter (fun pid ->
658
+
Printf.bprintf buf "\027[58;2;%d;%d;%dm"
659
+
((pid lsr 16) land 0xFF)
660
+
((pid lsr 8) land 0xFF)
662
+
(* High byte diacritic *)
663
+
let high_byte = (image_id lsr 24) land 0xFF in
664
+
let high_diac = if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None in
666
+
for row = 0 to rows - 1 do
667
+
for col = 0 to cols - 1 do
668
+
add_uchar buf placeholder_char;
669
+
add_uchar buf (row_diacritic row);
670
+
add_uchar buf (column_diacritic col);
671
+
high_diac |> Option.iter (add_uchar buf)
673
+
if row < rows - 1 then Buffer.add_string buf "\n\r"
676
+
Buffer.add_string buf "\027[39m";
677
+
if Option.is_some placement_id then Buffer.add_string buf "\027[59m"
680
+
module Detect = struct
681
+
let make_query () =
682
+
let cmd = Command.query ~format:`Rgb24 ~transmission:`Direct ~width:1 ~height:1 () in
683
+
Command.to_string cmd ~data:"\x00\x00\x00"
685
+
let supports_graphics response ~da1_received =
686
+
response |> Option.map Response.is_ok |> Option.value ~default:(not da1_received)