···
(* 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 *)
4
-
type t = Rgba32 | Rgb24 | Png
6
-
let to_int = function Rgba32 -> 32 | Rgb24 -> 24 | Png -> 100
41
+
let to_int : t -> int = function
module Transmission = struct
10
-
type t = Direct | File | Tempfile
48
+
type t = transmission
12
-
let to_char = function Direct -> 'd' | File -> 'f' | Tempfile -> 't'
50
+
let to_char : t -> char = function
module Compression = struct
16
-
type t = None | Zlib
57
+
type t = compression
18
-
let to_char = function None -> Option.none | Zlib -> Some 'z'
59
+
let to_char : t -> char option = function
22
-
type t = Noisy | Errors_only | Silent
24
-
let to_int = function Noisy -> 0 | Errors_only -> 1 | Silent -> 2
67
+
let to_int : t -> int = function
28
-
type t = Move | Static
30
-
let to_int = function Move -> 0 | Static -> 1
76
+
let to_int : t -> int = function
module Composition = struct
34
-
type t = Alpha_blend | Overwrite
82
+
type t = composition
36
-
let to_int = function Alpha_blend -> 0 | Overwrite -> 1
84
+
let to_int : t -> int = function
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 }
module Placement = struct
···
placement_id : int option;
77
-
cursor : Cursor.t option;
105
+
cursor : cursor option;
unicode_placeholder : bool;
···
123
-
composition : Composition.t option;
151
+
composition : composition option;
background_color : int32 option;
···
module Animation = struct
144
-
type state = Stop | Loading | Run
172
+
type state = animation_state
147
-
| Set_state of { state : state; loops : int option }
148
-
| Set_gap of { frame : int; gap_ms : int }
149
-
| Set_current of int
175
+
[ `Set_state of state * int option
176
+
| `Set_gap of int * int
177
+
| `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
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
···
166
-
composition : Composition.t option;
194
+
composition : composition option;
let make ~source_frame ~dest_frame ?width ?height ?source_x ?source_y ?dest_x
···
187
-
| Transmit_and_display
215
+
| `Transmit_and_display
197
-
format : Format.t option;
198
-
transmission : Transmission.t option;
199
-
compression : Compression.t option;
225
+
format : format option;
226
+
transmission : transmission option;
227
+
compression : compression option;
204
-
quiet : Quiet.t option;
232
+
quiet : quiet option;
image_number : int option;
placement : Placement.t option;
208
-
delete : Delete.t option;
236
+
delete : delete option;
animation : Animation.t option;
compose : Compose.t option;
214
-
let make_base action =
···
let transmit ?image_id ?image_number ?format ?transmission ?compression ?width
?height ?size ?offset ?quiet () =
237
-
(make_base Transmit) with
265
+
(make `Transmit) with
···
let transmit_and_display ?image_id ?image_number ?format ?transmission
?compression ?width ?height ?size ?offset ?quiet ?placement () =
253
-
(make_base Transmit_and_display) with
281
+
(make `Transmit_and_display) with
···
let query ?format ?transmission ?width ?height ?quiet () =
268
-
{ (make_base Query) with format; transmission; width; height; quiet }
296
+
{ (make `Query) with format; transmission; width; height; quiet }
let display ?image_id ?image_number ?placement ?quiet () =
271
-
{ (make_base Display) with image_id; image_number; placement; quiet }
299
+
{ (make `Display) with image_id; image_number; placement; quiet }
273
-
let delete ?quiet del =
274
-
{ (make_base Delete) with quiet; delete = Some del }
301
+
let delete ?quiet del = { (make `Delete) with quiet; delete = Some del }
let frame ?image_id ?image_number ?format ?transmission ?compression ?width
?height ?quiet ~frame () =
279
-
(make_base Frame) with
···
let animate ?image_id ?image_number ?quiet anim =
292
-
{ (make_base Animate) with image_id; image_number; quiet; animation = Some anim }
319
+
{ (make `Animate) with image_id; image_number; quiet; animation = Some anim }
let compose ?image_id ?image_number ?quiet comp =
295
-
{ (make_base Compose) with image_id; image_number; quiet; compose = Some comp }
322
+
{ (make `Compose) with image_id; image_number; quiet; compose = Some comp }
297
-
(* APC escape sequences *)
324
+
(* Serialization helpers *)
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
328
+
(* Key-value writer with separator handling *)
329
+
type kv_writer = { mutable first : bool; buf : Buffer.t }
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)
331
+
let kv_writer buf = { first = true; buf }
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)
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
317
-
let add_comma buf = Buffer.add_char buf ','
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)
319
-
let action_char = function
321
-
| Transmit_and_display -> 'T'
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)
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'
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)))
let write_control_data buf cmd =
354
-
let first = ref true in
356
-
if !first then first := false else add_comma buf
458
+
let w = kv_writer buf in
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))
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)));
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)))
464
+
cmd.format |> Option.iter (fun f -> kv_int w 'f' (Format.to_int f));
465
+
(* Transmission - only if non-default *)
467
+
|> Option.iter (fun t ->
468
+
let c = Transmission.to_char t in
469
+
if c <> 'd' then kv_char w 't' c);
386
-
match Compression.to_char c with
389
-
add_kv buf 'o' (String.make 1 ch)
471
+
cmd.compression |> Option.iter (fun c -> Compression.to_char c |> Option.iter (kv_char w 'o'));
396
-
add_kv_int buf 's' w)
401
-
add_kv_int buf 'v' h)
473
+
kv_int_opt w 's' cmd.width;
474
+
kv_int_opt w 'v' cmd.height;
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))
476
+
kv_int_opt w 'S' cmd.size;
477
+
kv_int_opt w 'O' cmd.offset;
478
+
(* Image ID/number *)
479
+
kv_int_opt w 'i' cmd.image_id;
480
+
kv_int_opt w 'I' cmd.image_number;
481
+
(* Complex options *)
482
+
cmd.placement |> Option.iter (write_placement w);
483
+
cmd.delete |> Option.iter (write_delete w);
484
+
cmd.frame |> Option.iter (write_frame w);
485
+
cmd.animation |> Option.iter (write_animation w);
486
+
cmd.compose |> Option.iter (write_compose w);
let write buf cmd ~data =
Buffer.add_string buf apc_start;
662
-
write_control_data buf cmd;
493
+
let w = write_control_data buf cmd in
if String.length data > 0 then begin
let encoded = Base64.encode_string data in
let len = String.length encoded in
···
Buffer.add_string buf apc_end)
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
503
+
let rec write_chunks pos first =
504
+
if pos < len then begin
505
+
let remaining = len - pos in
506
+
let this_chunk = min chunk_size remaining in
507
+
let is_last = pos + this_chunk >= len in
510
+
Buffer.add_char buf ';';
511
+
Buffer.add_substring buf encoded pos this_chunk;
512
+
Buffer.add_string buf apc_end)
514
+
Buffer.add_string buf apc_start;
515
+
Buffer.add_string buf (if is_last then "m=0" else "m=1");
516
+
Buffer.add_char buf ';';
517
+
Buffer.add_substring buf encoded pos this_chunk;
518
+
Buffer.add_string buf apc_end);
519
+
write_chunks (pos + this_chunk) false
522
+
write_chunks 0 true
else Buffer.add_string buf apc_end
···
719
-
match String.index_opt t.message ':' with
720
-
| Some i -> Some (String.sub t.message 0 i)
721
-
| None -> Some t.message
546
+
else String.index_opt t.message ':' |> Option.fold ~none:(Some t.message) ~some:(fun i -> Some (String.sub t.message 0 i))
let image_id t = t.image_id
let image_number t = t.image_number
let placement_id t = t.placement_id
728
-
(* Format: <ESC>_G<keys>;message<ESC>\ *)
553
+
let ( let* ) = Option.bind in
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;
556
+
let* () = if len >= 5 && s.[0] = esc && s.[1] = '_' && s.[2] = 'G' then Some () else None in
557
+
let* semi_pos = String.index_from_opt s 3 ';' in
558
+
let rec find_end pos =
559
+
if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then Some pos
560
+
else if pos + 1 < len then find_end (pos + 1)
563
+
let* end_pos = find_end (semi_pos + 1) in
564
+
let keys_str = String.sub s 3 (semi_pos - 3) in
565
+
let message = String.sub s (semi_pos + 1) (end_pos - semi_pos - 1) in
566
+
let parse_kv part =
567
+
if String.length part >= 3 && part.[1] = '=' then
568
+
Some (part.[0], String.sub part 2 (String.length part - 2))
571
+
let keys = String.split_on_char ',' keys_str |> List.filter_map parse_kv in
572
+
let find_int key = List.assoc_opt key keys |> Fun.flip Option.bind int_of_string_opt in
576
+
image_id = find_int 'i';
577
+
image_number = find_int 'I';
578
+
placement_id = find_int 'p';
module Unicode_placeholder = struct
let placeholder_char = Uchar.of_int 0x10EEEE
780
-
(* Row/column diacritics from the protocol spec *)
0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F;
···
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)
623
+
Uchar.of_int diacritics.(n mod Array.length diacritics)
823
-
let column_diacritic = row_diacritic
824
-
let id_high_byte_diacritic = row_diacritic
625
+
let row_diacritic = diacritic
626
+
let column_diacritic = diacritic
627
+
let id_high_byte_diacritic = diacritic
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 *)
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)
631
+
let put = Buffer.add_char buf in
632
+
if code < 0x80 then put (Char.chr code)
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)
634
+
put (Char.chr (0xC0 lor (code lsr 6)));
635
+
put (Char.chr (0x80 lor (code land 0x3F))))
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)
637
+
put (Char.chr (0xE0 lor (code lsr 12)));
638
+
put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
639
+
put (Char.chr (0x80 lor (code land 0x3F))))
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)
641
+
put (Char.chr (0xF0 lor (code lsr 18)));
642
+
put (Char.chr (0x80 lor ((code lsr 12) land 0x3F)));
643
+
put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
644
+
put (Char.chr (0x80 lor (code land 0x3F))))
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 *)
647
+
(* Set foreground color *)
648
+
Printf.bprintf buf "\027[38;2;%d;%d;%dm"
649
+
((image_id lsr 16) land 0xFF)
650
+
((image_id lsr 8) land 0xFF)
651
+
(image_id land 0xFF);
652
+
(* Optional placement ID in underline color *)
654
+
|> Option.iter (fun pid ->
655
+
Printf.bprintf buf "\027[58;2;%d;%d;%dm"
656
+
((pid lsr 16) land 0xFF)
657
+
((pid lsr 8) land 0xFF)
659
+
(* High byte diacritic *)
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 *)
661
+
let high_diac = if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None in
for row = 0 to rows - 1 do
for col = 0 to cols - 1 do
add_uchar buf placeholder_char;
add_uchar buf (row_diacritic row);
add_uchar buf (column_diacritic col);
876
-
Option.iter (add_uchar buf) high_diac
668
+
high_diac |> Option.iter (add_uchar buf)
if row < rows - 1 then Buffer.add_string buf "\n\r"
Buffer.add_string buf "\027[39m";
882
-
match placement_id with Some _ -> Buffer.add_string buf "\027[59m" | None -> ()
674
+
if Option.is_some placement_id then Buffer.add_string buf "\027[59m"
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 *)
679
+
let cmd = Command.query ~format:`Rgb24 ~transmission:`Direct ~width:1 ~height:1 () in
680
+
Command.to_string cmd ~data:"\x00\x00\x00" ^ "\027[c"
let supports_graphics response ~da1_received =
898
-
match response with
899
-
| Some r -> Response.is_ok r
900
-
| None -> not da1_received
683
+
response |> Option.map Response.is_ok |> Option.value ~default:(not da1_received)