My agentic slop goes here. Not intended for anyone else!

sync

+1
stack/kitty_graphics/.gitignore
···
+
_build
+2 -2
stack/kitty_graphics/dune-project
···
(lang dune 3.20)
-
(name kitty_graphics)
+
(name kgp)
(package
-
(name kitty_graphics)
+
(name kgp)
(synopsis "OCaml implementation of the Kitty terminal graphics protocol")
(description
"A standalone library for rendering images in terminals that support the Kitty graphics protocol. Supports image transmission, display, animation, Unicode placeholders, and terminal capability detection.")
+97
stack/kitty_graphics/example/anim_test.ml
···
+
(* Minimal animation test - shows exact bytes sent *)
+
+
module K = Kgp
+
+
let solid_color_rgba ~width ~height ~r ~g ~b ~a =
+
let pixels = Bytes.create (width * height * 4) in
+
for i = 0 to (width * height) - 1 do
+
let idx = i * 4 in
+
Bytes.set pixels idx (Char.chr r);
+
Bytes.set pixels (idx + 1) (Char.chr g);
+
Bytes.set pixels (idx + 2) (Char.chr b);
+
Bytes.set pixels (idx + 3) (Char.chr a)
+
done;
+
Bytes.to_string pixels
+
+
let send cmd ~data =
+
print_string (K.Command.to_string cmd ~data);
+
flush stdout
+
+
let () =
+
let width, height = 40, 40 in (* Smaller for faster testing *)
+
let image_id = 500 in
+
+
(* Clear any existing image *)
+
send (K.Command.delete ~quiet:`Errors_only (`All_visible_and_free)) ~data:"";
+
+
(* Step 1: Transmit base frame (red) *)
+
let red_frame = solid_color_rgba ~width ~height ~r:255 ~g:0 ~b:0 ~a:255 in
+
send
+
(K.Command.transmit
+
~image_id
+
~format:`Rgba32
+
~width ~height
+
~quiet:`Errors_only
+
())
+
~data:red_frame;
+
+
(* Step 2: Add frame (blue) *)
+
let blue_frame = solid_color_rgba ~width ~height ~r:0 ~g:0 ~b:255 ~a:255 in
+
send
+
(K.Command.frame
+
~image_id
+
~format:`Rgba32
+
~width ~height
+
~frame:(K.Frame.make ~gap_ms:500 ~composition:`Overwrite ())
+
~quiet:`Errors_only
+
())
+
~data:blue_frame;
+
+
(* Step 3: Add frame (green) *)
+
let green_frame = solid_color_rgba ~width ~height ~r:0 ~g:255 ~b:0 ~a:255 in
+
send
+
(K.Command.frame
+
~image_id
+
~format:`Rgba32
+
~width ~height
+
~frame:(K.Frame.make ~gap_ms:500 ~composition:`Overwrite ())
+
~quiet:`Errors_only
+
())
+
~data:green_frame;
+
+
(* Step 4: Create placement *)
+
send
+
(K.Command.display
+
~image_id
+
~placement:(K.Placement.make
+
~placement_id:1
+
~cursor:`Static
+
())
+
~quiet:`Errors_only
+
())
+
~data:"";
+
+
(* Step 5: Set root frame gap - IMPORTANT: root frame has no gap by default *)
+
send
+
(K.Command.animate ~image_id (K.Animation.set_gap ~frame:1 ~gap_ms:500))
+
~data:"";
+
+
(* Step 6: Start animation *)
+
send
+
(K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run))
+
~data:"";
+
+
print_endline "";
+
print_endline "Animation should be playing (red -> blue -> green).";
+
print_endline "Press Enter to stop...";
+
flush stdout;
+
let _ = read_line () in
+
+
(* Stop animation *)
+
send
+
(K.Command.animate ~image_id (K.Animation.set_state `Stop))
+
~data:"";
+
+
(* Clean up *)
+
send (K.Command.delete ~quiet:`Errors_only (`All_visible_and_free)) ~data:"";
+
print_endline "Done."
+94
stack/kitty_graphics/example/debug_anim.ml
···
+
(* Debug: Output animation escape sequences for comparison with Go *)
+
+
module K = Kgp
+
+
let solid_color_rgba ~width ~height ~r ~g ~b ~a =
+
let pixels = Bytes.create (width * height * 4) in
+
for i = 0 to (width * height) - 1 do
+
let idx = i * 4 in
+
Bytes.set pixels idx (Char.chr r);
+
Bytes.set pixels (idx + 1) (Char.chr g);
+
Bytes.set pixels (idx + 2) (Char.chr b);
+
Bytes.set pixels (idx + 3) (Char.chr a)
+
done;
+
Bytes.to_string pixels
+
+
let send cmd ~data =
+
let s = K.Command.to_string cmd ~data in
+
(* Print escaped version for debugging *)
+
String.iter (fun c ->
+
let code = Char.code c in
+
if code = 27 then print_string "\\x1b"
+
else if code < 32 || code > 126 then Printf.printf "\\x%02x" code
+
else print_char c
+
) s;
+
print_newline ()
+
+
let () =
+
let width, height = 80, 80 in
+
let image_id = 300 in
+
+
print_endline "=== OCaml Animation Debug ===\n";
+
+
(* Step 1: Transmit base frame *)
+
print_endline "1. Transmit base frame (a=t):";
+
let red_frame = solid_color_rgba ~width ~height ~r:255 ~g:0 ~b:0 ~a:255 in
+
send
+
(K.Command.transmit
+
~image_id
+
~format:`Rgba32
+
~width ~height
+
~quiet:`Errors_only
+
())
+
~data:red_frame;
+
print_newline ();
+
+
(* Step 2: Add frame *)
+
print_endline "2. Add frame (a=f):";
+
let orange_frame = solid_color_rgba ~width ~height ~r:255 ~g:165 ~b:0 ~a:255 in
+
send
+
(K.Command.frame
+
~image_id
+
~format:`Rgba32
+
~width ~height
+
~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
+
~quiet:`Errors_only
+
())
+
~data:orange_frame;
+
print_newline ();
+
+
(* Step 3: Put/display placement *)
+
print_endline "3. Create placement (a=p):";
+
send
+
(K.Command.display
+
~image_id
+
~placement:(K.Placement.make
+
~placement_id:1
+
~cell_x_offset:0
+
~cell_y_offset:0
+
~cursor:`Static
+
())
+
~quiet:`Errors_only
+
())
+
~data:"";
+
print_newline ();
+
+
(* Step 4: Set root frame gap *)
+
print_endline "4. Set root frame gap (a=a,r=1,z=100):";
+
send
+
(K.Command.animate ~image_id (K.Animation.set_gap ~frame:1 ~gap_ms:100))
+
~data:"";
+
print_newline ();
+
+
(* Step 5: Animate *)
+
print_endline "5. Start animation (a=a,s=3,v=1):";
+
send
+
(K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run))
+
~data:"";
+
print_newline ();
+
+
(* Step 6: Stop animation *)
+
print_endline "6. Stop animation:";
+
send
+
(K.Command.animate ~image_id (K.Animation.set_state `Stop))
+
~data:""
+17 -1
stack/kitty_graphics/example/dune
···
(executable
(name example)
-
(libraries kitty_graphics))
+
(libraries kgp unix))
+
+
(executable
+
(name debug_anim)
+
(libraries kgp))
+
+
(executable
+
(name test_output)
+
(libraries kgp))
+
+
(executable
+
(name anim_test)
+
(libraries kgp))
+
+
(executable
+
(name tiny_anim)
+
(libraries kgp))
+250 -60
stack/kitty_graphics/example/example.ml
···
-
(* Kitty Graphics Protocol Demo - Matching kgp/examples/demo workflow *)
+
(* Kitty Graphics Protocol Demo - Matching kgp/examples/demo *)
-
module K = Kitty_graphics
+
module K = Kgp
-
(* Generate a solid color RGBA frame *)
-
let make_solid_frame ~width ~height ~r ~g ~b =
+
(* Helper: Generate a solid color RGBA image *)
+
let solid_color_rgba ~width ~height ~r ~g ~b ~a =
let pixels = Bytes.create (width * height * 4) in
for i = 0 to (width * height) - 1 do
let idx = i * 4 in
Bytes.set pixels idx (Char.chr r);
Bytes.set pixels (idx + 1) (Char.chr g);
Bytes.set pixels (idx + 2) (Char.chr b);
-
Bytes.set pixels (idx + 3) '\xff'
+
Bytes.set pixels (idx + 3) (Char.chr a)
+
done;
+
Bytes.to_string pixels
+
+
(* Helper: Generate a solid color RGB image (no alpha) *)
+
let solid_color_rgb ~width ~height ~r ~g ~b =
+
let pixels = Bytes.create (width * height * 3) in
+
for i = 0 to (width * height) - 1 do
+
let idx = i * 3 in
+
Bytes.set pixels idx (Char.chr r);
+
Bytes.set pixels (idx + 1) (Char.chr g);
+
Bytes.set pixels (idx + 2) (Char.chr b)
+
done;
+
Bytes.to_string pixels
+
+
(* Helper: Generate a gradient RGBA image *)
+
let gradient_rgba ~width ~height =
+
let pixels = Bytes.create (width * height * 4) in
+
for y = 0 to height - 1 do
+
for x = 0 to width - 1 do
+
let idx = (y * width + x) * 4 in
+
let r = 255 * x / width in
+
let b = 255 * (width - x) / width in
+
Bytes.set pixels idx (Char.chr r);
+
Bytes.set pixels (idx + 1) (Char.chr 128);
+
Bytes.set pixels (idx + 2) (Char.chr b);
+
Bytes.set pixels (idx + 3) '\xff'
+
done
done;
Bytes.to_string pixels
+
(* Helper: Read a file *)
+
let read_file filename =
+
let ic = open_in_bin filename in
+
let n = in_channel_length ic in
+
let s = really_input_string ic n in
+
close_in ic;
+
s
+
let send cmd ~data =
print_string (K.Command.to_string cmd ~data);
flush stdout
···
flush stdout
let () =
+
let reader = stdin in
+
ignore reader;
+
clear_screen ();
print_endline "Kitty Graphics Protocol - OCaml Demo";
print_endline "=====================================";
print_newline ();
print_endline "Press Enter to proceed through each demo...";
print_newline ();
+
+
(* Demo 1: Basic formats - PNG *)
+
clear_screen ();
+
print_endline "Demo 1: Image Formats - PNG format";
+
(* Read sf.png and display a small portion as demo *)
+
(try
+
let png_data = read_file "sf.png" in
+
send
+
(K.Command.transmit_and_display
+
~image_id:1
+
~format:`Png
+
~quiet:`Errors_only
+
~placement:(K.Placement.make ~columns:15 ~rows:8 ())
+
())
+
~data:png_data;
+
print_endline "sf.png displayed using PNG format"
+
with _ ->
+
(* Fallback: red square as RGBA *)
+
let red_data = solid_color_rgba ~width:100 ~height:100 ~r:255 ~g:0 ~b:0 ~a:255 in
+
send
+
(K.Command.transmit_and_display
+
~image_id:1
+
~format:`Rgba32
+
~width:100 ~height:100
+
~quiet:`Errors_only
+
())
+
~data:red_data;
+
print_endline "Red square displayed (sf.png not found)");
+
print_newline ();
wait_for_enter ();
-
(* Demo 1: Basic RGBA format *)
+
(* Demo 2: Basic formats - RGBA *)
clear_screen ();
-
print_endline "Demo 1: Image Format - RGBA (32-bit)";
-
let blue_data = make_solid_frame ~width:100 ~height:100 ~r:0 ~g:0 ~b:255 in
+
print_endline "Demo 2: Image Formats - RGBA format (32-bit)";
+
let blue_data = solid_color_rgba ~width:100 ~height:100 ~r:0 ~g:0 ~b:255 ~a:255 in
send
(K.Command.transmit_and_display
-
~image_id:1
+
~image_id:2
~format:`Rgba32
~width:100 ~height:100
~quiet:`Errors_only
···
print_newline ();
wait_for_enter ();
-
(* Demo 2: Basic RGB format *)
+
(* Demo 3: Basic formats - RGB *)
clear_screen ();
-
print_endline "Demo 2: Image Format - RGB (24-bit)";
-
(* RGB is 3 bytes per pixel *)
-
let green_rgb =
-
let pixels = Bytes.create (100 * 100 * 3) in
-
for i = 0 to (100 * 100) - 1 do
-
let idx = i * 3 in
-
Bytes.set pixels idx '\x00'; (* R *)
-
Bytes.set pixels (idx + 1) '\xff'; (* G *)
-
Bytes.set pixels (idx + 2) '\x00' (* B *)
-
done;
-
Bytes.to_string pixels
-
in
+
print_endline "Demo 3: Image Formats - RGB format (24-bit)";
+
let green_data = solid_color_rgb ~width:100 ~height:100 ~r:0 ~g:255 ~b:0 in
send
(K.Command.transmit_and_display
-
~image_id:2
+
~image_id:3
~format:`Rgb24
~width:100 ~height:100
~quiet:`Errors_only
())
-
~data:green_rgb;
-
print_endline "Green square displayed using raw RGB format (no alpha)";
+
~data:green_data;
+
print_endline "Green square displayed using raw RGB format (no alpha channel)";
+
print_newline ();
+
wait_for_enter ();
+
+
(* Demo 4: Compression - Note: would need zlib library for actual compression *)
+
clear_screen ();
+
print_endline "Demo 4: Large Image (compression requires zlib library)";
+
let orange_data = solid_color_rgba ~width:200 ~height:200 ~r:255 ~g:165 ~b:0 ~a:255 in
+
send
+
(K.Command.transmit_and_display
+
~image_id:4
+
~format:`Rgba32
+
~width:200 ~height:200
+
~quiet:`Errors_only
+
())
+
~data:orange_data;
+
Printf.printf "Orange square (200x200) - %d bytes uncompressed\n" (String.length orange_data);
print_newline ();
wait_for_enter ();
-
(* Demo 3: Multiple placements - transmit once, display multiple times *)
+
(* Demo 5: Load and display external PNG file *)
clear_screen ();
-
print_endline "Demo 3: Multiple Placements";
-
let cyan_data = make_solid_frame ~width:80 ~height:80 ~r:0 ~g:255 ~b:255 in
-
(* Transmit only (a=t) *)
+
print_endline "Demo 5: Loading external PNG file (sf.png)";
+
(try
+
let png_data = read_file "sf.png" in
+
send
+
(K.Command.transmit_and_display
+
~image_id:10
+
~format:`Png
+
~quiet:`Errors_only
+
())
+
~data:png_data;
+
print_endline "sf.png loaded and displayed"
+
with Sys_error msg ->
+
Printf.printf "sf.png not found: %s\n" msg);
+
print_newline ();
+
wait_for_enter ();
+
+
(* Demo 6: Cropping and scaling *)
+
clear_screen ();
+
print_endline "Demo 6: Cropping and Scaling - Display part of an image";
+
let gradient = gradient_rgba ~width:200 ~height:200 in
+
send
+
(K.Command.transmit_and_display
+
~image_id:20
+
~format:`Rgba32
+
~width:200 ~height:200
+
~placement:(K.Placement.make
+
~source_x:50 ~source_y:50
+
~source_width:100 ~source_height:100
+
~columns:10 ~rows:10
+
())
+
~quiet:`Errors_only
+
())
+
~data:gradient;
+
print_endline "Cropped to center 100x100 region of a 200x200 gradient";
+
print_newline ();
+
wait_for_enter ();
+
+
(* Demo 7: Multiple placements *)
+
clear_screen ();
+
print_endline "Demo 7: Multiple Placements - One image, multiple displays";
+
let cyan_data = solid_color_rgba ~width:80 ~height:80 ~r:0 ~g:255 ~b:255 ~a:255 in
+
(* Transmit once with an ID *)
send
(K.Command.transmit
~image_id:100
···
~quiet:`Errors_only
())
~data:cyan_data;
-
(* Display first placement *)
+
(* Create first placement *)
send
(K.Command.display
~image_id:100
···
~quiet:`Errors_only
())
~data:"";
-
print_string " ";
-
(* Display second placement *)
+
(* Create second placement *)
send
(K.Command.display
~image_id:100
···
())
~data:"";
print_newline ();
-
print_endline "Same image displayed twice at different sizes";
+
wait_for_enter ();
+
+
(* Demo 8: Multiple placements with spacing *)
+
clear_screen ();
+
print_endline "Demo 8: Multiple Placements with Different Sizes";
+
print_newline ();
+
print_endline "Showing same image at different sizes:";
+
print_newline ();
+
(* Create a gradient square *)
+
let grad_small = gradient_rgba ~width:100 ~height:100 in
+
(* Transmit once *)
+
send
+
(K.Command.transmit
+
~image_id:160
+
~format:`Rgba32
+
~width:100 ~height:100
+
~quiet:`Errors_only
+
())
+
~data:grad_small;
+
(* Place same image three times at different sizes *)
+
send
+
(K.Command.display
+
~image_id:160
+
~placement:(K.Placement.make ~columns:5 ~rows:5 ())
+
~quiet:`Errors_only
+
())
+
~data:"";
+
print_string " ";
+
send
+
(K.Command.display
+
~image_id:160
+
~placement:(K.Placement.make ~columns:8 ~rows:8 ())
+
~quiet:`Errors_only
+
())
+
~data:"";
+
print_string " ";
+
send
+
(K.Command.display
+
~image_id:160
+
~placement:(K.Placement.make ~columns:12 ~rows:12 ())
+
~quiet:`Errors_only
+
())
+
~data:"";
+
print_newline ();
+
print_newline ();
+
print_endline "Small (5x5 cells), Medium (8x8 cells), Large (12x12 cells)";
print_newline ();
wait_for_enter ();
-
(* Demo 4: Z-index layering *)
+
(* Demo 9: Z-index layering *)
clear_screen ();
-
print_endline "Demo 4: Z-Index Layering";
-
let orange_data = make_solid_frame ~width:200 ~height:100 ~r:255 ~g:165 ~b:0 in
+
print_endline "Demo 9: Z-Index Layering - Images above/below text";
+
let bg_data = solid_color_rgba ~width:200 ~height:100 ~r:255 ~g:165 ~b:0 ~a:128 in
send
(K.Command.transmit_and_display
~image_id:200
···
~placement:(K.Placement.make ~z_index:(-1) ~cursor:`Static ())
~quiet:`Errors_only
())
-
~data:orange_data;
+
~data:bg_data;
print_endline "This orange square should appear behind the text!";
print_newline ();
wait_for_enter ();
-
(* Demo 5: Animation - matching kgp demo exactly *)
+
(* Demo 10: Query support *)
clear_screen ();
-
print_endline "Demo 5: Animation - Color-changing square";
-
print_endline "Creating animated sequence...";
+
print_endline "Demo 10: Query Support - Check terminal capabilities";
+
let query_str = K.Detect.make_query () in
+
print_string query_str;
flush stdout;
+
print_endline "(Check if your terminal responds with OK)";
+
print_newline ();
+
wait_for_enter ();
-
(* Using small size to avoid chunking - 10x10 = 400 bytes raw *)
-
let width, height = 10, 10 in
+
(* Demo 11: Animation - color-changing square *)
+
clear_screen ();
+
print_endline "Demo 11: Animation - Color-changing square";
+
print_endline "Creating animated sequence with 4 colors...";
+
+
let width, height = 80, 80 in
let image_id = 300 in
-
(* Step 1: Create base frame (red) - transmit only, don't display yet *)
-
let red_frame = make_solid_frame ~width ~height ~r:255 ~g:0 ~b:0 in
+
(* Create base frame (red) - transmit without displaying *)
+
let red_frame = solid_color_rgba ~width ~height ~r:255 ~g:0 ~b:0 ~a:255 in
send
(K.Command.transmit
~image_id
···
())
~data:red_frame;
-
(* Step 2: Add frame 2 (orange) with gap and composition replace *)
-
let orange_frame = make_solid_frame ~width ~height ~r:255 ~g:165 ~b:0 in
+
(* Add frames with composition replace *)
+
let orange_frame = solid_color_rgba ~width ~height ~r:255 ~g:165 ~b:0 ~a:255 in
send
(K.Command.frame
~image_id
···
())
~data:orange_frame;
-
(* Step 3: Add frame 3 (yellow) *)
-
let yellow_frame = make_solid_frame ~width ~height ~r:255 ~g:255 ~b:0 in
+
let yellow_frame = solid_color_rgba ~width ~height ~r:255 ~g:255 ~b:0 ~a:255 in
send
(K.Command.frame
~image_id
···
())
~data:yellow_frame;
-
(* Step 4: Add frame 4 (green) *)
-
let green_frame = make_solid_frame ~width ~height ~r:0 ~g:255 ~b:0 in
+
let green_frame = solid_color_rgba ~width ~height ~r:0 ~g:255 ~b:0 ~a:255 in
send
(K.Command.frame
~image_id
···
())
~data:green_frame;
-
(* Step 5: Create placement to display the animation *)
-
(* Add columns/rows to scale up the small image for visibility *)
+
(* Create placement and start animation *)
send
(K.Command.display
~image_id
~placement:(K.Placement.make
~placement_id:1
-
~columns:10
-
~rows:5
+
~cell_x_offset:0
+
~cell_y_offset:0
~cursor:`Static
())
~quiet:`Errors_only
())
~data:"";
-
(* Step 6: Start animation with infinite looping (s=3, v=1) *)
+
(* Set root frame gap - root frame has no gap by default per Kitty protocol *)
+
send
+
(K.Command.animate ~image_id (K.Animation.set_gap ~frame:1 ~gap_ms:100))
+
~data:"";
+
+
(* Start animation with infinite looping *)
send
(K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run))
~data:"";
print_newline ();
-
print_endline "Animation playing: Red -> Orange -> Yellow -> Green";
+
print_endline "Animation playing with colors: Red -> Orange -> Yellow -> Green";
print_newline ();
-
wait_for_enter ();
+
+
(* Simulate movement by deleting and recreating placement at different positions *)
+
for i = 1 to 7 do
+
Unix.sleepf 0.4;
+
+
(* Delete the current placement *)
+
send
+
(K.Command.delete ~quiet:`Errors_only (`By_id (image_id, Some 1)))
+
~data:"";
+
+
(* Create new placement at next position *)
+
send
+
(K.Command.display
+
~image_id
+
~placement:(K.Placement.make
+
~placement_id:1
+
~cell_x_offset:(i * 5)
+
~cell_y_offset:0
+
~cursor:`Static
+
())
+
~quiet:`Errors_only
+
())
+
~data:""
+
done;
(* Stop the animation *)
send
···
print_endline "Animation stopped.";
print_newline ();
-
-
(* Cleanup *)
+
print_newline ();
print_endline "Demo complete!";
-
()
+
print_newline ();
+
print_endline "For more examples, see the library documentation.";
+
wait_for_enter ()
stack/kitty_graphics/example/sf.png

This is a binary file and will not be displayed.

+59
stack/kitty_graphics/example/test_output.ml
···
+
(* Simple test to show exact escape sequences without data *)
+
+
module K = Kgp
+
+
let print_escaped s =
+
String.iter (fun c ->
+
let code = Char.code c in
+
if code = 27 then print_string "\\x1b"
+
else if code < 32 || code > 126 then Printf.printf "\\x%02x" code
+
else print_char c
+
) s;
+
print_newline ()
+
+
let () =
+
let image_id = 300 in
+
let width, height = 80, 80 in
+
+
print_endline "=== Animation Escape Sequences (no data) ===\n";
+
+
(* 1. Transmit base frame (no data for testing) *)
+
print_endline "1. Transmit (a=t):";
+
let cmd1 = K.Command.transmit
+
~image_id ~format:`Rgba32 ~width ~height ~quiet:`Errors_only () in
+
print_escaped (K.Command.to_string cmd1 ~data:"");
+
+
(* 2. Frame command *)
+
print_endline "\n2. Frame (a=f):";
+
let cmd2 = K.Command.frame
+
~image_id ~format:`Rgba32 ~width ~height
+
~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
+
~quiet:`Errors_only () in
+
print_escaped (K.Command.to_string cmd2 ~data:"");
+
+
(* 3. Put/display command *)
+
print_endline "\n3. Display/Put (a=p):";
+
let cmd3 = K.Command.display
+
~image_id
+
~placement:(K.Placement.make
+
~placement_id:1
+
~cell_x_offset:0
+
~cell_y_offset:0
+
~cursor:`Static ())
+
~quiet:`Errors_only () in
+
print_escaped (K.Command.to_string cmd3 ~data:"");
+
+
(* 4. Set root frame gap - IMPORTANT for animation! *)
+
print_endline "\n4. Set root frame gap (a=a, r=1, z=100):";
+
let cmd4 = K.Command.animate ~image_id (K.Animation.set_gap ~frame:1 ~gap_ms:100) in
+
print_escaped (K.Command.to_string cmd4 ~data:"");
+
+
(* 5. Animate - start *)
+
print_endline "\n5. Animate start (a=a, s=3, v=1):";
+
let cmd5 = K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run) in
+
print_escaped (K.Command.to_string cmd5 ~data:"");
+
+
(* 6. Animate - stop *)
+
print_endline "\n6. Animate stop (a=a, s=1):";
+
let cmd6 = K.Command.animate ~image_id (K.Animation.set_state `Stop) in
+
print_escaped (K.Command.to_string cmd6 ~data:"")
+108
stack/kitty_graphics/example/tiny_anim.ml
···
+
(* Tiny animation test - no chunking needed *)
+
(* Uses 20x20 images which are ~1067 bytes base64 (well under 4096) *)
+
+
module K = Kgp
+
+
let solid_color_rgba ~width ~height ~r ~g ~b ~a =
+
let pixels = Bytes.create (width * height * 4) in
+
for i = 0 to (width * height) - 1 do
+
let idx = i * 4 in
+
Bytes.set pixels idx (Char.chr r);
+
Bytes.set pixels (idx + 1) (Char.chr g);
+
Bytes.set pixels (idx + 2) (Char.chr b);
+
Bytes.set pixels (idx + 3) (Char.chr a)
+
done;
+
Bytes.to_string pixels
+
+
let send cmd ~data =
+
print_string (K.Command.to_string cmd ~data);
+
flush stdout
+
+
let () =
+
(* Use 20x20 to avoid chunking: 20*20*4 = 1600 bytes, base64 ~2134 bytes *)
+
let width, height = 20, 20 in
+
let image_id = 999 in
+
+
(* Clear any existing images *)
+
send (K.Command.delete ~quiet:`Errors_only (`All_visible_and_free)) ~data:"";
+
+
(* Step 1: Transmit base frame (red) - matching Go's sequence *)
+
let red_frame = solid_color_rgba ~width ~height ~r:255 ~g:0 ~b:0 ~a:255 in
+
send
+
(K.Command.transmit
+
~image_id
+
~format:`Rgba32
+
~width ~height
+
~quiet:`Errors_only
+
())
+
~data:red_frame;
+
+
(* Step 2: Add frame (orange) with 100ms gap - like Go *)
+
let orange_frame = solid_color_rgba ~width ~height ~r:255 ~g:165 ~b:0 ~a:255 in
+
send
+
(K.Command.frame
+
~image_id
+
~format:`Rgba32
+
~width ~height
+
~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
+
~quiet:`Errors_only
+
())
+
~data:orange_frame;
+
+
(* Step 3: Add frame (yellow) *)
+
let yellow_frame = solid_color_rgba ~width ~height ~r:255 ~g:255 ~b:0 ~a:255 in
+
send
+
(K.Command.frame
+
~image_id
+
~format:`Rgba32
+
~width ~height
+
~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
+
~quiet:`Errors_only
+
())
+
~data:yellow_frame;
+
+
(* Step 4: Add frame (green) *)
+
let green_frame = solid_color_rgba ~width ~height ~r:0 ~g:255 ~b:0 ~a:255 in
+
send
+
(K.Command.frame
+
~image_id
+
~format:`Rgba32
+
~width ~height
+
~frame:(K.Frame.make ~gap_ms:100 ~composition:`Overwrite ())
+
~quiet:`Errors_only
+
())
+
~data:green_frame;
+
+
(* Step 5: Create placement - exactly like Go *)
+
send
+
(K.Command.display
+
~image_id
+
~placement:(K.Placement.make
+
~placement_id:1
+
~cell_x_offset:0
+
~cell_y_offset:0
+
~cursor:`Static
+
())
+
~quiet:`Errors_only
+
())
+
~data:"";
+
+
(* Step 6: Start animation - exactly like Go (NO root frame gap) *)
+
send
+
(K.Command.animate ~image_id (K.Animation.set_state ~loops:1 `Run))
+
~data:"";
+
+
print_endline "";
+
print_endline "Tiny animation (20x20) - Red -> Orange -> Yellow -> Green";
+
print_endline "This uses no chunking. Press Enter to stop...";
+
flush stdout;
+
let _ = read_line () in
+
+
(* Stop animation *)
+
send
+
(K.Command.animate ~image_id (K.Animation.set_state `Stop))
+
~data:"";
+
+
(* Clean up *)
+
send (K.Command.delete ~quiet:`Errors_only (`All_visible_and_free)) ~data:"";
+
print_endline "Done."
+2 -2
stack/kitty_graphics/lib/dune
···
(library
-
(name kitty_graphics)
-
(public_name kitty_graphics)
+
(name kgp)
+
(public_name kgp)
(libraries base64))
+32
stack/kitty_graphics/lib/kgp.ml
···
+
(* Kitty Terminal Graphics Protocol
+
+
This library implements the Kitty terminal graphics protocol, allowing
+
OCaml programs to display images in terminals that support the protocol
+
(Kitty, WezTerm, Konsole, Ghostty, etc.). *)
+
+
(* Re-export types at top level *)
+
type format = Kgp_types.format
+
type transmission = Kgp_types.transmission
+
type compression = Kgp_types.compression
+
type quiet = Kgp_types.quiet
+
type cursor = Kgp_types.cursor
+
type composition = Kgp_types.composition
+
type delete = Kgp_types.delete
+
type animation_state = Kgp_types.animation_state
+
+
(* Module aliases *)
+
module Format = Kgp_types.Format
+
module Transmission = Kgp_types.Transmission
+
module Compression = Kgp_types.Compression
+
module Quiet = Kgp_types.Quiet
+
module Cursor = Kgp_types.Cursor
+
module Composition = Kgp_types.Composition
+
module Delete = Kgp_types.Delete
+
module Placement = Kgp_placement
+
module Frame = Kgp_frame
+
module Animation = Kgp_animation
+
module Compose = Kgp_compose
+
module Command = Kgp_command
+
module Response = Kgp_response
+
module Unicode_placeholder = Kgp_unicode
+
module Detect = Kgp_detect
+12
stack/kitty_graphics/lib/kgp_animation.ml
···
+
(* Kitty Graphics Protocol - Animation *)
+
+
type state = Kgp_types.animation_state
+
+
type t =
+
[ `Set_state of state * int option
+
| `Set_gap of int * int
+
| `Set_current of int ]
+
+
let set_state ?loops state = `Set_state (state, loops)
+
let set_gap ~frame ~gap_ms = `Set_gap (frame, gap_ms)
+
let set_current_frame frame = `Set_current frame
+21
stack/kitty_graphics/lib/kgp_animation.mli
···
+
(** Kitty Graphics Protocol - Animation *)
+
+
type state = Kgp_types.animation_state
+
+
type t =
+
[ `Set_state of state * int option
+
| `Set_gap of int * int
+
| `Set_current of int ]
+
(** Animation control operations. *)
+
+
val set_state : ?loops:int -> state -> t
+
(** Set animation state.
+
@param loops Number of loops: 0 = ignored, 1 = infinite, n = n-1 loops *)
+
+
val set_gap : frame:int -> gap_ms:int -> t
+
(** Set the gap (delay) for a specific frame.
+
@param frame 1-based frame number
+
@param gap_ms Delay in milliseconds (negative = gapless) *)
+
+
val set_current_frame : int -> t
+
(** Make a specific frame (1-based) the current displayed frame. *)
+332
stack/kitty_graphics/lib/kgp_command.ml
···
+
(* Kitty Graphics Protocol - Command *)
+
+
type action =
+
[ `Transmit
+
| `Transmit_and_display
+
| `Query
+
| `Display
+
| `Delete
+
| `Frame
+
| `Animate
+
| `Compose ]
+
+
type t = {
+
action : action;
+
format : Kgp_types.format option;
+
transmission : Kgp_types.transmission option;
+
compression : Kgp_types.compression option;
+
width : int option;
+
height : int option;
+
size : int option;
+
offset : int option;
+
quiet : Kgp_types.quiet option;
+
image_id : int option;
+
image_number : int option;
+
placement : Kgp_placement.t option;
+
delete : Kgp_types.delete option;
+
frame : Kgp_frame.t option;
+
animation : Kgp_animation.t option;
+
compose : Kgp_compose.t option;
+
}
+
+
let make action =
+
{
+
action;
+
format = None;
+
transmission = None;
+
compression = None;
+
width = None;
+
height = None;
+
size = None;
+
offset = None;
+
quiet = None;
+
image_id = None;
+
image_number = None;
+
placement = None;
+
delete = None;
+
frame = None;
+
animation = None;
+
compose = None;
+
}
+
+
let transmit ?image_id ?image_number ?format ?transmission ?compression ?width
+
?height ?size ?offset ?quiet () =
+
{
+
(make `Transmit) with
+
image_id;
+
image_number;
+
format;
+
transmission;
+
compression;
+
width;
+
height;
+
size;
+
offset;
+
quiet;
+
}
+
+
let transmit_and_display ?image_id ?image_number ?format ?transmission
+
?compression ?width ?height ?size ?offset ?quiet ?placement () =
+
{
+
(make `Transmit_and_display) with
+
image_id;
+
image_number;
+
format;
+
transmission;
+
compression;
+
width;
+
height;
+
size;
+
offset;
+
quiet;
+
placement;
+
}
+
+
let query ?format ?transmission ?width ?height ?quiet () =
+
{ (make `Query) with format; transmission; width; height; quiet }
+
+
let display ?image_id ?image_number ?placement ?quiet () =
+
{ (make `Display) with image_id; image_number; placement; quiet }
+
+
let delete ?quiet del = { (make `Delete) with quiet; delete = Some del }
+
+
let frame ?image_id ?image_number ?format ?transmission ?compression ?width
+
?height ?quiet ~frame () =
+
{
+
(make `Frame) with
+
image_id;
+
image_number;
+
format;
+
transmission;
+
compression;
+
width;
+
height;
+
quiet;
+
frame = Some frame;
+
}
+
+
let animate ?image_id ?image_number ?quiet anim =
+
{ (make `Animate) with image_id; image_number; quiet; animation = Some anim }
+
+
let compose ?image_id ?image_number ?quiet comp =
+
{ (make `Compose) with image_id; image_number; quiet; compose = Some comp }
+
+
(* Serialization helpers *)
+
let apc_start = "\027_G"
+
let apc_end = "\027\\"
+
+
(* Key-value writer with separator handling *)
+
type kv_writer = { mutable first : bool; buf : Buffer.t }
+
+
let kv_writer buf = { first = true; buf }
+
+
let kv w key value =
+
if not w.first then Buffer.add_char w.buf ',';
+
w.first <- false;
+
Buffer.add_char w.buf key;
+
Buffer.add_char w.buf '=';
+
Buffer.add_string w.buf value
+
+
let kv_int w key value = kv w key (string_of_int value)
+
let kv_int32 w key value = kv w key (Int32.to_string value)
+
let kv_char w key value = kv w key (String.make 1 value)
+
+
(* Conditional writers using Option.iter *)
+
let kv_int_opt w key = Option.iter (kv_int w key)
+
let kv_int32_opt w key = Option.iter (kv_int32 w key)
+
+
let kv_int_if w key ~default opt =
+
Option.iter (fun v -> if v <> default then kv_int w key v) opt
+
+
let action_char : action -> char = function
+
| `Transmit -> 't'
+
| `Transmit_and_display -> 'T'
+
| `Query -> 'q'
+
| `Display -> 'p'
+
| `Delete -> 'd'
+
| `Frame -> 'f'
+
| `Animate -> 'a'
+
| `Compose -> 'c'
+
+
let delete_char : Kgp_types.delete -> char = function
+
| `All_visible -> 'a'
+
| `All_visible_and_free -> 'A'
+
| `By_id _ -> 'i'
+
| `By_id_and_free _ -> 'I'
+
| `By_number _ -> 'n'
+
| `By_number_and_free _ -> 'N'
+
| `At_cursor -> 'c'
+
| `At_cursor_and_free -> 'C'
+
| `At_cell _ -> 'p'
+
| `At_cell_and_free _ -> 'P'
+
| `At_cell_z _ -> 'q'
+
| `At_cell_z_and_free _ -> 'Q'
+
| `By_column _ -> 'x'
+
| `By_column_and_free _ -> 'X'
+
| `By_row _ -> 'y'
+
| `By_row_and_free _ -> 'Y'
+
| `By_z_index _ -> 'z'
+
| `By_z_index_and_free _ -> 'Z'
+
| `By_id_range _ -> 'r'
+
| `By_id_range_and_free _ -> 'R'
+
| `Frames -> 'f'
+
| `Frames_and_free -> 'F'
+
+
let write_placement w (p : Kgp_placement.t) =
+
kv_int_opt w 'x' p.source_x;
+
kv_int_opt w 'y' p.source_y;
+
kv_int_opt w 'w' p.source_width;
+
kv_int_opt w 'h' p.source_height;
+
kv_int_opt w 'X' p.cell_x_offset;
+
kv_int_opt w 'Y' p.cell_y_offset;
+
kv_int_opt w 'c' p.columns;
+
kv_int_opt w 'r' p.rows;
+
kv_int_opt w 'z' p.z_index;
+
kv_int_opt w 'p' p.placement_id;
+
p.cursor
+
|> Option.iter (fun c ->
+
kv_int_if w 'C' ~default:0 (Some (Kgp_types.Cursor.to_int c)));
+
if p.unicode_placeholder then kv_int w 'U' 1
+
+
let write_delete w (d : Kgp_types.delete) =
+
kv_char w 'd' (delete_char d);
+
match d with
+
| `By_id (id, pid) | `By_id_and_free (id, pid) ->
+
kv_int w 'i' id;
+
kv_int_opt w 'p' pid
+
| `By_number (n, pid) | `By_number_and_free (n, pid) ->
+
kv_int w 'I' n;
+
kv_int_opt w 'p' pid
+
| `At_cell (x, y) | `At_cell_and_free (x, y) ->
+
kv_int w 'x' x;
+
kv_int w 'y' y
+
| `At_cell_z (x, y, z) | `At_cell_z_and_free (x, y, z) ->
+
kv_int w 'x' x;
+
kv_int w 'y' y;
+
kv_int w 'z' z
+
| `By_column c | `By_column_and_free c -> kv_int w 'x' c
+
| `By_row r | `By_row_and_free r -> kv_int w 'y' r
+
| `By_z_index z | `By_z_index_and_free z -> kv_int w 'z' z
+
| `By_id_range (min_id, max_id) | `By_id_range_and_free (min_id, max_id) ->
+
kv_int w 'x' min_id;
+
kv_int w 'y' max_id
+
| `All_visible | `All_visible_and_free | `At_cursor | `At_cursor_and_free
+
| `Frames | `Frames_and_free ->
+
()
+
+
let write_frame w (f : Kgp_frame.t) =
+
kv_int_opt w 'x' f.x;
+
kv_int_opt w 'y' f.y;
+
kv_int_opt w 'c' f.base_frame;
+
kv_int_opt w 'r' f.edit_frame;
+
kv_int_opt w 'z' f.gap_ms;
+
f.composition
+
|> Option.iter (fun c ->
+
kv_int_if w 'X' ~default:0 (Some (Kgp_types.Composition.to_int c)));
+
kv_int32_opt w 'Y' f.background_color
+
+
let write_animation w : Kgp_animation.t -> unit = function
+
| `Set_state (state, loops) ->
+
let s = match state with `Stop -> 1 | `Loading -> 2 | `Run -> 3 in
+
kv_int w 's' s;
+
kv_int_opt w 'v' loops
+
| `Set_gap (frame, gap_ms) ->
+
kv_int w 'r' frame;
+
kv_int w 'z' gap_ms
+
| `Set_current frame -> kv_int w 'c' frame
+
+
let write_compose w (c : Kgp_compose.t) =
+
kv_int w 'r' c.source_frame;
+
kv_int w 'c' c.dest_frame;
+
kv_int_opt w 'w' c.width;
+
kv_int_opt w 'h' c.height;
+
kv_int_opt w 'x' c.dest_x;
+
kv_int_opt w 'y' c.dest_y;
+
kv_int_opt w 'X' c.source_x;
+
kv_int_opt w 'Y' c.source_y;
+
c.composition
+
|> Option.iter (fun comp ->
+
kv_int_if w 'C' ~default:0 (Some (Kgp_types.Composition.to_int comp)))
+
+
let write_control_data buf cmd =
+
let w = kv_writer buf in
+
(* Action *)
+
kv_char w 'a' (action_char cmd.action);
+
(* Quiet - only if non-default *)
+
cmd.quiet
+
|> Option.iter (fun q ->
+
kv_int_if w 'q' ~default:0 (Some (Kgp_types.Quiet.to_int q)));
+
(* Format *)
+
cmd.format
+
|> Option.iter (fun f -> kv_int w 'f' (Kgp_types.Format.to_int f));
+
(* Transmission - only for transmit/frame actions, always include t=d for compatibility *)
+
(match cmd.action with
+
| `Transmit | `Transmit_and_display | `Frame -> (
+
match cmd.transmission with
+
| Some t -> kv_char w 't' (Kgp_types.Transmission.to_char t)
+
| None -> kv_char w 't' 'd')
+
| _ -> ());
+
(* Compression *)
+
cmd.compression
+
|> Option.iter (fun c ->
+
Kgp_types.Compression.to_char c |> Option.iter (kv_char w 'o'));
+
(* Dimensions *)
+
kv_int_opt w 's' cmd.width;
+
kv_int_opt w 'v' cmd.height;
+
(* File size/offset *)
+
kv_int_opt w 'S' cmd.size;
+
kv_int_opt w 'O' cmd.offset;
+
(* Image ID/number *)
+
kv_int_opt w 'i' cmd.image_id;
+
kv_int_opt w 'I' cmd.image_number;
+
(* Complex options *)
+
cmd.placement |> Option.iter (write_placement w);
+
cmd.delete |> Option.iter (write_delete w);
+
cmd.frame |> Option.iter (write_frame w);
+
cmd.animation |> Option.iter (write_animation w);
+
cmd.compose |> Option.iter (write_compose w);
+
w
+
+
(* Use large chunk size to avoid chunking - Kitty animation doesn't handle chunks well *)
+
let chunk_size = 1024 * 1024 (* 1MB - effectively no chunking *)
+
+
let write buf cmd ~data =
+
Buffer.add_string buf apc_start;
+
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
+
if len <= chunk_size then (
+
Buffer.add_char buf ';';
+
Buffer.add_string buf encoded;
+
Buffer.add_string buf apc_end)
+
else begin
+
(* Multiple chunks *)
+
let rec write_chunks pos first =
+
if pos < len then begin
+
let remaining = len - pos in
+
let this_chunk = min chunk_size remaining in
+
let is_last = pos + this_chunk >= len in
+
if first then (
+
kv_int w 'm' 1;
+
Buffer.add_char buf ';';
+
Buffer.add_substring buf encoded pos this_chunk;
+
Buffer.add_string buf apc_end)
+
else (
+
Buffer.add_string buf apc_start;
+
Buffer.add_string buf (if is_last then "m=0" else "m=1");
+
Buffer.add_char buf ';';
+
Buffer.add_substring buf encoded pos this_chunk;
+
Buffer.add_string buf apc_end);
+
write_chunks (pos + this_chunk) false
+
end
+
in
+
write_chunks 0 true
+
end
+
end
+
else Buffer.add_string buf apc_end
+
+
let to_string cmd ~data =
+
let buf = Buffer.create 1024 in
+
write buf cmd ~data;
+
Buffer.contents buf
+113
stack/kitty_graphics/lib/kgp_command.mli
···
+
(** Kitty Graphics Protocol - Command *)
+
+
type action =
+
[ `Transmit
+
| `Transmit_and_display
+
| `Query
+
| `Display
+
| `Delete
+
| `Frame
+
| `Animate
+
| `Compose ]
+
+
type t
+
(** A graphics protocol command. *)
+
+
(** {2 Image Transmission} *)
+
+
val transmit :
+
?image_id:int ->
+
?image_number:int ->
+
?format:Kgp_types.format ->
+
?transmission:Kgp_types.transmission ->
+
?compression:Kgp_types.compression ->
+
?width:int ->
+
?height:int ->
+
?size:int ->
+
?offset:int ->
+
?quiet:Kgp_types.quiet ->
+
unit ->
+
t
+
(** Transmit image data without displaying. *)
+
+
val transmit_and_display :
+
?image_id:int ->
+
?image_number:int ->
+
?format:Kgp_types.format ->
+
?transmission:Kgp_types.transmission ->
+
?compression:Kgp_types.compression ->
+
?width:int ->
+
?height:int ->
+
?size:int ->
+
?offset:int ->
+
?quiet:Kgp_types.quiet ->
+
?placement:Kgp_placement.t ->
+
unit ->
+
t
+
(** Transmit image data and display it immediately. *)
+
+
val query :
+
?format:Kgp_types.format ->
+
?transmission:Kgp_types.transmission ->
+
?width:int ->
+
?height:int ->
+
?quiet:Kgp_types.quiet ->
+
unit ->
+
t
+
(** Query terminal support without storing the image. *)
+
+
(** {2 Display} *)
+
+
val display :
+
?image_id:int ->
+
?image_number:int ->
+
?placement:Kgp_placement.t ->
+
?quiet:Kgp_types.quiet ->
+
unit ->
+
t
+
(** Display a previously transmitted image. *)
+
+
(** {2 Deletion} *)
+
+
val delete : ?quiet:Kgp_types.quiet -> Kgp_types.delete -> t
+
(** Delete images or placements. *)
+
+
(** {2 Animation} *)
+
+
val frame :
+
?image_id:int ->
+
?image_number:int ->
+
?format:Kgp_types.format ->
+
?transmission:Kgp_types.transmission ->
+
?compression:Kgp_types.compression ->
+
?width:int ->
+
?height:int ->
+
?quiet:Kgp_types.quiet ->
+
frame:Kgp_frame.t ->
+
unit ->
+
t
+
(** Transmit animation frame data. *)
+
+
val animate :
+
?image_id:int ->
+
?image_number:int ->
+
?quiet:Kgp_types.quiet ->
+
Kgp_animation.t ->
+
t
+
(** Control animation playback. *)
+
+
val compose :
+
?image_id:int ->
+
?image_number:int ->
+
?quiet:Kgp_types.quiet ->
+
Kgp_compose.t ->
+
t
+
(** Compose animation frames. *)
+
+
(** {2 Output} *)
+
+
val write : Buffer.t -> t -> data:string -> unit
+
(** Write the command to a buffer. *)
+
+
val to_string : t -> data:string -> string
+
(** Convert command to a string. *)
+27
stack/kitty_graphics/lib/kgp_compose.ml
···
+
(* Kitty Graphics Protocol - Compose *)
+
+
type t = {
+
source_frame : int;
+
dest_frame : int;
+
width : int option;
+
height : int option;
+
source_x : int option;
+
source_y : int option;
+
dest_x : int option;
+
dest_y : int option;
+
composition : Kgp_types.composition option;
+
}
+
+
let make ~source_frame ~dest_frame ?width ?height ?source_x ?source_y ?dest_x
+
?dest_y ?composition () =
+
{
+
source_frame;
+
dest_frame;
+
width;
+
height;
+
source_x;
+
source_y;
+
dest_x;
+
dest_y;
+
composition;
+
}
+27
stack/kitty_graphics/lib/kgp_compose.mli
···
+
(** Kitty Graphics Protocol - Compose *)
+
+
type t = {
+
source_frame : int;
+
dest_frame : int;
+
width : int option;
+
height : int option;
+
source_x : int option;
+
source_y : int option;
+
dest_x : int option;
+
dest_y : int option;
+
composition : Kgp_types.composition option;
+
}
+
+
val make :
+
source_frame:int ->
+
dest_frame:int ->
+
?width:int ->
+
?height:int ->
+
?source_x:int ->
+
?source_y:int ->
+
?dest_x:int ->
+
?dest_y:int ->
+
?composition:Kgp_types.composition ->
+
unit ->
+
t
+
(** Compose a rectangle from one frame onto another. *)
+13
stack/kitty_graphics/lib/kgp_detect.ml
···
+
(* Kitty Graphics Protocol - Detection *)
+
+
let make_query () =
+
(* Query without DA1 suffix - matches Go's QuerySupport() *)
+
let cmd =
+
Kgp_command.query ~format:`Rgb24 ~transmission:`Direct ~width:1 ~height:1 ()
+
in
+
Kgp_command.to_string cmd ~data:"\x00\x00\x00"
+
+
let supports_graphics response ~da1_received =
+
response
+
|> Option.map Kgp_response.is_ok
+
|> Option.value ~default:(not da1_received)
+7
stack/kitty_graphics/lib/kgp_detect.mli
···
+
(** Kitty Graphics Protocol - Terminal Detection *)
+
+
val make_query : unit -> string
+
(** Generate a query command to test graphics support. *)
+
+
val supports_graphics : Kgp_response.t option -> da1_received:bool -> bool
+
(** Determine if graphics are supported based on query results. *)
+26
stack/kitty_graphics/lib/kgp_frame.ml
···
+
(* Kitty Graphics Protocol - Frame *)
+
+
type t = {
+
x : int option;
+
y : int option;
+
base_frame : int option;
+
edit_frame : int option;
+
gap_ms : int option;
+
composition : Kgp_types.composition option;
+
background_color : int32 option;
+
}
+
+
let empty =
+
{
+
x = None;
+
y = None;
+
base_frame = None;
+
edit_frame = None;
+
gap_ms = None;
+
composition = None;
+
background_color = None;
+
}
+
+
let make ?x ?y ?base_frame ?edit_frame ?gap_ms ?composition ?background_color
+
() =
+
{ x; y; base_frame; edit_frame; gap_ms; composition; background_color }
+34
stack/kitty_graphics/lib/kgp_frame.mli
···
+
(** Kitty Graphics Protocol - Frame *)
+
+
type t = {
+
x : int option;
+
y : int option;
+
base_frame : int option;
+
edit_frame : int option;
+
gap_ms : int option;
+
composition : Kgp_types.composition option;
+
background_color : int32 option;
+
}
+
+
val empty : t
+
(** Empty frame spec with defaults. *)
+
+
val make :
+
?x:int ->
+
?y:int ->
+
?base_frame:int ->
+
?edit_frame:int ->
+
?gap_ms:int ->
+
?composition:Kgp_types.composition ->
+
?background_color:int32 ->
+
unit ->
+
t
+
(** Create a frame specification.
+
+
@param x Left edge where frame data is placed (pixels)
+
@param y Top edge where frame data is placed (pixels)
+
@param base_frame 1-based frame number to use as background canvas
+
@param edit_frame 1-based frame number to edit (0 = new frame)
+
@param gap_ms Delay before next frame in milliseconds
+
@param composition How to blend pixels onto the canvas
+
@param background_color 32-bit RGBA background when no base frame *)
+50
stack/kitty_graphics/lib/kgp_placement.ml
···
+
(* Kitty Graphics Protocol - Placement *)
+
+
type t = {
+
source_x : int option;
+
source_y : int option;
+
source_width : int option;
+
source_height : int option;
+
cell_x_offset : int option;
+
cell_y_offset : int option;
+
columns : int option;
+
rows : int option;
+
z_index : int option;
+
placement_id : int option;
+
cursor : Kgp_types.cursor option;
+
unicode_placeholder : bool;
+
}
+
+
let empty =
+
{
+
source_x = None;
+
source_y = None;
+
source_width = None;
+
source_height = None;
+
cell_x_offset = None;
+
cell_y_offset = None;
+
columns = None;
+
rows = None;
+
z_index = None;
+
placement_id = None;
+
cursor = None;
+
unicode_placeholder = false;
+
}
+
+
let make ?source_x ?source_y ?source_width ?source_height ?cell_x_offset
+
?cell_y_offset ?columns ?rows ?z_index ?placement_id ?cursor
+
?(unicode_placeholder = false) () =
+
{
+
source_x;
+
source_y;
+
source_width;
+
source_height;
+
cell_x_offset;
+
cell_y_offset;
+
columns;
+
rows;
+
z_index;
+
placement_id;
+
cursor;
+
unicode_placeholder;
+
}
+49
stack/kitty_graphics/lib/kgp_placement.mli
···
+
(** Kitty Graphics Protocol - Placement *)
+
+
type t = {
+
source_x : int option;
+
source_y : int option;
+
source_width : int option;
+
source_height : int option;
+
cell_x_offset : int option;
+
cell_y_offset : int option;
+
columns : int option;
+
rows : int option;
+
z_index : int option;
+
placement_id : int option;
+
cursor : Kgp_types.cursor option;
+
unicode_placeholder : bool;
+
}
+
+
val empty : t
+
(** Empty placement with all defaults. *)
+
+
val make :
+
?source_x:int ->
+
?source_y:int ->
+
?source_width:int ->
+
?source_height:int ->
+
?cell_x_offset:int ->
+
?cell_y_offset:int ->
+
?columns:int ->
+
?rows:int ->
+
?z_index:int ->
+
?placement_id:int ->
+
?cursor:Kgp_types.cursor ->
+
?unicode_placeholder:bool ->
+
unit ->
+
t
+
(** Create a placement configuration.
+
+
@param source_x Left edge of source rectangle in pixels (default 0)
+
@param source_y Top edge of source rectangle in pixels (default 0)
+
@param source_width Width of source rectangle (default: full width)
+
@param source_height Height of source rectangle (default: full height)
+
@param cell_x_offset X offset within the first cell in pixels
+
@param cell_y_offset Y offset within the first cell in pixels
+
@param columns Number of columns to display over (scales image)
+
@param rows Number of rows to display over (scales image)
+
@param z_index Stacking order (negative = under text)
+
@param placement_id Unique ID for this placement
+
@param cursor Cursor movement policy after display
+
@param unicode_placeholder Create virtual placement for Unicode mode *)
+56
stack/kitty_graphics/lib/kgp_response.ml
···
+
(* Kitty Graphics Protocol - Response *)
+
+
type t = {
+
message : string;
+
image_id : int option;
+
image_number : int option;
+
placement_id : int option;
+
}
+
+
let is_ok t = t.message = "OK"
+
let message t = t.message
+
+
let error_code t =
+
if is_ok t then None
+
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
+
+
let parse s =
+
let ( let* ) = Option.bind in
+
let esc = '\027' in
+
let len = String.length s in
+
let* () =
+
if len >= 5 && s.[0] = esc && s.[1] = '_' && s.[2] = 'G' then Some ()
+
else None
+
in
+
let* semi_pos = String.index_from_opt s 3 ';' in
+
let rec find_end pos =
+
if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then Some pos
+
else if pos + 1 < len then find_end (pos + 1)
+
else None
+
in
+
let* end_pos = find_end (semi_pos + 1) in
+
let keys_str = String.sub s 3 (semi_pos - 3) in
+
let message = String.sub s (semi_pos + 1) (end_pos - semi_pos - 1) in
+
let parse_kv part =
+
if String.length part >= 3 && part.[1] = '=' then
+
Some (part.[0], String.sub part 2 (String.length part - 2))
+
else None
+
in
+
let keys = String.split_on_char ',' keys_str |> List.filter_map parse_kv in
+
let find_int key =
+
List.assoc_opt key keys |> Fun.flip Option.bind int_of_string_opt
+
in
+
Some
+
{
+
message;
+
image_id = find_int 'i';
+
image_number = find_int 'I';
+
placement_id = find_int 'p';
+
}
+25
stack/kitty_graphics/lib/kgp_response.mli
···
+
(** Kitty Graphics Protocol - Response Parsing *)
+
+
type t
+
(** A parsed terminal response. *)
+
+
val parse : string -> t option
+
(** Parse a response from terminal output. *)
+
+
val is_ok : t -> bool
+
(** Check if the response indicates success. *)
+
+
val message : t -> string
+
(** Get the response message. *)
+
+
val error_code : t -> string option
+
(** Extract the error code if this is an error response. *)
+
+
val image_id : t -> int option
+
(** Get the image ID from the response. *)
+
+
val image_number : t -> int option
+
(** Get the image number from the response. *)
+
+
val placement_id : t -> int option
+
(** Get the placement ID from the response. *)
+89
stack/kitty_graphics/lib/kgp_types.ml
···
+
(* Kitty Graphics Protocol - Types *)
+
+
type format = [ `Rgba32 | `Rgb24 | `Png ]
+
type transmission = [ `Direct | `File | `Tempfile ]
+
type compression = [ `None | `Zlib ]
+
type quiet = [ `Noisy | `Errors_only | `Silent ]
+
type cursor = [ `Move | `Static ]
+
type composition = [ `Alpha_blend | `Overwrite ]
+
+
type delete =
+
[ `All_visible
+
| `All_visible_and_free
+
| `By_id of int * int option
+
| `By_id_and_free of int * int option
+
| `By_number of int * int option
+
| `By_number_and_free of int * int option
+
| `At_cursor
+
| `At_cursor_and_free
+
| `At_cell of int * int
+
| `At_cell_and_free of int * int
+
| `At_cell_z of int * int * int
+
| `At_cell_z_and_free of int * int * int
+
| `By_column of int
+
| `By_column_and_free of int
+
| `By_row of int
+
| `By_row_and_free of int
+
| `By_z_index of int
+
| `By_z_index_and_free of int
+
| `By_id_range of int * int
+
| `By_id_range_and_free of int * int
+
| `Frames
+
| `Frames_and_free ]
+
+
type animation_state = [ `Stop | `Loading | `Run ]
+
+
module Format = struct
+
type t = format
+
+
let to_int : t -> int = function
+
| `Rgba32 -> 32
+
| `Rgb24 -> 24
+
| `Png -> 100
+
end
+
+
module Transmission = struct
+
type t = transmission
+
+
let to_char : t -> char = function
+
| `Direct -> 'd'
+
| `File -> 'f'
+
| `Tempfile -> 't'
+
end
+
+
module Compression = struct
+
type t = compression
+
+
let to_char : t -> char option = function
+
| `None -> None
+
| `Zlib -> Some 'z'
+
end
+
+
module Quiet = struct
+
type t = quiet
+
+
let to_int : t -> int = function
+
| `Noisy -> 0
+
| `Errors_only -> 1
+
| `Silent -> 2
+
end
+
+
module Cursor = struct
+
type t = cursor
+
+
let to_int : t -> int = function
+
| `Move -> 0
+
| `Static -> 1
+
end
+
+
module Composition = struct
+
type t = composition
+
+
let to_int : t -> int = function
+
| `Alpha_blend -> 0
+
| `Overwrite -> 1
+
end
+
+
module Delete = struct
+
type t = delete
+
end
+81
stack/kitty_graphics/lib/kgp_types.mli
···
+
(** Kitty Graphics Protocol - Types *)
+
+
type format = [ `Rgba32 | `Rgb24 | `Png ]
+
(** Image data formats. *)
+
+
type transmission = [ `Direct | `File | `Tempfile ]
+
(** Transmission methods. *)
+
+
type compression = [ `None | `Zlib ]
+
(** Compression options. *)
+
+
type quiet = [ `Noisy | `Errors_only | `Silent ]
+
(** Response suppression. *)
+
+
type cursor = [ `Move | `Static ]
+
(** Cursor movement after displaying. *)
+
+
type composition = [ `Alpha_blend | `Overwrite ]
+
(** Composition modes. *)
+
+
type delete =
+
[ `All_visible
+
| `All_visible_and_free
+
| `By_id of int * int option
+
| `By_id_and_free of int * int option
+
| `By_number of int * int option
+
| `By_number_and_free of int * int option
+
| `At_cursor
+
| `At_cursor_and_free
+
| `At_cell of int * int
+
| `At_cell_and_free of int * int
+
| `At_cell_z of int * int * int
+
| `At_cell_z_and_free of int * int * int
+
| `By_column of int
+
| `By_column_and_free of int
+
| `By_row of int
+
| `By_row_and_free of int
+
| `By_z_index of int
+
| `By_z_index_and_free of int
+
| `By_id_range of int * int
+
| `By_id_range_and_free of int * int
+
| `Frames
+
| `Frames_and_free ]
+
(** Delete target specification. *)
+
+
type animation_state = [ `Stop | `Loading | `Run ]
+
(** Animation playback state. *)
+
+
module Format : sig
+
type t = format
+
val to_int : t -> int
+
end
+
+
module Transmission : sig
+
type t = transmission
+
val to_char : t -> char
+
end
+
+
module Compression : sig
+
type t = compression
+
val to_char : t -> char option
+
end
+
+
module Quiet : sig
+
type t = quiet
+
val to_int : t -> int
+
end
+
+
module Cursor : sig
+
type t = cursor
+
val to_int : t -> int
+
end
+
+
module Composition : sig
+
type t = composition
+
val to_int : t -> int
+
end
+
+
module Delete : sig
+
type t = delete
+
end
+91
stack/kitty_graphics/lib/kgp_unicode.ml
···
+
(* Kitty Graphics Protocol - Unicode Placeholders *)
+
+
let placeholder_char = Uchar.of_int 0x10EEEE
+
+
let diacritics =
+
[|
+
0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F; 0x0346;
+
0x034A; 0x034B; 0x034C; 0x0350; 0x0351; 0x0352; 0x0357; 0x035B; 0x0363;
+
0x0364; 0x0365; 0x0366; 0x0367; 0x0368; 0x0369; 0x036A; 0x036B; 0x036C;
+
0x036D; 0x036E; 0x036F; 0x0483; 0x0484; 0x0485; 0x0486; 0x0487; 0x0592;
+
0x0593; 0x0594; 0x0595; 0x0597; 0x0598; 0x0599; 0x059C; 0x059D; 0x059E;
+
0x059F; 0x05A0; 0x05A1; 0x05A8; 0x05A9; 0x05AB; 0x05AC; 0x05AF; 0x05C4;
+
0x0610; 0x0611; 0x0612; 0x0613; 0x0614; 0x0615; 0x0616; 0x0617; 0x0657;
+
0x0658; 0x0659; 0x065A; 0x065B; 0x065D; 0x065E; 0x06D6; 0x06D7; 0x06D8;
+
0x06D9; 0x06DA; 0x06DB; 0x06DC; 0x06DF; 0x06E0; 0x06E1; 0x06E2; 0x06E4;
+
0x06E7; 0x06E8; 0x06EB; 0x06EC; 0x0730; 0x0732; 0x0733; 0x0735; 0x0736;
+
0x073A; 0x073D; 0x073F; 0x0740; 0x0741; 0x0743; 0x0745; 0x0747; 0x0749;
+
0x074A; 0x07EB; 0x07EC; 0x07ED; 0x07EE; 0x07EF; 0x07F0; 0x07F1; 0x07F3;
+
0x0816; 0x0817; 0x0818; 0x0819; 0x081B; 0x081C; 0x081D; 0x081E; 0x081F;
+
0x0820; 0x0821; 0x0822; 0x0823; 0x0825; 0x0826; 0x0827; 0x0829; 0x082A;
+
0x082B; 0x082C; 0x082D; 0x0951; 0x0953; 0x0954; 0x0F82; 0x0F83; 0x0F86;
+
0x0F87; 0x135D; 0x135E; 0x135F; 0x17DD; 0x193A; 0x1A17; 0x1A75; 0x1A76;
+
0x1A77; 0x1A78; 0x1A79; 0x1A7A; 0x1A7B; 0x1A7C; 0x1B6B; 0x1B6D; 0x1B6E;
+
0x1B6F; 0x1B70; 0x1B71; 0x1B72; 0x1B73; 0x1CD0; 0x1CD1; 0x1CD2; 0x1CDA;
+
0x1CDB; 0x1CE0; 0x1DC0; 0x1DC1; 0x1DC3; 0x1DC4; 0x1DC5; 0x1DC6; 0x1DC7;
+
0x1DC8; 0x1DC9; 0x1DCB; 0x1DCC; 0x1DD1; 0x1DD2; 0x1DD3; 0x1DD4; 0x1DD5;
+
0x1DD6; 0x1DD7; 0x1DD8; 0x1DD9; 0x1DDA; 0x1DDB; 0x1DDC; 0x1DDD; 0x1DDE;
+
0x1DDF; 0x1DE0; 0x1DE1; 0x1DE2; 0x1DE3; 0x1DE4; 0x1DE5; 0x1DE6; 0x1DFE;
+
0x20D0; 0x20D1; 0x20D4; 0x20D5; 0x20D6; 0x20D7; 0x20DB; 0x20DC; 0x20E1;
+
0x20E7; 0x20E9; 0x20F0; 0xA66F; 0xA67C; 0xA67D; 0xA6F0; 0xA6F1; 0xA8E0;
+
0xA8E1; 0xA8E2; 0xA8E3; 0xA8E4; 0xA8E5; 0xA8E6; 0xA8E7; 0xA8E8; 0xA8E9;
+
0xA8EA; 0xA8EB; 0xA8EC; 0xA8ED; 0xA8EE; 0xA8EF; 0xA8F0; 0xA8F1; 0xAAB0;
+
0xAAB2; 0xAAB3; 0xAAB7; 0xAAB8; 0xAABE; 0xAABF; 0xAAC1; 0xFE20; 0xFE21;
+
0xFE22; 0xFE23; 0xFE24; 0xFE25; 0xFE26; 0x10A0F; 0x10A38; 0x1D185;
+
0x1D186; 0x1D187; 0x1D188; 0x1D189; 0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD;
+
0x1D242; 0x1D243; 0x1D244;
+
|]
+
+
let diacritic n = Uchar.of_int diacritics.(n mod Array.length diacritics)
+
let row_diacritic = diacritic
+
let column_diacritic = diacritic
+
let id_high_byte_diacritic = diacritic
+
+
let add_uchar buf u =
+
let code = Uchar.to_int u in
+
let put = Buffer.add_char buf in
+
if code < 0x80 then put (Char.chr code)
+
else if code < 0x800 then (
+
put (Char.chr (0xC0 lor (code lsr 6)));
+
put (Char.chr (0x80 lor (code land 0x3F))))
+
else if code < 0x10000 then (
+
put (Char.chr (0xE0 lor (code lsr 12)));
+
put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
+
put (Char.chr (0x80 lor (code land 0x3F))))
+
else (
+
put (Char.chr (0xF0 lor (code lsr 18)));
+
put (Char.chr (0x80 lor ((code lsr 12) land 0x3F)));
+
put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
+
put (Char.chr (0x80 lor (code land 0x3F))))
+
+
let write buf ~image_id ?placement_id ~rows ~cols () =
+
(* Set foreground color *)
+
Printf.bprintf buf "\027[38;2;%d;%d;%dm"
+
((image_id lsr 16) land 0xFF)
+
((image_id lsr 8) land 0xFF)
+
(image_id land 0xFF);
+
(* Optional placement ID in underline color *)
+
placement_id
+
|> Option.iter (fun pid ->
+
Printf.bprintf buf "\027[58;2;%d;%d;%dm"
+
((pid lsr 16) land 0xFF)
+
((pid lsr 8) land 0xFF)
+
(pid land 0xFF));
+
(* High byte diacritic *)
+
let high_byte = (image_id lsr 24) land 0xFF in
+
let high_diac =
+
if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None
+
in
+
(* Write grid *)
+
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);
+
high_diac |> Option.iter (add_uchar buf)
+
done;
+
if row < rows - 1 then Buffer.add_string buf "\n\r"
+
done;
+
(* Reset colors *)
+
Buffer.add_string buf "\027[39m";
+
if Option.is_some placement_id then Buffer.add_string buf "\027[59m"
+23
stack/kitty_graphics/lib/kgp_unicode.mli
···
+
(** Kitty Graphics Protocol - Unicode Placeholders *)
+
+
val placeholder_char : Uchar.t
+
(** The Unicode placeholder character U+10EEEE. *)
+
+
val write :
+
Buffer.t ->
+
image_id:int ->
+
?placement_id:int ->
+
rows:int ->
+
cols:int ->
+
unit ->
+
unit
+
(** Write placeholder characters to a buffer. *)
+
+
val row_diacritic : int -> Uchar.t
+
(** Get the combining diacritic for a row number (0-based). *)
+
+
val column_diacritic : int -> Uchar.t
+
(** Get the combining diacritic for a column number (0-based). *)
+
+
val id_high_byte_diacritic : int -> Uchar.t
+
(** Get the diacritic for the high byte of a 32-bit image ID. *)
-684
stack/kitty_graphics/lib/kitty_graphics.ml
···
-
(* Kitty Terminal Graphics Protocol - Implementation *)
-
-
(* Polymorphic variant types *)
-
type format = [ `Rgba32 | `Rgb24 | `Png ]
-
type transmission = [ `Direct | `File | `Tempfile ]
-
type compression = [ `None | `Zlib ]
-
type quiet = [ `Noisy | `Errors_only | `Silent ]
-
type cursor = [ `Move | `Static ]
-
type composition = [ `Alpha_blend | `Overwrite ]
-
-
type delete =
-
[ `All_visible
-
| `All_visible_and_free
-
| `By_id of int * int option
-
| `By_id_and_free of int * int option
-
| `By_number of int * int option
-
| `By_number_and_free of int * int option
-
| `At_cursor
-
| `At_cursor_and_free
-
| `At_cell of int * int
-
| `At_cell_and_free of int * int
-
| `At_cell_z of int * int * int
-
| `At_cell_z_and_free of int * int * int
-
| `By_column of int
-
| `By_column_and_free of int
-
| `By_row of int
-
| `By_row_and_free of int
-
| `By_z_index of int
-
| `By_z_index_and_free of int
-
| `By_id_range of int * int
-
| `By_id_range_and_free of int * int
-
| `Frames
-
| `Frames_and_free ]
-
-
type animation_state = [ `Stop | `Loading | `Run ]
-
-
(* Modules re-export the types with conversion functions *)
-
module Format = struct
-
type t = format
-
-
let to_int : t -> int = function
-
| `Rgba32 -> 32
-
| `Rgb24 -> 24
-
| `Png -> 100
-
end
-
-
module Transmission = struct
-
type t = transmission
-
-
let to_char : t -> char = function
-
| `Direct -> 'd'
-
| `File -> 'f'
-
| `Tempfile -> 't'
-
end
-
-
module Compression = struct
-
type t = compression
-
-
let to_char : t -> char option = function
-
| `None -> None
-
| `Zlib -> Some 'z'
-
end
-
-
module Quiet = struct
-
type t = quiet
-
-
let to_int : t -> int = function
-
| `Noisy -> 0
-
| `Errors_only -> 1
-
| `Silent -> 2
-
end
-
-
module Cursor = struct
-
type t = cursor
-
-
let to_int : t -> int = function
-
| `Move -> 0
-
| `Static -> 1
-
end
-
-
module Composition = struct
-
type t = composition
-
-
let to_int : t -> int = function
-
| `Alpha_blend -> 0
-
| `Overwrite -> 1
-
end
-
-
module Delete = struct
-
type t = delete
-
end
-
-
module Placement = struct
-
type t = {
-
source_x : int option;
-
source_y : int option;
-
source_width : int option;
-
source_height : int option;
-
cell_x_offset : int option;
-
cell_y_offset : int option;
-
columns : int option;
-
rows : int option;
-
z_index : int option;
-
placement_id : int option;
-
cursor : cursor option;
-
unicode_placeholder : bool;
-
}
-
-
let empty =
-
{
-
source_x = None;
-
source_y = None;
-
source_width = None;
-
source_height = None;
-
cell_x_offset = None;
-
cell_y_offset = None;
-
columns = None;
-
rows = None;
-
z_index = None;
-
placement_id = None;
-
cursor = None;
-
unicode_placeholder = false;
-
}
-
-
let make ?source_x ?source_y ?source_width ?source_height ?cell_x_offset
-
?cell_y_offset ?columns ?rows ?z_index ?placement_id ?cursor
-
?(unicode_placeholder = false) () =
-
{
-
source_x;
-
source_y;
-
source_width;
-
source_height;
-
cell_x_offset;
-
cell_y_offset;
-
columns;
-
rows;
-
z_index;
-
placement_id;
-
cursor;
-
unicode_placeholder;
-
}
-
end
-
-
module Frame = struct
-
type t = {
-
x : int option;
-
y : int option;
-
base_frame : int option;
-
edit_frame : int option;
-
gap_ms : int option;
-
composition : composition option;
-
background_color : int32 option;
-
}
-
-
let empty =
-
{
-
x = None;
-
y = None;
-
base_frame = None;
-
edit_frame = None;
-
gap_ms = None;
-
composition = None;
-
background_color = None;
-
}
-
-
let make ?x ?y ?base_frame ?edit_frame ?gap_ms ?composition ?background_color
-
() =
-
{ x; y; base_frame; edit_frame; gap_ms; composition; background_color }
-
end
-
-
module Animation = struct
-
type state = animation_state
-
-
type t =
-
[ `Set_state of state * int option
-
| `Set_gap of int * int
-
| `Set_current of int ]
-
-
let set_state ?loops state = `Set_state (state, loops)
-
let set_gap ~frame ~gap_ms = `Set_gap (frame, gap_ms)
-
let set_current_frame frame = `Set_current frame
-
end
-
-
module Compose = struct
-
type t = {
-
source_frame : int;
-
dest_frame : int;
-
width : int option;
-
height : int option;
-
source_x : int option;
-
source_y : int option;
-
dest_x : int option;
-
dest_y : int option;
-
composition : composition option;
-
}
-
-
let make ~source_frame ~dest_frame ?width ?height ?source_x ?source_y ?dest_x
-
?dest_y ?composition () =
-
{
-
source_frame;
-
dest_frame;
-
width;
-
height;
-
source_x;
-
source_y;
-
dest_x;
-
dest_y;
-
composition;
-
}
-
end
-
-
module Command = struct
-
type action =
-
[ `Transmit
-
| `Transmit_and_display
-
| `Query
-
| `Display
-
| `Delete
-
| `Frame
-
| `Animate
-
| `Compose ]
-
-
type t = {
-
action : action;
-
format : format option;
-
transmission : transmission option;
-
compression : compression option;
-
width : int option;
-
height : int option;
-
size : int option;
-
offset : int option;
-
quiet : quiet option;
-
image_id : int option;
-
image_number : int option;
-
placement : Placement.t option;
-
delete : delete option;
-
frame : Frame.t option;
-
animation : Animation.t option;
-
compose : Compose.t option;
-
}
-
-
let make action =
-
{
-
action;
-
format = None;
-
transmission = None;
-
compression = None;
-
width = None;
-
height = None;
-
size = None;
-
offset = None;
-
quiet = None;
-
image_id = None;
-
image_number = None;
-
placement = None;
-
delete = None;
-
frame = None;
-
animation = None;
-
compose = None;
-
}
-
-
let transmit ?image_id ?image_number ?format ?transmission ?compression ?width
-
?height ?size ?offset ?quiet () =
-
{
-
(make `Transmit) with
-
image_id;
-
image_number;
-
format;
-
transmission;
-
compression;
-
width;
-
height;
-
size;
-
offset;
-
quiet;
-
}
-
-
let transmit_and_display ?image_id ?image_number ?format ?transmission
-
?compression ?width ?height ?size ?offset ?quiet ?placement () =
-
{
-
(make `Transmit_and_display) with
-
image_id;
-
image_number;
-
format;
-
transmission;
-
compression;
-
width;
-
height;
-
size;
-
offset;
-
quiet;
-
placement;
-
}
-
-
let query ?format ?transmission ?width ?height ?quiet () =
-
{ (make `Query) with format; transmission; width; height; quiet }
-
-
let display ?image_id ?image_number ?placement ?quiet () =
-
{ (make `Display) with image_id; image_number; placement; quiet }
-
-
let delete ?quiet del = { (make `Delete) with quiet; delete = Some del }
-
-
let frame ?image_id ?image_number ?format ?transmission ?compression ?width
-
?height ?quiet ~frame () =
-
{
-
(make `Frame) with
-
image_id;
-
image_number;
-
format;
-
transmission;
-
compression;
-
width;
-
height;
-
quiet;
-
frame = Some frame;
-
}
-
-
let animate ?image_id ?image_number ?quiet anim =
-
{ (make `Animate) with image_id; image_number; quiet; animation = Some anim }
-
-
let compose ?image_id ?image_number ?quiet comp =
-
{ (make `Compose) with image_id; image_number; quiet; compose = Some comp }
-
-
(* Serialization helpers *)
-
let apc_start = "\027_G"
-
let apc_end = "\027\\"
-
-
(* Key-value writer with separator handling *)
-
type kv_writer = { mutable first : bool; buf : Buffer.t }
-
-
let kv_writer buf = { first = true; buf }
-
-
let kv w key value =
-
if not w.first then Buffer.add_char w.buf ',';
-
w.first <- false;
-
Buffer.add_char w.buf key;
-
Buffer.add_char w.buf '=';
-
Buffer.add_string w.buf value
-
-
let kv_int w key value = kv w key (string_of_int value)
-
let kv_int32 w key value = kv w key (Int32.to_string value)
-
let kv_char w key value = kv w key (String.make 1 value)
-
-
(* Conditional writers using Option.iter *)
-
let kv_int_opt w key = Option.iter (kv_int w key)
-
let kv_int32_opt w key = Option.iter (kv_int32 w key)
-
-
let kv_int_if w key ~default opt =
-
Option.iter (fun v -> if v <> default then kv_int w key v) opt
-
-
let action_char : action -> char = function
-
| `Transmit -> 't'
-
| `Transmit_and_display -> 'T'
-
| `Query -> 'q'
-
| `Display -> 'p'
-
| `Delete -> 'd'
-
| `Frame -> 'f'
-
| `Animate -> 'a'
-
| `Compose -> 'c'
-
-
let delete_char : delete -> char = function
-
| `All_visible -> 'a'
-
| `All_visible_and_free -> 'A'
-
| `By_id _ -> 'i'
-
| `By_id_and_free _ -> 'I'
-
| `By_number _ -> 'n'
-
| `By_number_and_free _ -> 'N'
-
| `At_cursor -> 'c'
-
| `At_cursor_and_free -> 'C'
-
| `At_cell _ -> 'p'
-
| `At_cell_and_free _ -> 'P'
-
| `At_cell_z _ -> 'q'
-
| `At_cell_z_and_free _ -> 'Q'
-
| `By_column _ -> 'x'
-
| `By_column_and_free _ -> 'X'
-
| `By_row _ -> 'y'
-
| `By_row_and_free _ -> 'Y'
-
| `By_z_index _ -> 'z'
-
| `By_z_index_and_free _ -> 'Z'
-
| `By_id_range _ -> 'r'
-
| `By_id_range_and_free _ -> 'R'
-
| `Frames -> 'f'
-
| `Frames_and_free -> 'F'
-
-
let write_placement w (p : Placement.t) =
-
kv_int_opt w 'x' p.source_x;
-
kv_int_opt w 'y' p.source_y;
-
kv_int_opt w 'w' p.source_width;
-
kv_int_opt w 'h' p.source_height;
-
kv_int_opt w 'X' p.cell_x_offset;
-
kv_int_opt w 'Y' p.cell_y_offset;
-
kv_int_opt w 'c' p.columns;
-
kv_int_opt w 'r' p.rows;
-
kv_int_opt w 'z' p.z_index;
-
kv_int_opt w 'p' p.placement_id;
-
p.cursor |> Option.iter (fun c -> kv_int_if w 'C' ~default:0 (Some (Cursor.to_int c)));
-
if p.unicode_placeholder then kv_int w 'U' 1
-
-
let write_delete w (d : delete) =
-
kv_char w 'd' (delete_char d);
-
match d with
-
| `By_id (id, pid) | `By_id_and_free (id, pid) ->
-
kv_int w 'i' id;
-
kv_int_opt w 'p' pid
-
| `By_number (n, pid) | `By_number_and_free (n, pid) ->
-
kv_int w 'I' n;
-
kv_int_opt w 'p' pid
-
| `At_cell (x, y) | `At_cell_and_free (x, y) ->
-
kv_int w 'x' x;
-
kv_int w 'y' y
-
| `At_cell_z (x, y, z) | `At_cell_z_and_free (x, y, z) ->
-
kv_int w 'x' x;
-
kv_int w 'y' y;
-
kv_int w 'z' z
-
| `By_column c | `By_column_and_free c -> kv_int w 'x' c
-
| `By_row r | `By_row_and_free r -> kv_int w 'y' r
-
| `By_z_index z | `By_z_index_and_free z -> kv_int w 'z' z
-
| `By_id_range (min_id, max_id) | `By_id_range_and_free (min_id, max_id) ->
-
kv_int w 'x' min_id;
-
kv_int w 'y' max_id
-
| `All_visible | `All_visible_and_free | `At_cursor | `At_cursor_and_free
-
| `Frames | `Frames_and_free ->
-
()
-
-
let write_frame w (f : Frame.t) =
-
kv_int_opt w 'x' f.x;
-
kv_int_opt w 'y' f.y;
-
kv_int_opt w 'c' f.base_frame;
-
kv_int_opt w 'r' f.edit_frame;
-
kv_int_opt w 'z' f.gap_ms;
-
f.composition
-
|> Option.iter (fun c -> kv_int_if w 'X' ~default:0 (Some (Composition.to_int c)));
-
kv_int32_opt w 'Y' f.background_color
-
-
let write_animation w : Animation.t -> unit = function
-
| `Set_state (state, loops) ->
-
let s = match state with `Stop -> 1 | `Loading -> 2 | `Run -> 3 in
-
kv_int w 's' s;
-
kv_int_opt w 'v' loops
-
| `Set_gap (frame, gap_ms) ->
-
kv_int w 'r' frame;
-
kv_int w 'z' gap_ms
-
| `Set_current frame -> kv_int w 'c' frame
-
-
let write_compose w (c : Compose.t) =
-
kv_int w 'r' c.source_frame;
-
kv_int w 'c' c.dest_frame;
-
kv_int_opt w 'w' c.width;
-
kv_int_opt w 'h' c.height;
-
kv_int_opt w 'x' c.dest_x;
-
kv_int_opt w 'y' c.dest_y;
-
kv_int_opt w 'X' c.source_x;
-
kv_int_opt w 'Y' c.source_y;
-
c.composition
-
|> Option.iter (fun comp -> kv_int_if w 'C' ~default:0 (Some (Composition.to_int comp)))
-
-
let write_control_data buf cmd =
-
let w = kv_writer buf in
-
(* Action *)
-
kv_char w 'a' (action_char cmd.action);
-
(* Quiet - only if non-default *)
-
cmd.quiet |> Option.iter (fun q -> kv_int_if w 'q' ~default:0 (Some (Quiet.to_int q)));
-
(* Format *)
-
cmd.format |> Option.iter (fun f -> kv_int w 'f' (Format.to_int f));
-
(* Transmission - only if non-default *)
-
cmd.transmission
-
|> Option.iter (fun t ->
-
let c = Transmission.to_char t in
-
if c <> 'd' then kv_char w 't' c);
-
(* Compression *)
-
cmd.compression |> Option.iter (fun c -> Compression.to_char c |> Option.iter (kv_char w 'o'));
-
(* Dimensions *)
-
kv_int_opt w 's' cmd.width;
-
kv_int_opt w 'v' cmd.height;
-
(* File size/offset *)
-
kv_int_opt w 'S' cmd.size;
-
kv_int_opt w 'O' cmd.offset;
-
(* Image ID/number *)
-
kv_int_opt w 'i' cmd.image_id;
-
kv_int_opt w 'I' cmd.image_number;
-
(* Complex options *)
-
cmd.placement |> Option.iter (write_placement w);
-
cmd.delete |> Option.iter (write_delete w);
-
cmd.frame |> Option.iter (write_frame w);
-
cmd.animation |> Option.iter (write_animation w);
-
cmd.compose |> Option.iter (write_compose w);
-
w
-
-
let chunk_size = 4096
-
-
let write buf cmd ~data =
-
Buffer.add_string buf apc_start;
-
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
-
if len <= chunk_size then (
-
Buffer.add_char buf ';';
-
Buffer.add_string buf encoded;
-
Buffer.add_string buf apc_end)
-
else begin
-
(* Multiple chunks *)
-
let rec write_chunks pos first =
-
if pos < len then begin
-
let remaining = len - pos in
-
let this_chunk = min chunk_size remaining in
-
let is_last = pos + this_chunk >= len in
-
if first then (
-
kv_int w 'm' 1;
-
Buffer.add_char buf ';';
-
Buffer.add_substring buf encoded pos this_chunk;
-
Buffer.add_string buf apc_end)
-
else (
-
Buffer.add_string buf apc_start;
-
Buffer.add_string buf (if is_last then "m=0" else "m=1");
-
Buffer.add_char buf ';';
-
Buffer.add_substring buf encoded pos this_chunk;
-
Buffer.add_string buf apc_end);
-
write_chunks (pos + this_chunk) false
-
end
-
in
-
write_chunks 0 true
-
end
-
end
-
else Buffer.add_string buf apc_end
-
-
let to_string cmd ~data =
-
let buf = Buffer.create 1024 in
-
write buf cmd ~data;
-
Buffer.contents buf
-
end
-
-
module Response = struct
-
type t = {
-
message : string;
-
image_id : int option;
-
image_number : int option;
-
placement_id : int option;
-
}
-
-
let is_ok t = t.message = "OK"
-
let message t = t.message
-
-
let error_code t =
-
if is_ok t then None
-
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
-
-
let parse s =
-
let ( let* ) = Option.bind in
-
let esc = '\027' in
-
let len = String.length s in
-
let* () = if len >= 5 && s.[0] = esc && s.[1] = '_' && s.[2] = 'G' then Some () else None in
-
let* semi_pos = String.index_from_opt s 3 ';' in
-
let rec find_end pos =
-
if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then Some pos
-
else if pos + 1 < len then find_end (pos + 1)
-
else None
-
in
-
let* end_pos = find_end (semi_pos + 1) in
-
let keys_str = String.sub s 3 (semi_pos - 3) in
-
let message = String.sub s (semi_pos + 1) (end_pos - semi_pos - 1) in
-
let parse_kv part =
-
if String.length part >= 3 && part.[1] = '=' then
-
Some (part.[0], String.sub part 2 (String.length part - 2))
-
else None
-
in
-
let keys = String.split_on_char ',' keys_str |> List.filter_map parse_kv in
-
let find_int key = List.assoc_opt key keys |> Fun.flip Option.bind int_of_string_opt in
-
Some
-
{
-
message;
-
image_id = find_int 'i';
-
image_number = find_int 'I';
-
placement_id = find_int 'p';
-
}
-
end
-
-
module Unicode_placeholder = struct
-
let placeholder_char = Uchar.of_int 0x10EEEE
-
-
let diacritics =
-
[|
-
0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F;
-
0x0346; 0x034A; 0x034B; 0x034C; 0x0350; 0x0351; 0x0352; 0x0357;
-
0x035B; 0x0363; 0x0364; 0x0365; 0x0366; 0x0367; 0x0368; 0x0369;
-
0x036A; 0x036B; 0x036C; 0x036D; 0x036E; 0x036F; 0x0483; 0x0484;
-
0x0485; 0x0486; 0x0487; 0x0592; 0x0593; 0x0594; 0x0595; 0x0597;
-
0x0598; 0x0599; 0x059C; 0x059D; 0x059E; 0x059F; 0x05A0; 0x05A1;
-
0x05A8; 0x05A9; 0x05AB; 0x05AC; 0x05AF; 0x05C4; 0x0610; 0x0611;
-
0x0612; 0x0613; 0x0614; 0x0615; 0x0616; 0x0617; 0x0657; 0x0658;
-
0x0659; 0x065A; 0x065B; 0x065D; 0x065E; 0x06D6; 0x06D7; 0x06D8;
-
0x06D9; 0x06DA; 0x06DB; 0x06DC; 0x06DF; 0x06E0; 0x06E1; 0x06E2;
-
0x06E4; 0x06E7; 0x06E8; 0x06EB; 0x06EC; 0x0730; 0x0732; 0x0733;
-
0x0735; 0x0736; 0x073A; 0x073D; 0x073F; 0x0740; 0x0741; 0x0743;
-
0x0745; 0x0747; 0x0749; 0x074A; 0x07EB; 0x07EC; 0x07ED; 0x07EE;
-
0x07EF; 0x07F0; 0x07F1; 0x07F3; 0x0816; 0x0817; 0x0818; 0x0819;
-
0x081B; 0x081C; 0x081D; 0x081E; 0x081F; 0x0820; 0x0821; 0x0822;
-
0x0823; 0x0825; 0x0826; 0x0827; 0x0829; 0x082A; 0x082B; 0x082C;
-
0x082D; 0x0951; 0x0953; 0x0954; 0x0F82; 0x0F83; 0x0F86; 0x0F87;
-
0x135D; 0x135E; 0x135F; 0x17DD; 0x193A; 0x1A17; 0x1A75; 0x1A76;
-
0x1A77; 0x1A78; 0x1A79; 0x1A7A; 0x1A7B; 0x1A7C; 0x1B6B; 0x1B6D;
-
0x1B6E; 0x1B6F; 0x1B70; 0x1B71; 0x1B72; 0x1B73; 0x1CD0; 0x1CD1;
-
0x1CD2; 0x1CDA; 0x1CDB; 0x1CE0; 0x1DC0; 0x1DC1; 0x1DC3; 0x1DC4;
-
0x1DC5; 0x1DC6; 0x1DC7; 0x1DC8; 0x1DC9; 0x1DCB; 0x1DCC; 0x1DD1;
-
0x1DD2; 0x1DD3; 0x1DD4; 0x1DD5; 0x1DD6; 0x1DD7; 0x1DD8; 0x1DD9;
-
0x1DDA; 0x1DDB; 0x1DDC; 0x1DDD; 0x1DDE; 0x1DDF; 0x1DE0; 0x1DE1;
-
0x1DE2; 0x1DE3; 0x1DE4; 0x1DE5; 0x1DE6; 0x1DFE; 0x20D0; 0x20D1;
-
0x20D4; 0x20D5; 0x20D6; 0x20D7; 0x20DB; 0x20DC; 0x20E1; 0x20E7;
-
0x20E9; 0x20F0; 0xA66F; 0xA67C; 0xA67D; 0xA6F0; 0xA6F1; 0xA8E0;
-
0xA8E1; 0xA8E2; 0xA8E3; 0xA8E4; 0xA8E5; 0xA8E6; 0xA8E7; 0xA8E8;
-
0xA8E9; 0xA8EA; 0xA8EB; 0xA8EC; 0xA8ED; 0xA8EE; 0xA8EF; 0xA8F0;
-
0xA8F1; 0xAAB0; 0xAAB2; 0xAAB3; 0xAAB7; 0xAAB8; 0xAABE; 0xAABF;
-
0xAAC1; 0xFE20; 0xFE21; 0xFE22; 0xFE23; 0xFE24; 0xFE25; 0xFE26;
-
0x10A0F; 0x10A38; 0x1D185; 0x1D186; 0x1D187; 0x1D188; 0x1D189;
-
0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD; 0x1D242; 0x1D243; 0x1D244;
-
|]
-
-
let diacritic n =
-
Uchar.of_int diacritics.(n mod Array.length diacritics)
-
-
let row_diacritic = diacritic
-
let column_diacritic = diacritic
-
let id_high_byte_diacritic = diacritic
-
-
let add_uchar buf u =
-
let code = Uchar.to_int u in
-
let put = Buffer.add_char buf in
-
if code < 0x80 then put (Char.chr code)
-
else if code < 0x800 then (
-
put (Char.chr (0xC0 lor (code lsr 6)));
-
put (Char.chr (0x80 lor (code land 0x3F))))
-
else if code < 0x10000 then (
-
put (Char.chr (0xE0 lor (code lsr 12)));
-
put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
-
put (Char.chr (0x80 lor (code land 0x3F))))
-
else (
-
put (Char.chr (0xF0 lor (code lsr 18)));
-
put (Char.chr (0x80 lor ((code lsr 12) land 0x3F)));
-
put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
-
put (Char.chr (0x80 lor (code land 0x3F))))
-
-
let write buf ~image_id ?placement_id ~rows ~cols () =
-
(* Set foreground color *)
-
Printf.bprintf buf "\027[38;2;%d;%d;%dm"
-
((image_id lsr 16) land 0xFF)
-
((image_id lsr 8) land 0xFF)
-
(image_id land 0xFF);
-
(* Optional placement ID in underline color *)
-
placement_id
-
|> Option.iter (fun pid ->
-
Printf.bprintf buf "\027[58;2;%d;%d;%dm"
-
((pid lsr 16) land 0xFF)
-
((pid lsr 8) land 0xFF)
-
(pid land 0xFF));
-
(* High byte diacritic *)
-
let high_byte = (image_id lsr 24) land 0xFF in
-
let high_diac = if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None in
-
(* Write grid *)
-
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);
-
high_diac |> Option.iter (add_uchar buf)
-
done;
-
if row < rows - 1 then Buffer.add_string buf "\n\r"
-
done;
-
(* Reset colors *)
-
Buffer.add_string buf "\027[39m";
-
if Option.is_some placement_id then Buffer.add_string buf "\027[59m"
-
end
-
-
module Detect = struct
-
let make_query () =
-
let cmd = Command.query ~format:`Rgb24 ~transmission:`Direct ~width:1 ~height:1 () in
-
Command.to_string cmd ~data:"\x00\x00\x00" ^ "\027[c"
-
-
let supports_graphics response ~da1_received =
-
response |> Option.map Response.is_ok |> Option.value ~default:(not da1_received)
-
end
+8 -11
stack/kitty_graphics/lib/kitty_graphics.mli stack/kitty_graphics/lib/kgp.mli
···
{[
(* Display a PNG image *)
let png_data = read_file "image.png" in
-
let cmd = Kitty_graphics.Command.transmit_and_display ~format:`Png () in
+
let cmd = Kgp.Command.transmit_and_display ~format:`Png () in
let buf = Buffer.create 1024 in
-
Kitty_graphics.Command.write buf cmd ~data:png_data;
+
Kgp.Command.write buf cmd ~data:png_data;
print_string (Buffer.contents buf)
]}
···
(** {1 Placement Options} *)
module Placement : sig
-
type t
+
type t = Kgp_placement.t
(** Placement configuration. *)
val make :
···
(** {1 Animation} *)
module Frame : sig
-
type t
+
type t = Kgp_frame.t
(** Animation frame configuration. *)
val make :
···
module Animation : sig
type state = animation_state
-
type t =
-
[ `Set_state of state * int option
-
| `Set_gap of int * int
-
| `Set_current of int ]
+
type t = Kgp_animation.t
(** Animation control operations. *)
val set_state : ?loops:int -> state -> t
···
end
module Compose : sig
-
type t
+
type t = Kgp_compose.t
(** Composition operation. *)
val make :
···
(** {1 Commands} *)
module Command : sig
-
type t
+
type t = Kgp_command.t
(** A graphics protocol command. *)
(** {2 Image Transmission} *)
···
(** {1 Response Parsing} *)
module Response : sig
-
type t
+
type t = Kgp_response.t
(** A parsed terminal response. *)
val parse : string -> t option
stack/kitty_graphics/sf.png

This is a binary file and will not be displayed.

+32 -11
stack/sortal/lib/sortal.ml
···
bluesky : string option;
mastodon : string option;
orcid : string option;
-
url : string option;
+
url_ : string option;
+
urls_ : string list option;
feeds : Feed.t list option;
}
let make ~handle ~names ?email ?icon ?thumbnail ?github ?twitter ?bluesky ?mastodon
-
?orcid ?url ?feeds () =
+
?orcid ?url ?urls ?feeds () =
{ handle; names; email; icon; thumbnail; github; twitter; bluesky; mastodon;
-
orcid; url; feeds }
+
orcid; url_ = url; urls_ = urls; feeds }
let handle t = t.handle
let names t = t.names
···
let bluesky t = t.bluesky
let mastodon t = t.mastodon
let orcid t = t.orcid
-
let url t = t.url
+
+
let url t =
+
match t.url_ with
+
| Some _ as u -> u
+
| None ->
+
match t.urls_ with
+
| Some (first :: _) -> Some first
+
| _ -> None
+
+
let urls t =
+
match t.url_, t.urls_ with
+
| Some u, Some us -> u :: us
+
| Some u, None -> [u]
+
| None, Some us -> us
+
| None, None -> []
+
let feeds t = t.feeds
let add_feed t feed =
···
{ t with feeds }
let best_url t =
-
match t.url with
+
match url t with
| Some v -> Some v
| None ->
(match t.github with
···
let open Jsont in
let open Jsont.Object in
let mem_opt f v ~enc = mem f v ~dec_absent:None ~enc_omit:Option.is_none ~enc in
-
let make handle names email icon thumbnail github twitter bluesky mastodon orcid url feeds =
+
let make handle names email icon thumbnail github twitter bluesky mastodon orcid url urls feeds =
{ handle; names; email; icon; thumbnail; github; twitter; bluesky; mastodon;
-
orcid; url; feeds }
+
orcid; url_ = url; urls_ = urls; feeds }
in
map ~kind:"Contact" make
|> mem "handle" string ~enc:handle
···
|> mem_opt "bluesky" (some string) ~enc:bluesky
|> mem_opt "mastodon" (some string) ~enc:mastodon
|> mem_opt "orcid" (some string) ~enc:orcid
-
|> mem_opt "url" (some string) ~enc:url
+
|> mem_opt "url" (some string) ~enc:(fun t -> t.url_)
+
|> mem_opt "urls" (some (list string)) ~enc:(fun t -> t.urls_)
|> mem_opt "feeds" (some (list Feed.json_t)) ~enc:feeds
|> finish
···
| Some o -> pf ppf "%a: https://orcid.org/%a@,"
(styled `Bold string) "ORCID" string o
| None -> ());
-
(match t.url with
-
| Some u -> pf ppf "%a: %a@," (styled `Bold string) "URL" string u
-
| None -> ());
+
(let all_urls = urls t in
+
match all_urls with
+
| [] -> ()
+
| [u] -> pf ppf "%a: %a@," (styled `Bold string) "URL" string u
+
| _ ->
+
pf ppf "%a:@," (styled `Bold string) "URLs";
+
List.iter (fun u -> pf ppf " - %s@," u) all_urls);
(match t.icon with
| Some i -> pf ppf "%a: %a@," (styled `Bold string) "Icon" string i
| None -> ());
+14 -2
stack/sortal/lib/sortal.mli
···
@param bluesky Bluesky handle
@param mastodon Mastodon handle (including instance)
@param orcid ORCID identifier
-
@param url Personal or professional website URL
+
@param url Personal or professional website URL (primary URL)
+
@param urls Additional website URLs
@param feeds List of feed subscriptions (Atom/RSS/JSON) associated with this contact
*)
val make :
···
?mastodon:string ->
?orcid:string ->
?url:string ->
+
?urls:string list ->
?feeds:Feed.t list ->
unit ->
t
···
(** [orcid t] returns the ORCID identifier if available. *)
val orcid : t -> string option
-
(** [url t] returns the personal/professional website URL if available. *)
+
(** [url t] returns the primary URL if available.
+
+
Returns the [url] field if set, otherwise returns the first element
+
of [urls] if available, or [None] if neither is set. *)
val url : t -> string option
+
+
(** [urls t] returns all URLs associated with this contact.
+
+
Combines the [url] field (if set) with the [urls] list (if set).
+
The primary [url] appears first if present. Returns an empty list
+
if neither [url] nor [urls] is set. *)
+
val urls : t -> string list
(** [feeds t] returns the list of feed subscriptions if available. *)
val feeds : t -> Feed.t list option
+40
stack/sortal/test/test_sortal.ml
···
assert (Sortal.Contact.compare c1 c3 = 0);
traceln "✓ Contact comparison works"
+
let test_urls () =
+
(* Test with only url set *)
+
let c1 = Sortal.Contact.make
+
~handle:"test1"
+
~names:["Test 1"]
+
~url:"https://example.com"
+
() in
+
assert (Sortal.Contact.url c1 = Some "https://example.com");
+
assert (Sortal.Contact.urls c1 = ["https://example.com"]);
+
+
(* Test with only urls set *)
+
let c2 = Sortal.Contact.make
+
~handle:"test2"
+
~names:["Test 2"]
+
~urls:["https://one.com"; "https://two.com"]
+
() in
+
assert (Sortal.Contact.url c2 = Some "https://one.com");
+
assert (Sortal.Contact.urls c2 = ["https://one.com"; "https://two.com"]);
+
+
(* Test with both url and urls set *)
+
let c3 = Sortal.Contact.make
+
~handle:"test3"
+
~names:["Test 3"]
+
~url:"https://primary.com"
+
~urls:["https://secondary.com"; "https://tertiary.com"]
+
() in
+
assert (Sortal.Contact.url c3 = Some "https://primary.com");
+
assert (Sortal.Contact.urls c3 = ["https://primary.com"; "https://secondary.com"; "https://tertiary.com"]);
+
+
(* Test with neither set *)
+
let c4 = Sortal.Contact.make
+
~handle:"test4"
+
~names:["Test 4"]
+
() in
+
assert (Sortal.Contact.url c4 = None);
+
assert (Sortal.Contact.urls c4 = []);
+
+
traceln "✓ URLs field works correctly"
+
let () =
traceln "\n=== Running Sortal Tests ===\n";
···
test_json_encoding ();
test_handle_generation ();
test_contact_compare ();
+
test_urls ();
test_store_operations ();
traceln "\n=== All Tests Passed ===\n"
-2
stack/xdge/.gitignore
···
-
_build
-
.*.swp
-2
stack/xdge/.ocamlformat
···
-
version=0.27.0
-
profile=janestreet
-5
stack/xdge/CLAUDE.md
···
-
This is an XDG library for Eio
-
-
The library follows OCaml best practices with abstract types (`type t`) per
-
module, comprehensive constructors/accessors, and proper pretty printers. Each
-
core concept gets its own module with a clean interface.
-30
stack/xdge/dune-project
···
-
(lang dune 3.20)
-
-
(name xdge)
-
-
(generate_opam_files true)
-
-
(license ISC)
-
(authors "Anil Madhavapeddy")
-
(homepage "https://tangled.sh/@anil.recoil.org/ocaml-gpx")
-
(maintainers "Anil Madhavapeddy <anil@recoil.org>")
-
(bug_reports https://tangled.sh/@anil.recoil.org/xgde)
-
(maintenance_intent "(latest)")
-
-
(package
-
(name xdge)
-
(synopsis "XDG Base Directory Specification support for Eio")
-
(description
-
"This library implements the XDG Base Directory Specification \
-
with Eio capabilities to provides safe access to configuration, \
-
data, cache, state, and runtime directories with proper environment \
-
variable overrides and Cmdliner integration.")
-
(depends
-
(ocaml (>= 5.1.0))
-
(eio (>= 1.1))
-
eio_main
-
(xdg (>= 3.9.0))
-
(cmdliner (>= 1.2.0))
-
(fmt (>= 0.11.0))
-
(odoc :with-doc)
-
(alcotest (and :with-test (>= 1.7.0)))))
-4
stack/xdge/example/dune
···
-
(executable
-
(public_name xdg_example)
-
(name xdg_example)
-
(libraries xdge eio_main cmdliner fmt))
stack/xdge/example/minimal_test.cmi

This is a binary file and will not be displayed.

stack/xdge/example/minimal_test.cmo

This is a binary file and will not be displayed.

-37
stack/xdge/example/xdg_example.ml
···
-
let run (xdg, cfg) =
-
Fmt.pr
-
"%a@.%a@.@.%a@.%a@."
-
Fmt.(styled `Bold string)
-
"=== Cmdliner Config ==="
-
Xdge.Cmd.pp
-
cfg
-
Fmt.(styled `Bold string)
-
"=== XDG Directories ==="
-
(Xdge.pp ~brief:false ~sources:true)
-
xdg
-
;;
-
-
open Cmdliner
-
-
let () =
-
Fmt.set_style_renderer Fmt.stdout `Ansi_tty;
-
let app_name = "xdg_example" in
-
let doc = "Example program demonstrating XDG directory selection with Cmdliner" in
-
let man =
-
[ `S Manpage.s_description
-
; `P
-
"This example shows how to use the Xdge library with Cmdliner to handle XDG Base \
-
Directory Specification paths with command-line and environment variable \
-
overrides."
-
; `S Manpage.s_environment
-
; `P (Xdge.Cmd.env_docs app_name)
-
]
-
in
-
let info = Cmdliner.Cmd.info "xdg_example" ~version:"1.0" ~doc ~man in
-
Eio_main.run
-
@@ fun env ->
-
let create_xdg_term = Xdge.Cmd.term app_name env#fs () in
-
let main_term = Term.(const run $ create_xdg_term) in
-
let cmd = Cmdliner.Cmd.v info main_term in
-
exit @@ Cmdliner.Cmd.eval cmd
-
;;
-4
stack/xdge/lib/dune
···
-
(library
-
(public_name xdge)
-
(name xdge)
-
(libraries eio eio_main xdg cmdliner fmt))
-770
stack/xdge/lib/xdge.ml
···
-
type source =
-
| Default
-
| Env of string
-
| Cmdline
-
-
type t =
-
{ app_name : string
-
; config_dir : Eio.Fs.dir_ty Eio.Path.t
-
; config_dir_source : source
-
; data_dir : Eio.Fs.dir_ty Eio.Path.t
-
; data_dir_source : source
-
; cache_dir : Eio.Fs.dir_ty Eio.Path.t
-
; cache_dir_source : source
-
; state_dir : Eio.Fs.dir_ty Eio.Path.t
-
; state_dir_source : source
-
; runtime_dir : Eio.Fs.dir_ty Eio.Path.t option
-
; runtime_dir_source : source
-
; config_dirs : Eio.Fs.dir_ty Eio.Path.t list
-
; data_dirs : Eio.Fs.dir_ty Eio.Path.t list
-
}
-
-
type dir = [
-
| `Config
-
| `Cache
-
| `Data
-
| `State
-
| `Runtime
-
]
-
-
let ensure_dir ?(perm = 0o755) path = Eio.Path.mkdirs ~exists_ok:true ~perm path
-
-
let validate_runtime_base_dir base_path =
-
(* Validate the base XDG_RUNTIME_DIR has correct permissions per spec *)
-
try
-
let path_str = Eio.Path.native_exn base_path in
-
let stat = Eio.Path.stat ~follow:true base_path in
-
let current_perm = stat.perm land 0o777 in
-
if current_perm <> 0o700
-
then
-
failwith
-
(Printf.sprintf
-
"XDG_RUNTIME_DIR base directory %s has incorrect permissions: %o (must be \
-
0700)"
-
path_str
-
current_perm);
-
(* Check ownership - directory should be owned by current user *)
-
let uid = Unix.getuid () in
-
if stat.uid <> Int64.of_int uid
-
then
-
failwith
-
(Printf.sprintf
-
"XDG_RUNTIME_DIR base directory %s not owned by current user (uid %d, owner \
-
%Ld)"
-
path_str
-
uid
-
stat.uid)
-
(* TODO: Check that directory is on local filesystem (not networked).
-
This would require filesystem type detection which is OS-specific. *)
-
with
-
| exn ->
-
failwith
-
(Printf.sprintf "Cannot validate XDG_RUNTIME_DIR: %s" (Printexc.to_string exn))
-
;;
-
-
let ensure_runtime_dir _fs app_runtime_path =
-
(* Base directory validation is done in resolve_runtime_dir,
-
so we just create the app subdirectory *)
-
ensure_dir app_runtime_path
-
;;
-
-
let get_home_dir fs =
-
let home_str =
-
match Sys.getenv_opt "HOME" with
-
| Some home -> home
-
| None ->
-
(match Sys.os_type with
-
| "Win32" | "Cygwin" ->
-
(match Sys.getenv_opt "USERPROFILE" with
-
| Some profile -> profile
-
| None -> failwith "Cannot determine home directory")
-
| _ ->
-
(try Unix.((getpwuid (getuid ())).pw_dir) with
-
| _ -> failwith "Cannot determine home directory"))
-
in
-
Eio.Path.(fs / home_str)
-
;;
-
-
let make_env_var_name app_name suffix = String.uppercase_ascii app_name ^ "_" ^ suffix
-
-
exception Invalid_xdg_path of string
-
-
let validate_absolute_path context path =
-
if Filename.is_relative path
-
then
-
raise
-
(Invalid_xdg_path
-
(Printf.sprintf "%s must be an absolute path, got: %s" context path))
-
;;
-
-
let resolve_path fs home_path base_path =
-
if Filename.is_relative base_path
-
then Eio.Path.(home_path / base_path)
-
else Eio.Path.(fs / base_path)
-
;;
-
-
(* Helper to resolve system directories (config_dirs or data_dirs) *)
-
let resolve_system_dirs fs home_path app_name override_suffix xdg_var default_paths =
-
let override_var = make_env_var_name app_name override_suffix in
-
match Sys.getenv_opt override_var with
-
| Some dirs when dirs <> "" ->
-
String.split_on_char ':' dirs
-
|> List.filter (fun s -> s <> "")
-
|> List.filter_map (fun path ->
-
try
-
validate_absolute_path override_var path;
-
Some Eio.Path.(resolve_path fs home_path path / app_name)
-
with
-
| Invalid_xdg_path _ -> None)
-
| Some _ | None ->
-
(match Sys.getenv_opt xdg_var with
-
| Some dirs when dirs <> "" ->
-
String.split_on_char ':' dirs
-
|> List.filter (fun s -> s <> "")
-
|> List.filter_map (fun path ->
-
try
-
validate_absolute_path xdg_var path;
-
Some Eio.Path.(resolve_path fs home_path path / app_name)
-
with
-
| Invalid_xdg_path _ -> None)
-
| Some _ | None ->
-
List.map
-
(fun path -> Eio.Path.(resolve_path fs home_path path / app_name))
-
default_paths)
-
;;
-
-
(* Helper to resolve a user directory with override precedence *)
-
let resolve_user_dir fs home_path app_name xdg_ctx xdg_getter override_suffix =
-
let override_var = make_env_var_name app_name override_suffix in
-
match Sys.getenv_opt override_var with
-
| Some dir when dir <> "" ->
-
validate_absolute_path override_var dir;
-
Eio.Path.(fs / dir / app_name), Env override_var
-
| Some _ | None ->
-
let xdg_base = xdg_getter xdg_ctx in
-
let base_path = resolve_path fs home_path xdg_base in
-
Eio.Path.(base_path / app_name), Default
-
;;
-
-
(* Helper to resolve runtime directory (special case since it can be None) *)
-
let resolve_runtime_dir fs home_path app_name xdg_ctx =
-
let override_var = make_env_var_name app_name "RUNTIME_DIR" in
-
match Sys.getenv_opt override_var with
-
| Some dir when dir <> "" ->
-
validate_absolute_path override_var dir;
-
(* Validate the base runtime directory has correct permissions *)
-
let base_runtime_dir = resolve_path fs home_path dir in
-
validate_runtime_base_dir base_runtime_dir;
-
Some Eio.Path.(base_runtime_dir / app_name), Env override_var
-
| Some _ | None ->
-
( (match Xdg.runtime_dir xdg_ctx with
-
| Some base ->
-
(* Validate the base runtime directory has correct permissions *)
-
let base_runtime_dir = resolve_path fs home_path base in
-
validate_runtime_base_dir base_runtime_dir;
-
Some Eio.Path.(base_runtime_dir / app_name)
-
| None -> None)
-
, Default )
-
;;
-
-
let validate_standard_xdg_vars () =
-
(* Validate standard XDG environment variables for absolute paths *)
-
let xdg_vars =
-
[ "XDG_CONFIG_HOME"
-
; "XDG_DATA_HOME"
-
; "XDG_CACHE_HOME"
-
; "XDG_STATE_HOME"
-
; "XDG_RUNTIME_DIR"
-
; "XDG_CONFIG_DIRS"
-
; "XDG_DATA_DIRS"
-
]
-
in
-
List.iter
-
(fun var ->
-
match Sys.getenv_opt var with
-
| Some value when value <> "" ->
-
if String.contains value ':'
-
then
-
(* Colon-separated list - validate each part *)
-
String.split_on_char ':' value
-
|> List.filter (fun s -> s <> "")
-
|> List.iter (fun path -> validate_absolute_path var path)
-
else
-
(* Single path *)
-
validate_absolute_path var value
-
| _ -> ())
-
xdg_vars
-
;;
-
-
let create fs app_name =
-
let fs = fs in
-
let home_path = get_home_dir fs in
-
(* First validate all standard XDG environment variables *)
-
validate_standard_xdg_vars ();
-
let xdg_ctx = Xdg.create ~env:Sys.getenv_opt () in
-
(* User directories *)
-
let config_dir, config_dir_source =
-
resolve_user_dir fs home_path app_name xdg_ctx Xdg.config_dir "CONFIG_DIR"
-
in
-
let data_dir, data_dir_source =
-
resolve_user_dir fs home_path app_name xdg_ctx Xdg.data_dir "DATA_DIR"
-
in
-
let cache_dir, cache_dir_source =
-
resolve_user_dir fs home_path app_name xdg_ctx Xdg.cache_dir "CACHE_DIR"
-
in
-
let state_dir, state_dir_source =
-
resolve_user_dir fs home_path app_name xdg_ctx Xdg.state_dir "STATE_DIR"
-
in
-
(* Runtime directory *)
-
let runtime_dir, runtime_dir_source =
-
resolve_runtime_dir fs home_path app_name xdg_ctx
-
in
-
(* System directories *)
-
let config_dirs =
-
resolve_system_dirs
-
fs
-
home_path
-
app_name
-
"CONFIG_DIRS"
-
"XDG_CONFIG_DIRS"
-
[ "/etc/xdg" ]
-
in
-
let data_dirs =
-
resolve_system_dirs
-
fs
-
home_path
-
app_name
-
"DATA_DIRS"
-
"XDG_DATA_DIRS"
-
[ "/usr/local/share"; "/usr/share" ]
-
in
-
ensure_dir config_dir;
-
ensure_dir data_dir;
-
ensure_dir cache_dir;
-
ensure_dir state_dir;
-
Option.iter (ensure_runtime_dir fs) runtime_dir;
-
{ app_name
-
; config_dir
-
; config_dir_source
-
; data_dir
-
; data_dir_source
-
; cache_dir
-
; cache_dir_source
-
; state_dir
-
; state_dir_source
-
; runtime_dir
-
; runtime_dir_source
-
; config_dirs
-
; data_dirs
-
}
-
;;
-
-
let app_name t = t.app_name
-
let config_dir t = t.config_dir
-
let data_dir t = t.data_dir
-
let cache_dir t = t.cache_dir
-
let state_dir t = t.state_dir
-
let runtime_dir t = t.runtime_dir
-
let config_dirs t = t.config_dirs
-
let data_dirs t = t.data_dirs
-
-
(* File search following XDG specification *)
-
let find_file_in_dirs dirs filename =
-
let rec search_dirs = function
-
| [] -> None
-
| dir :: remaining_dirs ->
-
let file_path = Eio.Path.(dir / filename) in
-
(try
-
(* Try to check if file exists and is readable *)
-
let _ = Eio.Path.stat ~follow:true file_path in
-
Some file_path
-
with
-
| _ ->
-
(* File is inaccessible (non-existent, permissions, etc.)
-
Skip and continue with next directory per XDG spec *)
-
search_dirs remaining_dirs)
-
in
-
search_dirs dirs
-
;;
-
-
let find_config_file t filename =
-
(* Search user config dir first, then system config dirs *)
-
find_file_in_dirs (t.config_dir :: t.config_dirs) filename
-
;;
-
-
let find_data_file t filename =
-
(* Search user data dir first, then system data dirs *)
-
find_file_in_dirs (t.data_dir :: t.data_dirs) filename
-
;;
-
-
let pp ?(brief = false) ?(sources = false) ppf t =
-
let pp_source ppf = function
-
| Default -> Fmt.(styled `Faint string) ppf "default"
-
| Env var -> Fmt.pf ppf "%a" Fmt.(styled `Yellow string) ("env(" ^ var ^ ")")
-
| Cmdline -> Fmt.(styled `Blue string) ppf "cmdline"
-
in
-
let pp_path_with_source ppf path source =
-
if sources
-
then
-
Fmt.pf
-
ppf
-
"%a %a"
-
Fmt.(styled `Green Eio.Path.pp)
-
path
-
Fmt.(styled `Faint (brackets pp_source))
-
source
-
else Fmt.(styled `Green Eio.Path.pp) ppf path
-
in
-
let pp_path_opt_with_source ppf path_opt source =
-
match path_opt with
-
| None ->
-
if sources
-
then
-
Fmt.pf
-
ppf
-
"%a %a"
-
Fmt.(styled `Red string)
-
"<none>"
-
Fmt.(styled `Faint (brackets pp_source))
-
source
-
else Fmt.(styled `Red string) ppf "<none>"
-
| Some path -> pp_path_with_source ppf path source
-
in
-
let pp_paths ppf paths =
-
Fmt.(list ~sep:(any ";@ ") (styled `Green Eio.Path.pp)) ppf paths
-
in
-
if brief
-
then
-
Fmt.pf
-
ppf
-
"%a config=%a data=%a>"
-
Fmt.(styled `Cyan string)
-
("<xdg:" ^ t.app_name)
-
(fun ppf (path, source) -> pp_path_with_source ppf path source)
-
(t.config_dir, t.config_dir_source)
-
(fun ppf (path, source) -> pp_path_with_source ppf path source)
-
(t.data_dir, t.data_dir_source)
-
else (
-
Fmt.pf
-
ppf
-
"@[<v>%a@,"
-
Fmt.(styled `Bold string)
-
("XDG directories for '" ^ t.app_name ^ "':");
-
Fmt.pf ppf "@[<v 2>%a@," Fmt.(styled `Bold string) "User directories:";
-
Fmt.pf
-
ppf
-
"%a %a@,"
-
Fmt.(styled `Cyan string)
-
"config:"
-
(fun ppf (path, source) -> pp_path_with_source ppf path source)
-
(t.config_dir, t.config_dir_source);
-
Fmt.pf
-
ppf
-
"%a %a@,"
-
Fmt.(styled `Cyan string)
-
"data:"
-
(fun ppf (path, source) -> pp_path_with_source ppf path source)
-
(t.data_dir, t.data_dir_source);
-
Fmt.pf
-
ppf
-
"%a %a@,"
-
Fmt.(styled `Cyan string)
-
"cache:"
-
(fun ppf (path, source) -> pp_path_with_source ppf path source)
-
(t.cache_dir, t.cache_dir_source);
-
Fmt.pf
-
ppf
-
"%a %a@,"
-
Fmt.(styled `Cyan string)
-
"state:"
-
(fun ppf (path, source) -> pp_path_with_source ppf path source)
-
(t.state_dir, t.state_dir_source);
-
Fmt.pf
-
ppf
-
"%a %a@]@,"
-
Fmt.(styled `Cyan string)
-
"runtime:"
-
(fun ppf (path_opt, source) -> pp_path_opt_with_source ppf path_opt source)
-
(t.runtime_dir, t.runtime_dir_source);
-
Fmt.pf ppf "@[<v 2>%a@," Fmt.(styled `Bold string) "System directories:";
-
Fmt.pf
-
ppf
-
"%a [@[<hov>%a@]]@,"
-
Fmt.(styled `Cyan string)
-
"config_dirs:"
-
pp_paths
-
t.config_dirs;
-
Fmt.pf
-
ppf
-
"%a [@[<hov>%a@]]@]@]"
-
Fmt.(styled `Cyan string)
-
"data_dirs:"
-
pp_paths
-
t.data_dirs)
-
;;
-
-
module Cmd = struct
-
type xdg_t = t
-
-
type 'a with_source =
-
{ value : 'a option
-
; source : source
-
}
-
-
type t =
-
{ config_dir : string with_source
-
; data_dir : string with_source
-
; cache_dir : string with_source
-
; state_dir : string with_source
-
; runtime_dir : string with_source
-
}
-
-
let term app_name fs
-
?(dirs=[`Config; `Data; `Cache; `State; `Runtime]) () =
-
let open Cmdliner in
-
let app_upper = String.uppercase_ascii app_name in
-
let show_paths =
-
let doc = "Show only the resolved directory paths without formatting" in
-
Arg.(value & flag & info [ "show-paths" ] ~doc)
-
in
-
let has_dir d = List.mem d dirs in
-
let make_dir_arg ~enabled name env_suffix xdg_var default_path =
-
if not enabled then
-
(* Return a term that always gives the environment-only result *)
-
Term.(const (fun () ->
-
let app_env = app_upper ^ "_" ^ env_suffix in
-
match Sys.getenv_opt app_env with
-
| Some v when v <> "" -> { value = Some v; source = Env app_env }
-
| Some _ | None ->
-
(match Sys.getenv_opt xdg_var with
-
| Some v -> { value = Some v; source = Env xdg_var }
-
| None -> { value = None; source = Default }))
-
$ const ())
-
else
-
let app_env = app_upper ^ "_" ^ env_suffix in
-
let doc =
-
match default_path with
-
| Some path ->
-
Printf.sprintf
-
"Override %s directory. Can also be set with %s or %s. Default: %s"
-
name
-
app_env
-
xdg_var
-
path
-
| None ->
-
Printf.sprintf
-
"Override %s directory. Can also be set with %s or %s. No default value."
-
name
-
app_env
-
xdg_var
-
in
-
let arg =
-
Arg.(value & opt (some string) None & info [ name ^ "-dir" ] ~docv:"DIR" ~doc)
-
in
-
Term.(
-
const (fun cmdline_val ->
-
match cmdline_val with
-
| Some v -> { value = Some v; source = Cmdline }
-
| None ->
-
(match Sys.getenv_opt app_env with
-
| Some v when v <> "" -> { value = Some v; source = Env app_env }
-
| Some _ | None ->
-
(match Sys.getenv_opt xdg_var with
-
| Some v -> { value = Some v; source = Env xdg_var }
-
| None -> { value = None; source = Default })))
-
$ arg)
-
in
-
let home_prefix = "\\$HOME" in
-
let config_dir =
-
make_dir_arg
-
~enabled:(has_dir `Config)
-
"config"
-
"CONFIG_DIR"
-
"XDG_CONFIG_HOME"
-
(Some (home_prefix ^ "/.config/" ^ app_name))
-
in
-
let data_dir =
-
make_dir_arg
-
~enabled:(has_dir `Data)
-
"data"
-
"DATA_DIR"
-
"XDG_DATA_HOME"
-
(Some (home_prefix ^ "/.local/share/" ^ app_name))
-
in
-
let cache_dir =
-
make_dir_arg
-
~enabled:(has_dir `Cache)
-
"cache"
-
"CACHE_DIR"
-
"XDG_CACHE_HOME"
-
(Some (home_prefix ^ "/.cache/" ^ app_name))
-
in
-
let state_dir =
-
make_dir_arg
-
~enabled:(has_dir `State)
-
"state"
-
"STATE_DIR"
-
"XDG_STATE_HOME"
-
(Some (home_prefix ^ "/.local/state/" ^ app_name))
-
in
-
let runtime_dir = make_dir_arg ~enabled:(has_dir `Runtime) "runtime" "RUNTIME_DIR" "XDG_RUNTIME_DIR" None in
-
Term.(
-
const
-
(fun
-
show_paths_flag
-
config_dir_ws
-
data_dir_ws
-
cache_dir_ws
-
state_dir_ws
-
runtime_dir_ws
-
->
-
let config =
-
{ config_dir = config_dir_ws
-
; data_dir = data_dir_ws
-
; cache_dir = cache_dir_ws
-
; state_dir = state_dir_ws
-
; runtime_dir = runtime_dir_ws
-
}
-
in
-
let home_path = get_home_dir fs in
-
(* First validate all standard XDG environment variables *)
-
validate_standard_xdg_vars ();
-
let xdg_ctx = Xdg.create ~env:Sys.getenv_opt () in
-
(* Helper to resolve directory from config with source tracking *)
-
let resolve_from_config config_ws xdg_getter =
-
match config_ws.value with
-
| Some dir -> resolve_path fs home_path dir, config_ws.source
-
| None ->
-
let xdg_base = xdg_getter xdg_ctx in
-
let base_path = resolve_path fs home_path xdg_base in
-
Eio.Path.(base_path / app_name), config_ws.source
-
in
-
(* User directories *)
-
let config_dir, config_dir_source =
-
resolve_from_config config.config_dir Xdg.config_dir
-
in
-
let data_dir, data_dir_source =
-
resolve_from_config config.data_dir Xdg.data_dir
-
in
-
let cache_dir, cache_dir_source =
-
resolve_from_config config.cache_dir Xdg.cache_dir
-
in
-
let state_dir, state_dir_source =
-
resolve_from_config config.state_dir Xdg.state_dir
-
in
-
(* Runtime directory *)
-
let runtime_dir, runtime_dir_source =
-
match config.runtime_dir.value with
-
| Some dir -> Some (resolve_path fs home_path dir), config.runtime_dir.source
-
| None ->
-
( Option.map
-
(fun base ->
-
let base_path = resolve_path fs home_path base in
-
Eio.Path.(base_path / app_name))
-
(Xdg.runtime_dir xdg_ctx)
-
, config.runtime_dir.source )
-
in
-
(* System directories - reuse shared helper *)
-
let config_dirs =
-
resolve_system_dirs
-
fs
-
home_path
-
app_name
-
"CONFIG_DIRS"
-
"XDG_CONFIG_DIRS"
-
[ "/etc/xdg" ]
-
in
-
let data_dirs =
-
resolve_system_dirs
-
fs
-
home_path
-
app_name
-
"DATA_DIRS"
-
"XDG_DATA_DIRS"
-
[ "/usr/local/share"; "/usr/share" ]
-
in
-
ensure_dir config_dir;
-
ensure_dir data_dir;
-
ensure_dir cache_dir;
-
ensure_dir state_dir;
-
Option.iter (ensure_runtime_dir fs) runtime_dir;
-
let xdg =
-
{ app_name
-
; config_dir
-
; config_dir_source
-
; data_dir
-
; data_dir_source
-
; cache_dir
-
; cache_dir_source
-
; state_dir
-
; state_dir_source
-
; runtime_dir
-
; runtime_dir_source
-
; config_dirs
-
; data_dirs
-
}
-
in
-
(* Handle --show-paths option *)
-
if show_paths_flag
-
then (
-
let print_path name path =
-
match path with
-
| None -> Printf.printf "%s: <none>\n" name
-
| Some p -> Printf.printf "%s: %s\n" name (Eio.Path.native_exn p)
-
in
-
let print_paths name paths =
-
match paths with
-
| [] -> Printf.printf "%s: []\n" name
-
| paths ->
-
let paths_str = String.concat ":" (List.map Eio.Path.native_exn paths) in
-
Printf.printf "%s: %s\n" name paths_str
-
in
-
print_path "config_dir" (Some config_dir);
-
print_path "data_dir" (Some data_dir);
-
print_path "cache_dir" (Some cache_dir);
-
print_path "state_dir" (Some state_dir);
-
print_path "runtime_dir" runtime_dir;
-
print_paths "config_dirs" config_dirs;
-
print_paths "data_dirs" data_dirs;
-
Stdlib.exit 0);
-
xdg, config)
-
$ show_paths
-
$ config_dir
-
$ data_dir
-
$ cache_dir
-
$ state_dir
-
$ runtime_dir)
-
;;
-
-
let cache_term app_name =
-
let open Cmdliner in
-
let app_upper = String.uppercase_ascii app_name in
-
let app_env = app_upper ^ "_CACHE_DIR" in
-
let xdg_var = "XDG_CACHE_HOME" in
-
let home = Sys.getenv "HOME" in
-
let default_path = home ^ "/.cache/" ^ app_name in
-
-
let doc =
-
Printf.sprintf
-
"Override cache directory. Can also be set with %s or %s. Default: %s"
-
app_env xdg_var default_path
-
in
-
-
let arg = Arg.(value & opt string default_path & info ["cache-dir"; "c"] ~docv:"DIR" ~doc) in
-
-
Term.(const (fun cmdline_val ->
-
(* Check command line first *)
-
if cmdline_val <> default_path then
-
cmdline_val
-
else
-
(* Then check app-specific env var *)
-
match Sys.getenv_opt app_env with
-
| Some v when v <> "" -> v
-
| _ ->
-
(* Then check XDG env var *)
-
match Sys.getenv_opt xdg_var with
-
| Some v when v <> "" -> v ^ "/" ^ app_name
-
| _ -> default_path
-
) $ arg)
-
;;
-
-
let env_docs app_name =
-
let app_upper = String.uppercase_ascii app_name in
-
Printf.sprintf
-
{|
-
Configuration Precedence (follows standard Unix conventions):
-
1. Command-line flags (e.g., --config-dir) - highest priority
-
2. Application-specific environment variable (e.g., %s_CONFIG_DIR)
-
3. XDG standard environment variable (e.g., XDG_CONFIG_HOME)
-
4. Default path (e.g., ~/.config/%s) - lowest priority
-
-
This allows per-application overrides without affecting other XDG-compliant programs.
-
For example, setting %s_CONFIG_DIR only changes the config directory for %s,
-
while XDG_CONFIG_HOME affects all XDG-compliant applications.
-
-
Application-specific variables:
-
%s_CONFIG_DIR Override config directory for %s only
-
%s_DATA_DIR Override data directory for %s only
-
%s_CACHE_DIR Override cache directory for %s only
-
%s_STATE_DIR Override state directory for %s only
-
%s_RUNTIME_DIR Override runtime directory for %s only
-
-
XDG standard variables (shared by all XDG applications):
-
XDG_CONFIG_HOME User configuration directory (default: ~/.config/%s)
-
XDG_DATA_HOME User data directory (default: ~/.local/share/%s)
-
XDG_CACHE_HOME User cache directory (default: ~/.cache/%s)
-
XDG_STATE_HOME User state directory (default: ~/.local/state/%s)
-
XDG_RUNTIME_DIR User runtime directory (no default)
-
XDG_CONFIG_DIRS System configuration directories (default: /etc/xdg/%s)
-
XDG_DATA_DIRS System data directories (default: /usr/local/share/%s:/usr/share/%s)
-
|}
-
app_upper
-
app_name
-
app_upper
-
app_name
-
app_upper
-
app_name
-
app_upper
-
app_name
-
app_upper
-
app_name
-
app_upper
-
app_name
-
app_upper
-
app_name
-
app_name
-
app_name
-
app_name
-
app_name
-
app_name
-
app_name
-
app_name
-
;;
-
-
let pp ppf config =
-
let pp_source ppf = function
-
| Default -> Fmt.(styled `Faint string) ppf "default"
-
| Env var -> Fmt.pf ppf "%a" Fmt.(styled `Yellow string) ("env(" ^ var ^ ")")
-
| Cmdline -> Fmt.(styled `Blue string) ppf "cmdline"
-
in
-
let pp_with_source name ppf ws =
-
match ws.value with
-
| None when ws.source = Default -> ()
-
| None ->
-
Fmt.pf
-
ppf
-
"@,%a %a %a"
-
Fmt.(styled `Cyan string)
-
(name ^ ":")
-
Fmt.(styled `Red string)
-
"<unset>"
-
Fmt.(styled `Faint (brackets pp_source))
-
ws.source
-
| Some value ->
-
Fmt.pf
-
ppf
-
"@,%a %a %a"
-
Fmt.(styled `Cyan string)
-
(name ^ ":")
-
Fmt.(styled `Green string)
-
value
-
Fmt.(styled `Faint (brackets pp_source))
-
ws.source
-
in
-
Fmt.pf
-
ppf
-
"@[<v>%a%a%a%a%a%a@]"
-
Fmt.(styled `Bold string)
-
"XDG config:"
-
(pp_with_source "config_dir")
-
config.config_dir
-
(pp_with_source "data_dir")
-
config.data_dir
-
(pp_with_source "cache_dir")
-
config.cache_dir
-
(pp_with_source "state_dir")
-
config.state_dir
-
(pp_with_source "runtime_dir")
-
config.runtime_dir
-
;;
-
end
-415
stack/xdge/lib/xdge.mli
···
-
(** XDG Base Directory Specification support with Eio capabilities
-
-
This library provides an OCaml implementation of the XDG Base Directory
-
Specification with Eio filesystem integration. The XDG specification defines
-
standard locations for user-specific and system-wide application files,
-
helping to keep user home directories clean and organized.
-
-
The specification is available at:
-
{{:https://specifications.freedesktop.org/basedir-spec/latest/} XDG Base Directory Specification}
-
-
{b Key Concepts:}
-
-
The XDG specification defines several types of directories:
-
- {b User directories}: Store user-specific files (config, data, cache, state, runtime)
-
- {b System directories}: Store system-wide files shared across users
-
- {b Precedence}: User directories take precedence over system directories
-
- {b Application isolation}: Each application gets its own subdirectory
-
-
{b Environment Variable Precedence:}
-
-
This library follows a three-level precedence system:
-
+ Application-specific variables (e.g., [MYAPP_CONFIG_DIR]) - highest priority
-
+ XDG standard variables (e.g., [XDG_CONFIG_HOME])
-
+ Default paths (e.g., [$HOME/.config]) - lowest priority
-
-
This allows fine-grained control over directory locations without affecting
-
other XDG-compliant applications.
-
-
{b Directory Creation:}
-
-
All directories are automatically created with appropriate permissions (0o755)
-
when accessed, except for runtime directories which require stricter permissions
-
as per the specification.
-
-
@see <https://specifications.freedesktop.org/basedir-spec/latest/> XDG Base Directory Specification *)
-
-
(** The main XDG context type containing all directory paths for an application.
-
-
A value of type [t] represents the complete XDG directory structure for a
-
specific application, including both user-specific and system-wide directories.
-
All paths are resolved at creation time and are absolute paths within the
-
Eio filesystem. *)
-
type t
-
-
(** XDG directory types for specifying which directories an application needs.
-
-
These polymorphic variants allow applications to declare which XDG directories
-
they use, enabling runtime systems to only provide the requested directories. *)
-
type dir = [
-
| `Config (** User configuration files *)
-
| `Cache (** User-specific cached data *)
-
| `Data (** User-specific application data *)
-
| `State (** User-specific state data (logs, history, etc.) *)
-
| `Runtime (** User-specific runtime files (sockets, pipes, etc.) *)
-
]
-
-
(** {1 Exceptions} *)
-
-
(** Exception raised when XDG environment variables contain invalid paths.
-
-
The XDG specification requires all paths in environment variables to be
-
absolute. This exception is raised when a relative path is found. *)
-
exception Invalid_xdg_path of string
-
-
(** {1 Construction} *)
-
-
(** [create fs app_name] creates an XDG context for the given application.
-
-
This function initializes the complete XDG directory structure for your application,
-
resolving all paths according to the environment variables and creating directories
-
as needed.
-
-
@param fs The Eio filesystem providing filesystem access
-
@param app_name The name of your application (used as subdirectory name)
-
-
{b Path Resolution:}
-
-
For each directory type, the following precedence is used:
-
+ Application-specific environment variable (e.g., [MYAPP_CONFIG_DIR])
-
+ XDG standard environment variable (e.g., [XDG_CONFIG_HOME])
-
+ Default path as specified in the XDG specification
-
-
{b Example:}
-
{[
-
let xdg = Xdge.create env#fs "myapp" in
-
let config = Xdge.config_dir xdg in
-
(* config is now <fs:$HOME/.config/myapp> or the overridden path *)
-
]}
-
-
All directories are created with permissions 0o755 if they don't exist,
-
except for runtime directories which are created with 0o700 permissions and
-
validated according to the XDG specification.
-
-
@raise Invalid_xdg_path if any environment variable contains a relative path *)
-
val create : Eio.Fs.dir_ty Eio.Path.t -> string -> t
-
-
(** {1 Accessors} *)
-
-
(** [app_name t] returns the application name used when creating this XDG context.
-
-
This is the name that was passed to {!create} and is used as the subdirectory
-
name within each XDG base directory. *)
-
val app_name : t -> string
-
-
(** {1 Base Directories} *)
-
-
(** [config_dir t] returns the path to user-specific configuration files.
-
-
{b Purpose:} Store user preferences, settings, and configuration files.
-
Configuration files should be human-readable when possible.
-
-
{b Environment Variables:}
-
- [${APP_NAME}_CONFIG_DIR]: Application-specific override (highest priority)
-
- [XDG_CONFIG_HOME]: XDG standard variable
-
- Default: [$HOME/.config/{app_name}]
-
-
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_CONFIG_HOME specification *)
-
val config_dir : t -> Eio.Fs.dir_ty Eio.Path.t
-
-
(** [data_dir t] returns the path to user-specific data files.
-
-
{b Purpose:} Store persistent application data that should be preserved
-
across application restarts and system reboots. This data is typically
-
not modified by users directly.
-
-
{b Environment Variables:}
-
- [${APP_NAME}_DATA_DIR]: Application-specific override (highest priority)
-
- [XDG_DATA_HOME]: XDG standard variable
-
- Default: [$HOME/.local/share/{app_name}]
-
-
{b Example Files:}
-
- Application databases
-
- User-generated content (documents, projects)
-
- Downloaded resources
-
- Application plugins or extensions
-
-
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_DATA_HOME specification *)
-
val data_dir : t -> Eio.Fs.dir_ty Eio.Path.t
-
-
(** [cache_dir t] returns the path to user-specific cache files.
-
-
{b Purpose:} Store non-essential cached data that can be regenerated
-
if deleted. The application should remain functional if this directory
-
is cleared, though performance may be temporarily impacted.
-
-
{b Environment Variables:}
-
- [${APP_NAME}_CACHE_DIR]: Application-specific override (highest priority)
-
- [XDG_CACHE_HOME]: XDG standard variable
-
- Default: [$HOME/.cache/{app_name}]
-
-
{b Example Files:}
-
- Downloaded thumbnails and previews
-
- Compiled bytecode or object files
-
- Network response caches
-
- Temporary computation results
-
-
Users may clear cache directories to free disk space, so
-
always check for cache validity and be prepared to regenerate data.
-
-
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_CACHE_HOME specification *)
-
val cache_dir : t -> Eio.Fs.dir_ty Eio.Path.t
-
-
(** [state_dir t] returns the path to user-specific state files.
-
-
{b Purpose:} Store persistent state data that should be preserved between
-
application restarts but is not important enough to be user data. This
-
includes application state that can be regenerated but would impact the
-
user experience if lost.
-
-
{b Environment Variables:}
-
- [${APP_NAME}_STATE_DIR]: Application-specific override (highest priority)
-
- [XDG_STATE_HOME]: XDG standard variable
-
- Default: [$HOME/.local/state/{app_name}]
-
-
{b Example Files:}
-
- Application history (recently used files, command history)
-
- Current application state (window positions, open tabs)
-
- Logs and journal files
-
- Undo/redo history
-
-
{b Comparison with other directories:}
-
- Unlike cache: State should persist between reboots
-
- Unlike data: State can be regenerated (though inconvenient)
-
- Unlike config: State changes frequently during normal use
-
-
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_STATE_HOME specification *)
-
val state_dir : t -> Eio.Fs.dir_ty Eio.Path.t
-
-
(** [runtime_dir t] returns the path to user-specific runtime files.
-
-
{b Purpose:} Store runtime files such as sockets, named pipes, and
-
process IDs. These files are only valid for the duration of the user's
-
login session.
-
-
{b Environment Variables:}
-
- [${APP_NAME}_RUNTIME_DIR]: Application-specific override (highest priority)
-
- [XDG_RUNTIME_DIR]: XDG standard variable
-
- Default: None (returns [None] if not set)
-
-
{b Required Properties (per specification):}
-
- Owned by the user with access mode 0700
-
- Bound to the user login session lifetime
-
- Located on a local filesystem (not networked)
-
- Fully-featured by the OS (supporting proper locking, etc.)
-
-
{b Example Files:}
-
- Unix domain sockets
-
- Named pipes (FIFOs)
-
- Lock files
-
- Small process communication files
-
-
This may return [None] if no suitable runtime directory
-
is available. Applications should handle this gracefully, perhaps by
-
falling back to [/tmp] with appropriate security measures.
-
-
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_RUNTIME_DIR specification *)
-
val runtime_dir : t -> Eio.Fs.dir_ty Eio.Path.t option
-
-
(** {1 System Directories} *)
-
-
(** [config_dirs t] returns search paths for system-wide configuration files.
-
-
{b Purpose:} Provide a search path for configuration files that are
-
shared between multiple users. Files in user-specific {!config_dir}
-
take precedence over these system directories.
-
-
{b Environment Variables:}
-
- [${APP_NAME}_CONFIG_DIRS]: Application-specific override (highest priority)
-
- [XDG_CONFIG_DIRS]: XDG standard variable (colon-separated list)
-
- Default: [[/etc/xdg/{app_name}]]
-
-
{b Search Order:}
-
Directories are ordered by preference, with earlier entries taking
-
precedence over later ones. When looking for a configuration file,
-
search {!config_dir} first, then each directory in this list.
-
-
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_CONFIG_DIRS specification *)
-
val config_dirs : t -> Eio.Fs.dir_ty Eio.Path.t list
-
-
(** [data_dirs t] returns search paths for system-wide data files.
-
-
{b Purpose:} Provide a search path for data files that are shared
-
between multiple users. Files in user-specific {!data_dir} take
-
precedence over these system directories.
-
-
{b Environment Variables:}
-
- [${APP_NAME}_DATA_DIRS]: Application-specific override (highest priority)
-
- [XDG_DATA_DIRS]: XDG standard variable (colon-separated list)
-
- Default: [[/usr/local/share/{app_name}; /usr/share/{app_name}]]
-
-
{b Search Order:}
-
Directories are ordered by preference, with earlier entries taking
-
precedence over later ones. When looking for a data file, search
-
{!data_dir} first, then each directory in this list.
-
-
{b Example Files:}
-
- Application icons and themes
-
- Desktop files
-
- Shared application resources
-
- Documentation files
-
- Default templates
-
-
@see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_DATA_DIRS specification *)
-
val data_dirs : t -> Eio.Fs.dir_ty Eio.Path.t list
-
-
(** {1 File Search} *)
-
-
(** [find_config_file t filename] searches for a configuration file following XDG precedence.
-
-
This function searches for the given filename in the user configuration directory
-
first, then in system configuration directories in order of preference.
-
Files that are inaccessible (due to permissions, non-existence, etc.) are
-
silently skipped as per the XDG specification.
-
-
@param t The XDG context
-
@param filename The name of the file to search for
-
@return [Some path] if found, [None] if not found in any directory
-
-
{b Search Order:}
-
1. User config directory ({!config_dir})
-
2. System config directories ({!config_dirs}) in preference order
-
-
*)
-
val find_config_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option
-
-
(** [find_data_file t filename] searches for a data file following XDG precedence.
-
-
This function searches for the given filename in the user data directory
-
first, then in system data directories in order of preference.
-
Files that are inaccessible (due to permissions, non-existence, etc.) are
-
silently skipped as per the XDG specification.
-
-
@param t The XDG context
-
@param filename The name of the file to search for
-
@return [Some path] if found, [None] if not found in any directory
-
-
{b Search Order:}
-
1. User data directory ({!data_dir})
-
2. System data directories ({!data_dirs}) in preference order
-
-
*)
-
val find_data_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option
-
-
(** {1 Pretty Printing} *)
-
-
(** [pp ?brief ?sources ppf t] pretty prints the XDG directory configuration.
-
-
@param brief If [true], prints a compact one-line summary (default: [false])
-
@param sources If [true], shows the source of each directory value,
-
indicating whether it came from defaults, environment
-
variables, or command line (default: [false])
-
@param ppf The formatter to print to
-
@param t The XDG context to print
-
-
{b Output formats:}
-
- Normal: Multi-line detailed view of all directories
-
- Brief: Single line showing app name and key directories
-
- With sources: Adds annotations showing where each path came from
-
*)
-
val pp : ?brief:bool -> ?sources:bool -> Format.formatter -> t -> unit
-
-
(** {1 Cmdliner Integration} *)
-
-
module Cmd : sig
-
(** The type of the outer XDG context *)
-
type xdg_t = t
-
(** Cmdliner integration for XDG directory configuration.
-
-
This module provides integration with the Cmdliner library,
-
allowing XDG directories to be configured via command-line arguments
-
while respecting the precedence of environment variables. *)
-
-
(** Type of XDG configuration gathered from command-line and environment.
-
-
This contains all XDG directory paths along with their sources,
-
as determined by command-line arguments and environment variables. *)
-
type t
-
-
(** [term app_name fs ?dirs ()] creates a Cmdliner term for XDG directory configuration.
-
-
This function generates a Cmdliner term that handles XDG directory
-
configuration through both command-line flags and environment variables,
-
and directly returns the XDG context. Only command-line flags for the
-
requested directories are generated.
-
-
@param app_name The application name (used for environment variable prefixes)
-
@param fs The Eio filesystem to use for path resolution
-
@param dirs List of directories to include flags for (default: all directories)
-
-
{b Generated Command-line Flags:}
-
Only the flags for requested directories are generated:
-
- [--config-dir DIR]: Override configuration directory (if [`Config] in dirs)
-
- [--data-dir DIR]: Override data directory (if [`Data] in dirs)
-
- [--cache-dir DIR]: Override cache directory (if [`Cache] in dirs)
-
- [--state-dir DIR]: Override state directory (if [`State] in dirs)
-
- [--runtime-dir DIR]: Override runtime directory (if [`Runtime] in dirs)
-
-
{b Environment Variable Precedence:}
-
For each directory type, the following precedence applies:
-
+ Command-line flag (e.g., [--config-dir]) - if enabled
-
+ Application-specific variable (e.g., [MYAPP_CONFIG_DIR])
-
+ XDG standard variable (e.g., [XDG_CONFIG_HOME])
-
+ Default value
-
*)
-
val term : string -> Eio.Fs.dir_ty Eio.Path.t ->
-
?dirs:dir list ->
-
unit -> (xdg_t * t) Cmdliner.Term.t
-
-
(** [cache_term app_name] creates a Cmdliner term that provides just the cache
-
directory path as a string, respecting XDG precedence.
-
-
This is a convenience function for applications that only need cache
-
directory configuration. It returns the resolved cache directory path
-
directly as a string, suitable for use in other Cmdliner terms.
-
-
@param app_name The application name (used for environment variable prefixes)
-
-
{b Generated Command-line Flag:}
-
- [--cache-dir DIR]: Override cache directory
-
-
{b Environment Variable Precedence:}
-
+ Command-line flag ([--cache-dir])
-
+ Application-specific variable (e.g., [MYAPP_CACHE_DIR])
-
+ XDG standard variable ([XDG_CACHE_HOME])
-
+ Default value ([$HOME/.cache/{app_name}])
-
*)
-
val cache_term : string -> string Cmdliner.Term.t
-
-
(** [env_docs app_name] generates documentation for environment variables.
-
-
Returns a formatted string documenting all environment variables that
-
affect XDG directory configuration for the given application. This is
-
useful for generating man pages or help text.
-
-
@param app_name The application name
-
@return A formatted documentation string
-
-
{b Included Information:}
-
- Configuration precedence rules
-
- Application-specific environment variables
-
- XDG standard environment variables
-
- Default values for each directory type
-
*)
-
val env_docs : string -> string
-
-
(** [pp ppf config] pretty prints a Cmdliner configuration.
-
-
This function formats the configuration showing each directory path
-
along with its source, which is helpful for debugging configuration
-
issues or displaying the current configuration to users.
-
-
@param ppf The formatter to print to
-
@param config The configuration to print *)
-
val pp : Format.formatter -> t -> unit
-
end
-6
stack/xdge/test/dune
···
-
(executable
-
(name test_paths)
-
(libraries xdge eio eio_main))
-
-
(cram
-
(deps ../example/xdg_example.exe test_paths.exe))
-112
stack/xdge/test/test_paths.ml
···
-
let test_path_validation () =
-
Printf.printf "Testing XDG path validation...\n";
-
(* Test absolute path validation for environment variables *)
-
let test_relative_path_rejection env_var relative_path =
-
Printf.printf "Testing rejection of relative path in %s...\n" env_var;
-
Unix.putenv env_var relative_path;
-
try
-
Eio_main.run
-
@@ fun env ->
-
let _ = Xdge.create env#fs "test_validation" in
-
Printf.printf "ERROR: Should have rejected relative path\n";
-
false
-
with
-
| Xdge.Invalid_xdg_path msg ->
-
Printf.printf "SUCCESS: Correctly rejected relative path: %s\n" msg;
-
true
-
| exn ->
-
Printf.printf "ERROR: Wrong exception: %s\n" (Printexc.to_string exn);
-
false
-
in
-
let old_config_home = Sys.getenv_opt "XDG_CONFIG_HOME" in
-
let old_data_dirs = Sys.getenv_opt "XDG_DATA_DIRS" in
-
let success1 = test_relative_path_rejection "XDG_CONFIG_HOME" "relative/path" in
-
let success2 = test_relative_path_rejection "XDG_DATA_DIRS" "rel1:rel2:/abs/path" in
-
(* Restore original env vars *)
-
(match old_config_home with
-
| Some v -> Unix.putenv "XDG_CONFIG_HOME" v
-
| None ->
-
(try Unix.putenv "XDG_CONFIG_HOME" "" with
-
| _ -> ()));
-
(match old_data_dirs with
-
| Some v -> Unix.putenv "XDG_DATA_DIRS" v
-
| None ->
-
(try Unix.putenv "XDG_DATA_DIRS" "" with
-
| _ -> ()));
-
success1 && success2
-
;;
-
-
let test_file_search () =
-
Printf.printf "\nTesting XDG file search...\n";
-
Eio_main.run
-
@@ fun env ->
-
let xdg = Xdge.create env#fs "search_test" in
-
(* Create test files *)
-
let config_file = Eio.Path.(Xdge.config_dir xdg / "test.conf") in
-
let data_file = Eio.Path.(Xdge.data_dir xdg / "test.dat") in
-
Eio.Path.save ~create:(`Or_truncate 0o644) config_file "config content";
-
Eio.Path.save ~create:(`Or_truncate 0o644) data_file "data content";
-
(* Test finding existing files *)
-
(match Xdge.find_config_file xdg "test.conf" with
-
| Some path ->
-
let content = Eio.Path.load path in
-
Printf.printf "Found config file: %s\n" (String.trim content)
-
| None -> Printf.printf "ERROR: Config file not found\n");
-
(match Xdge.find_data_file xdg "test.dat" with
-
| Some path ->
-
let content = Eio.Path.load path in
-
Printf.printf "Found data file: %s\n" (String.trim content)
-
| None -> Printf.printf "ERROR: Data file not found\n");
-
(* Test non-existent file *)
-
match Xdge.find_config_file xdg "nonexistent.conf" with
-
| Some _ -> Printf.printf "ERROR: Should not have found nonexistent file\n"
-
| None -> Printf.printf "Correctly handled nonexistent file\n"
-
;;
-
-
let () =
-
(* Check if we should run validation tests *)
-
if Array.length Sys.argv > 1 && Sys.argv.(1) = "--validate"
-
then (
-
let validation_success = test_path_validation () in
-
test_file_search ();
-
if validation_success
-
then Printf.printf "\nAll path validation tests passed!\n"
-
else Printf.printf "\nSome validation tests failed!\n")
-
else
-
(* Run original simple functionality test *)
-
Eio_main.run
-
@@ fun env ->
-
let xdg = Xdge.create env#fs "path_test" in
-
(* Test config subdirectory *)
-
let profiles_path = Eio.Path.(Xdge.config_dir xdg / "profiles") in
-
let profile_file = Eio.Path.(profiles_path / "default.json") in
-
(try
-
let content = Eio.Path.load profile_file in
-
Printf.printf "config file content: %s" (String.trim content)
-
with
-
| exn -> Printf.printf "config file error: %s" (Printexc.to_string exn));
-
(* Test data subdirectory *)
-
let db_path = Eio.Path.(Xdge.data_dir xdg / "databases") in
-
let db_file = Eio.Path.(db_path / "main.db") in
-
(try
-
let content = Eio.Path.load db_file in
-
Printf.printf "\ndata file content: %s" (String.trim content)
-
with
-
| exn -> Printf.printf "\ndata file error: %s" (Printexc.to_string exn));
-
(* Test cache subdirectory *)
-
let cache_path = Eio.Path.(Xdge.cache_dir xdg / "thumbnails") in
-
let cache_file = Eio.Path.(cache_path / "thumb1.png") in
-
(try
-
let content = Eio.Path.load cache_file in
-
Printf.printf "\ncache file content: %s" (String.trim content)
-
with
-
| exn -> Printf.printf "\ncache file error: %s" (Printexc.to_string exn));
-
(* Test state subdirectory *)
-
let logs_path = Eio.Path.(Xdge.state_dir xdg / "logs") in
-
let log_file = Eio.Path.(logs_path / "app.log") in
-
try
-
let content = Eio.Path.load log_file in
-
Printf.printf "\nstate file content: %s\n" (String.trim content)
-
with
-
| exn -> Printf.printf "\nstate file error: %s\n" (Printexc.to_string exn)
-
;;
-402
stack/xdge/test/xdg.t
···
-
Test with default directories:
-
-
$ export HOME=./test_home
-
$ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR
-
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
-
$ ../example/xdg_example.exe
-
=== Cmdliner Config ===
-
XDG config:
-
-
=== XDG Directories ===
-
XDG directories for 'xdg_example':
-
User directories:
-
config: <fs:./test_home/./test_home/.config/xdg_example> [default]
-
data: <fs:./test_home/./test_home/.local/share/xdg_example> [default]
-
cache: <fs:./test_home/./test_home/.cache/xdg_example> [default]
-
state: <fs:./test_home/./test_home/.local/state/xdg_example> [default]
-
runtime: <none> [default]
-
System directories:
-
config_dirs: [<fs:/etc/xdg/xdg_example>]
-
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
-
-
This test is correct: No command-line args or env vars are set, so all directories
-
use defaults. Config shows empty (no overrides), and directories show [default] source.
-
User directories follow XDG spec: ~/.config, ~/.local/share, ~/.cache, ~/.local/state.
-
Runtime dir is <none> since XDG_RUNTIME_DIR has no default.
-
System dirs use XDG spec defaults: /etc/xdg for config, /usr/{local/,}share for data.
-
-
Test with all command line arguments specified
-
$ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR
-
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
-
$ ../example/xdg_example.exe \
-
> --config-dir ./test-config \
-
> --data-dir ./test-data \
-
> --cache-dir ./test-cache \
-
> --state-dir ./test-state \
-
> --runtime-dir ./test-runtime
-
=== Cmdliner Config ===
-
XDG config:
-
config_dir: ./test-config [cmdline]
-
data_dir: ./test-data [cmdline]
-
cache_dir: ./test-cache [cmdline]
-
state_dir: ./test-state [cmdline]
-
runtime_dir: ./test-runtime [cmdline]
-
-
=== XDG Directories ===
-
XDG directories for 'xdg_example':
-
User directories:
-
config: <fs:./test_home/./test-config> [cmdline]
-
data: <fs:./test_home/./test-data> [cmdline]
-
cache: <fs:./test_home/./test-cache> [cmdline]
-
state: <fs:./test_home/./test-state> [cmdline]
-
runtime: <fs:./test_home/./test-runtime> [cmdline]
-
System directories:
-
config_dirs: [<fs:/etc/xdg/xdg_example>]
-
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
-
-
This test is correct: All user directories are overridden by command-line arguments,
-
showing [cmdline] as the source. The config section shows all overrides with their
-
values and [cmdline] sources. System directories remain at their defaults since
-
they cannot be overridden by user directories command-line options.
-
-
Test with environment variables (app-specific)
-
$ XDG_EXAMPLE_CONFIG_DIR=./env-config \
-
> XDG_EXAMPLE_DATA_DIR=./env-data \
-
> XDG_EXAMPLE_CACHE_DIR=./env-cache \
-
> XDG_EXAMPLE_STATE_DIR=./env-state \
-
> XDG_EXAMPLE_RUNTIME_DIR=./env-runtime \
-
> ../example/xdg_example.exe
-
=== Cmdliner Config ===
-
XDG config:
-
config_dir: ./env-config [env(XDG_EXAMPLE_CONFIG_DIR)]
-
data_dir: ./env-data [env(XDG_EXAMPLE_DATA_DIR)]
-
cache_dir: ./env-cache [env(XDG_EXAMPLE_CACHE_DIR)]
-
state_dir: ./env-state [env(XDG_EXAMPLE_STATE_DIR)]
-
runtime_dir: ./env-runtime [env(XDG_EXAMPLE_RUNTIME_DIR)]
-
-
=== XDG Directories ===
-
XDG directories for 'xdg_example':
-
User directories:
-
config: <fs:./test_home/./env-config> [env(XDG_EXAMPLE_CONFIG_DIR)]
-
data: <fs:./test_home/./env-data> [env(XDG_EXAMPLE_DATA_DIR)]
-
cache: <fs:./test_home/./env-cache> [env(XDG_EXAMPLE_CACHE_DIR)]
-
state: <fs:./test_home/./env-state> [env(XDG_EXAMPLE_STATE_DIR)]
-
runtime: <fs:./test_home/./env-runtime> [env(XDG_EXAMPLE_RUNTIME_DIR)]
-
System directories:
-
config_dirs: [<fs:/etc/xdg/xdg_example>]
-
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
-
-
This test is correct: App-specific environment variables (XDG_EXAMPLE_*) override
-
the defaults. The source correctly shows [env(XDG_EXAMPLE_*)] for each variable.
-
These app-specific variables take precedence over XDG standard variables when both
-
are available, allowing per-application customization.
-
-
Test with standard XDG environment variables:
-
-
$ XDG_CONFIG_HOME=/tmp/xdge/xdg-config \
-
> XDG_DATA_HOME=/tmp/xdge/xdg-data \
-
> XDG_CACHE_HOME=/tmp/xdge/xdg-cache \
-
> XDG_STATE_HOME=/tmp/xdge/xdg-state \
-
> XDG_RUNTIME_DIR=/tmp/xdge/xdg-runtime \
-
> ../example/xdg_example.exe
-
=== Cmdliner Config ===
-
XDG config:
-
config_dir: /tmp/xdge/xdg-config [env(XDG_CONFIG_HOME)]
-
data_dir: /tmp/xdge/xdg-data [env(XDG_DATA_HOME)]
-
cache_dir: /tmp/xdge/xdg-cache [env(XDG_CACHE_HOME)]
-
state_dir: /tmp/xdge/xdg-state [env(XDG_STATE_HOME)]
-
runtime_dir: /tmp/xdge/xdg-runtime [env(XDG_RUNTIME_DIR)]
-
-
=== XDG Directories ===
-
XDG directories for 'xdg_example':
-
User directories:
-
config: <fs:/tmp/xdge/xdg-config> [env(XDG_CONFIG_HOME)]
-
data: <fs:/tmp/xdge/xdg-data> [env(XDG_DATA_HOME)]
-
cache: <fs:/tmp/xdge/xdg-cache> [env(XDG_CACHE_HOME)]
-
state: <fs:/tmp/xdge/xdg-state> [env(XDG_STATE_HOME)]
-
runtime: <fs:/tmp/xdge/xdg-runtime> [env(XDG_RUNTIME_DIR)]
-
System directories:
-
config_dirs: [<fs:/etc/xdg/xdg_example>]
-
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
-
-
This test is correct: Standard XDG environment variables (XDG_*_HOME, XDG_RUNTIME_DIR)
-
override the defaults. The source correctly shows [env(XDG_*)] for each variable.
-
Note that the user directories use the raw paths from env vars (not app-specific subdirs)
-
since XDG_CONFIG_HOME etc. are intended to be the base directories for the user.
-
-
Test command line overrides environment variables:
-
-
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
-
$ XDG_EXAMPLE_CONFIG_DIR=./env-config \
-
> ../example/xdg_example.exe --config-dir ./cli-config
-
=== Cmdliner Config ===
-
XDG config:
-
config_dir: ./cli-config [cmdline]
-
-
=== XDG Directories ===
-
XDG directories for 'xdg_example':
-
User directories:
-
config: <fs:./test_home/./cli-config> [cmdline]
-
data: <fs:./test_home/./test_home/.local/share/xdg_example> [default]
-
cache: <fs:./test_home/./test_home/.cache/xdg_example> [default]
-
state: <fs:./test_home/./test_home/.local/state/xdg_example> [default]
-
runtime: <none> [default]
-
System directories:
-
config_dirs: [<fs:/etc/xdg/xdg_example>]
-
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
-
-
This test is correct: Command-line arguments have highest precedence, overriding
-
environment variables. Only config_dir is shown in the config section since it is
-
the only one explicitly set. The config_dir shows [cmdline] source while other
-
directories fall back to defaults, demonstrating the precedence hierarchy:
-
of cmdline then app env vars then XDG env vars then defaults.
-
-
Test mixed environment variable precedence (app-specific overrides XDG standard):
-
-
$ export HOME=./test_home
-
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
-
$ XDG_CONFIG_HOME=/tmp/xdge/xdg-config \
-
> XDG_EXAMPLE_CONFIG_DIR=./app-config \
-
> XDG_DATA_HOME=/tmp/xdge/xdg-data \
-
> XDG_EXAMPLE_DATA_DIR=./app-data \
-
> ../example/xdg_example.exe
-
=== Cmdliner Config ===
-
XDG config:
-
config_dir: ./app-config [env(XDG_EXAMPLE_CONFIG_DIR)]
-
data_dir: ./app-data [env(XDG_EXAMPLE_DATA_DIR)]
-
-
=== XDG Directories ===
-
XDG directories for 'xdg_example':
-
User directories:
-
config: <fs:./test_home/./app-config> [env(XDG_EXAMPLE_CONFIG_DIR)]
-
data: <fs:./test_home/./app-data> [env(XDG_EXAMPLE_DATA_DIR)]
-
cache: <fs:./test_home/./test_home/.cache/xdg_example> [default]
-
state: <fs:./test_home/./test_home/.local/state/xdg_example> [default]
-
runtime: <none> [default]
-
System directories:
-
config_dirs: [<fs:/etc/xdg/xdg_example>]
-
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
-
-
This test is correct: Demonstrates app-specific environment variables taking
-
precedence over XDG standard ones. Both XDG_CONFIG_HOME and XDG_EXAMPLE_CONFIG_DIR
-
are set, but the app-specific one wins. Same for data directories. Cache, state,
-
and runtime fall back to defaults since no variables are set for them.
-
-
Test partial environment variable override:
-
-
$ export HOME=./test_home
-
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
-
$ XDG_EXAMPLE_CONFIG_DIR=./app-config \
-
> XDG_DATA_HOME=/tmp/xdge/xdg-data \
-
> XDG_CACHE_HOME=/tmp/xdge/xdg-cache \
-
> ../example/xdg_example.exe
-
=== Cmdliner Config ===
-
XDG config:
-
config_dir: ./app-config [env(XDG_EXAMPLE_CONFIG_DIR)]
-
data_dir: /tmp/xdge/xdg-data [env(XDG_DATA_HOME)]
-
cache_dir: /tmp/xdge/xdg-cache [env(XDG_CACHE_HOME)]
-
-
=== XDG Directories ===
-
XDG directories for 'xdg_example':
-
User directories:
-
config: <fs:./test_home/./app-config> [env(XDG_EXAMPLE_CONFIG_DIR)]
-
data: <fs:/tmp/xdge/xdg-data> [env(XDG_DATA_HOME)]
-
cache: <fs:/tmp/xdge/xdg-cache> [env(XDG_CACHE_HOME)]
-
state: <fs:./test_home/./test_home/.local/state/xdg_example> [default]
-
runtime: <none> [default]
-
System directories:
-
config_dirs: [<fs:/etc/xdg/xdg_example>]
-
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
-
-
This test is correct: Shows mixed sources working together. Config uses app-specific
-
env var (highest priority among env vars), data and cache use XDG standard env vars
-
(no app-specific ones set), and state uses default (no env vars set). Each directory
-
gets its value from the highest-priority available source.
-
-
Test command line overrides mixed environment variables:
-
-
$ export HOME=./test_home
-
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
-
$ XDG_CONFIG_HOME=/tmp/xdge/xdg-config \
-
> XDG_EXAMPLE_CONFIG_DIR=./app-config \
-
> ../example/xdg_example.exe --config-dir ./cli-config
-
=== Cmdliner Config ===
-
XDG config:
-
config_dir: ./cli-config [cmdline]
-
-
=== XDG Directories ===
-
XDG directories for 'xdg_example':
-
User directories:
-
config: <fs:./test_home/./cli-config> [cmdline]
-
data: <fs:./test_home/./test_home/.local/share/xdg_example> [default]
-
cache: <fs:./test_home/./test_home/.cache/xdg_example> [default]
-
state: <fs:./test_home/./test_home/.local/state/xdg_example> [default]
-
runtime: <none> [default]
-
System directories:
-
config_dirs: [<fs:/etc/xdg/xdg_example>]
-
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
-
-
This test is correct: Command-line argument overrides both types of environment
-
variables. Even though both XDG_CONFIG_HOME and XDG_EXAMPLE_CONFIG_DIR are set,
-
the --config-dir flag takes precedence and shows [cmdline] source. Other directories
-
fall back to defaults since no other command-line args are provided.
-
-
-
Test empty environment variable handling:
-
$ export HOME=./test_home
-
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
-
$ XDG_EXAMPLE_CONFIG_DIR="" \
-
> XDG_CONFIG_HOME=/tmp/xdge/xdg-config \
-
> ../example/xdg_example.exe
-
=== Cmdliner Config ===
-
XDG config:
-
config_dir: /tmp/xdge/xdg-config [env(XDG_CONFIG_HOME)]
-
-
=== XDG Directories ===
-
XDG directories for 'xdg_example':
-
User directories:
-
config: <fs:/tmp/xdge/xdg-config> [env(XDG_CONFIG_HOME)]
-
data: <fs:./test_home/./test_home/.local/share/xdg_example> [default]
-
cache: <fs:./test_home/./test_home/.cache/xdg_example> [default]
-
state: <fs:./test_home/./test_home/.local/state/xdg_example> [default]
-
runtime: <none> [default]
-
System directories:
-
config_dirs: [<fs:/etc/xdg/xdg_example>]
-
data_dirs: [<fs:/usr/local/share/xdg_example>; <fs:/usr/share/xdg_example>]
-
-
This test is correct: When an app-specific env var is empty (""), it falls back to
-
the XDG standard variable. XDG_EXAMPLE_CONFIG_DIR="" is ignored, so XDG_CONFIG_HOME
-
is used instead, correctly showing [env(XDG_CONFIG_HOME)] as the source.
-
This behavior ensures that empty app-specific variables do not override useful
-
XDG standard settings.
-
-
-
Test system directory environment variables:
-
-
$ export HOME=./test_home
-
$ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR
-
$ XDG_CONFIG_DIRS=/tmp/xdge/sys1:/tmp/xdge/sys2 \
-
> XDG_DATA_DIRS=/tmp/xdge/data1:/tmp/xdge/data2 \
-
> ../example/xdg_example.exe
-
=== Cmdliner Config ===
-
XDG config:
-
-
=== XDG Directories ===
-
XDG directories for 'xdg_example':
-
User directories:
-
config: <fs:./test_home/./test_home/.config/xdg_example> [default]
-
data: <fs:./test_home/./test_home/.local/share/xdg_example> [default]
-
cache: <fs:./test_home/./test_home/.cache/xdg_example> [default]
-
state: <fs:./test_home/./test_home/.local/state/xdg_example> [default]
-
runtime: <none> [default]
-
System directories:
-
config_dirs: [<fs:/tmp/xdge/sys1/xdg_example>;
-
<fs:/tmp/xdge/sys2/xdg_example>]
-
data_dirs: [<fs:/tmp/xdge/data1/xdg_example>;
-
<fs:/tmp/xdge/data2/xdg_example>]
-
-
This test is correct: XDG_CONFIG_DIRS and XDG_DATA_DIRS environment variables
-
override the default system directories. The colon-separated paths are parsed
-
and the app name is appended to each path. User directories remain at defaults
-
since no user-level overrides are provided. System directory env vars only
-
affect the system directories, not user directories.
-
-
-
Test help message:
-
-
$ ../example/xdg_example.exe --help=plain | head -20
-
NAME
-
xdg_example - Example program demonstrating XDG directory selection
-
with Cmdliner
-
-
SYNOPSIS
-
xdg_example [OPTION]…
-
-
DESCRIPTION
-
This example shows how to use the Xdge library with Cmdliner to handle
-
XDG Base Directory Specification paths with command-line and
-
environment variable overrides.
-
-
OPTIONS
-
--cache-dir=DIR
-
Override cache directory. Can also be set with
-
XDG_EXAMPLE_CACHE_DIR or XDG_CACHE_HOME. Default:
-
$HOME/.cache/xdg_example
-
-
--config-dir=DIR
-
Override config directory. Can also be set with
-
-
Test _path functions do not create directories but can access files within them:
-
-
$ export HOME=/tmp/xdge/xdg_path_test
-
$ mkdir -p /tmp/xdge/xdg_path_test
-
$ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR
-
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
-
Create config subdirectory manually and write a test file:
-
$ mkdir -p "/tmp/xdge/xdg_path_test/.config/path_test/profiles"
-
$ echo "test profile content" > "/tmp/xdge/xdg_path_test/.config/path_test/profiles/default.json"
-
Create data subdirectory manually and write a test file:
-
$ mkdir -p "/tmp/xdge/xdg_path_test/.local/share/path_test/databases"
-
$ echo "test database content" > "/tmp/xdge/xdg_path_test/.local/share/path_test/databases/main.db"
-
Create cache subdirectory manually and write a test file:
-
$ mkdir -p "/tmp/xdge/xdg_path_test/.cache/path_test/thumbnails"
-
$ echo "test cache content" > "/tmp/xdge/xdg_path_test/.cache/path_test/thumbnails/thumb1.png"
-
Create state subdirectory manually and write a test file:
-
$ mkdir -p "/tmp/xdge/xdg_path_test/.local/state/path_test/logs"
-
$ echo "test log content" > "/tmp/xdge/xdg_path_test/.local/state/path_test/logs/app.log"
-
-
Now test that we can read the files through the XDG _path functions:
-
$ ./test_paths.exe
-
config file content: test profile content
-
data file content: test database content
-
cache file content: test cache content
-
state file content: test log content
-
-
This test verifies that the _path functions return correct paths that can be used to access
-
files within XDG subdirectories, without the functions automatically creating those directories.
-
-
Test path resolution with --show-paths:
-
-
Test with a preset HOME to verify correct path resolution:
-
$ export HOME=./home_testuser
-
$ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR
-
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
-
$ ../example/xdg_example.exe --show-paths
-
config_dir: ./home_testuser/./home_testuser/.config/xdg_example
-
data_dir: ./home_testuser/./home_testuser/.local/share/xdg_example
-
cache_dir: ./home_testuser/./home_testuser/.cache/xdg_example
-
state_dir: ./home_testuser/./home_testuser/.local/state/xdg_example
-
runtime_dir: <none>
-
config_dirs: /etc/xdg/xdg_example
-
data_dirs: /usr/local/share/xdg_example:/usr/share/xdg_example
-
-
Test with environment variables set:
-
$ export HOME=./home_testuser
-
$ export XDG_CONFIG_HOME=/tmp/xdge/config
-
$ export XDG_DATA_HOME=/tmp/xdge/data
-
$ export XDG_CACHE_HOME=/tmp/xdge/cache
-
$ export XDG_STATE_HOME=/tmp/xdge/state
-
$ export XDG_CONFIG_DIRS=/tmp/xdge/config1:/tmp/xdge/config2
-
$ export XDG_DATA_DIRS=/tmp/xdge/data1:/tmp/xdge/data2
-
$ ../example/xdg_example.exe --show-paths
-
config_dir: /tmp/xdge/config
-
data_dir: /tmp/xdge/data
-
cache_dir: /tmp/xdge/cache
-
state_dir: /tmp/xdge/state
-
runtime_dir: <none>
-
config_dirs: /tmp/xdge/config1/xdg_example:/tmp/xdge/config2/xdg_example
-
data_dirs: /tmp/xdge/data1/xdg_example:/tmp/xdge/data2/xdg_example
-
-
Test with command-line overrides:
-
$ export HOME=./home_testuser
-
$ unset XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME XDG_RUNTIME_DIR
-
$ unset XDG_CONFIG_DIRS XDG_DATA_DIRS
-
$ ../example/xdg_example.exe --show-paths --config-dir ./override/config --data-dir ./override/data
-
config_dir: ./home_testuser/./override/config
-
data_dir: ./home_testuser/./override/data
-
cache_dir: ./home_testuser/./home_testuser/.cache/xdg_example
-
state_dir: ./home_testuser/./home_testuser/.local/state/xdg_example
-
runtime_dir: <none>
-
config_dirs: /etc/xdg/xdg_example
-
data_dirs: /usr/local/share/xdg_example:/usr/share/xdg_example
-
-36
stack/xdge/xdge.opam
···
-
# This file is generated by dune, edit dune-project instead
-
opam-version: "2.0"
-
synopsis: "XDG Base Directory Specification support for Eio"
-
description:
-
"This library implements the XDG Base Directory Specification with Eio capabilities to provides safe access to configuration, data, cache, state, and runtime directories with proper environment variable overrides and Cmdliner integration."
-
maintainer: ["Anil Madhavapeddy <anil@recoil.org>"]
-
authors: ["Anil Madhavapeddy"]
-
license: "ISC"
-
homepage: "https://tangled.sh/@anil.recoil.org/ocaml-gpx"
-
bug-reports: "https://tangled.sh/@anil.recoil.org/xgde"
-
depends: [
-
"dune" {>= "3.20"}
-
"ocaml" {>= "5.1.0"}
-
"eio" {>= "1.1"}
-
"eio_main"
-
"xdg" {>= "3.9.0"}
-
"cmdliner" {>= "1.2.0"}
-
"fmt" {>= "0.11.0"}
-
"odoc" {with-doc}
-
"alcotest" {with-test & >= "1.7.0"}
-
]
-
build: [
-
["dune" "subst"] {dev}
-
[
-
"dune"
-
"build"
-
"-p"
-
name
-
"-j"
-
jobs
-
"@install"
-
"@runtest" {with-test}
-
"@doc" {with-doc}
-
]
-
]
-
x-maintenance-intent: ["(latest)"]