Kitty Graphics Protocol in OCaml
terminal
graphics
ocaml
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(* Kitty Graphics Protocol Unicode Placeholders - Implementation *)
7
8let placeholder_char = Uchar.of_int 0x10EEEE
9
10let diacritics =
11 [|
12 0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F;
13 0x0346; 0x034A; 0x034B; 0x034C; 0x0350; 0x0351; 0x0352; 0x0357;
14 0x035B; 0x0363; 0x0364; 0x0365; 0x0366; 0x0367; 0x0368; 0x0369;
15 0x036A; 0x036B; 0x036C; 0x036D; 0x036E; 0x036F; 0x0483; 0x0484;
16 0x0485; 0x0486; 0x0487; 0x0592; 0x0593; 0x0594; 0x0595; 0x0597;
17 0x0598; 0x0599; 0x059C; 0x059D; 0x059E; 0x059F; 0x05A0; 0x05A1;
18 0x05A8; 0x05A9; 0x05AB; 0x05AC; 0x05AF; 0x05C4; 0x0610; 0x0611;
19 0x0612; 0x0613; 0x0614; 0x0615; 0x0616; 0x0617; 0x0657; 0x0658;
20 0x0659; 0x065A; 0x065B; 0x065D; 0x065E; 0x06D6; 0x06D7; 0x06D8;
21 0x06D9; 0x06DA; 0x06DB; 0x06DC; 0x06DF; 0x06E0; 0x06E1; 0x06E2;
22 0x06E4; 0x06E7; 0x06E8; 0x06EB; 0x06EC; 0x0730; 0x0732; 0x0733;
23 0x0735; 0x0736; 0x073A; 0x073D; 0x073F; 0x0740; 0x0741; 0x0743;
24 0x0745; 0x0747; 0x0749; 0x074A; 0x07EB; 0x07EC; 0x07ED; 0x07EE;
25 0x07EF; 0x07F0; 0x07F1; 0x07F3; 0x0816; 0x0817; 0x0818; 0x0819;
26 0x081B; 0x081C; 0x081D; 0x081E; 0x081F; 0x0820; 0x0821; 0x0822;
27 0x0823; 0x0825; 0x0826; 0x0827; 0x0829; 0x082A; 0x082B; 0x082C;
28 0x082D; 0x0951; 0x0953; 0x0954; 0x0F82; 0x0F83; 0x0F86; 0x0F87;
29 0x135D; 0x135E; 0x135F; 0x17DD; 0x193A; 0x1A17; 0x1A75; 0x1A76;
30 0x1A77; 0x1A78; 0x1A79; 0x1A7A; 0x1A7B; 0x1A7C; 0x1B6B; 0x1B6D;
31 0x1B6E; 0x1B6F; 0x1B70; 0x1B71; 0x1B72; 0x1B73; 0x1CD0; 0x1CD1;
32 0x1CD2; 0x1CDA; 0x1CDB; 0x1CE0; 0x1DC0; 0x1DC1; 0x1DC3; 0x1DC4;
33 0x1DC5; 0x1DC6; 0x1DC7; 0x1DC8; 0x1DC9; 0x1DCB; 0x1DCC; 0x1DD1;
34 0x1DD2; 0x1DD3; 0x1DD4; 0x1DD5; 0x1DD6; 0x1DD7; 0x1DD8; 0x1DD9;
35 0x1DDA; 0x1DDB; 0x1DDC; 0x1DDD; 0x1DDE; 0x1DDF; 0x1DE0; 0x1DE1;
36 0x1DE2; 0x1DE3; 0x1DE4; 0x1DE5; 0x1DE6; 0x1DFE; 0x20D0; 0x20D1;
37 0x20D4; 0x20D5; 0x20D6; 0x20D7; 0x20DB; 0x20DC; 0x20E1; 0x20E7;
38 0x20E9; 0x20F0; 0xA66F; 0xA67C; 0xA67D; 0xA6F0; 0xA6F1; 0xA8E0;
39 0xA8E1; 0xA8E2; 0xA8E3; 0xA8E4; 0xA8E5; 0xA8E6; 0xA8E7; 0xA8E8;
40 0xA8E9; 0xA8EA; 0xA8EB; 0xA8EC; 0xA8ED; 0xA8EE; 0xA8EF; 0xA8F0;
41 0xA8F1; 0xAAB0; 0xAAB2; 0xAAB3; 0xAAB7; 0xAAB8; 0xAABE; 0xAABF;
42 0xAAC1; 0xFE20; 0xFE21; 0xFE22; 0xFE23; 0xFE24; 0xFE25; 0xFE26;
43 0x10A0F; 0x10A38; 0x1D185; 0x1D186; 0x1D187; 0x1D188; 0x1D189;
44 0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD; 0x1D242; 0x1D243; 0x1D244;
45 |]
46
47let diacritic n = Uchar.of_int diacritics.(n mod Array.length diacritics)
48let row_diacritic = diacritic
49let column_diacritic = diacritic
50let id_high_byte_diacritic = diacritic
51
52let next_image_id () =
53 let rec gen () =
54 let id = Random.int32 Int32.max_int |> Int32.to_int in
55 (* Ensure high byte and middle bytes are non-zero *)
56 if id land 0xFF000000 = 0 || id land 0x00FFFF00 = 0 then gen ()
57 else id
58 in
59 gen ()
60
61let add_uchar buf u =
62 let code = Uchar.to_int u in
63 let put = Buffer.add_char buf in
64 if code < 0x80 then put (Char.chr code)
65 else if code < 0x800 then (
66 put (Char.chr (0xC0 lor (code lsr 6)));
67 put (Char.chr (0x80 lor (code land 0x3F))))
68 else if code < 0x10000 then (
69 put (Char.chr (0xE0 lor (code lsr 12)));
70 put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
71 put (Char.chr (0x80 lor (code land 0x3F))))
72 else (
73 put (Char.chr (0xF0 lor (code lsr 18)));
74 put (Char.chr (0x80 lor ((code lsr 12) land 0x3F)));
75 put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
76 put (Char.chr (0x80 lor (code land 0x3F))))
77
78let write buf ~image_id ?placement_id ~rows ~cols () =
79 (* Set foreground color using colon subparameter format *)
80 Printf.bprintf buf "\027[38:2:%d:%d:%dm"
81 ((image_id lsr 16) land 0xFF)
82 ((image_id lsr 8) land 0xFF)
83 (image_id land 0xFF);
84 (* Optional placement ID in underline color *)
85 placement_id
86 |> Option.iter (fun pid ->
87 Printf.bprintf buf "\027[58:2:%d:%d:%dm"
88 ((pid lsr 16) land 0xFF)
89 ((pid lsr 8) land 0xFF)
90 (pid land 0xFF));
91 (* High byte diacritic - always written, even when 0 *)
92 let high_byte = (image_id lsr 24) land 0xFF in
93 let id_diac = id_high_byte_diacritic high_byte in
94 (* Write grid *)
95 for row = 0 to rows - 1 do
96 for col = 0 to cols - 1 do
97 add_uchar buf placeholder_char;
98 add_uchar buf (row_diacritic row);
99 add_uchar buf (column_diacritic col);
100 add_uchar buf id_diac
101 done;
102 if row < rows - 1 then Buffer.add_string buf "\n\r"
103 done;
104 (* Reset colors *)
105 Buffer.add_string buf "\027[39m";
106 if Option.is_some placement_id then Buffer.add_string buf "\027[59m"