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;
13 0x030D;
14 0x030E;
15 0x0310;
16 0x0312;
17 0x033D;
18 0x033E;
19 0x033F;
20 0x0346;
21 0x034A;
22 0x034B;
23 0x034C;
24 0x0350;
25 0x0351;
26 0x0352;
27 0x0357;
28 0x035B;
29 0x0363;
30 0x0364;
31 0x0365;
32 0x0366;
33 0x0367;
34 0x0368;
35 0x0369;
36 0x036A;
37 0x036B;
38 0x036C;
39 0x036D;
40 0x036E;
41 0x036F;
42 0x0483;
43 0x0484;
44 0x0485;
45 0x0486;
46 0x0487;
47 0x0592;
48 0x0593;
49 0x0594;
50 0x0595;
51 0x0597;
52 0x0598;
53 0x0599;
54 0x059C;
55 0x059D;
56 0x059E;
57 0x059F;
58 0x05A0;
59 0x05A1;
60 0x05A8;
61 0x05A9;
62 0x05AB;
63 0x05AC;
64 0x05AF;
65 0x05C4;
66 0x0610;
67 0x0611;
68 0x0612;
69 0x0613;
70 0x0614;
71 0x0615;
72 0x0616;
73 0x0617;
74 0x0657;
75 0x0658;
76 0x0659;
77 0x065A;
78 0x065B;
79 0x065D;
80 0x065E;
81 0x06D6;
82 0x06D7;
83 0x06D8;
84 0x06D9;
85 0x06DA;
86 0x06DB;
87 0x06DC;
88 0x06DF;
89 0x06E0;
90 0x06E1;
91 0x06E2;
92 0x06E4;
93 0x06E7;
94 0x06E8;
95 0x06EB;
96 0x06EC;
97 0x0730;
98 0x0732;
99 0x0733;
100 0x0735;
101 0x0736;
102 0x073A;
103 0x073D;
104 0x073F;
105 0x0740;
106 0x0741;
107 0x0743;
108 0x0745;
109 0x0747;
110 0x0749;
111 0x074A;
112 0x07EB;
113 0x07EC;
114 0x07ED;
115 0x07EE;
116 0x07EF;
117 0x07F0;
118 0x07F1;
119 0x07F3;
120 0x0816;
121 0x0817;
122 0x0818;
123 0x0819;
124 0x081B;
125 0x081C;
126 0x081D;
127 0x081E;
128 0x081F;
129 0x0820;
130 0x0821;
131 0x0822;
132 0x0823;
133 0x0825;
134 0x0826;
135 0x0827;
136 0x0829;
137 0x082A;
138 0x082B;
139 0x082C;
140 0x082D;
141 0x0951;
142 0x0953;
143 0x0954;
144 0x0F82;
145 0x0F83;
146 0x0F86;
147 0x0F87;
148 0x135D;
149 0x135E;
150 0x135F;
151 0x17DD;
152 0x193A;
153 0x1A17;
154 0x1A75;
155 0x1A76;
156 0x1A77;
157 0x1A78;
158 0x1A79;
159 0x1A7A;
160 0x1A7B;
161 0x1A7C;
162 0x1B6B;
163 0x1B6D;
164 0x1B6E;
165 0x1B6F;
166 0x1B70;
167 0x1B71;
168 0x1B72;
169 0x1B73;
170 0x1CD0;
171 0x1CD1;
172 0x1CD2;
173 0x1CDA;
174 0x1CDB;
175 0x1CE0;
176 0x1DC0;
177 0x1DC1;
178 0x1DC3;
179 0x1DC4;
180 0x1DC5;
181 0x1DC6;
182 0x1DC7;
183 0x1DC8;
184 0x1DC9;
185 0x1DCB;
186 0x1DCC;
187 0x1DD1;
188 0x1DD2;
189 0x1DD3;
190 0x1DD4;
191 0x1DD5;
192 0x1DD6;
193 0x1DD7;
194 0x1DD8;
195 0x1DD9;
196 0x1DDA;
197 0x1DDB;
198 0x1DDC;
199 0x1DDD;
200 0x1DDE;
201 0x1DDF;
202 0x1DE0;
203 0x1DE1;
204 0x1DE2;
205 0x1DE3;
206 0x1DE4;
207 0x1DE5;
208 0x1DE6;
209 0x1DFE;
210 0x20D0;
211 0x20D1;
212 0x20D4;
213 0x20D5;
214 0x20D6;
215 0x20D7;
216 0x20DB;
217 0x20DC;
218 0x20E1;
219 0x20E7;
220 0x20E9;
221 0x20F0;
222 0xA66F;
223 0xA67C;
224 0xA67D;
225 0xA6F0;
226 0xA6F1;
227 0xA8E0;
228 0xA8E1;
229 0xA8E2;
230 0xA8E3;
231 0xA8E4;
232 0xA8E5;
233 0xA8E6;
234 0xA8E7;
235 0xA8E8;
236 0xA8E9;
237 0xA8EA;
238 0xA8EB;
239 0xA8EC;
240 0xA8ED;
241 0xA8EE;
242 0xA8EF;
243 0xA8F0;
244 0xA8F1;
245 0xAAB0;
246 0xAAB2;
247 0xAAB3;
248 0xAAB7;
249 0xAAB8;
250 0xAABE;
251 0xAABF;
252 0xAAC1;
253 0xFE20;
254 0xFE21;
255 0xFE22;
256 0xFE23;
257 0xFE24;
258 0xFE25;
259 0xFE26;
260 0x10A0F;
261 0x10A38;
262 0x1D185;
263 0x1D186;
264 0x1D187;
265 0x1D188;
266 0x1D189;
267 0x1D1AA;
268 0x1D1AB;
269 0x1D1AC;
270 0x1D1AD;
271 0x1D242;
272 0x1D243;
273 0x1D244;
274 |]
275
276let diacritic n = Uchar.of_int diacritics.(n mod Array.length diacritics)
277let row_diacritic = diacritic
278let column_diacritic = diacritic
279let id_high_byte_diacritic = diacritic
280
281let next_image_id () =
282 let rec gen () =
283 let id = Random.int32 Int32.max_int |> Int32.to_int in
284 (* Ensure high byte and middle bytes are non-zero *)
285 if id land 0xFF000000 = 0 || id land 0x00FFFF00 = 0 then gen () else id
286 in
287 gen ()
288
289let add_uchar buf u =
290 let code = Uchar.to_int u in
291 let put = Buffer.add_char buf in
292 if code < 0x80 then put (Char.chr code)
293 else if code < 0x800 then (
294 put (Char.chr (0xC0 lor (code lsr 6)));
295 put (Char.chr (0x80 lor (code land 0x3F))))
296 else if code < 0x10000 then (
297 put (Char.chr (0xE0 lor (code lsr 12)));
298 put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
299 put (Char.chr (0x80 lor (code land 0x3F))))
300 else (
301 put (Char.chr (0xF0 lor (code lsr 18)));
302 put (Char.chr (0x80 lor ((code lsr 12) land 0x3F)));
303 put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
304 put (Char.chr (0x80 lor (code land 0x3F))))
305
306let write buf ~image_id ?placement_id ~rows ~cols () =
307 (* Set foreground color using colon subparameter format *)
308 Printf.bprintf buf "\027[38:2:%d:%d:%dm"
309 ((image_id lsr 16) land 0xFF)
310 ((image_id lsr 8) land 0xFF)
311 (image_id land 0xFF);
312 (* Optional placement ID in underline color *)
313 placement_id
314 |> Option.iter (fun pid ->
315 Printf.bprintf buf "\027[58:2:%d:%d:%dm"
316 ((pid lsr 16) land 0xFF)
317 ((pid lsr 8) land 0xFF)
318 (pid land 0xFF));
319 (* High byte diacritic - always written, even when 0 *)
320 let high_byte = (image_id lsr 24) land 0xFF in
321 let id_diac = id_high_byte_diacritic high_byte in
322 (* Write grid *)
323 for row = 0 to rows - 1 do
324 for col = 0 to cols - 1 do
325 add_uchar buf placeholder_char;
326 add_uchar buf (row_diacritic row);
327 add_uchar buf (column_diacritic col);
328 add_uchar buf id_diac
329 done;
330 if row < rows - 1 then Buffer.add_string buf "\n\r"
331 done;
332 (* Reset colors *)
333 Buffer.add_string buf "\027[39m";
334 if Option.is_some placement_id then Buffer.add_string buf "\027[59m"