···11-(* Example usage of the Kitty Graphics Protocol library *)
11+(* Kitty Graphics Protocol Demo - Matching kgp/examples/demo workflow *)
2233-(* Create a 64x64 colorful gradient image in RGBA format *)
44-let test_rgba_image () =
55- let size = 64 in
66- let pixels = Bytes.create (size * size * 4) in
77- for y = 0 to size - 1 do
88- for x = 0 to size - 1 do
99- let offset = (y * size + x) * 4 in
1010- (* Red gradient left to right *)
1111- Bytes.set pixels offset (Char.chr (x * 4 land 0xFF));
1212- (* Green gradient top to bottom *)
1313- Bytes.set pixels (offset + 1) (Char.chr (y * 4 land 0xFF));
1414- (* Blue diagonal gradient *)
1515- Bytes.set pixels (offset + 2) (Char.chr ((x + y) * 2 land 0xFF));
1616- (* Fully opaque *)
1717- Bytes.set pixels (offset + 3) '\xff'
1818- done
33+module K = Kitty_graphics
44+55+(* Generate a solid color RGBA frame *)
66+let make_solid_frame ~width ~height ~r ~g ~b =
77+ let pixels = Bytes.create (width * height * 4) in
88+ for i = 0 to (width * height) - 1 do
99+ let idx = i * 4 in
1010+ Bytes.set pixels idx (Char.chr r);
1111+ Bytes.set pixels (idx + 1) (Char.chr g);
1212+ Bytes.set pixels (idx + 2) (Char.chr b);
1313+ Bytes.set pixels (idx + 3) '\xff'
1914 done;
2020- (size, Bytes.to_string pixels)
1515+ Bytes.to_string pixels
1616+1717+let send cmd ~data =
1818+ print_string (K.Command.to_string cmd ~data);
1919+ flush stdout
2020+2121+let wait_for_enter () =
2222+ print_string "Press Enter to continue...";
2323+ flush stdout;
2424+ let _ = read_line () in
2525+ print_newline ()
2626+2727+let clear_screen () =
2828+ print_string "\x1b[2J\x1b[H";
2929+ for _ = 1 to 5 do print_newline () done;
3030+ flush stdout
21312232let () =
2323- print_endline "Kitty Graphics Protocol Example";
2424- print_endline "================================";
3333+ clear_screen ();
3434+ print_endline "Kitty Graphics Protocol - OCaml Demo";
3535+ print_endline "=====================================";
3636+ print_newline ();
3737+ print_endline "Press Enter to proceed through each demo...";
2538 print_newline ();
3939+ wait_for_enter ();
26402727- (* Example 1: Display a simple RGBA image *)
2828- print_endline "1. Displaying a 64x64 RGBA gradient image:";
4141+ (* Demo 1: Basic RGBA format *)
4242+ clear_screen ();
4343+ print_endline "Demo 1: Image Format - RGBA (32-bit)";
4444+ let blue_data = make_solid_frame ~width:100 ~height:100 ~r:0 ~g:0 ~b:255 in
4545+ send
4646+ (K.Command.transmit_and_display
4747+ ~image_id:1
4848+ ~format:`Rgba32
4949+ ~width:100 ~height:100
5050+ ~quiet:`Errors_only
5151+ ())
5252+ ~data:blue_data;
5353+ print_endline "Blue square displayed using raw RGBA format";
2954 print_newline ();
3030- flush stdout;
5555+ wait_for_enter ();
31563232- let (size, image_data) = test_rgba_image () in
3333- let cmd =
3434- Kitty_graphics.Command.transmit_and_display
3535- ~format:Kitty_graphics.Format.Rgba32
3636- ~width:size ~height:size
3737- ()
5757+ (* Demo 2: Basic RGB format *)
5858+ clear_screen ();
5959+ print_endline "Demo 2: Image Format - RGB (24-bit)";
6060+ (* RGB is 3 bytes per pixel *)
6161+ let green_rgb =
6262+ let pixels = Bytes.create (100 * 100 * 3) in
6363+ for i = 0 to (100 * 100) - 1 do
6464+ let idx = i * 3 in
6565+ Bytes.set pixels idx '\x00'; (* R *)
6666+ Bytes.set pixels (idx + 1) '\xff'; (* G *)
6767+ Bytes.set pixels (idx + 2) '\x00' (* B *)
6868+ done;
6969+ Bytes.to_string pixels
3870 in
3939- let buf = Buffer.create 4096 in
4040- Kitty_graphics.Command.write buf cmd ~data:image_data;
4141- print_string (Buffer.contents buf);
4242- flush stdout;
7171+ send
7272+ (K.Command.transmit_and_display
7373+ ~image_id:2
7474+ ~format:`Rgb24
7575+ ~width:100 ~height:100
7676+ ~quiet:`Errors_only
7777+ ())
7878+ ~data:green_rgb;
7979+ print_endline "Green square displayed using raw RGB format (no alpha)";
4380 print_newline ();
8181+ wait_for_enter ();
8282+8383+ (* Demo 3: Multiple placements - transmit once, display multiple times *)
8484+ clear_screen ();
8585+ print_endline "Demo 3: Multiple Placements";
8686+ let cyan_data = make_solid_frame ~width:80 ~height:80 ~r:0 ~g:255 ~b:255 in
8787+ (* Transmit only (a=t) *)
8888+ send
8989+ (K.Command.transmit
9090+ ~image_id:100
9191+ ~format:`Rgba32
9292+ ~width:80 ~height:80
9393+ ~quiet:`Errors_only
9494+ ())
9595+ ~data:cyan_data;
9696+ (* Display first placement *)
9797+ send
9898+ (K.Command.display
9999+ ~image_id:100
100100+ ~placement:(K.Placement.make ~columns:10 ~rows:5 ())
101101+ ~quiet:`Errors_only
102102+ ())
103103+ ~data:"";
104104+ print_string " ";
105105+ (* Display second placement *)
106106+ send
107107+ (K.Command.display
108108+ ~image_id:100
109109+ ~placement:(K.Placement.make ~columns:5 ~rows:3 ())
110110+ ~quiet:`Errors_only
111111+ ())
112112+ ~data:"";
44113 print_newline ();
114114+ print_endline "Same image displayed twice at different sizes";
115115+ print_newline ();
116116+ wait_for_enter ();
451174646- (* Example 2: Display scaled to specific cell size *)
4747- print_endline "2. Same image scaled to 20 columns x 10 rows:";
118118+ (* Demo 4: Z-index layering *)
119119+ clear_screen ();
120120+ print_endline "Demo 4: Z-Index Layering";
121121+ let orange_data = make_solid_frame ~width:200 ~height:100 ~r:255 ~g:165 ~b:0 in
122122+ send
123123+ (K.Command.transmit_and_display
124124+ ~image_id:200
125125+ ~format:`Rgba32
126126+ ~width:200 ~height:100
127127+ ~placement:(K.Placement.make ~z_index:(-1) ~cursor:`Static ())
128128+ ~quiet:`Errors_only
129129+ ())
130130+ ~data:orange_data;
131131+ print_endline "This orange square should appear behind the text!";
48132 print_newline ();
4949- flush stdout;
133133+ wait_for_enter ();
501345151- let placement =
5252- Kitty_graphics.Placement.make ~columns:20 ~rows:10 ()
5353- in
5454- let cmd =
5555- Kitty_graphics.Command.transmit_and_display
5656- ~format:Kitty_graphics.Format.Rgba32
5757- ~width:size ~height:size
5858- ~placement
5959- ()
6060- in
6161- Buffer.clear buf;
6262- Kitty_graphics.Command.write buf cmd ~data:image_data;
6363- print_string (Buffer.contents buf);
135135+ (* Demo 5: Animation - matching kgp demo exactly *)
136136+ clear_screen ();
137137+ print_endline "Demo 5: Animation - Color-changing square";
138138+ print_endline "Creating animated sequence...";
64139 flush stdout;
6565- print_newline ();
6666- print_newline ();
671406868- (* Example 3: Query terminal support *)
6969- print_endline "3. Query command (to test graphics support):";
7070- let query = Kitty_graphics.Detect.make_query () in
7171- Printf.printf " Query escape sequence: %S\n" query;
7272- print_newline ();
141141+ (* Using small size to avoid chunking - 10x10 = 400 bytes raw *)
142142+ let width, height = 10, 10 in
143143+ let image_id = 300 in
731447474- (* Example 4: Delete command *)
7575- print_endline "4. Delete all visible images:";
7676- let del_cmd =
7777- Kitty_graphics.Command.delete Kitty_graphics.Delete.All_visible
7878- in
7979- Buffer.clear buf;
8080- Kitty_graphics.Command.write buf del_cmd ~data:"";
8181- Printf.printf " Delete escape sequence: %S\n" (Buffer.contents buf);
8282- print_newline ();
145145+ (* Step 1: Create base frame (red) - transmit only, don't display yet *)
146146+ let red_frame = make_solid_frame ~width ~height ~r:255 ~g:0 ~b:0 in
147147+ send
148148+ (K.Command.transmit
149149+ ~image_id
150150+ ~format:`Rgba32
151151+ ~width ~height
152152+ ~quiet:`Errors_only
153153+ ())
154154+ ~data:red_frame;
155155+156156+ (* Step 2: Add frame 2 (orange) with gap and composition replace *)
157157+ let orange_frame = make_solid_frame ~width ~height ~r:255 ~g:165 ~b:0 in
158158+ send
159159+ (K.Command.frame
160160+ ~image_id
161161+ ~format:`Rgba32
162162+ ~width ~height
163163+ ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
164164+ ~quiet:`Errors_only
165165+ ())
166166+ ~data:orange_frame;
831678484- (* Example 5: Unicode placeholder *)
8585- print_endline "5. Unicode placeholder (for tmux compatibility):";
8686- print_newline ();
8787- Buffer.clear buf;
8888- Kitty_graphics.Unicode_placeholder.write buf ~image_id:42 ~rows:2 ~cols:4 ();
8989- print_string (Buffer.contents buf);
168168+ (* Step 3: Add frame 3 (yellow) *)
169169+ let yellow_frame = make_solid_frame ~width ~height ~r:255 ~g:255 ~b:0 in
170170+ send
171171+ (K.Command.frame
172172+ ~image_id
173173+ ~format:`Rgba32
174174+ ~width ~height
175175+ ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
176176+ ~quiet:`Errors_only
177177+ ())
178178+ ~data:yellow_frame;
179179+180180+ (* Step 4: Add frame 4 (green) *)
181181+ let green_frame = make_solid_frame ~width ~height ~r:0 ~g:255 ~b:0 in
182182+ send
183183+ (K.Command.frame
184184+ ~image_id
185185+ ~format:`Rgba32
186186+ ~width ~height
187187+ ~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
188188+ ~quiet:`Errors_only
189189+ ())
190190+ ~data:green_frame;
191191+192192+ (* Step 5: Create placement to display the animation *)
193193+ (* Add columns/rows to scale up the small image for visibility *)
194194+ send
195195+ (K.Command.display
196196+ ~image_id
197197+ ~placement:(K.Placement.make
198198+ ~placement_id:1
199199+ ~columns:10
200200+ ~rows:5
201201+ ~cursor:`Static
202202+ ())
203203+ ~quiet:`Errors_only
204204+ ())
205205+ ~data:"";
206206+207207+ (* Step 6: Start animation with infinite looping (s=3, v=1) *)
208208+ send
209209+ (K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run))
210210+ ~data:"";
211211+90212 print_newline ();
213213+ print_endline "Animation playing: Red -> Orange -> Yellow -> Green";
91214 print_newline ();
215215+ wait_for_enter ();
922169393- (* Example 6: Parse a response *)
9494- print_endline "6. Parsing terminal responses:";
9595- let test_response = "\027_Gi=123;OK\027\\" in
9696- (match Kitty_graphics.Response.parse test_response with
9797- | Some r ->
9898- Printf.printf " Parsed response: is_ok=%b, image_id=%s\n"
9999- (Kitty_graphics.Response.is_ok r)
100100- (match Kitty_graphics.Response.image_id r with
101101- | Some id -> string_of_int id
102102- | None -> "none")
103103- | None -> print_endline " Failed to parse");
217217+ (* Stop the animation *)
218218+ send
219219+ (K.Command.animate ~image_id (K.Animation.set_state `Stop))
220220+ ~data:"";
104221105105- let error_response = "\027_Gi=456;ENOENT:Image not found\027\\" in
106106- (match Kitty_graphics.Response.parse error_response with
107107- | Some r ->
108108- Printf.printf " Error response: code=%s, message=%s\n"
109109- (match Kitty_graphics.Response.error_code r with
110110- | Some c -> c
111111- | None -> "none")
112112- (Kitty_graphics.Response.message r)
113113- | None -> print_endline " Failed to parse");
222222+ print_endline "Animation stopped.";
223223+ print_newline ();
114224115115- print_newline ();
116116- print_endline "Done!"
225225+ (* Cleanup *)
226226+ print_endline "Demo complete!";
227227+ ()
+327-544
stack/kitty_graphics/lib/kitty_graphics.ml
···11(* Kitty Terminal Graphics Protocol - Implementation *)
2233+(* Polymorphic variant types *)
44+type format = [ `Rgba32 | `Rgb24 | `Png ]
55+type transmission = [ `Direct | `File | `Tempfile ]
66+type compression = [ `None | `Zlib ]
77+type quiet = [ `Noisy | `Errors_only | `Silent ]
88+type cursor = [ `Move | `Static ]
99+type composition = [ `Alpha_blend | `Overwrite ]
1010+1111+type delete =
1212+ [ `All_visible
1313+ | `All_visible_and_free
1414+ | `By_id of int * int option
1515+ | `By_id_and_free of int * int option
1616+ | `By_number of int * int option
1717+ | `By_number_and_free of int * int option
1818+ | `At_cursor
1919+ | `At_cursor_and_free
2020+ | `At_cell of int * int
2121+ | `At_cell_and_free of int * int
2222+ | `At_cell_z of int * int * int
2323+ | `At_cell_z_and_free of int * int * int
2424+ | `By_column of int
2525+ | `By_column_and_free of int
2626+ | `By_row of int
2727+ | `By_row_and_free of int
2828+ | `By_z_index of int
2929+ | `By_z_index_and_free of int
3030+ | `By_id_range of int * int
3131+ | `By_id_range_and_free of int * int
3232+ | `Frames
3333+ | `Frames_and_free ]
3434+3535+type animation_state = [ `Stop | `Loading | `Run ]
3636+3737+(* Modules re-export the types with conversion functions *)
338module Format = struct
44- type t = Rgba32 | Rgb24 | Png
3939+ type t = format
54066- let to_int = function Rgba32 -> 32 | Rgb24 -> 24 | Png -> 100
4141+ let to_int : t -> int = function
4242+ | `Rgba32 -> 32
4343+ | `Rgb24 -> 24
4444+ | `Png -> 100
745end
846947module Transmission = struct
1010- type t = Direct | File | Tempfile
4848+ type t = transmission
11491212- let to_char = function Direct -> 'd' | File -> 'f' | Tempfile -> 't'
5050+ let to_char : t -> char = function
5151+ | `Direct -> 'd'
5252+ | `File -> 'f'
5353+ | `Tempfile -> 't'
1354end
14551556module Compression = struct
1616- type t = None | Zlib
5757+ type t = compression
17581818- let to_char = function None -> Option.none | Zlib -> Some 'z'
5959+ let to_char : t -> char option = function
6060+ | `None -> None
6161+ | `Zlib -> Some 'z'
1962end
20632164module Quiet = struct
2222- type t = Noisy | Errors_only | Silent
6565+ type t = quiet
23662424- let to_int = function Noisy -> 0 | Errors_only -> 1 | Silent -> 2
6767+ let to_int : t -> int = function
6868+ | `Noisy -> 0
6969+ | `Errors_only -> 1
7070+ | `Silent -> 2
2571end
26722773module Cursor = struct
2828- type t = Move | Static
7474+ type t = cursor
29753030- let to_int = function Move -> 0 | Static -> 1
7676+ let to_int : t -> int = function
7777+ | `Move -> 0
7878+ | `Static -> 1
3179end
32803381module Composition = struct
3434- type t = Alpha_blend | Overwrite
8282+ type t = composition
35833636- let to_int = function Alpha_blend -> 0 | Overwrite -> 1
8484+ let to_int : t -> int = function
8585+ | `Alpha_blend -> 0
8686+ | `Overwrite -> 1
3787end
38883989module Delete = struct
4040- type t =
4141- | All_visible
4242- | All_visible_and_free
4343- | By_id of { image_id : int; placement_id : int option }
4444- | By_id_and_free of { image_id : int; placement_id : int option }
4545- | By_number of { image_number : int; placement_id : int option }
4646- | By_number_and_free of { image_number : int; placement_id : int option }
4747- | At_cursor
4848- | At_cursor_and_free
4949- | At_cell of { x : int; y : int }
5050- | At_cell_and_free of { x : int; y : int }
5151- | At_cell_z of { x : int; y : int; z : int }
5252- | At_cell_z_and_free of { x : int; y : int; z : int }
5353- | By_column of int
5454- | By_column_and_free of int
5555- | By_row of int
5656- | By_row_and_free of int
5757- | By_z_index of int
5858- | By_z_index_and_free of int
5959- | By_id_range of { min_id : int; max_id : int }
6060- | By_id_range_and_free of { min_id : int; max_id : int }
6161- | Frames
6262- | Frames_and_free
9090+ type t = delete
6391end
64926593module Placement = struct
···74102 rows : int option;
75103 z_index : int option;
76104 placement_id : int option;
7777- cursor : Cursor.t option;
105105+ cursor : cursor option;
78106 unicode_placeholder : bool;
79107 }
80108···120148 base_frame : int option;
121149 edit_frame : int option;
122150 gap_ms : int option;
123123- composition : Composition.t option;
151151+ composition : composition option;
124152 background_color : int32 option;
125153 }
126154···141169end
142170143171module Animation = struct
144144- type state = Stop | Loading | Run
172172+ type state = animation_state
145173146174 type t =
147147- | Set_state of { state : state; loops : int option }
148148- | Set_gap of { frame : int; gap_ms : int }
149149- | Set_current of int
175175+ [ `Set_state of state * int option
176176+ | `Set_gap of int * int
177177+ | `Set_current of int ]
150178151151- let set_state ?loops state = Set_state { state; loops }
152152- let set_gap ~frame ~gap_ms = Set_gap { frame; gap_ms }
153153- let set_current_frame frame = Set_current frame
179179+ let set_state ?loops state = `Set_state (state, loops)
180180+ let set_gap ~frame ~gap_ms = `Set_gap (frame, gap_ms)
181181+ let set_current_frame frame = `Set_current frame
154182end
155183156184module Compose = struct
···163191 source_y : int option;
164192 dest_x : int option;
165193 dest_y : int option;
166166- composition : Composition.t option;
194194+ composition : composition option;
167195 }
168196169197 let make ~source_frame ~dest_frame ?width ?height ?source_x ?source_y ?dest_x
···183211184212module Command = struct
185213 type action =
186186- | Transmit
187187- | Transmit_and_display
188188- | Query
189189- | Display
190190- | Delete
191191- | Frame
192192- | Animate
193193- | Compose
214214+ [ `Transmit
215215+ | `Transmit_and_display
216216+ | `Query
217217+ | `Display
218218+ | `Delete
219219+ | `Frame
220220+ | `Animate
221221+ | `Compose ]
194222195223 type t = {
196224 action : action;
197197- format : Format.t option;
198198- transmission : Transmission.t option;
199199- compression : Compression.t option;
225225+ format : format option;
226226+ transmission : transmission option;
227227+ compression : compression option;
200228 width : int option;
201229 height : int option;
202230 size : int option;
203231 offset : int option;
204204- quiet : Quiet.t option;
232232+ quiet : quiet option;
205233 image_id : int option;
206234 image_number : int option;
207235 placement : Placement.t option;
208208- delete : Delete.t option;
236236+ delete : delete option;
209237 frame : Frame.t option;
210238 animation : Animation.t option;
211239 compose : Compose.t option;
212240 }
213241214214- let make_base action =
242242+ let make action =
215243 {
216244 action;
217245 format = None;
···234262 let transmit ?image_id ?image_number ?format ?transmission ?compression ?width
235263 ?height ?size ?offset ?quiet () =
236264 {
237237- (make_base Transmit) with
265265+ (make `Transmit) with
238266 image_id;
239267 image_number;
240268 format;
···250278 let transmit_and_display ?image_id ?image_number ?format ?transmission
251279 ?compression ?width ?height ?size ?offset ?quiet ?placement () =
252280 {
253253- (make_base Transmit_and_display) with
281281+ (make `Transmit_and_display) with
254282 image_id;
255283 image_number;
256284 format;
···265293 }
266294267295 let query ?format ?transmission ?width ?height ?quiet () =
268268- { (make_base Query) with format; transmission; width; height; quiet }
296296+ { (make `Query) with format; transmission; width; height; quiet }
269297270298 let display ?image_id ?image_number ?placement ?quiet () =
271271- { (make_base Display) with image_id; image_number; placement; quiet }
299299+ { (make `Display) with image_id; image_number; placement; quiet }
272300273273- let delete ?quiet del =
274274- { (make_base Delete) with quiet; delete = Some del }
301301+ let delete ?quiet del = { (make `Delete) with quiet; delete = Some del }
275302276303 let frame ?image_id ?image_number ?format ?transmission ?compression ?width
277304 ?height ?quiet ~frame () =
278305 {
279279- (make_base Frame) with
306306+ (make `Frame) with
280307 image_id;
281308 image_number;
282309 format;
···289316 }
290317291318 let animate ?image_id ?image_number ?quiet anim =
292292- { (make_base Animate) with image_id; image_number; quiet; animation = Some anim }
319319+ { (make `Animate) with image_id; image_number; quiet; animation = Some anim }
293320294321 let compose ?image_id ?image_number ?quiet comp =
295295- { (make_base Compose) with image_id; image_number; quiet; compose = Some comp }
322322+ { (make `Compose) with image_id; image_number; quiet; compose = Some comp }
296323297297- (* APC escape sequences *)
324324+ (* Serialization helpers *)
298325 let apc_start = "\027_G"
299326 let apc_end = "\027\\"
300327301301- (* Helper to add key=value pairs *)
302302- let add_kv buf key value =
303303- Buffer.add_char buf key;
304304- Buffer.add_char buf '=';
305305- Buffer.add_string buf value
328328+ (* Key-value writer with separator handling *)
329329+ type kv_writer = { mutable first : bool; buf : Buffer.t }
306330307307- let add_kv_int buf key value =
308308- Buffer.add_char buf key;
309309- Buffer.add_char buf '=';
310310- Buffer.add_string buf (string_of_int value)
331331+ let kv_writer buf = { first = true; buf }
311332312312- let add_kv_int32 buf key value =
313313- Buffer.add_char buf key;
314314- Buffer.add_char buf '=';
315315- Buffer.add_string buf (Int32.to_string value)
333333+ let kv w key value =
334334+ if not w.first then Buffer.add_char w.buf ',';
335335+ w.first <- false;
336336+ Buffer.add_char w.buf key;
337337+ Buffer.add_char w.buf '=';
338338+ Buffer.add_string w.buf value
316339317317- let add_comma buf = Buffer.add_char buf ','
340340+ let kv_int w key value = kv w key (string_of_int value)
341341+ let kv_int32 w key value = kv w key (Int32.to_string value)
342342+ let kv_char w key value = kv w key (String.make 1 value)
318343319319- let action_char = function
320320- | Transmit -> 't'
321321- | Transmit_and_display -> 'T'
322322- | Query -> 'q'
323323- | Display -> 'p'
324324- | Delete -> 'd'
325325- | Frame -> 'f'
326326- | Animate -> 'a'
327327- | Compose -> 'c'
344344+ (* Conditional writers using Option.iter *)
345345+ let kv_int_opt w key = Option.iter (kv_int w key)
346346+ let kv_int32_opt w key = Option.iter (kv_int32 w key)
328347329329- let delete_char = function
330330- | Delete.All_visible -> 'a'
331331- | All_visible_and_free -> 'A'
332332- | By_id _ -> 'i'
333333- | By_id_and_free _ -> 'I'
334334- | By_number _ -> 'n'
335335- | By_number_and_free _ -> 'N'
336336- | At_cursor -> 'c'
337337- | At_cursor_and_free -> 'C'
338338- | At_cell _ -> 'p'
339339- | At_cell_and_free _ -> 'P'
340340- | At_cell_z _ -> 'q'
341341- | At_cell_z_and_free _ -> 'Q'
342342- | By_column _ -> 'x'
343343- | By_column_and_free _ -> 'X'
344344- | By_row _ -> 'y'
345345- | By_row_and_free _ -> 'Y'
346346- | By_z_index _ -> 'z'
347347- | By_z_index_and_free _ -> 'Z'
348348- | By_id_range _ -> 'r'
349349- | By_id_range_and_free _ -> 'R'
350350- | Frames -> 'f'
351351- | Frames_and_free -> 'F'
348348+ let kv_int_if w key ~default opt =
349349+ Option.iter (fun v -> if v <> default then kv_int w key v) opt
350350+351351+ let action_char : action -> char = function
352352+ | `Transmit -> 't'
353353+ | `Transmit_and_display -> 'T'
354354+ | `Query -> 'q'
355355+ | `Display -> 'p'
356356+ | `Delete -> 'd'
357357+ | `Frame -> 'f'
358358+ | `Animate -> 'a'
359359+ | `Compose -> 'c'
360360+361361+ let delete_char : delete -> char = function
362362+ | `All_visible -> 'a'
363363+ | `All_visible_and_free -> 'A'
364364+ | `By_id _ -> 'i'
365365+ | `By_id_and_free _ -> 'I'
366366+ | `By_number _ -> 'n'
367367+ | `By_number_and_free _ -> 'N'
368368+ | `At_cursor -> 'c'
369369+ | `At_cursor_and_free -> 'C'
370370+ | `At_cell _ -> 'p'
371371+ | `At_cell_and_free _ -> 'P'
372372+ | `At_cell_z _ -> 'q'
373373+ | `At_cell_z_and_free _ -> 'Q'
374374+ | `By_column _ -> 'x'
375375+ | `By_column_and_free _ -> 'X'
376376+ | `By_row _ -> 'y'
377377+ | `By_row_and_free _ -> 'Y'
378378+ | `By_z_index _ -> 'z'
379379+ | `By_z_index_and_free _ -> 'Z'
380380+ | `By_id_range _ -> 'r'
381381+ | `By_id_range_and_free _ -> 'R'
382382+ | `Frames -> 'f'
383383+ | `Frames_and_free -> 'F'
384384+385385+ let write_placement w (p : Placement.t) =
386386+ kv_int_opt w 'x' p.source_x;
387387+ kv_int_opt w 'y' p.source_y;
388388+ kv_int_opt w 'w' p.source_width;
389389+ kv_int_opt w 'h' p.source_height;
390390+ kv_int_opt w 'X' p.cell_x_offset;
391391+ kv_int_opt w 'Y' p.cell_y_offset;
392392+ kv_int_opt w 'c' p.columns;
393393+ kv_int_opt w 'r' p.rows;
394394+ kv_int_opt w 'z' p.z_index;
395395+ kv_int_opt w 'p' p.placement_id;
396396+ p.cursor |> Option.iter (fun c -> kv_int_if w 'C' ~default:0 (Some (Cursor.to_int c)));
397397+ if p.unicode_placeholder then kv_int w 'U' 1
398398+399399+ let write_delete w (d : delete) =
400400+ kv_char w 'd' (delete_char d);
401401+ match d with
402402+ | `By_id (id, pid) | `By_id_and_free (id, pid) ->
403403+ kv_int w 'i' id;
404404+ kv_int_opt w 'p' pid
405405+ | `By_number (n, pid) | `By_number_and_free (n, pid) ->
406406+ kv_int w 'I' n;
407407+ kv_int_opt w 'p' pid
408408+ | `At_cell (x, y) | `At_cell_and_free (x, y) ->
409409+ kv_int w 'x' x;
410410+ kv_int w 'y' y
411411+ | `At_cell_z (x, y, z) | `At_cell_z_and_free (x, y, z) ->
412412+ kv_int w 'x' x;
413413+ kv_int w 'y' y;
414414+ kv_int w 'z' z
415415+ | `By_column c | `By_column_and_free c -> kv_int w 'x' c
416416+ | `By_row r | `By_row_and_free r -> kv_int w 'y' r
417417+ | `By_z_index z | `By_z_index_and_free z -> kv_int w 'z' z
418418+ | `By_id_range (min_id, max_id) | `By_id_range_and_free (min_id, max_id) ->
419419+ kv_int w 'x' min_id;
420420+ kv_int w 'y' max_id
421421+ | `All_visible | `All_visible_and_free | `At_cursor | `At_cursor_and_free
422422+ | `Frames | `Frames_and_free ->
423423+ ()
424424+425425+ let write_frame w (f : Frame.t) =
426426+ kv_int_opt w 'x' f.x;
427427+ kv_int_opt w 'y' f.y;
428428+ kv_int_opt w 'c' f.base_frame;
429429+ kv_int_opt w 'r' f.edit_frame;
430430+ kv_int_opt w 'z' f.gap_ms;
431431+ f.composition
432432+ |> Option.iter (fun c -> kv_int_if w 'X' ~default:0 (Some (Composition.to_int c)));
433433+ kv_int32_opt w 'Y' f.background_color
434434+435435+ let write_animation w : Animation.t -> unit = function
436436+ | `Set_state (state, loops) ->
437437+ let s = match state with `Stop -> 1 | `Loading -> 2 | `Run -> 3 in
438438+ kv_int w 's' s;
439439+ kv_int_opt w 'v' loops
440440+ | `Set_gap (frame, gap_ms) ->
441441+ kv_int w 'r' frame;
442442+ kv_int w 'z' gap_ms
443443+ | `Set_current frame -> kv_int w 'c' frame
444444+445445+ let write_compose w (c : Compose.t) =
446446+ kv_int w 'r' c.source_frame;
447447+ kv_int w 'c' c.dest_frame;
448448+ kv_int_opt w 'w' c.width;
449449+ kv_int_opt w 'h' c.height;
450450+ kv_int_opt w 'x' c.dest_x;
451451+ kv_int_opt w 'y' c.dest_y;
452452+ kv_int_opt w 'X' c.source_x;
453453+ kv_int_opt w 'Y' c.source_y;
454454+ c.composition
455455+ |> Option.iter (fun comp -> kv_int_if w 'C' ~default:0 (Some (Composition.to_int comp)))
352456353457 let write_control_data buf cmd =
354354- let first = ref true in
355355- let sep () =
356356- if !first then first := false else add_comma buf
357357- in
458458+ let w = kv_writer buf in
358459 (* Action *)
359359- sep ();
360360- add_kv buf 'a' (String.make 1 (action_char cmd.action));
361361- (* Quiet *)
362362- Option.iter
363363- (fun q ->
364364- let v = Quiet.to_int q in
365365- if v <> 0 then (
366366- sep ();
367367- add_kv_int buf 'q' v))
368368- cmd.quiet;
460460+ kv_char w 'a' (action_char cmd.action);
461461+ (* Quiet - only if non-default *)
462462+ cmd.quiet |> Option.iter (fun q -> kv_int_if w 'q' ~default:0 (Some (Quiet.to_int q)));
369463 (* Format *)
370370- Option.iter
371371- (fun f ->
372372- sep ();
373373- add_kv_int buf 'f' (Format.to_int f))
374374- cmd.format;
375375- (* Transmission *)
376376- Option.iter
377377- (fun t ->
378378- let c = Transmission.to_char t in
379379- if c <> 'd' then (
380380- sep ();
381381- add_kv buf 't' (String.make 1 c)))
382382- cmd.transmission;
464464+ cmd.format |> Option.iter (fun f -> kv_int w 'f' (Format.to_int f));
465465+ (* Transmission - only if non-default *)
466466+ cmd.transmission
467467+ |> Option.iter (fun t ->
468468+ let c = Transmission.to_char t in
469469+ if c <> 'd' then kv_char w 't' c);
383470 (* Compression *)
384384- Option.iter
385385- (fun c ->
386386- match Compression.to_char c with
387387- | Some ch ->
388388- sep ();
389389- add_kv buf 'o' (String.make 1 ch)
390390- | None -> ())
391391- cmd.compression;
471471+ cmd.compression |> Option.iter (fun c -> Compression.to_char c |> Option.iter (kv_char w 'o'));
392472 (* Dimensions *)
393393- Option.iter
394394- (fun w ->
395395- sep ();
396396- add_kv_int buf 's' w)
397397- cmd.width;
398398- Option.iter
399399- (fun h ->
400400- sep ();
401401- add_kv_int buf 'v' h)
402402- cmd.height;
473473+ kv_int_opt w 's' cmd.width;
474474+ kv_int_opt w 'v' cmd.height;
403475 (* File size/offset *)
404404- Option.iter
405405- (fun s ->
406406- sep ();
407407- add_kv_int buf 'S' s)
408408- cmd.size;
409409- Option.iter
410410- (fun o ->
411411- sep ();
412412- add_kv_int buf 'O' o)
413413- cmd.offset;
414414- (* Image ID *)
415415- Option.iter
416416- (fun id ->
417417- sep ();
418418- add_kv_int buf 'i' id)
419419- cmd.image_id;
420420- (* Image number *)
421421- Option.iter
422422- (fun n ->
423423- sep ();
424424- add_kv_int buf 'I' n)
425425- cmd.image_number;
426426- (* Placement options *)
427427- Option.iter
428428- (fun (p : Placement.t) ->
429429- Option.iter
430430- (fun v ->
431431- sep ();
432432- add_kv_int buf 'x' v)
433433- p.source_x;
434434- Option.iter
435435- (fun v ->
436436- sep ();
437437- add_kv_int buf 'y' v)
438438- p.source_y;
439439- Option.iter
440440- (fun v ->
441441- sep ();
442442- add_kv_int buf 'w' v)
443443- p.source_width;
444444- Option.iter
445445- (fun v ->
446446- sep ();
447447- add_kv_int buf 'h' v)
448448- p.source_height;
449449- Option.iter
450450- (fun v ->
451451- sep ();
452452- add_kv_int buf 'X' v)
453453- p.cell_x_offset;
454454- Option.iter
455455- (fun v ->
456456- sep ();
457457- add_kv_int buf 'Y' v)
458458- p.cell_y_offset;
459459- Option.iter
460460- (fun v ->
461461- sep ();
462462- add_kv_int buf 'c' v)
463463- p.columns;
464464- Option.iter
465465- (fun v ->
466466- sep ();
467467- add_kv_int buf 'r' v)
468468- p.rows;
469469- Option.iter
470470- (fun v ->
471471- sep ();
472472- add_kv_int buf 'z' v)
473473- p.z_index;
474474- Option.iter
475475- (fun v ->
476476- sep ();
477477- add_kv_int buf 'p' v)
478478- p.placement_id;
479479- Option.iter
480480- (fun c ->
481481- let v = Cursor.to_int c in
482482- if v <> 0 then (
483483- sep ();
484484- add_kv_int buf 'C' v))
485485- p.cursor;
486486- if p.unicode_placeholder then (
487487- sep ();
488488- add_kv_int buf 'U' 1))
489489- cmd.placement;
490490- (* Delete options *)
491491- Option.iter
492492- (fun d ->
493493- sep ();
494494- add_kv buf 'd' (String.make 1 (delete_char d));
495495- match d with
496496- | Delete.By_id { image_id; placement_id }
497497- | Delete.By_id_and_free { image_id; placement_id } ->
498498- sep ();
499499- add_kv_int buf 'i' image_id;
500500- Option.iter
501501- (fun p ->
502502- sep ();
503503- add_kv_int buf 'p' p)
504504- placement_id
505505- | Delete.By_number { image_number; placement_id }
506506- | Delete.By_number_and_free { image_number; placement_id } ->
507507- sep ();
508508- add_kv_int buf 'I' image_number;
509509- Option.iter
510510- (fun p ->
511511- sep ();
512512- add_kv_int buf 'p' p)
513513- placement_id
514514- | Delete.At_cell { x; y } | Delete.At_cell_and_free { x; y } ->
515515- sep ();
516516- add_kv_int buf 'x' x;
517517- sep ();
518518- add_kv_int buf 'y' y
519519- | Delete.At_cell_z { x; y; z }
520520- | Delete.At_cell_z_and_free { x; y; z } ->
521521- sep ();
522522- add_kv_int buf 'x' x;
523523- sep ();
524524- add_kv_int buf 'y' y;
525525- sep ();
526526- add_kv_int buf 'z' z
527527- | Delete.By_column c | Delete.By_column_and_free c ->
528528- sep ();
529529- add_kv_int buf 'x' c
530530- | Delete.By_row r | Delete.By_row_and_free r ->
531531- sep ();
532532- add_kv_int buf 'y' r
533533- | Delete.By_z_index z | Delete.By_z_index_and_free z ->
534534- sep ();
535535- add_kv_int buf 'z' z
536536- | Delete.By_id_range { min_id; max_id }
537537- | Delete.By_id_range_and_free { min_id; max_id } ->
538538- sep ();
539539- add_kv_int buf 'x' min_id;
540540- sep ();
541541- add_kv_int buf 'y' max_id
542542- | _ -> ())
543543- cmd.delete;
544544- (* Frame options *)
545545- Option.iter
546546- (fun (f : Frame.t) ->
547547- Option.iter
548548- (fun v ->
549549- sep ();
550550- add_kv_int buf 'x' v)
551551- f.x;
552552- Option.iter
553553- (fun v ->
554554- sep ();
555555- add_kv_int buf 'y' v)
556556- f.y;
557557- Option.iter
558558- (fun v ->
559559- sep ();
560560- add_kv_int buf 'c' v)
561561- f.base_frame;
562562- Option.iter
563563- (fun v ->
564564- sep ();
565565- add_kv_int buf 'r' v)
566566- f.edit_frame;
567567- Option.iter
568568- (fun v ->
569569- sep ();
570570- add_kv_int buf 'z' v)
571571- f.gap_ms;
572572- Option.iter
573573- (fun c ->
574574- let v = Composition.to_int c in
575575- if v <> 0 then (
576576- sep ();
577577- add_kv_int buf 'X' v))
578578- f.composition;
579579- Option.iter
580580- (fun v ->
581581- sep ();
582582- add_kv_int32 buf 'Y' v)
583583- f.background_color)
584584- cmd.frame;
585585- (* Animation options *)
586586- Option.iter
587587- (fun a ->
588588- match a with
589589- | Animation.Set_state { state; loops } ->
590590- let s =
591591- match state with
592592- | Animation.Stop -> 1
593593- | Animation.Loading -> 2
594594- | Animation.Run -> 3
595595- in
596596- sep ();
597597- add_kv_int buf 's' s;
598598- Option.iter
599599- (fun v ->
600600- sep ();
601601- add_kv_int buf 'v' v)
602602- loops
603603- | Animation.Set_gap { frame; gap_ms } ->
604604- sep ();
605605- add_kv_int buf 'r' frame;
606606- sep ();
607607- add_kv_int buf 'z' gap_ms
608608- | Animation.Set_current frame ->
609609- sep ();
610610- add_kv_int buf 'c' frame)
611611- cmd.animation;
612612- (* Compose options *)
613613- Option.iter
614614- (fun (c : Compose.t) ->
615615- sep ();
616616- add_kv_int buf 'r' c.source_frame;
617617- sep ();
618618- add_kv_int buf 'c' c.dest_frame;
619619- Option.iter
620620- (fun v ->
621621- sep ();
622622- add_kv_int buf 'w' v)
623623- c.width;
624624- Option.iter
625625- (fun v ->
626626- sep ();
627627- add_kv_int buf 'h' v)
628628- c.height;
629629- Option.iter
630630- (fun v ->
631631- sep ();
632632- add_kv_int buf 'x' v)
633633- c.dest_x;
634634- Option.iter
635635- (fun v ->
636636- sep ();
637637- add_kv_int buf 'y' v)
638638- c.dest_y;
639639- Option.iter
640640- (fun v ->
641641- sep ();
642642- add_kv_int buf 'X' v)
643643- c.source_x;
644644- Option.iter
645645- (fun v ->
646646- sep ();
647647- add_kv_int buf 'Y' v)
648648- c.source_y;
649649- Option.iter
650650- (fun comp ->
651651- let v = Composition.to_int comp in
652652- if v <> 0 then (
653653- sep ();
654654- add_kv_int buf 'C' v))
655655- c.composition)
656656- cmd.compose
476476+ kv_int_opt w 'S' cmd.size;
477477+ kv_int_opt w 'O' cmd.offset;
478478+ (* Image ID/number *)
479479+ kv_int_opt w 'i' cmd.image_id;
480480+ kv_int_opt w 'I' cmd.image_number;
481481+ (* Complex options *)
482482+ cmd.placement |> Option.iter (write_placement w);
483483+ cmd.delete |> Option.iter (write_delete w);
484484+ cmd.frame |> Option.iter (write_frame w);
485485+ cmd.animation |> Option.iter (write_animation w);
486486+ cmd.compose |> Option.iter (write_compose w);
487487+ w
657488658489 let chunk_size = 4096
659490660491 let write buf cmd ~data =
661492 Buffer.add_string buf apc_start;
662662- write_control_data buf cmd;
493493+ let w = write_control_data buf cmd in
663494 if String.length data > 0 then begin
664495 let encoded = Base64.encode_string data in
665496 let len = String.length encoded in
···669500 Buffer.add_string buf apc_end)
670501 else begin
671502 (* Multiple chunks *)
672672- let pos = ref 0 in
673673- let first = ref true in
674674- while !pos < len do
675675- let remaining = len - !pos in
676676- let this_chunk = min chunk_size remaining in
677677- let is_last = !pos + this_chunk >= len in
678678- if !first then (
679679- (* First chunk *)
680680- first := false;
681681- add_comma buf;
682682- add_kv_int buf 'm' 1;
683683- Buffer.add_char buf ';';
684684- Buffer.add_substring buf encoded !pos this_chunk;
685685- Buffer.add_string buf apc_end)
686686- else (
687687- (* Continuation chunk *)
688688- Buffer.add_string buf apc_start;
689689- add_kv_int buf 'm' (if is_last then 0 else 1);
690690- Buffer.add_char buf ';';
691691- Buffer.add_substring buf encoded !pos this_chunk;
692692- Buffer.add_string buf apc_end);
693693- pos := !pos + this_chunk
694694- done
503503+ let rec write_chunks pos first =
504504+ if pos < len then begin
505505+ let remaining = len - pos in
506506+ let this_chunk = min chunk_size remaining in
507507+ let is_last = pos + this_chunk >= len in
508508+ if first then (
509509+ kv_int w 'm' 1;
510510+ Buffer.add_char buf ';';
511511+ Buffer.add_substring buf encoded pos this_chunk;
512512+ Buffer.add_string buf apc_end)
513513+ else (
514514+ Buffer.add_string buf apc_start;
515515+ Buffer.add_string buf (if is_last then "m=0" else "m=1");
516516+ Buffer.add_char buf ';';
517517+ Buffer.add_substring buf encoded pos this_chunk;
518518+ Buffer.add_string buf apc_end);
519519+ write_chunks (pos + this_chunk) false
520520+ end
521521+ in
522522+ write_chunks 0 true
695523 end
696524 end
697525 else Buffer.add_string buf apc_end
···715543716544 let error_code t =
717545 if is_ok t then None
718718- else
719719- match String.index_opt t.message ':' with
720720- | Some i -> Some (String.sub t.message 0 i)
721721- | None -> Some t.message
546546+ else String.index_opt t.message ':' |> Option.fold ~none:(Some t.message) ~some:(fun i -> Some (String.sub t.message 0 i))
722547723548 let image_id t = t.image_id
724549 let image_number t = t.image_number
725550 let placement_id t = t.placement_id
726551727552 let parse s =
728728- (* Format: <ESC>_G<keys>;message<ESC>\ *)
553553+ let ( let* ) = Option.bind in
729554 let esc = '\027' in
730555 let len = String.length s in
731731- if len < 5 then None
732732- else if s.[0] <> esc || s.[1] <> '_' || s.[2] <> 'G' then None
733733- else
734734- (* Find the semicolon and end *)
735735- match String.index_from_opt s 3 ';' with
736736- | None -> None
737737- | Some semi_pos -> (
738738- (* Find the APC terminator *)
739739- let rec find_end pos =
740740- if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then
741741- Some pos
742742- else if pos + 1 < len then find_end (pos + 1)
743743- else None
744744- in
745745- match find_end (semi_pos + 1) with
746746- | None -> None
747747- | Some end_pos ->
748748- let keys_str = String.sub s 3 (semi_pos - 3) in
749749- let message =
750750- String.sub s (semi_pos + 1) (end_pos - semi_pos - 1)
751751- in
752752- (* Parse keys *)
753753- let image_id = ref None in
754754- let image_number = ref None in
755755- let placement_id = ref None in
756756- let parts = String.split_on_char ',' keys_str in
757757- List.iter
758758- (fun part ->
759759- if String.length part >= 3 && part.[1] = '=' then
760760- let key = part.[0] in
761761- let value = String.sub part 2 (String.length part - 2) in
762762- match key with
763763- | 'i' -> image_id := int_of_string_opt value
764764- | 'I' -> image_number := int_of_string_opt value
765765- | 'p' -> placement_id := int_of_string_opt value
766766- | _ -> ())
767767- parts;
768768- Some
769769- {
770770- message;
771771- image_id = !image_id;
772772- image_number = !image_number;
773773- placement_id = !placement_id;
774774- })
556556+ let* () = if len >= 5 && s.[0] = esc && s.[1] = '_' && s.[2] = 'G' then Some () else None in
557557+ let* semi_pos = String.index_from_opt s 3 ';' in
558558+ let rec find_end pos =
559559+ if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then Some pos
560560+ else if pos + 1 < len then find_end (pos + 1)
561561+ else None
562562+ in
563563+ let* end_pos = find_end (semi_pos + 1) in
564564+ let keys_str = String.sub s 3 (semi_pos - 3) in
565565+ let message = String.sub s (semi_pos + 1) (end_pos - semi_pos - 1) in
566566+ let parse_kv part =
567567+ if String.length part >= 3 && part.[1] = '=' then
568568+ Some (part.[0], String.sub part 2 (String.length part - 2))
569569+ else None
570570+ in
571571+ let keys = String.split_on_char ',' keys_str |> List.filter_map parse_kv in
572572+ let find_int key = List.assoc_opt key keys |> Fun.flip Option.bind int_of_string_opt in
573573+ Some
574574+ {
575575+ message;
576576+ image_id = find_int 'i';
577577+ image_number = find_int 'I';
578578+ placement_id = find_int 'p';
579579+ }
775580end
776581777582module Unicode_placeholder = struct
778583 let placeholder_char = Uchar.of_int 0x10EEEE
779584780780- (* Row/column diacritics from the protocol spec *)
781585 let diacritics =
782586 [|
783587 0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F;
···815619 0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD; 0x1D242; 0x1D243; 0x1D244;
816620 |]
817621818818- let row_diacritic n =
819819- if n >= 0 && n < Array.length diacritics then
820820- Uchar.of_int diacritics.(n)
821821- else Uchar.of_int diacritics.(0)
622622+ let diacritic n =
623623+ Uchar.of_int diacritics.(n mod Array.length diacritics)
822624823823- let column_diacritic = row_diacritic
824824- let id_high_byte_diacritic = row_diacritic
625625+ let row_diacritic = diacritic
626626+ let column_diacritic = diacritic
627627+ let id_high_byte_diacritic = diacritic
825628826629 let add_uchar buf u =
827827- let b = Bytes.create 4 in
828828- let len = Uchar.utf_8_byte_length u in
829829- let _ = Uchar.unsafe_to_char u in
830830- (* Encode UTF-8 manually *)
831630 let code = Uchar.to_int u in
832832- if code < 0x80 then (
833833- Bytes.set b 0 (Char.chr code);
834834- Buffer.add_subbytes buf b 0 1)
631631+ let put = Buffer.add_char buf in
632632+ if code < 0x80 then put (Char.chr code)
835633 else if code < 0x800 then (
836836- Bytes.set b 0 (Char.chr (0xC0 lor (code lsr 6)));
837837- Bytes.set b 1 (Char.chr (0x80 lor (code land 0x3F)));
838838- Buffer.add_subbytes buf b 0 2)
634634+ put (Char.chr (0xC0 lor (code lsr 6)));
635635+ put (Char.chr (0x80 lor (code land 0x3F))))
839636 else if code < 0x10000 then (
840840- Bytes.set b 0 (Char.chr (0xE0 lor (code lsr 12)));
841841- Bytes.set b 1 (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
842842- Bytes.set b 2 (Char.chr (0x80 lor (code land 0x3F)));
843843- Buffer.add_subbytes buf b 0 3)
637637+ put (Char.chr (0xE0 lor (code lsr 12)));
638638+ put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
639639+ put (Char.chr (0x80 lor (code land 0x3F))))
844640 else (
845845- Bytes.set b 0 (Char.chr (0xF0 lor (code lsr 18)));
846846- Bytes.set b 1 (Char.chr (0x80 lor ((code lsr 12) land 0x3F)));
847847- Bytes.set b 2 (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
848848- Bytes.set b 3 (Char.chr (0x80 lor (code land 0x3F)));
849849- Buffer.add_subbytes buf b 0 len)
641641+ put (Char.chr (0xF0 lor (code lsr 18)));
642642+ put (Char.chr (0x80 lor ((code lsr 12) land 0x3F)));
643643+ put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
644644+ put (Char.chr (0x80 lor (code land 0x3F))))
850645851646 let write buf ~image_id ?placement_id ~rows ~cols () =
852852- (* Set foreground color using 24-bit mode *)
853853- let r = (image_id lsr 16) land 0xFF in
854854- let g = (image_id lsr 8) land 0xFF in
855855- let b = image_id land 0xFF in
856856- Buffer.add_string buf (Printf.sprintf "\027[38;2;%d;%d;%dm" r g b);
857857- (* Optionally set underline color for placement ID *)
858858- (match placement_id with
859859- | Some pid ->
860860- let pr = (pid lsr 16) land 0xFF in
861861- let pg = (pid lsr 8) land 0xFF in
862862- let pb = pid land 0xFF in
863863- Buffer.add_string buf (Printf.sprintf "\027[58;2;%d;%d;%dm" pr pg pb)
864864- | None -> ());
865865- (* High byte diacritic if needed *)
647647+ (* Set foreground color *)
648648+ Printf.bprintf buf "\027[38;2;%d;%d;%dm"
649649+ ((image_id lsr 16) land 0xFF)
650650+ ((image_id lsr 8) land 0xFF)
651651+ (image_id land 0xFF);
652652+ (* Optional placement ID in underline color *)
653653+ placement_id
654654+ |> Option.iter (fun pid ->
655655+ Printf.bprintf buf "\027[58;2;%d;%d;%dm"
656656+ ((pid lsr 16) land 0xFF)
657657+ ((pid lsr 8) land 0xFF)
658658+ (pid land 0xFF));
659659+ (* High byte diacritic *)
866660 let high_byte = (image_id lsr 24) land 0xFF in
867867- let high_diac =
868868- if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None
869869- in
870870- (* Write placeholder grid *)
661661+ let high_diac = if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None in
662662+ (* Write grid *)
871663 for row = 0 to rows - 1 do
872664 for col = 0 to cols - 1 do
873665 add_uchar buf placeholder_char;
874666 add_uchar buf (row_diacritic row);
875667 add_uchar buf (column_diacritic col);
876876- Option.iter (add_uchar buf) high_diac
668668+ high_diac |> Option.iter (add_uchar buf)
877669 done;
878670 if row < rows - 1 then Buffer.add_string buf "\n\r"
879671 done;
880672 (* Reset colors *)
881673 Buffer.add_string buf "\027[39m";
882882- match placement_id with Some _ -> Buffer.add_string buf "\027[59m" | None -> ()
674674+ if Option.is_some placement_id then Buffer.add_string buf "\027[59m"
883675end
884676885677module Detect = struct
886678 let make_query () =
887887- (* Send a 1x1 transparent pixel query *)
888888- let cmd =
889889- Command.query ~format:Format.Rgb24 ~transmission:Transmission.Direct
890890- ~width:1 ~height:1 ()
891891- in
892892- let data = "\x00\x00\x00" in
893893- let query = Command.to_string cmd ~data in
894894- (* Add DA1 query to detect non-supporting terminals *)
895895- query ^ "\027[c"
679679+ let cmd = Command.query ~format:`Rgb24 ~transmission:`Direct ~width:1 ~height:1 () in
680680+ Command.to_string cmd ~data:"\x00\x00\x00" ^ "\027[c"
896681897682 let supports_graphics response ~da1_received =
898898- match response with
899899- | Some r -> Response.is_ok r
900900- | None -> not da1_received
683683+ response |> Option.map Response.is_ok |> Option.value ~default:(not da1_received)
901684end
+112-230
stack/kitty_graphics/lib/kitty_graphics.mli
···1414 {[
1515 (* Display a PNG image *)
1616 let png_data = read_file "image.png" in
1717- let cmd = Kitty_graphics.Command.transmit_and_display
1818- ~format:Kitty_graphics.Format.Png
1919- ()
2020- in
1717+ let cmd = Kitty_graphics.Command.transmit_and_display ~format:`Png () in
2118 let buf = Buffer.create 1024 in
2219 Kitty_graphics.Command.write buf cmd ~data:png_data;
2320 print_string (Buffer.contents buf)
···2825 See {{:https://sw.kovidgoyal.net/kitty/graphics-protocol/} Kitty Graphics Protocol}
2926 for the full specification. *)
30273131-(** {1 Core Types} *)
2828+(** {1 Polymorphic Variant Types} *)
2929+3030+type format = [ `Rgba32 | `Rgb24 | `Png ]
3131+(** Image data formats. [`Rgba32] is 32-bit RGBA (4 bytes per pixel),
3232+ [`Rgb24] is 24-bit RGB (3 bytes per pixel), [`Png] is PNG encoded data. *)
3333+3434+type transmission = [ `Direct | `File | `Tempfile ]
3535+(** Transmission methods. [`Direct] sends data inline, [`File] reads from a path,
3636+ [`Tempfile] reads from a temp file that the terminal deletes after reading. *)
3737+3838+type compression = [ `None | `Zlib ]
3939+(** Compression options. [`None] for raw data, [`Zlib] for RFC 1950 compression. *)
4040+4141+type quiet = [ `Noisy | `Errors_only | `Silent ]
4242+(** Response suppression. [`Noisy] sends all responses (default),
4343+ [`Errors_only] suppresses OK responses, [`Silent] suppresses all. *)
4444+4545+type cursor = [ `Move | `Static ]
4646+(** Cursor movement after displaying. [`Move] advances cursor (default),
4747+ [`Static] keeps cursor in place. *)
4848+4949+type composition = [ `Alpha_blend | `Overwrite ]
5050+(** Composition modes. [`Alpha_blend] for full blending (default),
5151+ [`Overwrite] for simple pixel replacement. *)
32523333-(** Image data formats. *)
5353+type delete =
5454+ [ `All_visible
5555+ | `All_visible_and_free
5656+ | `By_id of int * int option
5757+ | `By_id_and_free of int * int option
5858+ | `By_number of int * int option
5959+ | `By_number_and_free of int * int option
6060+ | `At_cursor
6161+ | `At_cursor_and_free
6262+ | `At_cell of int * int
6363+ | `At_cell_and_free of int * int
6464+ | `At_cell_z of int * int * int
6565+ | `At_cell_z_and_free of int * int * int
6666+ | `By_column of int
6767+ | `By_column_and_free of int
6868+ | `By_row of int
6969+ | `By_row_and_free of int
7070+ | `By_z_index of int
7171+ | `By_z_index_and_free of int
7272+ | `By_id_range of int * int
7373+ | `By_id_range_and_free of int * int
7474+ | `Frames
7575+ | `Frames_and_free ]
7676+(** Delete target specification. Each variant has two forms: one that only
7777+ removes placements (e.g., [`All_visible]) and one that also frees the
7878+ image data (e.g., [`All_visible_and_free]). Tuple variants contain
7979+ (image_id, optional_placement_id) or (x, y) coordinates. *)
8080+8181+type animation_state = [ `Stop | `Loading | `Run ]
8282+(** Animation playback state. [`Stop] halts animation, [`Loading] runs but
8383+ waits for new frames at end, [`Run] runs normally and loops. *)
8484+8585+(** {1 Type Modules} *)
8686+3487module Format : sig
3535- type t =
3636- | Rgba32 (** 32-bit RGBA, 4 bytes per pixel *)
3737- | Rgb24 (** 24-bit RGB, 3 bytes per pixel *)
3838- | Png (** PNG encoded data *)
8888+ type t = format
39894090 val to_int : t -> int
4191 (** Convert to protocol integer value (32, 24, or 100). *)
4292end
43934444-(** Transmission methods for image data. *)
4594module Transmission : sig
4646- type t =
4747- | Direct (** Data transmitted inline in the escape sequence *)
4848- | File (** Data read from a file path *)
4949- | Tempfile (** Data read from a temp file, deleted after reading *)
9595+ type t = transmission
50965197 val to_char : t -> char
5298 (** Convert to protocol character ('d', 'f', or 't'). *)
5399end
541005555-(** Compression options for transmitted data. *)
56101module Compression : sig
5757- type t =
5858- | None (** No compression *)
5959- | Zlib (** RFC 1950 zlib compression *)
102102+ type t = compression
6010361104 val to_char : t -> char option
6262- (** Convert to protocol character (None or Some 'z'). *)
105105+ (** Convert to protocol character ([None] or [Some 'z']). *)
63106end
641076565-(** Response suppression modes. *)
66108module Quiet : sig
6767- type t =
6868- | Noisy (** Terminal sends all responses (default) *)
6969- | Errors_only (** Terminal only sends error responses *)
7070- | Silent (** Terminal sends no responses *)
109109+ type t = quiet
7111072111 val to_int : t -> int
73112 (** Convert to protocol integer (0, 1, or 2). *)
74113end
751147676-(** Cursor movement policy after displaying an image. *)
77115module Cursor : sig
7878- type t =
7979- | Move (** Move cursor after image (default) *)
8080- | Static (** Keep cursor in place *)
116116+ type t = cursor
8111782118 val to_int : t -> int
83119 (** Convert to protocol integer (0 or 1). *)
84120end
851218686-(** Composition modes for blending. *)
87122module Composition : sig
8888- type t =
8989- | Alpha_blend (** Full alpha blending (default) *)
9090- | Overwrite (** Simple pixel replacement *)
123123+ type t = composition
9112492125 val to_int : t -> int
93126 (** Convert to protocol integer (0 or 1). *)
94127end
951289696-(** {1 Delete Operations} *)
9797-9898-(** Specifies what to delete when using delete commands. *)
99129module Delete : sig
100100- (** Delete target specification.
101101-102102- Each variant has two forms: one that only removes placements (keeping
103103- image data for potential reuse) and one that also frees the image data. *)
104104- type t =
105105- | All_visible
106106- (** Delete all visible placements. *)
107107- | All_visible_and_free
108108- (** Delete all visible placements and free their image data. *)
109109- | By_id of { image_id : int; placement_id : int option }
110110- (** Delete placements for a specific image ID, optionally filtered
111111- by placement ID. *)
112112- | By_id_and_free of { image_id : int; placement_id : int option }
113113- (** Delete and free by image ID. *)
114114- | By_number of { image_number : int; placement_id : int option }
115115- (** Delete by image number (newest with that number). *)
116116- | By_number_and_free of { image_number : int; placement_id : int option }
117117- (** Delete and free by image number. *)
118118- | At_cursor
119119- (** Delete placements intersecting cursor position. *)
120120- | At_cursor_and_free
121121- (** Delete and free at cursor position. *)
122122- | At_cell of { x : int; y : int }
123123- (** Delete placements intersecting a specific cell (1-based). *)
124124- | At_cell_and_free of { x : int; y : int }
125125- (** Delete and free at specific cell. *)
126126- | At_cell_z of { x : int; y : int; z : int }
127127- (** Delete at cell with specific z-index. *)
128128- | At_cell_z_and_free of { x : int; y : int; z : int }
129129- (** Delete and free at cell with z-index. *)
130130- | By_column of int
131131- (** Delete all placements intersecting a column (1-based). *)
132132- | By_column_and_free of int
133133- (** Delete and free by column. *)
134134- | By_row of int
135135- (** Delete all placements intersecting a row (1-based). *)
136136- | By_row_and_free of int
137137- (** Delete and free by row. *)
138138- | By_z_index of int
139139- (** Delete all placements with a specific z-index. *)
140140- | By_z_index_and_free of int
141141- (** Delete and free by z-index. *)
142142- | By_id_range of { min_id : int; max_id : int }
143143- (** Delete images with IDs in range [min_id, max_id]. *)
144144- | By_id_range_and_free of { min_id : int; max_id : int }
145145- (** Delete and free by ID range. *)
146146- | Frames
147147- (** Delete animation frames. *)
148148- | Frames_and_free
149149- (** Delete animation frames and free if no frames remain. *)
130130+ type t = delete
150131end
151132152133(** {1 Placement Options} *)
153134154154-(** Image placement configuration.
155155-156156- Controls how an image is positioned and scaled when displayed. *)
157135module Placement : sig
158136 type t
159137 (** Placement configuration. *)
···169147 ?rows:int ->
170148 ?z_index:int ->
171149 ?placement_id:int ->
172172- ?cursor:Cursor.t ->
150150+ ?cursor:cursor ->
173151 ?unicode_placeholder:bool ->
174152 unit ->
175153 t
···194172195173(** {1 Animation} *)
196174197197-(** Animation frame specification. *)
198175module Frame : sig
199176 type t
200177 (** Animation frame configuration. *)
···205182 ?base_frame:int ->
206183 ?edit_frame:int ->
207184 ?gap_ms:int ->
208208- ?composition:Composition.t ->
185185+ ?composition:composition ->
209186 ?background_color:int32 ->
210187 unit ->
211188 t
···223200 (** Empty frame spec with defaults. *)
224201end
225202226226-(** Animation control operations. *)
227203module Animation : sig
228228- type state =
229229- | Stop (** Stop the animation *)
230230- | Loading (** Run but wait for new frames at end *)
231231- | Run (** Run normally, loop at end *)
204204+ type state = animation_state
232205233233- type t
234234- (** Animation control configuration. *)
206206+ type t =
207207+ [ `Set_state of state * int option
208208+ | `Set_gap of int * int
209209+ | `Set_current of int ]
210210+ (** Animation control operations. *)
235211236212 val set_state : ?loops:int -> state -> t
237213 (** Set animation state.
238238-239214 @param loops Number of loops: 0 = ignored, 1 = infinite, n = n-1 loops *)
240215241216 val set_gap : frame:int -> gap_ms:int -> t
242217 (** Set the gap (delay) for a specific frame.
243243-244218 @param frame 1-based frame number
245219 @param gap_ms Delay in milliseconds (negative = gapless) *)
246220···248222 (** Make a specific frame (1-based) the current displayed frame. *)
249223end
250224251251-(** Frame composition for combining frame regions. *)
252225module Compose : sig
253226 type t
254227 (** Composition operation. *)
···262235 ?source_y:int ->
263236 ?dest_x:int ->
264237 ?dest_y:int ->
265265- ?composition:Composition.t ->
238238+ ?composition:composition ->
266239 unit ->
267240 t
268268- (** Compose a rectangle from one frame onto another.
269269-270270- @param source_frame 1-based source frame number
271271- @param dest_frame 1-based destination frame number
272272- @param width Rectangle width in pixels (default: full width)
273273- @param height Rectangle height in pixels (default: full height)
274274- @param source_x Left edge of source rectangle
275275- @param source_y Top edge of source rectangle
276276- @param dest_x Left edge of destination rectangle
277277- @param dest_y Top edge of destination rectangle
278278- @param composition Blend mode *)
241241+ (** Compose a rectangle from one frame onto another. *)
279242end
280243281244(** {1 Commands} *)
282245283283-(** Graphics command builder.
284284-285285- This is the main API for constructing graphics protocol commands.
286286- Commands are built using the various constructors, then written to
287287- a buffer with {!write}. *)
288246module Command : sig
289247 type t
290248 (** A graphics protocol command. *)
···294252 val transmit :
295253 ?image_id:int ->
296254 ?image_number:int ->
297297- ?format:Format.t ->
298298- ?transmission:Transmission.t ->
299299- ?compression:Compression.t ->
255255+ ?format:format ->
256256+ ?transmission:transmission ->
257257+ ?compression:compression ->
300258 ?width:int ->
301259 ?height:int ->
302260 ?size:int ->
303261 ?offset:int ->
304304- ?quiet:Quiet.t ->
262262+ ?quiet:quiet ->
305263 unit ->
306264 t
307307- (** Transmit image data without displaying.
308308-309309- @param image_id Unique ID for the image (1-4294967295)
310310- @param image_number Image number (terminal assigns ID)
311311- @param format Pixel format of the data
312312- @param transmission How data is transmitted
313313- @param compression Compression applied to data
314314- @param width Image width in pixels (required for RGB/RGBA)
315315- @param height Image height in pixels (required for RGB/RGBA)
316316- @param size Number of bytes to read (for file transmission)
317317- @param offset Byte offset to start reading (for file transmission)
318318- @param quiet Response suppression mode *)
265265+ (** Transmit image data without displaying. *)
319266320267 val transmit_and_display :
321268 ?image_id:int ->
322269 ?image_number:int ->
323323- ?format:Format.t ->
324324- ?transmission:Transmission.t ->
325325- ?compression:Compression.t ->
270270+ ?format:format ->
271271+ ?transmission:transmission ->
272272+ ?compression:compression ->
326273 ?width:int ->
327274 ?height:int ->
328275 ?size:int ->
329276 ?offset:int ->
330330- ?quiet:Quiet.t ->
277277+ ?quiet:quiet ->
331278 ?placement:Placement.t ->
332279 unit ->
333280 t
334334- (** Transmit image data and display it immediately.
335335-336336- This is the most common operation for displaying images.
337337- See {!transmit} for transmission parameters and {!Placement}
338338- for display options. *)
281281+ (** Transmit image data and display it immediately. *)
339282340283 val query :
341341- ?format:Format.t ->
342342- ?transmission:Transmission.t ->
284284+ ?format:format ->
285285+ ?transmission:transmission ->
343286 ?width:int ->
344287 ?height:int ->
345345- ?quiet:Quiet.t ->
288288+ ?quiet:quiet ->
346289 unit ->
347290 t
348348- (** Query terminal support without storing the image.
349349-350350- Send a small test image to check if the terminal supports
351351- the graphics protocol. The terminal responds with OK or
352352- an error without storing the image. *)
291291+ (** Query terminal support without storing the image. *)
353292354293 (** {2 Display} *)
355294···357296 ?image_id:int ->
358297 ?image_number:int ->
359298 ?placement:Placement.t ->
360360- ?quiet:Quiet.t ->
299299+ ?quiet:quiet ->
361300 unit ->
362301 t
363363- (** Display a previously transmitted image.
364364-365365- @param image_id ID of a previously transmitted image
366366- @param image_number Number of the image to display
367367- @param placement Display placement options
368368- @param quiet Response suppression *)
302302+ (** Display a previously transmitted image. *)
369303370304 (** {2 Deletion} *)
371305372372- val delete : ?quiet:Quiet.t -> Delete.t -> t
373373- (** Delete images or placements.
374374-375375- See {!Delete} for the various deletion modes. *)
306306+ val delete : ?quiet:quiet -> delete -> t
307307+ (** Delete images or placements. *)
376308377309 (** {2 Animation} *)
378310379311 val frame :
380312 ?image_id:int ->
381313 ?image_number:int ->
382382- ?format:Format.t ->
383383- ?transmission:Transmission.t ->
384384- ?compression:Compression.t ->
314314+ ?format:format ->
315315+ ?transmission:transmission ->
316316+ ?compression:compression ->
385317 ?width:int ->
386318 ?height:int ->
387387- ?quiet:Quiet.t ->
319319+ ?quiet:quiet ->
388320 frame:Frame.t ->
389321 unit ->
390322 t
391391- (** Transmit animation frame data.
392392-393393- Similar to {!transmit} but adds frame-specific parameters. *)
323323+ (** Transmit animation frame data. *)
394324395395- val animate :
396396- ?image_id:int ->
397397- ?image_number:int ->
398398- ?quiet:Quiet.t ->
399399- Animation.t ->
400400- t
325325+ val animate : ?image_id:int -> ?image_number:int -> ?quiet:quiet -> Animation.t -> t
401326 (** Control animation playback. *)
402327403403- val compose :
404404- ?image_id:int ->
405405- ?image_number:int ->
406406- ?quiet:Quiet.t ->
407407- Compose.t ->
408408- t
328328+ val compose : ?image_id:int -> ?image_number:int -> ?quiet:quiet -> Compose.t -> t
409329 (** Compose animation frames. *)
410330411331 (** {2 Output} *)
412332413333 val write : Buffer.t -> t -> data:string -> unit
414414- (** Write the command to a buffer.
415415-416416- @param data The payload data (image bytes, file path, etc.).
417417- For {!display}, {!delete}, {!animate}, pass empty string. *)
334334+ (** Write the command to a buffer. *)
418335419336 val to_string : t -> data:string -> string
420337 (** Convert command to a string. *)
···422339423340(** {1 Response Parsing} *)
424341425425-(** Terminal response parsing.
426426-427427- When the terminal processes a graphics command, it may send back
428428- a response indicating success or failure. *)
429342module Response : sig
430343 type t
431344 (** A parsed terminal response. *)
432345433346 val parse : string -> t option
434434- (** Parse a response from terminal output.
435435-436436- Expects the format: [<ESC>_G...;message<ESC>\]
437437- Returns [None] if the string is not a valid graphics response. *)
347347+ (** Parse a response from terminal output. *)
438348439349 val is_ok : t -> bool
440350 (** Check if the response indicates success. *)
441351442352 val message : t -> string
443443- (** Get the response message ("OK" or error description). *)
353353+ (** Get the response message. *)
444354445355 val error_code : t -> string option
446446- (** Extract the error code if this is an error response.
447447-448448- Error codes include: ENOENT, EINVAL, ENOSPC, EBADPNG, etc. *)
356356+ (** Extract the error code if this is an error response. *)
449357450358 val image_id : t -> int option
451451- (** Get the image ID from the response, if present. *)
359359+ (** Get the image ID from the response. *)
452360453361 val image_number : t -> int option
454454- (** Get the image number from the response, if present. *)
362362+ (** Get the image number from the response. *)
455363456364 val placement_id : t -> int option
457457- (** Get the placement ID from the response, if present. *)
365365+ (** Get the placement ID from the response. *)
458366end
459367460368(** {1 Unicode Placeholders} *)
461369462462-(** Unicode placeholder generation for tmux/vim compatibility.
463463-464464- Unicode placeholders allow images to work with applications that
465465- don't understand the graphics protocol but support Unicode and
466466- foreground colors. The image is transmitted with a virtual placement,
467467- then placeholder characters are written to the terminal. *)
468370module Unicode_placeholder : sig
469371 val placeholder_char : Uchar.t
470372 (** The Unicode placeholder character U+10EEEE. *)
···477379 cols:int ->
478380 unit ->
479381 unit
480480- (** Write placeholder characters to a buffer.
481481-482482- The image ID is encoded in the foreground color (24-bit mode).
483483- Row and column positions are encoded using combining diacritics.
484484-485485- @param image_id The image ID (should have non-zero bytes for 24-bit)
486486- @param placement_id Optional placement ID (encoded in underline color)
487487- @param rows Number of rows to fill
488488- @param cols Number of columns per row *)
382382+ (** Write placeholder characters to a buffer. *)
489383490384 val row_diacritic : int -> Uchar.t
491385 (** Get the combining diacritic for a row number (0-based). *)
···499393500394(** {1 Terminal Detection} *)
501395502502-(** Helpers for detecting terminal graphics support. *)
503396module Detect : sig
504397 val make_query : unit -> string
505505- (** Generate a query command to test graphics support.
506506-507507- Send this to stdout and read the terminal's response.
508508- Follow with a DA1 query ([<ESC>[c]) to detect terminals
509509- that don't support graphics (they'll answer DA1 but not
510510- the graphics query). *)
398398+ (** Generate a query command to test graphics support. *)
511399512400 val supports_graphics : Response.t option -> da1_received:bool -> bool
513513- (** Determine if graphics are supported based on query results.
514514-515515- @param response The parsed graphics response, if any
516516- @param da1_received Whether a DA1 response was received
517517-518518- Returns [true] if a graphics OK response was received,
519519- or [false] if only DA1 was received (no graphics support). *)
401401+ (** Determine if graphics are supported based on query results. *)
520402end