···
1
+
(* Kitty Terminal Graphics Protocol - Implementation *)
3
+
module Format = struct
4
+
type t = Rgba32 | Rgb24 | Png
6
+
let to_int = function Rgba32 -> 32 | Rgb24 -> 24 | Png -> 100
9
+
module Transmission = struct
10
+
type t = Direct | File | Tempfile
12
+
let to_char = function Direct -> 'd' | File -> 'f' | Tempfile -> 't'
15
+
module Compression = struct
16
+
type t = None | Zlib
18
+
let to_char = function None -> Option.none | Zlib -> Some 'z'
21
+
module Quiet = struct
22
+
type t = Noisy | Errors_only | Silent
24
+
let to_int = function Noisy -> 0 | Errors_only -> 1 | Silent -> 2
27
+
module Cursor = struct
28
+
type t = Move | Static
30
+
let to_int = function Move -> 0 | Static -> 1
33
+
module Composition = struct
34
+
type t = Alpha_blend | Overwrite
36
+
let to_int = function Alpha_blend -> 0 | Overwrite -> 1
39
+
module Delete = struct
42
+
| All_visible_and_free
43
+
| By_id of { image_id : int; placement_id : int option }
44
+
| By_id_and_free of { image_id : int; placement_id : int option }
45
+
| By_number of { image_number : int; placement_id : int option }
46
+
| By_number_and_free of { image_number : int; placement_id : int option }
48
+
| At_cursor_and_free
49
+
| At_cell of { x : int; y : int }
50
+
| At_cell_and_free of { x : int; y : int }
51
+
| At_cell_z of { x : int; y : int; z : int }
52
+
| At_cell_z_and_free of { x : int; y : int; z : int }
54
+
| By_column_and_free of int
56
+
| By_row_and_free of int
58
+
| By_z_index_and_free of int
59
+
| By_id_range of { min_id : int; max_id : int }
60
+
| By_id_range_and_free of { min_id : int; max_id : int }
65
+
module Placement = struct
67
+
source_x : int option;
68
+
source_y : int option;
69
+
source_width : int option;
70
+
source_height : int option;
71
+
cell_x_offset : int option;
72
+
cell_y_offset : int option;
73
+
columns : int option;
75
+
z_index : int option;
76
+
placement_id : int option;
77
+
cursor : Cursor.t option;
78
+
unicode_placeholder : bool;
85
+
source_width = None;
86
+
source_height = None;
87
+
cell_x_offset = None;
88
+
cell_y_offset = None;
92
+
placement_id = None;
94
+
unicode_placeholder = false;
97
+
let make ?source_x ?source_y ?source_width ?source_height ?cell_x_offset
98
+
?cell_y_offset ?columns ?rows ?z_index ?placement_id ?cursor
99
+
?(unicode_placeholder = false) () =
112
+
unicode_placeholder;
116
+
module Frame = struct
120
+
base_frame : int option;
121
+
edit_frame : int option;
122
+
gap_ms : int option;
123
+
composition : Composition.t option;
124
+
background_color : int32 option;
134
+
composition = None;
135
+
background_color = None;
138
+
let make ?x ?y ?base_frame ?edit_frame ?gap_ms ?composition ?background_color
140
+
{ x; y; base_frame; edit_frame; gap_ms; composition; background_color }
143
+
module Animation = struct
144
+
type state = Stop | Loading | Run
147
+
| Set_state of { state : state; loops : int option }
148
+
| Set_gap of { frame : int; gap_ms : int }
149
+
| Set_current of int
151
+
let set_state ?loops state = Set_state { state; loops }
152
+
let set_gap ~frame ~gap_ms = Set_gap { frame; gap_ms }
153
+
let set_current_frame frame = Set_current frame
156
+
module Compose = struct
158
+
source_frame : int;
160
+
width : int option;
161
+
height : int option;
162
+
source_x : int option;
163
+
source_y : int option;
164
+
dest_x : int option;
165
+
dest_y : int option;
166
+
composition : Composition.t option;
169
+
let make ~source_frame ~dest_frame ?width ?height ?source_x ?source_y ?dest_x
170
+
?dest_y ?composition () =
184
+
module Command = struct
187
+
| Transmit_and_display
197
+
format : Format.t option;
198
+
transmission : Transmission.t option;
199
+
compression : Compression.t option;
200
+
width : int option;
201
+
height : int option;
203
+
offset : int option;
204
+
quiet : Quiet.t option;
205
+
image_id : int option;
206
+
image_number : int option;
207
+
placement : Placement.t option;
208
+
delete : Delete.t option;
209
+
frame : Frame.t option;
210
+
animation : Animation.t option;
211
+
compose : Compose.t option;
214
+
let make_base action =
218
+
transmission = None;
219
+
compression = None;
226
+
image_number = None;
234
+
let transmit ?image_id ?image_number ?format ?transmission ?compression ?width
235
+
?height ?size ?offset ?quiet () =
237
+
(make_base Transmit) with
250
+
let transmit_and_display ?image_id ?image_number ?format ?transmission
251
+
?compression ?width ?height ?size ?offset ?quiet ?placement () =
253
+
(make_base Transmit_and_display) with
267
+
let query ?format ?transmission ?width ?height ?quiet () =
268
+
{ (make_base Query) with format; transmission; width; height; quiet }
270
+
let display ?image_id ?image_number ?placement ?quiet () =
271
+
{ (make_base Display) with image_id; image_number; placement; quiet }
273
+
let delete ?quiet del =
274
+
{ (make_base Delete) with quiet; delete = Some del }
276
+
let frame ?image_id ?image_number ?format ?transmission ?compression ?width
277
+
?height ?quiet ~frame () =
279
+
(make_base Frame) with
288
+
frame = Some frame;
291
+
let animate ?image_id ?image_number ?quiet anim =
292
+
{ (make_base Animate) with image_id; image_number; quiet; animation = Some anim }
294
+
let compose ?image_id ?image_number ?quiet comp =
295
+
{ (make_base Compose) with image_id; image_number; quiet; compose = Some comp }
297
+
(* APC escape sequences *)
298
+
let apc_start = "\027_G"
299
+
let apc_end = "\027\\"
301
+
(* Helper to add key=value pairs *)
302
+
let add_kv buf key value =
303
+
Buffer.add_char buf key;
304
+
Buffer.add_char buf '=';
305
+
Buffer.add_string buf value
307
+
let add_kv_int buf key value =
308
+
Buffer.add_char buf key;
309
+
Buffer.add_char buf '=';
310
+
Buffer.add_string buf (string_of_int value)
312
+
let add_kv_int32 buf key value =
313
+
Buffer.add_char buf key;
314
+
Buffer.add_char buf '=';
315
+
Buffer.add_string buf (Int32.to_string value)
317
+
let add_comma buf = Buffer.add_char buf ','
319
+
let action_char = function
321
+
| Transmit_and_display -> 'T'
329
+
let delete_char = function
330
+
| Delete.All_visible -> 'a'
331
+
| All_visible_and_free -> 'A'
333
+
| By_id_and_free _ -> 'I'
334
+
| By_number _ -> 'n'
335
+
| By_number_and_free _ -> 'N'
337
+
| At_cursor_and_free -> 'C'
339
+
| At_cell_and_free _ -> 'P'
340
+
| At_cell_z _ -> 'q'
341
+
| At_cell_z_and_free _ -> 'Q'
342
+
| By_column _ -> 'x'
343
+
| By_column_and_free _ -> 'X'
345
+
| By_row_and_free _ -> 'Y'
346
+
| By_z_index _ -> 'z'
347
+
| By_z_index_and_free _ -> 'Z'
348
+
| By_id_range _ -> 'r'
349
+
| By_id_range_and_free _ -> 'R'
351
+
| Frames_and_free -> 'F'
353
+
let write_control_data buf cmd =
354
+
let first = ref true in
356
+
if !first then first := false else add_comma buf
360
+
add_kv buf 'a' (String.make 1 (action_char cmd.action));
364
+
let v = Quiet.to_int q in
367
+
add_kv_int buf 'q' v))
373
+
add_kv_int buf 'f' (Format.to_int f))
378
+
let c = Transmission.to_char t in
381
+
add_kv buf 't' (String.make 1 c)))
386
+
match Compression.to_char c with
389
+
add_kv buf 'o' (String.make 1 ch)
396
+
add_kv_int buf 's' w)
401
+
add_kv_int buf 'v' h)
403
+
(* File size/offset *)
407
+
add_kv_int buf 'S' s)
412
+
add_kv_int buf 'O' o)
418
+
add_kv_int buf 'i' id)
424
+
add_kv_int buf 'I' n)
426
+
(* Placement options *)
428
+
(fun (p : Placement.t) ->
432
+
add_kv_int buf 'x' v)
437
+
add_kv_int buf 'y' v)
442
+
add_kv_int buf 'w' v)
447
+
add_kv_int buf 'h' v)
452
+
add_kv_int buf 'X' v)
457
+
add_kv_int buf 'Y' v)
462
+
add_kv_int buf 'c' v)
467
+
add_kv_int buf 'r' v)
472
+
add_kv_int buf 'z' v)
477
+
add_kv_int buf 'p' v)
481
+
let v = Cursor.to_int c in
484
+
add_kv_int buf 'C' v))
486
+
if p.unicode_placeholder then (
488
+
add_kv_int buf 'U' 1))
490
+
(* Delete options *)
494
+
add_kv buf 'd' (String.make 1 (delete_char d));
496
+
| Delete.By_id { image_id; placement_id }
497
+
| Delete.By_id_and_free { image_id; placement_id } ->
499
+
add_kv_int buf 'i' image_id;
503
+
add_kv_int buf 'p' p)
505
+
| Delete.By_number { image_number; placement_id }
506
+
| Delete.By_number_and_free { image_number; placement_id } ->
508
+
add_kv_int buf 'I' image_number;
512
+
add_kv_int buf 'p' p)
514
+
| Delete.At_cell { x; y } | Delete.At_cell_and_free { x; y } ->
516
+
add_kv_int buf 'x' x;
518
+
add_kv_int buf 'y' y
519
+
| Delete.At_cell_z { x; y; z }
520
+
| Delete.At_cell_z_and_free { x; y; z } ->
522
+
add_kv_int buf 'x' x;
524
+
add_kv_int buf 'y' y;
526
+
add_kv_int buf 'z' z
527
+
| Delete.By_column c | Delete.By_column_and_free c ->
529
+
add_kv_int buf 'x' c
530
+
| Delete.By_row r | Delete.By_row_and_free r ->
532
+
add_kv_int buf 'y' r
533
+
| Delete.By_z_index z | Delete.By_z_index_and_free z ->
535
+
add_kv_int buf 'z' z
536
+
| Delete.By_id_range { min_id; max_id }
537
+
| Delete.By_id_range_and_free { min_id; max_id } ->
539
+
add_kv_int buf 'x' min_id;
541
+
add_kv_int buf 'y' max_id
544
+
(* Frame options *)
546
+
(fun (f : Frame.t) ->
550
+
add_kv_int buf 'x' v)
555
+
add_kv_int buf 'y' v)
560
+
add_kv_int buf 'c' v)
565
+
add_kv_int buf 'r' v)
570
+
add_kv_int buf 'z' v)
574
+
let v = Composition.to_int c in
577
+
add_kv_int buf 'X' v))
582
+
add_kv_int32 buf 'Y' v)
583
+
f.background_color)
585
+
(* Animation options *)
589
+
| Animation.Set_state { state; loops } ->
592
+
| Animation.Stop -> 1
593
+
| Animation.Loading -> 2
594
+
| Animation.Run -> 3
597
+
add_kv_int buf 's' s;
601
+
add_kv_int buf 'v' v)
603
+
| Animation.Set_gap { frame; gap_ms } ->
605
+
add_kv_int buf 'r' frame;
607
+
add_kv_int buf 'z' gap_ms
608
+
| Animation.Set_current frame ->
610
+
add_kv_int buf 'c' frame)
612
+
(* Compose options *)
614
+
(fun (c : Compose.t) ->
616
+
add_kv_int buf 'r' c.source_frame;
618
+
add_kv_int buf 'c' c.dest_frame;
622
+
add_kv_int buf 'w' v)
627
+
add_kv_int buf 'h' v)
632
+
add_kv_int buf 'x' v)
637
+
add_kv_int buf 'y' v)
642
+
add_kv_int buf 'X' v)
647
+
add_kv_int buf 'Y' v)
651
+
let v = Composition.to_int comp in
654
+
add_kv_int buf 'C' v))
658
+
let chunk_size = 4096
660
+
let write buf cmd ~data =
661
+
Buffer.add_string buf apc_start;
662
+
write_control_data buf cmd;
663
+
if String.length data > 0 then begin
664
+
let encoded = Base64.encode_string data in
665
+
let len = String.length encoded in
666
+
if len <= chunk_size then (
667
+
Buffer.add_char buf ';';
668
+
Buffer.add_string buf encoded;
669
+
Buffer.add_string buf apc_end)
671
+
(* Multiple chunks *)
673
+
let first = ref true in
674
+
while !pos < len do
675
+
let remaining = len - !pos in
676
+
let this_chunk = min chunk_size remaining in
677
+
let is_last = !pos + this_chunk >= len in
682
+
add_kv_int buf 'm' 1;
683
+
Buffer.add_char buf ';';
684
+
Buffer.add_substring buf encoded !pos this_chunk;
685
+
Buffer.add_string buf apc_end)
687
+
(* Continuation chunk *)
688
+
Buffer.add_string buf apc_start;
689
+
add_kv_int buf 'm' (if is_last then 0 else 1);
690
+
Buffer.add_char buf ';';
691
+
Buffer.add_substring buf encoded !pos this_chunk;
692
+
Buffer.add_string buf apc_end);
693
+
pos := !pos + this_chunk
697
+
else Buffer.add_string buf apc_end
699
+
let to_string cmd ~data =
700
+
let buf = Buffer.create 1024 in
701
+
write buf cmd ~data;
702
+
Buffer.contents buf
705
+
module Response = struct
708
+
image_id : int option;
709
+
image_number : int option;
710
+
placement_id : int option;
713
+
let is_ok t = t.message = "OK"
714
+
let message t = t.message
717
+
if is_ok t then None
719
+
match String.index_opt t.message ':' with
720
+
| Some i -> Some (String.sub t.message 0 i)
721
+
| None -> Some t.message
723
+
let image_id t = t.image_id
724
+
let image_number t = t.image_number
725
+
let placement_id t = t.placement_id
728
+
(* Format: <ESC>_G<keys>;message<ESC>\ *)
729
+
let esc = '\027' in
730
+
let len = String.length s in
731
+
if len < 5 then None
732
+
else if s.[0] <> esc || s.[1] <> '_' || s.[2] <> 'G' then None
734
+
(* Find the semicolon and end *)
735
+
match String.index_from_opt s 3 ';' with
737
+
| Some semi_pos -> (
738
+
(* Find the APC terminator *)
739
+
let rec find_end pos =
740
+
if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then
742
+
else if pos + 1 < len then find_end (pos + 1)
745
+
match find_end (semi_pos + 1) with
748
+
let keys_str = String.sub s 3 (semi_pos - 3) in
750
+
String.sub s (semi_pos + 1) (end_pos - semi_pos - 1)
753
+
let image_id = ref None in
754
+
let image_number = ref None in
755
+
let placement_id = ref None in
756
+
let parts = String.split_on_char ',' keys_str in
759
+
if String.length part >= 3 && part.[1] = '=' then
760
+
let key = part.[0] in
761
+
let value = String.sub part 2 (String.length part - 2) in
763
+
| 'i' -> image_id := int_of_string_opt value
764
+
| 'I' -> image_number := int_of_string_opt value
765
+
| 'p' -> placement_id := int_of_string_opt value
771
+
image_id = !image_id;
772
+
image_number = !image_number;
773
+
placement_id = !placement_id;
777
+
module Unicode_placeholder = struct
778
+
let placeholder_char = Uchar.of_int 0x10EEEE
780
+
(* Row/column diacritics from the protocol spec *)
783
+
0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F;
784
+
0x0346; 0x034A; 0x034B; 0x034C; 0x0350; 0x0351; 0x0352; 0x0357;
785
+
0x035B; 0x0363; 0x0364; 0x0365; 0x0366; 0x0367; 0x0368; 0x0369;
786
+
0x036A; 0x036B; 0x036C; 0x036D; 0x036E; 0x036F; 0x0483; 0x0484;
787
+
0x0485; 0x0486; 0x0487; 0x0592; 0x0593; 0x0594; 0x0595; 0x0597;
788
+
0x0598; 0x0599; 0x059C; 0x059D; 0x059E; 0x059F; 0x05A0; 0x05A1;
789
+
0x05A8; 0x05A9; 0x05AB; 0x05AC; 0x05AF; 0x05C4; 0x0610; 0x0611;
790
+
0x0612; 0x0613; 0x0614; 0x0615; 0x0616; 0x0617; 0x0657; 0x0658;
791
+
0x0659; 0x065A; 0x065B; 0x065D; 0x065E; 0x06D6; 0x06D7; 0x06D8;
792
+
0x06D9; 0x06DA; 0x06DB; 0x06DC; 0x06DF; 0x06E0; 0x06E1; 0x06E2;
793
+
0x06E4; 0x06E7; 0x06E8; 0x06EB; 0x06EC; 0x0730; 0x0732; 0x0733;
794
+
0x0735; 0x0736; 0x073A; 0x073D; 0x073F; 0x0740; 0x0741; 0x0743;
795
+
0x0745; 0x0747; 0x0749; 0x074A; 0x07EB; 0x07EC; 0x07ED; 0x07EE;
796
+
0x07EF; 0x07F0; 0x07F1; 0x07F3; 0x0816; 0x0817; 0x0818; 0x0819;
797
+
0x081B; 0x081C; 0x081D; 0x081E; 0x081F; 0x0820; 0x0821; 0x0822;
798
+
0x0823; 0x0825; 0x0826; 0x0827; 0x0829; 0x082A; 0x082B; 0x082C;
799
+
0x082D; 0x0951; 0x0953; 0x0954; 0x0F82; 0x0F83; 0x0F86; 0x0F87;
800
+
0x135D; 0x135E; 0x135F; 0x17DD; 0x193A; 0x1A17; 0x1A75; 0x1A76;
801
+
0x1A77; 0x1A78; 0x1A79; 0x1A7A; 0x1A7B; 0x1A7C; 0x1B6B; 0x1B6D;
802
+
0x1B6E; 0x1B6F; 0x1B70; 0x1B71; 0x1B72; 0x1B73; 0x1CD0; 0x1CD1;
803
+
0x1CD2; 0x1CDA; 0x1CDB; 0x1CE0; 0x1DC0; 0x1DC1; 0x1DC3; 0x1DC4;
804
+
0x1DC5; 0x1DC6; 0x1DC7; 0x1DC8; 0x1DC9; 0x1DCB; 0x1DCC; 0x1DD1;
805
+
0x1DD2; 0x1DD3; 0x1DD4; 0x1DD5; 0x1DD6; 0x1DD7; 0x1DD8; 0x1DD9;
806
+
0x1DDA; 0x1DDB; 0x1DDC; 0x1DDD; 0x1DDE; 0x1DDF; 0x1DE0; 0x1DE1;
807
+
0x1DE2; 0x1DE3; 0x1DE4; 0x1DE5; 0x1DE6; 0x1DFE; 0x20D0; 0x20D1;
808
+
0x20D4; 0x20D5; 0x20D6; 0x20D7; 0x20DB; 0x20DC; 0x20E1; 0x20E7;
809
+
0x20E9; 0x20F0; 0xA66F; 0xA67C; 0xA67D; 0xA6F0; 0xA6F1; 0xA8E0;
810
+
0xA8E1; 0xA8E2; 0xA8E3; 0xA8E4; 0xA8E5; 0xA8E6; 0xA8E7; 0xA8E8;
811
+
0xA8E9; 0xA8EA; 0xA8EB; 0xA8EC; 0xA8ED; 0xA8EE; 0xA8EF; 0xA8F0;
812
+
0xA8F1; 0xAAB0; 0xAAB2; 0xAAB3; 0xAAB7; 0xAAB8; 0xAABE; 0xAABF;
813
+
0xAAC1; 0xFE20; 0xFE21; 0xFE22; 0xFE23; 0xFE24; 0xFE25; 0xFE26;
814
+
0x10A0F; 0x10A38; 0x1D185; 0x1D186; 0x1D187; 0x1D188; 0x1D189;
815
+
0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD; 0x1D242; 0x1D243; 0x1D244;
818
+
let row_diacritic n =
819
+
if n >= 0 && n < Array.length diacritics then
820
+
Uchar.of_int diacritics.(n)
821
+
else Uchar.of_int diacritics.(0)
823
+
let column_diacritic = row_diacritic
824
+
let id_high_byte_diacritic = row_diacritic
826
+
let add_uchar buf u =
827
+
let b = Bytes.create 4 in
828
+
let len = Uchar.utf_8_byte_length u in
829
+
let _ = Uchar.unsafe_to_char u in
830
+
(* Encode UTF-8 manually *)
831
+
let code = Uchar.to_int u in
832
+
if code < 0x80 then (
833
+
Bytes.set b 0 (Char.chr code);
834
+
Buffer.add_subbytes buf b 0 1)
835
+
else if code < 0x800 then (
836
+
Bytes.set b 0 (Char.chr (0xC0 lor (code lsr 6)));
837
+
Bytes.set b 1 (Char.chr (0x80 lor (code land 0x3F)));
838
+
Buffer.add_subbytes buf b 0 2)
839
+
else if code < 0x10000 then (
840
+
Bytes.set b 0 (Char.chr (0xE0 lor (code lsr 12)));
841
+
Bytes.set b 1 (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
842
+
Bytes.set b 2 (Char.chr (0x80 lor (code land 0x3F)));
843
+
Buffer.add_subbytes buf b 0 3)
845
+
Bytes.set b 0 (Char.chr (0xF0 lor (code lsr 18)));
846
+
Bytes.set b 1 (Char.chr (0x80 lor ((code lsr 12) land 0x3F)));
847
+
Bytes.set b 2 (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
848
+
Bytes.set b 3 (Char.chr (0x80 lor (code land 0x3F)));
849
+
Buffer.add_subbytes buf b 0 len)
851
+
let write buf ~image_id ?placement_id ~rows ~cols () =
852
+
(* Set foreground color using 24-bit mode *)
853
+
let r = (image_id lsr 16) land 0xFF in
854
+
let g = (image_id lsr 8) land 0xFF in
855
+
let b = image_id land 0xFF in
856
+
Buffer.add_string buf (Printf.sprintf "\027[38;2;%d;%d;%dm" r g b);
857
+
(* Optionally set underline color for placement ID *)
858
+
(match placement_id with
860
+
let pr = (pid lsr 16) land 0xFF in
861
+
let pg = (pid lsr 8) land 0xFF in
862
+
let pb = pid land 0xFF in
863
+
Buffer.add_string buf (Printf.sprintf "\027[58;2;%d;%d;%dm" pr pg pb)
865
+
(* High byte diacritic if needed *)
866
+
let high_byte = (image_id lsr 24) land 0xFF in
868
+
if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None
870
+
(* Write placeholder grid *)
871
+
for row = 0 to rows - 1 do
872
+
for col = 0 to cols - 1 do
873
+
add_uchar buf placeholder_char;
874
+
add_uchar buf (row_diacritic row);
875
+
add_uchar buf (column_diacritic col);
876
+
Option.iter (add_uchar buf) high_diac
878
+
if row < rows - 1 then Buffer.add_string buf "\n\r"
881
+
Buffer.add_string buf "\027[39m";
882
+
match placement_id with Some _ -> Buffer.add_string buf "\027[59m" | None -> ()
885
+
module Detect = struct
886
+
let make_query () =
887
+
(* Send a 1x1 transparent pixel query *)
889
+
Command.query ~format:Format.Rgb24 ~transmission:Transmission.Direct
890
+
~width:1 ~height:1 ()
892
+
let data = "\x00\x00\x00" in
893
+
let query = Command.to_string cmd ~data in
894
+
(* Add DA1 query to detect non-supporting terminals *)
897
+
let supports_graphics response ~da1_received =
898
+
match response with
899
+
| Some r -> Response.is_ok r
900
+
| None -> not da1_received