···11+(* Kitty Graphics Protocol - Animation *)
22+33+type state = Kgp_types.animation_state
44+55+type t =
66+ [ `Set_state of state * int option
77+ | `Set_gap of int * int
88+ | `Set_current of int ]
99+1010+let set_state ?loops state = `Set_state (state, loops)
1111+let set_gap ~frame ~gap_ms = `Set_gap (frame, gap_ms)
1212+let set_current_frame frame = `Set_current frame
+21
stack/kitty_graphics/lib/kgp_animation.mli
···11+(** Kitty Graphics Protocol - Animation *)
22+33+type state = Kgp_types.animation_state
44+55+type t =
66+ [ `Set_state of state * int option
77+ | `Set_gap of int * int
88+ | `Set_current of int ]
99+(** Animation control operations. *)
1010+1111+val set_state : ?loops:int -> state -> t
1212+(** Set animation state.
1313+ @param loops Number of loops: 0 = ignored, 1 = infinite, n = n-1 loops *)
1414+1515+val set_gap : frame:int -> gap_ms:int -> t
1616+(** Set the gap (delay) for a specific frame.
1717+ @param frame 1-based frame number
1818+ @param gap_ms Delay in milliseconds (negative = gapless) *)
1919+2020+val set_current_frame : int -> t
2121+(** Make a specific frame (1-based) the current displayed frame. *)
+332
stack/kitty_graphics/lib/kgp_command.ml
···11+(* Kitty Graphics Protocol - Command *)
22+33+type action =
44+ [ `Transmit
55+ | `Transmit_and_display
66+ | `Query
77+ | `Display
88+ | `Delete
99+ | `Frame
1010+ | `Animate
1111+ | `Compose ]
1212+1313+type t = {
1414+ action : action;
1515+ format : Kgp_types.format option;
1616+ transmission : Kgp_types.transmission option;
1717+ compression : Kgp_types.compression option;
1818+ width : int option;
1919+ height : int option;
2020+ size : int option;
2121+ offset : int option;
2222+ quiet : Kgp_types.quiet option;
2323+ image_id : int option;
2424+ image_number : int option;
2525+ placement : Kgp_placement.t option;
2626+ delete : Kgp_types.delete option;
2727+ frame : Kgp_frame.t option;
2828+ animation : Kgp_animation.t option;
2929+ compose : Kgp_compose.t option;
3030+}
3131+3232+let make action =
3333+ {
3434+ action;
3535+ format = None;
3636+ transmission = None;
3737+ compression = None;
3838+ width = None;
3939+ height = None;
4040+ size = None;
4141+ offset = None;
4242+ quiet = None;
4343+ image_id = None;
4444+ image_number = None;
4545+ placement = None;
4646+ delete = None;
4747+ frame = None;
4848+ animation = None;
4949+ compose = None;
5050+ }
5151+5252+let transmit ?image_id ?image_number ?format ?transmission ?compression ?width
5353+ ?height ?size ?offset ?quiet () =
5454+ {
5555+ (make `Transmit) with
5656+ image_id;
5757+ image_number;
5858+ format;
5959+ transmission;
6060+ compression;
6161+ width;
6262+ height;
6363+ size;
6464+ offset;
6565+ quiet;
6666+ }
6767+6868+let transmit_and_display ?image_id ?image_number ?format ?transmission
6969+ ?compression ?width ?height ?size ?offset ?quiet ?placement () =
7070+ {
7171+ (make `Transmit_and_display) with
7272+ image_id;
7373+ image_number;
7474+ format;
7575+ transmission;
7676+ compression;
7777+ width;
7878+ height;
7979+ size;
8080+ offset;
8181+ quiet;
8282+ placement;
8383+ }
8484+8585+let query ?format ?transmission ?width ?height ?quiet () =
8686+ { (make `Query) with format; transmission; width; height; quiet }
8787+8888+let display ?image_id ?image_number ?placement ?quiet () =
8989+ { (make `Display) with image_id; image_number; placement; quiet }
9090+9191+let delete ?quiet del = { (make `Delete) with quiet; delete = Some del }
9292+9393+let frame ?image_id ?image_number ?format ?transmission ?compression ?width
9494+ ?height ?quiet ~frame () =
9595+ {
9696+ (make `Frame) with
9797+ image_id;
9898+ image_number;
9999+ format;
100100+ transmission;
101101+ compression;
102102+ width;
103103+ height;
104104+ quiet;
105105+ frame = Some frame;
106106+ }
107107+108108+let animate ?image_id ?image_number ?quiet anim =
109109+ { (make `Animate) with image_id; image_number; quiet; animation = Some anim }
110110+111111+let compose ?image_id ?image_number ?quiet comp =
112112+ { (make `Compose) with image_id; image_number; quiet; compose = Some comp }
113113+114114+(* Serialization helpers *)
115115+let apc_start = "\027_G"
116116+let apc_end = "\027\\"
117117+118118+(* Key-value writer with separator handling *)
119119+type kv_writer = { mutable first : bool; buf : Buffer.t }
120120+121121+let kv_writer buf = { first = true; buf }
122122+123123+let kv w key value =
124124+ if not w.first then Buffer.add_char w.buf ',';
125125+ w.first <- false;
126126+ Buffer.add_char w.buf key;
127127+ Buffer.add_char w.buf '=';
128128+ Buffer.add_string w.buf value
129129+130130+let kv_int w key value = kv w key (string_of_int value)
131131+let kv_int32 w key value = kv w key (Int32.to_string value)
132132+let kv_char w key value = kv w key (String.make 1 value)
133133+134134+(* Conditional writers using Option.iter *)
135135+let kv_int_opt w key = Option.iter (kv_int w key)
136136+let kv_int32_opt w key = Option.iter (kv_int32 w key)
137137+138138+let kv_int_if w key ~default opt =
139139+ Option.iter (fun v -> if v <> default then kv_int w key v) opt
140140+141141+let action_char : action -> char = function
142142+ | `Transmit -> 't'
143143+ | `Transmit_and_display -> 'T'
144144+ | `Query -> 'q'
145145+ | `Display -> 'p'
146146+ | `Delete -> 'd'
147147+ | `Frame -> 'f'
148148+ | `Animate -> 'a'
149149+ | `Compose -> 'c'
150150+151151+let delete_char : Kgp_types.delete -> char = function
152152+ | `All_visible -> 'a'
153153+ | `All_visible_and_free -> 'A'
154154+ | `By_id _ -> 'i'
155155+ | `By_id_and_free _ -> 'I'
156156+ | `By_number _ -> 'n'
157157+ | `By_number_and_free _ -> 'N'
158158+ | `At_cursor -> 'c'
159159+ | `At_cursor_and_free -> 'C'
160160+ | `At_cell _ -> 'p'
161161+ | `At_cell_and_free _ -> 'P'
162162+ | `At_cell_z _ -> 'q'
163163+ | `At_cell_z_and_free _ -> 'Q'
164164+ | `By_column _ -> 'x'
165165+ | `By_column_and_free _ -> 'X'
166166+ | `By_row _ -> 'y'
167167+ | `By_row_and_free _ -> 'Y'
168168+ | `By_z_index _ -> 'z'
169169+ | `By_z_index_and_free _ -> 'Z'
170170+ | `By_id_range _ -> 'r'
171171+ | `By_id_range_and_free _ -> 'R'
172172+ | `Frames -> 'f'
173173+ | `Frames_and_free -> 'F'
174174+175175+let write_placement w (p : Kgp_placement.t) =
176176+ kv_int_opt w 'x' p.source_x;
177177+ kv_int_opt w 'y' p.source_y;
178178+ kv_int_opt w 'w' p.source_width;
179179+ kv_int_opt w 'h' p.source_height;
180180+ kv_int_opt w 'X' p.cell_x_offset;
181181+ kv_int_opt w 'Y' p.cell_y_offset;
182182+ kv_int_opt w 'c' p.columns;
183183+ kv_int_opt w 'r' p.rows;
184184+ kv_int_opt w 'z' p.z_index;
185185+ kv_int_opt w 'p' p.placement_id;
186186+ p.cursor
187187+ |> Option.iter (fun c ->
188188+ kv_int_if w 'C' ~default:0 (Some (Kgp_types.Cursor.to_int c)));
189189+ if p.unicode_placeholder then kv_int w 'U' 1
190190+191191+let write_delete w (d : Kgp_types.delete) =
192192+ kv_char w 'd' (delete_char d);
193193+ match d with
194194+ | `By_id (id, pid) | `By_id_and_free (id, pid) ->
195195+ kv_int w 'i' id;
196196+ kv_int_opt w 'p' pid
197197+ | `By_number (n, pid) | `By_number_and_free (n, pid) ->
198198+ kv_int w 'I' n;
199199+ kv_int_opt w 'p' pid
200200+ | `At_cell (x, y) | `At_cell_and_free (x, y) ->
201201+ kv_int w 'x' x;
202202+ kv_int w 'y' y
203203+ | `At_cell_z (x, y, z) | `At_cell_z_and_free (x, y, z) ->
204204+ kv_int w 'x' x;
205205+ kv_int w 'y' y;
206206+ kv_int w 'z' z
207207+ | `By_column c | `By_column_and_free c -> kv_int w 'x' c
208208+ | `By_row r | `By_row_and_free r -> kv_int w 'y' r
209209+ | `By_z_index z | `By_z_index_and_free z -> kv_int w 'z' z
210210+ | `By_id_range (min_id, max_id) | `By_id_range_and_free (min_id, max_id) ->
211211+ kv_int w 'x' min_id;
212212+ kv_int w 'y' max_id
213213+ | `All_visible | `All_visible_and_free | `At_cursor | `At_cursor_and_free
214214+ | `Frames | `Frames_and_free ->
215215+ ()
216216+217217+let write_frame w (f : Kgp_frame.t) =
218218+ kv_int_opt w 'x' f.x;
219219+ kv_int_opt w 'y' f.y;
220220+ kv_int_opt w 'c' f.base_frame;
221221+ kv_int_opt w 'r' f.edit_frame;
222222+ kv_int_opt w 'z' f.gap_ms;
223223+ f.composition
224224+ |> Option.iter (fun c ->
225225+ kv_int_if w 'X' ~default:0 (Some (Kgp_types.Composition.to_int c)));
226226+ kv_int32_opt w 'Y' f.background_color
227227+228228+let write_animation w : Kgp_animation.t -> unit = function
229229+ | `Set_state (state, loops) ->
230230+ let s = match state with `Stop -> 1 | `Loading -> 2 | `Run -> 3 in
231231+ kv_int w 's' s;
232232+ kv_int_opt w 'v' loops
233233+ | `Set_gap (frame, gap_ms) ->
234234+ kv_int w 'r' frame;
235235+ kv_int w 'z' gap_ms
236236+ | `Set_current frame -> kv_int w 'c' frame
237237+238238+let write_compose w (c : Kgp_compose.t) =
239239+ kv_int w 'r' c.source_frame;
240240+ kv_int w 'c' c.dest_frame;
241241+ kv_int_opt w 'w' c.width;
242242+ kv_int_opt w 'h' c.height;
243243+ kv_int_opt w 'x' c.dest_x;
244244+ kv_int_opt w 'y' c.dest_y;
245245+ kv_int_opt w 'X' c.source_x;
246246+ kv_int_opt w 'Y' c.source_y;
247247+ c.composition
248248+ |> Option.iter (fun comp ->
249249+ kv_int_if w 'C' ~default:0 (Some (Kgp_types.Composition.to_int comp)))
250250+251251+let write_control_data buf cmd =
252252+ let w = kv_writer buf in
253253+ (* Action *)
254254+ kv_char w 'a' (action_char cmd.action);
255255+ (* Quiet - only if non-default *)
256256+ cmd.quiet
257257+ |> Option.iter (fun q ->
258258+ kv_int_if w 'q' ~default:0 (Some (Kgp_types.Quiet.to_int q)));
259259+ (* Format *)
260260+ cmd.format
261261+ |> Option.iter (fun f -> kv_int w 'f' (Kgp_types.Format.to_int f));
262262+ (* Transmission - only for transmit/frame actions, always include t=d for compatibility *)
263263+ (match cmd.action with
264264+ | `Transmit | `Transmit_and_display | `Frame -> (
265265+ match cmd.transmission with
266266+ | Some t -> kv_char w 't' (Kgp_types.Transmission.to_char t)
267267+ | None -> kv_char w 't' 'd')
268268+ | _ -> ());
269269+ (* Compression *)
270270+ cmd.compression
271271+ |> Option.iter (fun c ->
272272+ Kgp_types.Compression.to_char c |> Option.iter (kv_char w 'o'));
273273+ (* Dimensions *)
274274+ kv_int_opt w 's' cmd.width;
275275+ kv_int_opt w 'v' cmd.height;
276276+ (* File size/offset *)
277277+ kv_int_opt w 'S' cmd.size;
278278+ kv_int_opt w 'O' cmd.offset;
279279+ (* Image ID/number *)
280280+ kv_int_opt w 'i' cmd.image_id;
281281+ kv_int_opt w 'I' cmd.image_number;
282282+ (* Complex options *)
283283+ cmd.placement |> Option.iter (write_placement w);
284284+ cmd.delete |> Option.iter (write_delete w);
285285+ cmd.frame |> Option.iter (write_frame w);
286286+ cmd.animation |> Option.iter (write_animation w);
287287+ cmd.compose |> Option.iter (write_compose w);
288288+ w
289289+290290+(* Use large chunk size to avoid chunking - Kitty animation doesn't handle chunks well *)
291291+let chunk_size = 1024 * 1024 (* 1MB - effectively no chunking *)
292292+293293+let write buf cmd ~data =
294294+ Buffer.add_string buf apc_start;
295295+ let w = write_control_data buf cmd in
296296+ if String.length data > 0 then begin
297297+ let encoded = Base64.encode_string data in
298298+ let len = String.length encoded in
299299+ if len <= chunk_size then (
300300+ Buffer.add_char buf ';';
301301+ Buffer.add_string buf encoded;
302302+ Buffer.add_string buf apc_end)
303303+ else begin
304304+ (* Multiple chunks *)
305305+ let rec write_chunks pos first =
306306+ if pos < len then begin
307307+ let remaining = len - pos in
308308+ let this_chunk = min chunk_size remaining in
309309+ let is_last = pos + this_chunk >= len in
310310+ if first then (
311311+ kv_int w 'm' 1;
312312+ Buffer.add_char buf ';';
313313+ Buffer.add_substring buf encoded pos this_chunk;
314314+ Buffer.add_string buf apc_end)
315315+ else (
316316+ Buffer.add_string buf apc_start;
317317+ Buffer.add_string buf (if is_last then "m=0" else "m=1");
318318+ Buffer.add_char buf ';';
319319+ Buffer.add_substring buf encoded pos this_chunk;
320320+ Buffer.add_string buf apc_end);
321321+ write_chunks (pos + this_chunk) false
322322+ end
323323+ in
324324+ write_chunks 0 true
325325+ end
326326+ end
327327+ else Buffer.add_string buf apc_end
328328+329329+let to_string cmd ~data =
330330+ let buf = Buffer.create 1024 in
331331+ write buf cmd ~data;
332332+ Buffer.contents buf
···11+(** Kitty Graphics Protocol - Terminal Detection *)
22+33+val make_query : unit -> string
44+(** Generate a query command to test graphics support. *)
55+66+val supports_graphics : Kgp_response.t option -> da1_received:bool -> bool
77+(** Determine if graphics are supported based on query results. *)
+26
stack/kitty_graphics/lib/kgp_frame.ml
···11+(* Kitty Graphics Protocol - Frame *)
22+33+type t = {
44+ x : int option;
55+ y : int option;
66+ base_frame : int option;
77+ edit_frame : int option;
88+ gap_ms : int option;
99+ composition : Kgp_types.composition option;
1010+ background_color : int32 option;
1111+}
1212+1313+let empty =
1414+ {
1515+ x = None;
1616+ y = None;
1717+ base_frame = None;
1818+ edit_frame = None;
1919+ gap_ms = None;
2020+ composition = None;
2121+ background_color = None;
2222+ }
2323+2424+let make ?x ?y ?base_frame ?edit_frame ?gap_ms ?composition ?background_color
2525+ () =
2626+ { x; y; base_frame; edit_frame; gap_ms; composition; background_color }
+34
stack/kitty_graphics/lib/kgp_frame.mli
···11+(** Kitty Graphics Protocol - Frame *)
22+33+type t = {
44+ x : int option;
55+ y : int option;
66+ base_frame : int option;
77+ edit_frame : int option;
88+ gap_ms : int option;
99+ composition : Kgp_types.composition option;
1010+ background_color : int32 option;
1111+}
1212+1313+val empty : t
1414+(** Empty frame spec with defaults. *)
1515+1616+val make :
1717+ ?x:int ->
1818+ ?y:int ->
1919+ ?base_frame:int ->
2020+ ?edit_frame:int ->
2121+ ?gap_ms:int ->
2222+ ?composition:Kgp_types.composition ->
2323+ ?background_color:int32 ->
2424+ unit ->
2525+ t
2626+(** Create a frame specification.
2727+2828+ @param x Left edge where frame data is placed (pixels)
2929+ @param y Top edge where frame data is placed (pixels)
3030+ @param base_frame 1-based frame number to use as background canvas
3131+ @param edit_frame 1-based frame number to edit (0 = new frame)
3232+ @param gap_ms Delay before next frame in milliseconds
3333+ @param composition How to blend pixels onto the canvas
3434+ @param background_color 32-bit RGBA background when no base frame *)
···11+(** Kitty Graphics Protocol - Placement *)
22+33+type t = {
44+ source_x : int option;
55+ source_y : int option;
66+ source_width : int option;
77+ source_height : int option;
88+ cell_x_offset : int option;
99+ cell_y_offset : int option;
1010+ columns : int option;
1111+ rows : int option;
1212+ z_index : int option;
1313+ placement_id : int option;
1414+ cursor : Kgp_types.cursor option;
1515+ unicode_placeholder : bool;
1616+}
1717+1818+val empty : t
1919+(** Empty placement with all defaults. *)
2020+2121+val make :
2222+ ?source_x:int ->
2323+ ?source_y:int ->
2424+ ?source_width:int ->
2525+ ?source_height:int ->
2626+ ?cell_x_offset:int ->
2727+ ?cell_y_offset:int ->
2828+ ?columns:int ->
2929+ ?rows:int ->
3030+ ?z_index:int ->
3131+ ?placement_id:int ->
3232+ ?cursor:Kgp_types.cursor ->
3333+ ?unicode_placeholder:bool ->
3434+ unit ->
3535+ t
3636+(** Create a placement configuration.
3737+3838+ @param source_x Left edge of source rectangle in pixels (default 0)
3939+ @param source_y Top edge of source rectangle in pixels (default 0)
4040+ @param source_width Width of source rectangle (default: full width)
4141+ @param source_height Height of source rectangle (default: full height)
4242+ @param cell_x_offset X offset within the first cell in pixels
4343+ @param cell_y_offset Y offset within the first cell in pixels
4444+ @param columns Number of columns to display over (scales image)
4545+ @param rows Number of rows to display over (scales image)
4646+ @param z_index Stacking order (negative = under text)
4747+ @param placement_id Unique ID for this placement
4848+ @param cursor Cursor movement policy after display
4949+ @param unicode_placeholder Create virtual placement for Unicode mode *)
+56
stack/kitty_graphics/lib/kgp_response.ml
···11+(* Kitty Graphics Protocol - Response *)
22+33+type t = {
44+ message : string;
55+ image_id : int option;
66+ image_number : int option;
77+ placement_id : int option;
88+}
99+1010+let is_ok t = t.message = "OK"
1111+let message t = t.message
1212+1313+let error_code t =
1414+ if is_ok t then None
1515+ else
1616+ String.index_opt t.message ':'
1717+ |> Option.fold ~none:(Some t.message) ~some:(fun i ->
1818+ Some (String.sub t.message 0 i))
1919+2020+let image_id t = t.image_id
2121+let image_number t = t.image_number
2222+let placement_id t = t.placement_id
2323+2424+let parse s =
2525+ let ( let* ) = Option.bind in
2626+ let esc = '\027' in
2727+ let len = String.length s in
2828+ let* () =
2929+ if len >= 5 && s.[0] = esc && s.[1] = '_' && s.[2] = 'G' then Some ()
3030+ else None
3131+ in
3232+ let* semi_pos = String.index_from_opt s 3 ';' in
3333+ let rec find_end pos =
3434+ if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then Some pos
3535+ else if pos + 1 < len then find_end (pos + 1)
3636+ else None
3737+ in
3838+ let* end_pos = find_end (semi_pos + 1) in
3939+ let keys_str = String.sub s 3 (semi_pos - 3) in
4040+ let message = String.sub s (semi_pos + 1) (end_pos - semi_pos - 1) in
4141+ let parse_kv part =
4242+ if String.length part >= 3 && part.[1] = '=' then
4343+ Some (part.[0], String.sub part 2 (String.length part - 2))
4444+ else None
4545+ in
4646+ let keys = String.split_on_char ',' keys_str |> List.filter_map parse_kv in
4747+ let find_int key =
4848+ List.assoc_opt key keys |> Fun.flip Option.bind int_of_string_opt
4949+ in
5050+ Some
5151+ {
5252+ message;
5353+ image_id = find_int 'i';
5454+ image_number = find_int 'I';
5555+ placement_id = find_int 'p';
5656+ }
+25
stack/kitty_graphics/lib/kgp_response.mli
···11+(** Kitty Graphics Protocol - Response Parsing *)
22+33+type t
44+(** A parsed terminal response. *)
55+66+val parse : string -> t option
77+(** Parse a response from terminal output. *)
88+99+val is_ok : t -> bool
1010+(** Check if the response indicates success. *)
1111+1212+val message : t -> string
1313+(** Get the response message. *)
1414+1515+val error_code : t -> string option
1616+(** Extract the error code if this is an error response. *)
1717+1818+val image_id : t -> int option
1919+(** Get the image ID from the response. *)
2020+2121+val image_number : t -> int option
2222+(** Get the image number from the response. *)
2323+2424+val placement_id : t -> int option
2525+(** Get the placement ID from the response. *)
+89
stack/kitty_graphics/lib/kgp_types.ml
···11+(* Kitty Graphics Protocol - Types *)
22+33+type format = [ `Rgba32 | `Rgb24 | `Png ]
44+type transmission = [ `Direct | `File | `Tempfile ]
55+type compression = [ `None | `Zlib ]
66+type quiet = [ `Noisy | `Errors_only | `Silent ]
77+type cursor = [ `Move | `Static ]
88+type composition = [ `Alpha_blend | `Overwrite ]
99+1010+type delete =
1111+ [ `All_visible
1212+ | `All_visible_and_free
1313+ | `By_id of int * int option
1414+ | `By_id_and_free of int * int option
1515+ | `By_number of int * int option
1616+ | `By_number_and_free of int * int option
1717+ | `At_cursor
1818+ | `At_cursor_and_free
1919+ | `At_cell of int * int
2020+ | `At_cell_and_free of int * int
2121+ | `At_cell_z of int * int * int
2222+ | `At_cell_z_and_free of int * int * int
2323+ | `By_column of int
2424+ | `By_column_and_free of int
2525+ | `By_row of int
2626+ | `By_row_and_free of int
2727+ | `By_z_index of int
2828+ | `By_z_index_and_free of int
2929+ | `By_id_range of int * int
3030+ | `By_id_range_and_free of int * int
3131+ | `Frames
3232+ | `Frames_and_free ]
3333+3434+type animation_state = [ `Stop | `Loading | `Run ]
3535+3636+module Format = struct
3737+ type t = format
3838+3939+ let to_int : t -> int = function
4040+ | `Rgba32 -> 32
4141+ | `Rgb24 -> 24
4242+ | `Png -> 100
4343+end
4444+4545+module Transmission = struct
4646+ type t = transmission
4747+4848+ let to_char : t -> char = function
4949+ | `Direct -> 'd'
5050+ | `File -> 'f'
5151+ | `Tempfile -> 't'
5252+end
5353+5454+module Compression = struct
5555+ type t = compression
5656+5757+ let to_char : t -> char option = function
5858+ | `None -> None
5959+ | `Zlib -> Some 'z'
6060+end
6161+6262+module Quiet = struct
6363+ type t = quiet
6464+6565+ let to_int : t -> int = function
6666+ | `Noisy -> 0
6767+ | `Errors_only -> 1
6868+ | `Silent -> 2
6969+end
7070+7171+module Cursor = struct
7272+ type t = cursor
7373+7474+ let to_int : t -> int = function
7575+ | `Move -> 0
7676+ | `Static -> 1
7777+end
7878+7979+module Composition = struct
8080+ type t = composition
8181+8282+ let to_int : t -> int = function
8383+ | `Alpha_blend -> 0
8484+ | `Overwrite -> 1
8585+end
8686+8787+module Delete = struct
8888+ type t = delete
8989+end
+81
stack/kitty_graphics/lib/kgp_types.mli
···11+(** Kitty Graphics Protocol - Types *)
22+33+type format = [ `Rgba32 | `Rgb24 | `Png ]
44+(** Image data formats. *)
55+66+type transmission = [ `Direct | `File | `Tempfile ]
77+(** Transmission methods. *)
88+99+type compression = [ `None | `Zlib ]
1010+(** Compression options. *)
1111+1212+type quiet = [ `Noisy | `Errors_only | `Silent ]
1313+(** Response suppression. *)
1414+1515+type cursor = [ `Move | `Static ]
1616+(** Cursor movement after displaying. *)
1717+1818+type composition = [ `Alpha_blend | `Overwrite ]
1919+(** Composition modes. *)
2020+2121+type delete =
2222+ [ `All_visible
2323+ | `All_visible_and_free
2424+ | `By_id of int * int option
2525+ | `By_id_and_free of int * int option
2626+ | `By_number of int * int option
2727+ | `By_number_and_free of int * int option
2828+ | `At_cursor
2929+ | `At_cursor_and_free
3030+ | `At_cell of int * int
3131+ | `At_cell_and_free of int * int
3232+ | `At_cell_z of int * int * int
3333+ | `At_cell_z_and_free of int * int * int
3434+ | `By_column of int
3535+ | `By_column_and_free of int
3636+ | `By_row of int
3737+ | `By_row_and_free of int
3838+ | `By_z_index of int
3939+ | `By_z_index_and_free of int
4040+ | `By_id_range of int * int
4141+ | `By_id_range_and_free of int * int
4242+ | `Frames
4343+ | `Frames_and_free ]
4444+(** Delete target specification. *)
4545+4646+type animation_state = [ `Stop | `Loading | `Run ]
4747+(** Animation playback state. *)
4848+4949+module Format : sig
5050+ type t = format
5151+ val to_int : t -> int
5252+end
5353+5454+module Transmission : sig
5555+ type t = transmission
5656+ val to_char : t -> char
5757+end
5858+5959+module Compression : sig
6060+ type t = compression
6161+ val to_char : t -> char option
6262+end
6363+6464+module Quiet : sig
6565+ type t = quiet
6666+ val to_int : t -> int
6767+end
6868+6969+module Cursor : sig
7070+ type t = cursor
7171+ val to_int : t -> int
7272+end
7373+7474+module Composition : sig
7575+ type t = composition
7676+ val to_int : t -> int
7777+end
7878+7979+module Delete : sig
8080+ type t = delete
8181+end
+91
stack/kitty_graphics/lib/kgp_unicode.ml
···11+(* Kitty Graphics Protocol - Unicode Placeholders *)
22+33+let placeholder_char = Uchar.of_int 0x10EEEE
44+55+let diacritics =
66+ [|
77+ 0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F; 0x0346;
88+ 0x034A; 0x034B; 0x034C; 0x0350; 0x0351; 0x0352; 0x0357; 0x035B; 0x0363;
99+ 0x0364; 0x0365; 0x0366; 0x0367; 0x0368; 0x0369; 0x036A; 0x036B; 0x036C;
1010+ 0x036D; 0x036E; 0x036F; 0x0483; 0x0484; 0x0485; 0x0486; 0x0487; 0x0592;
1111+ 0x0593; 0x0594; 0x0595; 0x0597; 0x0598; 0x0599; 0x059C; 0x059D; 0x059E;
1212+ 0x059F; 0x05A0; 0x05A1; 0x05A8; 0x05A9; 0x05AB; 0x05AC; 0x05AF; 0x05C4;
1313+ 0x0610; 0x0611; 0x0612; 0x0613; 0x0614; 0x0615; 0x0616; 0x0617; 0x0657;
1414+ 0x0658; 0x0659; 0x065A; 0x065B; 0x065D; 0x065E; 0x06D6; 0x06D7; 0x06D8;
1515+ 0x06D9; 0x06DA; 0x06DB; 0x06DC; 0x06DF; 0x06E0; 0x06E1; 0x06E2; 0x06E4;
1616+ 0x06E7; 0x06E8; 0x06EB; 0x06EC; 0x0730; 0x0732; 0x0733; 0x0735; 0x0736;
1717+ 0x073A; 0x073D; 0x073F; 0x0740; 0x0741; 0x0743; 0x0745; 0x0747; 0x0749;
1818+ 0x074A; 0x07EB; 0x07EC; 0x07ED; 0x07EE; 0x07EF; 0x07F0; 0x07F1; 0x07F3;
1919+ 0x0816; 0x0817; 0x0818; 0x0819; 0x081B; 0x081C; 0x081D; 0x081E; 0x081F;
2020+ 0x0820; 0x0821; 0x0822; 0x0823; 0x0825; 0x0826; 0x0827; 0x0829; 0x082A;
2121+ 0x082B; 0x082C; 0x082D; 0x0951; 0x0953; 0x0954; 0x0F82; 0x0F83; 0x0F86;
2222+ 0x0F87; 0x135D; 0x135E; 0x135F; 0x17DD; 0x193A; 0x1A17; 0x1A75; 0x1A76;
2323+ 0x1A77; 0x1A78; 0x1A79; 0x1A7A; 0x1A7B; 0x1A7C; 0x1B6B; 0x1B6D; 0x1B6E;
2424+ 0x1B6F; 0x1B70; 0x1B71; 0x1B72; 0x1B73; 0x1CD0; 0x1CD1; 0x1CD2; 0x1CDA;
2525+ 0x1CDB; 0x1CE0; 0x1DC0; 0x1DC1; 0x1DC3; 0x1DC4; 0x1DC5; 0x1DC6; 0x1DC7;
2626+ 0x1DC8; 0x1DC9; 0x1DCB; 0x1DCC; 0x1DD1; 0x1DD2; 0x1DD3; 0x1DD4; 0x1DD5;
2727+ 0x1DD6; 0x1DD7; 0x1DD8; 0x1DD9; 0x1DDA; 0x1DDB; 0x1DDC; 0x1DDD; 0x1DDE;
2828+ 0x1DDF; 0x1DE0; 0x1DE1; 0x1DE2; 0x1DE3; 0x1DE4; 0x1DE5; 0x1DE6; 0x1DFE;
2929+ 0x20D0; 0x20D1; 0x20D4; 0x20D5; 0x20D6; 0x20D7; 0x20DB; 0x20DC; 0x20E1;
3030+ 0x20E7; 0x20E9; 0x20F0; 0xA66F; 0xA67C; 0xA67D; 0xA6F0; 0xA6F1; 0xA8E0;
3131+ 0xA8E1; 0xA8E2; 0xA8E3; 0xA8E4; 0xA8E5; 0xA8E6; 0xA8E7; 0xA8E8; 0xA8E9;
3232+ 0xA8EA; 0xA8EB; 0xA8EC; 0xA8ED; 0xA8EE; 0xA8EF; 0xA8F0; 0xA8F1; 0xAAB0;
3333+ 0xAAB2; 0xAAB3; 0xAAB7; 0xAAB8; 0xAABE; 0xAABF; 0xAAC1; 0xFE20; 0xFE21;
3434+ 0xFE22; 0xFE23; 0xFE24; 0xFE25; 0xFE26; 0x10A0F; 0x10A38; 0x1D185;
3535+ 0x1D186; 0x1D187; 0x1D188; 0x1D189; 0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD;
3636+ 0x1D242; 0x1D243; 0x1D244;
3737+ |]
3838+3939+let diacritic n = Uchar.of_int diacritics.(n mod Array.length diacritics)
4040+let row_diacritic = diacritic
4141+let column_diacritic = diacritic
4242+let id_high_byte_diacritic = diacritic
4343+4444+let add_uchar buf u =
4545+ let code = Uchar.to_int u in
4646+ let put = Buffer.add_char buf in
4747+ if code < 0x80 then put (Char.chr code)
4848+ else if code < 0x800 then (
4949+ put (Char.chr (0xC0 lor (code lsr 6)));
5050+ put (Char.chr (0x80 lor (code land 0x3F))))
5151+ else if code < 0x10000 then (
5252+ put (Char.chr (0xE0 lor (code lsr 12)));
5353+ put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
5454+ put (Char.chr (0x80 lor (code land 0x3F))))
5555+ else (
5656+ put (Char.chr (0xF0 lor (code lsr 18)));
5757+ put (Char.chr (0x80 lor ((code lsr 12) land 0x3F)));
5858+ put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
5959+ put (Char.chr (0x80 lor (code land 0x3F))))
6060+6161+let write buf ~image_id ?placement_id ~rows ~cols () =
6262+ (* Set foreground color *)
6363+ Printf.bprintf buf "\027[38;2;%d;%d;%dm"
6464+ ((image_id lsr 16) land 0xFF)
6565+ ((image_id lsr 8) land 0xFF)
6666+ (image_id land 0xFF);
6767+ (* Optional placement ID in underline color *)
6868+ placement_id
6969+ |> Option.iter (fun pid ->
7070+ Printf.bprintf buf "\027[58;2;%d;%d;%dm"
7171+ ((pid lsr 16) land 0xFF)
7272+ ((pid lsr 8) land 0xFF)
7373+ (pid land 0xFF));
7474+ (* High byte diacritic *)
7575+ let high_byte = (image_id lsr 24) land 0xFF in
7676+ let high_diac =
7777+ if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None
7878+ in
7979+ (* Write grid *)
8080+ for row = 0 to rows - 1 do
8181+ for col = 0 to cols - 1 do
8282+ add_uchar buf placeholder_char;
8383+ add_uchar buf (row_diacritic row);
8484+ add_uchar buf (column_diacritic col);
8585+ high_diac |> Option.iter (add_uchar buf)
8686+ done;
8787+ if row < rows - 1 then Buffer.add_string buf "\n\r"
8888+ done;
8989+ (* Reset colors *)
9090+ Buffer.add_string buf "\027[39m";
9191+ if Option.is_some placement_id then Buffer.add_string buf "\027[59m"
+23
stack/kitty_graphics/lib/kgp_unicode.mli
···11+(** Kitty Graphics Protocol - Unicode Placeholders *)
22+33+val placeholder_char : Uchar.t
44+(** The Unicode placeholder character U+10EEEE. *)
55+66+val write :
77+ Buffer.t ->
88+ image_id:int ->
99+ ?placement_id:int ->
1010+ rows:int ->
1111+ cols:int ->
1212+ unit ->
1313+ unit
1414+(** Write placeholder characters to a buffer. *)
1515+1616+val row_diacritic : int -> Uchar.t
1717+(** Get the combining diacritic for a row number (0-based). *)
1818+1919+val column_diacritic : int -> Uchar.t
2020+(** Get the combining diacritic for a column number (0-based). *)
2121+2222+val id_high_byte_diacritic : int -> Uchar.t
2323+(** Get the diacritic for the high byte of a 32-bit image ID. *)
-684
stack/kitty_graphics/lib/kitty_graphics.ml
···11-(* Kitty Terminal Graphics Protocol - Implementation *)
22-33-(* Polymorphic variant types *)
44-type format = [ `Rgba32 | `Rgb24 | `Png ]
55-type transmission = [ `Direct | `File | `Tempfile ]
66-type compression = [ `None | `Zlib ]
77-type quiet = [ `Noisy | `Errors_only | `Silent ]
88-type cursor = [ `Move | `Static ]
99-type composition = [ `Alpha_blend | `Overwrite ]
1010-1111-type delete =
1212- [ `All_visible
1313- | `All_visible_and_free
1414- | `By_id of int * int option
1515- | `By_id_and_free of int * int option
1616- | `By_number of int * int option
1717- | `By_number_and_free of int * int option
1818- | `At_cursor
1919- | `At_cursor_and_free
2020- | `At_cell of int * int
2121- | `At_cell_and_free of int * int
2222- | `At_cell_z of int * int * int
2323- | `At_cell_z_and_free of int * int * int
2424- | `By_column of int
2525- | `By_column_and_free of int
2626- | `By_row of int
2727- | `By_row_and_free of int
2828- | `By_z_index of int
2929- | `By_z_index_and_free of int
3030- | `By_id_range of int * int
3131- | `By_id_range_and_free of int * int
3232- | `Frames
3333- | `Frames_and_free ]
3434-3535-type animation_state = [ `Stop | `Loading | `Run ]
3636-3737-(* Modules re-export the types with conversion functions *)
3838-module Format = struct
3939- type t = format
4040-4141- let to_int : t -> int = function
4242- | `Rgba32 -> 32
4343- | `Rgb24 -> 24
4444- | `Png -> 100
4545-end
4646-4747-module Transmission = struct
4848- type t = transmission
4949-5050- let to_char : t -> char = function
5151- | `Direct -> 'd'
5252- | `File -> 'f'
5353- | `Tempfile -> 't'
5454-end
5555-5656-module Compression = struct
5757- type t = compression
5858-5959- let to_char : t -> char option = function
6060- | `None -> None
6161- | `Zlib -> Some 'z'
6262-end
6363-6464-module Quiet = struct
6565- type t = quiet
6666-6767- let to_int : t -> int = function
6868- | `Noisy -> 0
6969- | `Errors_only -> 1
7070- | `Silent -> 2
7171-end
7272-7373-module Cursor = struct
7474- type t = cursor
7575-7676- let to_int : t -> int = function
7777- | `Move -> 0
7878- | `Static -> 1
7979-end
8080-8181-module Composition = struct
8282- type t = composition
8383-8484- let to_int : t -> int = function
8585- | `Alpha_blend -> 0
8686- | `Overwrite -> 1
8787-end
8888-8989-module Delete = struct
9090- type t = delete
9191-end
9292-9393-module Placement = struct
9494- type t = {
9595- source_x : int option;
9696- source_y : int option;
9797- source_width : int option;
9898- source_height : int option;
9999- cell_x_offset : int option;
100100- cell_y_offset : int option;
101101- columns : int option;
102102- rows : int option;
103103- z_index : int option;
104104- placement_id : int option;
105105- cursor : cursor option;
106106- unicode_placeholder : bool;
107107- }
108108-109109- let empty =
110110- {
111111- source_x = None;
112112- source_y = None;
113113- source_width = None;
114114- source_height = None;
115115- cell_x_offset = None;
116116- cell_y_offset = None;
117117- columns = None;
118118- rows = None;
119119- z_index = None;
120120- placement_id = None;
121121- cursor = None;
122122- unicode_placeholder = false;
123123- }
124124-125125- let make ?source_x ?source_y ?source_width ?source_height ?cell_x_offset
126126- ?cell_y_offset ?columns ?rows ?z_index ?placement_id ?cursor
127127- ?(unicode_placeholder = false) () =
128128- {
129129- source_x;
130130- source_y;
131131- source_width;
132132- source_height;
133133- cell_x_offset;
134134- cell_y_offset;
135135- columns;
136136- rows;
137137- z_index;
138138- placement_id;
139139- cursor;
140140- unicode_placeholder;
141141- }
142142-end
143143-144144-module Frame = struct
145145- type t = {
146146- x : int option;
147147- y : int option;
148148- base_frame : int option;
149149- edit_frame : int option;
150150- gap_ms : int option;
151151- composition : composition option;
152152- background_color : int32 option;
153153- }
154154-155155- let empty =
156156- {
157157- x = None;
158158- y = None;
159159- base_frame = None;
160160- edit_frame = None;
161161- gap_ms = None;
162162- composition = None;
163163- background_color = None;
164164- }
165165-166166- let make ?x ?y ?base_frame ?edit_frame ?gap_ms ?composition ?background_color
167167- () =
168168- { x; y; base_frame; edit_frame; gap_ms; composition; background_color }
169169-end
170170-171171-module Animation = struct
172172- type state = animation_state
173173-174174- type t =
175175- [ `Set_state of state * int option
176176- | `Set_gap of int * int
177177- | `Set_current of int ]
178178-179179- let set_state ?loops state = `Set_state (state, loops)
180180- let set_gap ~frame ~gap_ms = `Set_gap (frame, gap_ms)
181181- let set_current_frame frame = `Set_current frame
182182-end
183183-184184-module Compose = struct
185185- type t = {
186186- source_frame : int;
187187- dest_frame : int;
188188- width : int option;
189189- height : int option;
190190- source_x : int option;
191191- source_y : int option;
192192- dest_x : int option;
193193- dest_y : int option;
194194- composition : composition option;
195195- }
196196-197197- let make ~source_frame ~dest_frame ?width ?height ?source_x ?source_y ?dest_x
198198- ?dest_y ?composition () =
199199- {
200200- source_frame;
201201- dest_frame;
202202- width;
203203- height;
204204- source_x;
205205- source_y;
206206- dest_x;
207207- dest_y;
208208- composition;
209209- }
210210-end
211211-212212-module Command = struct
213213- type action =
214214- [ `Transmit
215215- | `Transmit_and_display
216216- | `Query
217217- | `Display
218218- | `Delete
219219- | `Frame
220220- | `Animate
221221- | `Compose ]
222222-223223- type t = {
224224- action : action;
225225- format : format option;
226226- transmission : transmission option;
227227- compression : compression option;
228228- width : int option;
229229- height : int option;
230230- size : int option;
231231- offset : int option;
232232- quiet : quiet option;
233233- image_id : int option;
234234- image_number : int option;
235235- placement : Placement.t option;
236236- delete : delete option;
237237- frame : Frame.t option;
238238- animation : Animation.t option;
239239- compose : Compose.t option;
240240- }
241241-242242- let make action =
243243- {
244244- action;
245245- format = None;
246246- transmission = None;
247247- compression = None;
248248- width = None;
249249- height = None;
250250- size = None;
251251- offset = None;
252252- quiet = None;
253253- image_id = None;
254254- image_number = None;
255255- placement = None;
256256- delete = None;
257257- frame = None;
258258- animation = None;
259259- compose = None;
260260- }
261261-262262- let transmit ?image_id ?image_number ?format ?transmission ?compression ?width
263263- ?height ?size ?offset ?quiet () =
264264- {
265265- (make `Transmit) with
266266- image_id;
267267- image_number;
268268- format;
269269- transmission;
270270- compression;
271271- width;
272272- height;
273273- size;
274274- offset;
275275- quiet;
276276- }
277277-278278- let transmit_and_display ?image_id ?image_number ?format ?transmission
279279- ?compression ?width ?height ?size ?offset ?quiet ?placement () =
280280- {
281281- (make `Transmit_and_display) with
282282- image_id;
283283- image_number;
284284- format;
285285- transmission;
286286- compression;
287287- width;
288288- height;
289289- size;
290290- offset;
291291- quiet;
292292- placement;
293293- }
294294-295295- let query ?format ?transmission ?width ?height ?quiet () =
296296- { (make `Query) with format; transmission; width; height; quiet }
297297-298298- let display ?image_id ?image_number ?placement ?quiet () =
299299- { (make `Display) with image_id; image_number; placement; quiet }
300300-301301- let delete ?quiet del = { (make `Delete) with quiet; delete = Some del }
302302-303303- let frame ?image_id ?image_number ?format ?transmission ?compression ?width
304304- ?height ?quiet ~frame () =
305305- {
306306- (make `Frame) with
307307- image_id;
308308- image_number;
309309- format;
310310- transmission;
311311- compression;
312312- width;
313313- height;
314314- quiet;
315315- frame = Some frame;
316316- }
317317-318318- let animate ?image_id ?image_number ?quiet anim =
319319- { (make `Animate) with image_id; image_number; quiet; animation = Some anim }
320320-321321- let compose ?image_id ?image_number ?quiet comp =
322322- { (make `Compose) with image_id; image_number; quiet; compose = Some comp }
323323-324324- (* Serialization helpers *)
325325- let apc_start = "\027_G"
326326- let apc_end = "\027\\"
327327-328328- (* Key-value writer with separator handling *)
329329- type kv_writer = { mutable first : bool; buf : Buffer.t }
330330-331331- let kv_writer buf = { first = true; buf }
332332-333333- let kv w key value =
334334- if not w.first then Buffer.add_char w.buf ',';
335335- w.first <- false;
336336- Buffer.add_char w.buf key;
337337- Buffer.add_char w.buf '=';
338338- Buffer.add_string w.buf value
339339-340340- let kv_int w key value = kv w key (string_of_int value)
341341- let kv_int32 w key value = kv w key (Int32.to_string value)
342342- let kv_char w key value = kv w key (String.make 1 value)
343343-344344- (* Conditional writers using Option.iter *)
345345- let kv_int_opt w key = Option.iter (kv_int w key)
346346- let kv_int32_opt w key = Option.iter (kv_int32 w key)
347347-348348- let kv_int_if w key ~default opt =
349349- Option.iter (fun v -> if v <> default then kv_int w key v) opt
350350-351351- let action_char : action -> char = function
352352- | `Transmit -> 't'
353353- | `Transmit_and_display -> 'T'
354354- | `Query -> 'q'
355355- | `Display -> 'p'
356356- | `Delete -> 'd'
357357- | `Frame -> 'f'
358358- | `Animate -> 'a'
359359- | `Compose -> 'c'
360360-361361- let delete_char : delete -> char = function
362362- | `All_visible -> 'a'
363363- | `All_visible_and_free -> 'A'
364364- | `By_id _ -> 'i'
365365- | `By_id_and_free _ -> 'I'
366366- | `By_number _ -> 'n'
367367- | `By_number_and_free _ -> 'N'
368368- | `At_cursor -> 'c'
369369- | `At_cursor_and_free -> 'C'
370370- | `At_cell _ -> 'p'
371371- | `At_cell_and_free _ -> 'P'
372372- | `At_cell_z _ -> 'q'
373373- | `At_cell_z_and_free _ -> 'Q'
374374- | `By_column _ -> 'x'
375375- | `By_column_and_free _ -> 'X'
376376- | `By_row _ -> 'y'
377377- | `By_row_and_free _ -> 'Y'
378378- | `By_z_index _ -> 'z'
379379- | `By_z_index_and_free _ -> 'Z'
380380- | `By_id_range _ -> 'r'
381381- | `By_id_range_and_free _ -> 'R'
382382- | `Frames -> 'f'
383383- | `Frames_and_free -> 'F'
384384-385385- let write_placement w (p : Placement.t) =
386386- kv_int_opt w 'x' p.source_x;
387387- kv_int_opt w 'y' p.source_y;
388388- kv_int_opt w 'w' p.source_width;
389389- kv_int_opt w 'h' p.source_height;
390390- kv_int_opt w 'X' p.cell_x_offset;
391391- kv_int_opt w 'Y' p.cell_y_offset;
392392- kv_int_opt w 'c' p.columns;
393393- kv_int_opt w 'r' p.rows;
394394- kv_int_opt w 'z' p.z_index;
395395- kv_int_opt w 'p' p.placement_id;
396396- p.cursor |> Option.iter (fun c -> kv_int_if w 'C' ~default:0 (Some (Cursor.to_int c)));
397397- if p.unicode_placeholder then kv_int w 'U' 1
398398-399399- let write_delete w (d : delete) =
400400- kv_char w 'd' (delete_char d);
401401- match d with
402402- | `By_id (id, pid) | `By_id_and_free (id, pid) ->
403403- kv_int w 'i' id;
404404- kv_int_opt w 'p' pid
405405- | `By_number (n, pid) | `By_number_and_free (n, pid) ->
406406- kv_int w 'I' n;
407407- kv_int_opt w 'p' pid
408408- | `At_cell (x, y) | `At_cell_and_free (x, y) ->
409409- kv_int w 'x' x;
410410- kv_int w 'y' y
411411- | `At_cell_z (x, y, z) | `At_cell_z_and_free (x, y, z) ->
412412- kv_int w 'x' x;
413413- kv_int w 'y' y;
414414- kv_int w 'z' z
415415- | `By_column c | `By_column_and_free c -> kv_int w 'x' c
416416- | `By_row r | `By_row_and_free r -> kv_int w 'y' r
417417- | `By_z_index z | `By_z_index_and_free z -> kv_int w 'z' z
418418- | `By_id_range (min_id, max_id) | `By_id_range_and_free (min_id, max_id) ->
419419- kv_int w 'x' min_id;
420420- kv_int w 'y' max_id
421421- | `All_visible | `All_visible_and_free | `At_cursor | `At_cursor_and_free
422422- | `Frames | `Frames_and_free ->
423423- ()
424424-425425- let write_frame w (f : Frame.t) =
426426- kv_int_opt w 'x' f.x;
427427- kv_int_opt w 'y' f.y;
428428- kv_int_opt w 'c' f.base_frame;
429429- kv_int_opt w 'r' f.edit_frame;
430430- kv_int_opt w 'z' f.gap_ms;
431431- f.composition
432432- |> Option.iter (fun c -> kv_int_if w 'X' ~default:0 (Some (Composition.to_int c)));
433433- kv_int32_opt w 'Y' f.background_color
434434-435435- let write_animation w : Animation.t -> unit = function
436436- | `Set_state (state, loops) ->
437437- let s = match state with `Stop -> 1 | `Loading -> 2 | `Run -> 3 in
438438- kv_int w 's' s;
439439- kv_int_opt w 'v' loops
440440- | `Set_gap (frame, gap_ms) ->
441441- kv_int w 'r' frame;
442442- kv_int w 'z' gap_ms
443443- | `Set_current frame -> kv_int w 'c' frame
444444-445445- let write_compose w (c : Compose.t) =
446446- kv_int w 'r' c.source_frame;
447447- kv_int w 'c' c.dest_frame;
448448- kv_int_opt w 'w' c.width;
449449- kv_int_opt w 'h' c.height;
450450- kv_int_opt w 'x' c.dest_x;
451451- kv_int_opt w 'y' c.dest_y;
452452- kv_int_opt w 'X' c.source_x;
453453- kv_int_opt w 'Y' c.source_y;
454454- c.composition
455455- |> Option.iter (fun comp -> kv_int_if w 'C' ~default:0 (Some (Composition.to_int comp)))
456456-457457- let write_control_data buf cmd =
458458- let w = kv_writer buf in
459459- (* Action *)
460460- kv_char w 'a' (action_char cmd.action);
461461- (* Quiet - only if non-default *)
462462- cmd.quiet |> Option.iter (fun q -> kv_int_if w 'q' ~default:0 (Some (Quiet.to_int q)));
463463- (* Format *)
464464- cmd.format |> Option.iter (fun f -> kv_int w 'f' (Format.to_int f));
465465- (* Transmission - only if non-default *)
466466- cmd.transmission
467467- |> Option.iter (fun t ->
468468- let c = Transmission.to_char t in
469469- if c <> 'd' then kv_char w 't' c);
470470- (* Compression *)
471471- cmd.compression |> Option.iter (fun c -> Compression.to_char c |> Option.iter (kv_char w 'o'));
472472- (* Dimensions *)
473473- kv_int_opt w 's' cmd.width;
474474- kv_int_opt w 'v' cmd.height;
475475- (* File size/offset *)
476476- kv_int_opt w 'S' cmd.size;
477477- kv_int_opt w 'O' cmd.offset;
478478- (* Image ID/number *)
479479- kv_int_opt w 'i' cmd.image_id;
480480- kv_int_opt w 'I' cmd.image_number;
481481- (* Complex options *)
482482- cmd.placement |> Option.iter (write_placement w);
483483- cmd.delete |> Option.iter (write_delete w);
484484- cmd.frame |> Option.iter (write_frame w);
485485- cmd.animation |> Option.iter (write_animation w);
486486- cmd.compose |> Option.iter (write_compose w);
487487- w
488488-489489- let chunk_size = 4096
490490-491491- let write buf cmd ~data =
492492- Buffer.add_string buf apc_start;
493493- let w = write_control_data buf cmd in
494494- if String.length data > 0 then begin
495495- let encoded = Base64.encode_string data in
496496- let len = String.length encoded in
497497- if len <= chunk_size then (
498498- Buffer.add_char buf ';';
499499- Buffer.add_string buf encoded;
500500- Buffer.add_string buf apc_end)
501501- else begin
502502- (* Multiple chunks *)
503503- let rec write_chunks pos first =
504504- if pos < len then begin
505505- let remaining = len - pos in
506506- let this_chunk = min chunk_size remaining in
507507- let is_last = pos + this_chunk >= len in
508508- if first then (
509509- kv_int w 'm' 1;
510510- Buffer.add_char buf ';';
511511- Buffer.add_substring buf encoded pos this_chunk;
512512- Buffer.add_string buf apc_end)
513513- else (
514514- Buffer.add_string buf apc_start;
515515- Buffer.add_string buf (if is_last then "m=0" else "m=1");
516516- Buffer.add_char buf ';';
517517- Buffer.add_substring buf encoded pos this_chunk;
518518- Buffer.add_string buf apc_end);
519519- write_chunks (pos + this_chunk) false
520520- end
521521- in
522522- write_chunks 0 true
523523- end
524524- end
525525- else Buffer.add_string buf apc_end
526526-527527- let to_string cmd ~data =
528528- let buf = Buffer.create 1024 in
529529- write buf cmd ~data;
530530- Buffer.contents buf
531531-end
532532-533533-module Response = struct
534534- type t = {
535535- message : string;
536536- image_id : int option;
537537- image_number : int option;
538538- placement_id : int option;
539539- }
540540-541541- let is_ok t = t.message = "OK"
542542- let message t = t.message
543543-544544- let error_code t =
545545- if is_ok t then None
546546- else String.index_opt t.message ':' |> Option.fold ~none:(Some t.message) ~some:(fun i -> Some (String.sub t.message 0 i))
547547-548548- let image_id t = t.image_id
549549- let image_number t = t.image_number
550550- let placement_id t = t.placement_id
551551-552552- let parse s =
553553- let ( let* ) = Option.bind in
554554- let esc = '\027' in
555555- let len = String.length s in
556556- let* () = if len >= 5 && s.[0] = esc && s.[1] = '_' && s.[2] = 'G' then Some () else None in
557557- let* semi_pos = String.index_from_opt s 3 ';' in
558558- let rec find_end pos =
559559- if pos + 1 < len && s.[pos] = esc && s.[pos + 1] = '\\' then Some pos
560560- else if pos + 1 < len then find_end (pos + 1)
561561- else None
562562- in
563563- let* end_pos = find_end (semi_pos + 1) in
564564- let keys_str = String.sub s 3 (semi_pos - 3) in
565565- let message = String.sub s (semi_pos + 1) (end_pos - semi_pos - 1) in
566566- let parse_kv part =
567567- if String.length part >= 3 && part.[1] = '=' then
568568- Some (part.[0], String.sub part 2 (String.length part - 2))
569569- else None
570570- in
571571- let keys = String.split_on_char ',' keys_str |> List.filter_map parse_kv in
572572- let find_int key = List.assoc_opt key keys |> Fun.flip Option.bind int_of_string_opt in
573573- Some
574574- {
575575- message;
576576- image_id = find_int 'i';
577577- image_number = find_int 'I';
578578- placement_id = find_int 'p';
579579- }
580580-end
581581-582582-module Unicode_placeholder = struct
583583- let placeholder_char = Uchar.of_int 0x10EEEE
584584-585585- let diacritics =
586586- [|
587587- 0x0305; 0x030D; 0x030E; 0x0310; 0x0312; 0x033D; 0x033E; 0x033F;
588588- 0x0346; 0x034A; 0x034B; 0x034C; 0x0350; 0x0351; 0x0352; 0x0357;
589589- 0x035B; 0x0363; 0x0364; 0x0365; 0x0366; 0x0367; 0x0368; 0x0369;
590590- 0x036A; 0x036B; 0x036C; 0x036D; 0x036E; 0x036F; 0x0483; 0x0484;
591591- 0x0485; 0x0486; 0x0487; 0x0592; 0x0593; 0x0594; 0x0595; 0x0597;
592592- 0x0598; 0x0599; 0x059C; 0x059D; 0x059E; 0x059F; 0x05A0; 0x05A1;
593593- 0x05A8; 0x05A9; 0x05AB; 0x05AC; 0x05AF; 0x05C4; 0x0610; 0x0611;
594594- 0x0612; 0x0613; 0x0614; 0x0615; 0x0616; 0x0617; 0x0657; 0x0658;
595595- 0x0659; 0x065A; 0x065B; 0x065D; 0x065E; 0x06D6; 0x06D7; 0x06D8;
596596- 0x06D9; 0x06DA; 0x06DB; 0x06DC; 0x06DF; 0x06E0; 0x06E1; 0x06E2;
597597- 0x06E4; 0x06E7; 0x06E8; 0x06EB; 0x06EC; 0x0730; 0x0732; 0x0733;
598598- 0x0735; 0x0736; 0x073A; 0x073D; 0x073F; 0x0740; 0x0741; 0x0743;
599599- 0x0745; 0x0747; 0x0749; 0x074A; 0x07EB; 0x07EC; 0x07ED; 0x07EE;
600600- 0x07EF; 0x07F0; 0x07F1; 0x07F3; 0x0816; 0x0817; 0x0818; 0x0819;
601601- 0x081B; 0x081C; 0x081D; 0x081E; 0x081F; 0x0820; 0x0821; 0x0822;
602602- 0x0823; 0x0825; 0x0826; 0x0827; 0x0829; 0x082A; 0x082B; 0x082C;
603603- 0x082D; 0x0951; 0x0953; 0x0954; 0x0F82; 0x0F83; 0x0F86; 0x0F87;
604604- 0x135D; 0x135E; 0x135F; 0x17DD; 0x193A; 0x1A17; 0x1A75; 0x1A76;
605605- 0x1A77; 0x1A78; 0x1A79; 0x1A7A; 0x1A7B; 0x1A7C; 0x1B6B; 0x1B6D;
606606- 0x1B6E; 0x1B6F; 0x1B70; 0x1B71; 0x1B72; 0x1B73; 0x1CD0; 0x1CD1;
607607- 0x1CD2; 0x1CDA; 0x1CDB; 0x1CE0; 0x1DC0; 0x1DC1; 0x1DC3; 0x1DC4;
608608- 0x1DC5; 0x1DC6; 0x1DC7; 0x1DC8; 0x1DC9; 0x1DCB; 0x1DCC; 0x1DD1;
609609- 0x1DD2; 0x1DD3; 0x1DD4; 0x1DD5; 0x1DD6; 0x1DD7; 0x1DD8; 0x1DD9;
610610- 0x1DDA; 0x1DDB; 0x1DDC; 0x1DDD; 0x1DDE; 0x1DDF; 0x1DE0; 0x1DE1;
611611- 0x1DE2; 0x1DE3; 0x1DE4; 0x1DE5; 0x1DE6; 0x1DFE; 0x20D0; 0x20D1;
612612- 0x20D4; 0x20D5; 0x20D6; 0x20D7; 0x20DB; 0x20DC; 0x20E1; 0x20E7;
613613- 0x20E9; 0x20F0; 0xA66F; 0xA67C; 0xA67D; 0xA6F0; 0xA6F1; 0xA8E0;
614614- 0xA8E1; 0xA8E2; 0xA8E3; 0xA8E4; 0xA8E5; 0xA8E6; 0xA8E7; 0xA8E8;
615615- 0xA8E9; 0xA8EA; 0xA8EB; 0xA8EC; 0xA8ED; 0xA8EE; 0xA8EF; 0xA8F0;
616616- 0xA8F1; 0xAAB0; 0xAAB2; 0xAAB3; 0xAAB7; 0xAAB8; 0xAABE; 0xAABF;
617617- 0xAAC1; 0xFE20; 0xFE21; 0xFE22; 0xFE23; 0xFE24; 0xFE25; 0xFE26;
618618- 0x10A0F; 0x10A38; 0x1D185; 0x1D186; 0x1D187; 0x1D188; 0x1D189;
619619- 0x1D1AA; 0x1D1AB; 0x1D1AC; 0x1D1AD; 0x1D242; 0x1D243; 0x1D244;
620620- |]
621621-622622- let diacritic n =
623623- Uchar.of_int diacritics.(n mod Array.length diacritics)
624624-625625- let row_diacritic = diacritic
626626- let column_diacritic = diacritic
627627- let id_high_byte_diacritic = diacritic
628628-629629- let add_uchar buf u =
630630- let code = Uchar.to_int u in
631631- let put = Buffer.add_char buf in
632632- if code < 0x80 then put (Char.chr code)
633633- else if code < 0x800 then (
634634- put (Char.chr (0xC0 lor (code lsr 6)));
635635- put (Char.chr (0x80 lor (code land 0x3F))))
636636- else if code < 0x10000 then (
637637- put (Char.chr (0xE0 lor (code lsr 12)));
638638- put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
639639- put (Char.chr (0x80 lor (code land 0x3F))))
640640- else (
641641- put (Char.chr (0xF0 lor (code lsr 18)));
642642- put (Char.chr (0x80 lor ((code lsr 12) land 0x3F)));
643643- put (Char.chr (0x80 lor ((code lsr 6) land 0x3F)));
644644- put (Char.chr (0x80 lor (code land 0x3F))))
645645-646646- let write buf ~image_id ?placement_id ~rows ~cols () =
647647- (* Set foreground color *)
648648- Printf.bprintf buf "\027[38;2;%d;%d;%dm"
649649- ((image_id lsr 16) land 0xFF)
650650- ((image_id lsr 8) land 0xFF)
651651- (image_id land 0xFF);
652652- (* Optional placement ID in underline color *)
653653- placement_id
654654- |> Option.iter (fun pid ->
655655- Printf.bprintf buf "\027[58;2;%d;%d;%dm"
656656- ((pid lsr 16) land 0xFF)
657657- ((pid lsr 8) land 0xFF)
658658- (pid land 0xFF));
659659- (* High byte diacritic *)
660660- let high_byte = (image_id lsr 24) land 0xFF in
661661- let high_diac = if high_byte > 0 then Some (id_high_byte_diacritic high_byte) else None in
662662- (* Write grid *)
663663- for row = 0 to rows - 1 do
664664- for col = 0 to cols - 1 do
665665- add_uchar buf placeholder_char;
666666- add_uchar buf (row_diacritic row);
667667- add_uchar buf (column_diacritic col);
668668- high_diac |> Option.iter (add_uchar buf)
669669- done;
670670- if row < rows - 1 then Buffer.add_string buf "\n\r"
671671- done;
672672- (* Reset colors *)
673673- Buffer.add_string buf "\027[39m";
674674- if Option.is_some placement_id then Buffer.add_string buf "\027[59m"
675675-end
676676-677677-module Detect = struct
678678- let make_query () =
679679- let cmd = Command.query ~format:`Rgb24 ~transmission:`Direct ~width:1 ~height:1 () in
680680- Command.to_string cmd ~data:"\x00\x00\x00" ^ "\027[c"
681681-682682- let supports_graphics response ~da1_received =
683683- response |> Option.map Response.is_ok |> Option.value ~default:(not da1_received)
684684-end
···1414 {[
1515 (* Display a PNG image *)
1616 let png_data = read_file "image.png" in
1717- let cmd = Kitty_graphics.Command.transmit_and_display ~format:`Png () in
1717+ let cmd = Kgp.Command.transmit_and_display ~format:`Png () in
1818 let buf = Buffer.create 1024 in
1919- Kitty_graphics.Command.write buf cmd ~data:png_data;
1919+ Kgp.Command.write buf cmd ~data:png_data;
2020 print_string (Buffer.contents buf)
2121 ]}
2222···133133(** {1 Placement Options} *)
134134135135module Placement : sig
136136- type t
136136+ type t = Kgp_placement.t
137137 (** Placement configuration. *)
138138139139 val make :
···173173(** {1 Animation} *)
174174175175module Frame : sig
176176- type t
176176+ type t = Kgp_frame.t
177177 (** Animation frame configuration. *)
178178179179 val make :
···203203module Animation : sig
204204 type state = animation_state
205205206206- type t =
207207- [ `Set_state of state * int option
208208- | `Set_gap of int * int
209209- | `Set_current of int ]
206206+ type t = Kgp_animation.t
210207 (** Animation control operations. *)
211208212209 val set_state : ?loops:int -> state -> t
···223220end
224221225222module Compose : sig
226226- type t
223223+ type t = Kgp_compose.t
227224 (** Composition operation. *)
228225229226 val make :
···244241(** {1 Commands} *)
245242246243module Command : sig
247247- type t
244244+ type t = Kgp_command.t
248245 (** A graphics protocol command. *)
249246250247 (** {2 Image Transmission} *)
···340337(** {1 Response Parsing} *)
341338342339module Response : sig
343343- type t
340340+ type t = Kgp_response.t
344341 (** A parsed terminal response. *)
345342346343 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
···6464 bluesky : string option;
6565 mastodon : string option;
6666 orcid : string option;
6767- url : string option;
6767+ url_ : string option;
6868+ urls_ : string list option;
6869 feeds : Feed.t list option;
6970 }
70717172 let make ~handle ~names ?email ?icon ?thumbnail ?github ?twitter ?bluesky ?mastodon
7272- ?orcid ?url ?feeds () =
7373+ ?orcid ?url ?urls ?feeds () =
7374 { handle; names; email; icon; thumbnail; github; twitter; bluesky; mastodon;
7474- orcid; url; feeds }
7575+ orcid; url_ = url; urls_ = urls; feeds }
75767677 let handle t = t.handle
7778 let names t = t.names
···8586 let bluesky t = t.bluesky
8687 let mastodon t = t.mastodon
8788 let orcid t = t.orcid
8888- let url t = t.url
8989+9090+ let url t =
9191+ match t.url_ with
9292+ | Some _ as u -> u
9393+ | None ->
9494+ match t.urls_ with
9595+ | Some (first :: _) -> Some first
9696+ | _ -> None
9797+9898+ let urls t =
9999+ match t.url_, t.urls_ with
100100+ | Some u, Some us -> u :: us
101101+ | Some u, None -> [u]
102102+ | None, Some us -> us
103103+ | None, None -> []
104104+89105 let feeds t = t.feeds
9010691107 let add_feed t feed =
···103119 { t with feeds }
104120105121 let best_url t =
106106- match t.url with
122122+ match url t with
107123 | Some v -> Some v
108124 | None ->
109125 (match t.github with
···117133 let open Jsont in
118134 let open Jsont.Object in
119135 let mem_opt f v ~enc = mem f v ~dec_absent:None ~enc_omit:Option.is_none ~enc in
120120- let make handle names email icon thumbnail github twitter bluesky mastodon orcid url feeds =
136136+ let make handle names email icon thumbnail github twitter bluesky mastodon orcid url urls feeds =
121137 { handle; names; email; icon; thumbnail; github; twitter; bluesky; mastodon;
122122- orcid; url; feeds }
138138+ orcid; url_ = url; urls_ = urls; feeds }
123139 in
124140 map ~kind:"Contact" make
125141 |> mem "handle" string ~enc:handle
···132148 |> mem_opt "bluesky" (some string) ~enc:bluesky
133149 |> mem_opt "mastodon" (some string) ~enc:mastodon
134150 |> mem_opt "orcid" (some string) ~enc:orcid
135135- |> mem_opt "url" (some string) ~enc:url
151151+ |> mem_opt "url" (some string) ~enc:(fun t -> t.url_)
152152+ |> mem_opt "urls" (some (list string)) ~enc:(fun t -> t.urls_)
136153 |> mem_opt "feeds" (some (list Feed.json_t)) ~enc:feeds
137154 |> finish
138155···168185 | Some o -> pf ppf "%a: https://orcid.org/%a@,"
169186 (styled `Bold string) "ORCID" string o
170187 | None -> ());
171171- (match t.url with
172172- | Some u -> pf ppf "%a: %a@," (styled `Bold string) "URL" string u
173173- | None -> ());
188188+ (let all_urls = urls t in
189189+ match all_urls with
190190+ | [] -> ()
191191+ | [u] -> pf ppf "%a: %a@," (styled `Bold string) "URL" string u
192192+ | _ ->
193193+ pf ppf "%a:@," (styled `Bold string) "URLs";
194194+ List.iter (fun u -> pf ppf " - %s@," u) all_urls);
174195 (match t.icon with
175196 | Some i -> pf ppf "%a: %a@," (styled `Bold string) "Icon" string i
176197 | None -> ());
+14-2
stack/sortal/lib/sortal.mli
···9999 @param bluesky Bluesky handle
100100 @param mastodon Mastodon handle (including instance)
101101 @param orcid ORCID identifier
102102- @param url Personal or professional website URL
102102+ @param url Personal or professional website URL (primary URL)
103103+ @param urls Additional website URLs
103104 @param feeds List of feed subscriptions (Atom/RSS/JSON) associated with this contact
104105 *)
105106 val make :
···114115 ?mastodon:string ->
115116 ?orcid:string ->
116117 ?url:string ->
118118+ ?urls:string list ->
117119 ?feeds:Feed.t list ->
118120 unit ->
119121 t
···158160 (** [orcid t] returns the ORCID identifier if available. *)
159161 val orcid : t -> string option
160162161161- (** [url t] returns the personal/professional website URL if available. *)
163163+ (** [url t] returns the primary URL if available.
164164+165165+ Returns the [url] field if set, otherwise returns the first element
166166+ of [urls] if available, or [None] if neither is set. *)
162167 val url : t -> string option
168168+169169+ (** [urls t] returns all URLs associated with this contact.
170170+171171+ Combines the [url] field (if set) with the [urls] list (if set).
172172+ The primary [url] appears first if present. Returns an empty list
173173+ if neither [url] nor [urls] is set. *)
174174+ val urls : t -> string list
163175164176 (** [feeds t] returns the list of feed subscriptions if available. *)
165177 val feeds : t -> Feed.t list option
+40
stack/sortal/test/test_sortal.ml
···154154 assert (Sortal.Contact.compare c1 c3 = 0);
155155 traceln "✓ Contact comparison works"
156156157157+let test_urls () =
158158+ (* Test with only url set *)
159159+ let c1 = Sortal.Contact.make
160160+ ~handle:"test1"
161161+ ~names:["Test 1"]
162162+ ~url:"https://example.com"
163163+ () in
164164+ assert (Sortal.Contact.url c1 = Some "https://example.com");
165165+ assert (Sortal.Contact.urls c1 = ["https://example.com"]);
166166+167167+ (* Test with only urls set *)
168168+ let c2 = Sortal.Contact.make
169169+ ~handle:"test2"
170170+ ~names:["Test 2"]
171171+ ~urls:["https://one.com"; "https://two.com"]
172172+ () in
173173+ assert (Sortal.Contact.url c2 = Some "https://one.com");
174174+ assert (Sortal.Contact.urls c2 = ["https://one.com"; "https://two.com"]);
175175+176176+ (* Test with both url and urls set *)
177177+ let c3 = Sortal.Contact.make
178178+ ~handle:"test3"
179179+ ~names:["Test 3"]
180180+ ~url:"https://primary.com"
181181+ ~urls:["https://secondary.com"; "https://tertiary.com"]
182182+ () in
183183+ assert (Sortal.Contact.url c3 = Some "https://primary.com");
184184+ assert (Sortal.Contact.urls c3 = ["https://primary.com"; "https://secondary.com"; "https://tertiary.com"]);
185185+186186+ (* Test with neither set *)
187187+ let c4 = Sortal.Contact.make
188188+ ~handle:"test4"
189189+ ~names:["Test 4"]
190190+ () in
191191+ assert (Sortal.Contact.url c4 = None);
192192+ assert (Sortal.Contact.urls c4 = []);
193193+194194+ traceln "✓ URLs field works correctly"
195195+157196let () =
158197 traceln "\n=== Running Sortal Tests ===\n";
159198···162201 test_json_encoding ();
163202 test_handle_generation ();
164203 test_contact_compare ();
204204+ test_urls ();
165205 test_store_operations ();
166206167207 traceln "\n=== All Tests Passed ===\n"
···11-This is an XDG library for Eio
22-33-The library follows OCaml best practices with abstract types (`type t`) per
44-module, comprehensive constructors/accessors, and proper pretty printers. Each
55-core concept gets its own module with a clean interface.
-30
stack/xdge/dune-project
···11-(lang dune 3.20)
22-33-(name xdge)
44-55-(generate_opam_files true)
66-77-(license ISC)
88-(authors "Anil Madhavapeddy")
99-(homepage "https://tangled.sh/@anil.recoil.org/ocaml-gpx")
1010-(maintainers "Anil Madhavapeddy <anil@recoil.org>")
1111-(bug_reports https://tangled.sh/@anil.recoil.org/xgde)
1212-(maintenance_intent "(latest)")
1313-1414-(package
1515- (name xdge)
1616- (synopsis "XDG Base Directory Specification support for Eio")
1717- (description
1818- "This library implements the XDG Base Directory Specification \
1919- with Eio capabilities to provides safe access to configuration, \
2020- data, cache, state, and runtime directories with proper environment \
2121- variable overrides and Cmdliner integration.")
2222- (depends
2323- (ocaml (>= 5.1.0))
2424- (eio (>= 1.1))
2525- eio_main
2626- (xdg (>= 3.9.0))
2727- (cmdliner (>= 1.2.0))
2828- (fmt (>= 0.11.0))
2929- (odoc :with-doc)
3030- (alcotest (and :with-test (>= 1.7.0)))))
···11-type source =
22- | Default
33- | Env of string
44- | Cmdline
55-66-type t =
77- { app_name : string
88- ; config_dir : Eio.Fs.dir_ty Eio.Path.t
99- ; config_dir_source : source
1010- ; data_dir : Eio.Fs.dir_ty Eio.Path.t
1111- ; data_dir_source : source
1212- ; cache_dir : Eio.Fs.dir_ty Eio.Path.t
1313- ; cache_dir_source : source
1414- ; state_dir : Eio.Fs.dir_ty Eio.Path.t
1515- ; state_dir_source : source
1616- ; runtime_dir : Eio.Fs.dir_ty Eio.Path.t option
1717- ; runtime_dir_source : source
1818- ; config_dirs : Eio.Fs.dir_ty Eio.Path.t list
1919- ; data_dirs : Eio.Fs.dir_ty Eio.Path.t list
2020- }
2121-2222-type dir = [
2323- | `Config
2424- | `Cache
2525- | `Data
2626- | `State
2727- | `Runtime
2828-]
2929-3030-let ensure_dir ?(perm = 0o755) path = Eio.Path.mkdirs ~exists_ok:true ~perm path
3131-3232-let validate_runtime_base_dir base_path =
3333- (* Validate the base XDG_RUNTIME_DIR has correct permissions per spec *)
3434- try
3535- let path_str = Eio.Path.native_exn base_path in
3636- let stat = Eio.Path.stat ~follow:true base_path in
3737- let current_perm = stat.perm land 0o777 in
3838- if current_perm <> 0o700
3939- then
4040- failwith
4141- (Printf.sprintf
4242- "XDG_RUNTIME_DIR base directory %s has incorrect permissions: %o (must be \
4343- 0700)"
4444- path_str
4545- current_perm);
4646- (* Check ownership - directory should be owned by current user *)
4747- let uid = Unix.getuid () in
4848- if stat.uid <> Int64.of_int uid
4949- then
5050- failwith
5151- (Printf.sprintf
5252- "XDG_RUNTIME_DIR base directory %s not owned by current user (uid %d, owner \
5353- %Ld)"
5454- path_str
5555- uid
5656- stat.uid)
5757- (* TODO: Check that directory is on local filesystem (not networked).
5858- This would require filesystem type detection which is OS-specific. *)
5959- with
6060- | exn ->
6161- failwith
6262- (Printf.sprintf "Cannot validate XDG_RUNTIME_DIR: %s" (Printexc.to_string exn))
6363-;;
6464-6565-let ensure_runtime_dir _fs app_runtime_path =
6666- (* Base directory validation is done in resolve_runtime_dir,
6767- so we just create the app subdirectory *)
6868- ensure_dir app_runtime_path
6969-;;
7070-7171-let get_home_dir fs =
7272- let home_str =
7373- match Sys.getenv_opt "HOME" with
7474- | Some home -> home
7575- | None ->
7676- (match Sys.os_type with
7777- | "Win32" | "Cygwin" ->
7878- (match Sys.getenv_opt "USERPROFILE" with
7979- | Some profile -> profile
8080- | None -> failwith "Cannot determine home directory")
8181- | _ ->
8282- (try Unix.((getpwuid (getuid ())).pw_dir) with
8383- | _ -> failwith "Cannot determine home directory"))
8484- in
8585- Eio.Path.(fs / home_str)
8686-;;
8787-8888-let make_env_var_name app_name suffix = String.uppercase_ascii app_name ^ "_" ^ suffix
8989-9090-exception Invalid_xdg_path of string
9191-9292-let validate_absolute_path context path =
9393- if Filename.is_relative path
9494- then
9595- raise
9696- (Invalid_xdg_path
9797- (Printf.sprintf "%s must be an absolute path, got: %s" context path))
9898-;;
9999-100100-let resolve_path fs home_path base_path =
101101- if Filename.is_relative base_path
102102- then Eio.Path.(home_path / base_path)
103103- else Eio.Path.(fs / base_path)
104104-;;
105105-106106-(* Helper to resolve system directories (config_dirs or data_dirs) *)
107107-let resolve_system_dirs fs home_path app_name override_suffix xdg_var default_paths =
108108- let override_var = make_env_var_name app_name override_suffix in
109109- match Sys.getenv_opt override_var with
110110- | Some dirs when dirs <> "" ->
111111- String.split_on_char ':' dirs
112112- |> List.filter (fun s -> s <> "")
113113- |> List.filter_map (fun path ->
114114- try
115115- validate_absolute_path override_var path;
116116- Some Eio.Path.(resolve_path fs home_path path / app_name)
117117- with
118118- | Invalid_xdg_path _ -> None)
119119- | Some _ | None ->
120120- (match Sys.getenv_opt xdg_var with
121121- | Some dirs when dirs <> "" ->
122122- String.split_on_char ':' dirs
123123- |> List.filter (fun s -> s <> "")
124124- |> List.filter_map (fun path ->
125125- try
126126- validate_absolute_path xdg_var path;
127127- Some Eio.Path.(resolve_path fs home_path path / app_name)
128128- with
129129- | Invalid_xdg_path _ -> None)
130130- | Some _ | None ->
131131- List.map
132132- (fun path -> Eio.Path.(resolve_path fs home_path path / app_name))
133133- default_paths)
134134-;;
135135-136136-(* Helper to resolve a user directory with override precedence *)
137137-let resolve_user_dir fs home_path app_name xdg_ctx xdg_getter override_suffix =
138138- let override_var = make_env_var_name app_name override_suffix in
139139- match Sys.getenv_opt override_var with
140140- | Some dir when dir <> "" ->
141141- validate_absolute_path override_var dir;
142142- Eio.Path.(fs / dir / app_name), Env override_var
143143- | Some _ | None ->
144144- let xdg_base = xdg_getter xdg_ctx in
145145- let base_path = resolve_path fs home_path xdg_base in
146146- Eio.Path.(base_path / app_name), Default
147147-;;
148148-149149-(* Helper to resolve runtime directory (special case since it can be None) *)
150150-let resolve_runtime_dir fs home_path app_name xdg_ctx =
151151- let override_var = make_env_var_name app_name "RUNTIME_DIR" in
152152- match Sys.getenv_opt override_var with
153153- | Some dir when dir <> "" ->
154154- validate_absolute_path override_var dir;
155155- (* Validate the base runtime directory has correct permissions *)
156156- let base_runtime_dir = resolve_path fs home_path dir in
157157- validate_runtime_base_dir base_runtime_dir;
158158- Some Eio.Path.(base_runtime_dir / app_name), Env override_var
159159- | Some _ | None ->
160160- ( (match Xdg.runtime_dir xdg_ctx with
161161- | Some base ->
162162- (* Validate the base runtime directory has correct permissions *)
163163- let base_runtime_dir = resolve_path fs home_path base in
164164- validate_runtime_base_dir base_runtime_dir;
165165- Some Eio.Path.(base_runtime_dir / app_name)
166166- | None -> None)
167167- , Default )
168168-;;
169169-170170-let validate_standard_xdg_vars () =
171171- (* Validate standard XDG environment variables for absolute paths *)
172172- let xdg_vars =
173173- [ "XDG_CONFIG_HOME"
174174- ; "XDG_DATA_HOME"
175175- ; "XDG_CACHE_HOME"
176176- ; "XDG_STATE_HOME"
177177- ; "XDG_RUNTIME_DIR"
178178- ; "XDG_CONFIG_DIRS"
179179- ; "XDG_DATA_DIRS"
180180- ]
181181- in
182182- List.iter
183183- (fun var ->
184184- match Sys.getenv_opt var with
185185- | Some value when value <> "" ->
186186- if String.contains value ':'
187187- then
188188- (* Colon-separated list - validate each part *)
189189- String.split_on_char ':' value
190190- |> List.filter (fun s -> s <> "")
191191- |> List.iter (fun path -> validate_absolute_path var path)
192192- else
193193- (* Single path *)
194194- validate_absolute_path var value
195195- | _ -> ())
196196- xdg_vars
197197-;;
198198-199199-let create fs app_name =
200200- let fs = fs in
201201- let home_path = get_home_dir fs in
202202- (* First validate all standard XDG environment variables *)
203203- validate_standard_xdg_vars ();
204204- let xdg_ctx = Xdg.create ~env:Sys.getenv_opt () in
205205- (* User directories *)
206206- let config_dir, config_dir_source =
207207- resolve_user_dir fs home_path app_name xdg_ctx Xdg.config_dir "CONFIG_DIR"
208208- in
209209- let data_dir, data_dir_source =
210210- resolve_user_dir fs home_path app_name xdg_ctx Xdg.data_dir "DATA_DIR"
211211- in
212212- let cache_dir, cache_dir_source =
213213- resolve_user_dir fs home_path app_name xdg_ctx Xdg.cache_dir "CACHE_DIR"
214214- in
215215- let state_dir, state_dir_source =
216216- resolve_user_dir fs home_path app_name xdg_ctx Xdg.state_dir "STATE_DIR"
217217- in
218218- (* Runtime directory *)
219219- let runtime_dir, runtime_dir_source =
220220- resolve_runtime_dir fs home_path app_name xdg_ctx
221221- in
222222- (* System directories *)
223223- let config_dirs =
224224- resolve_system_dirs
225225- fs
226226- home_path
227227- app_name
228228- "CONFIG_DIRS"
229229- "XDG_CONFIG_DIRS"
230230- [ "/etc/xdg" ]
231231- in
232232- let data_dirs =
233233- resolve_system_dirs
234234- fs
235235- home_path
236236- app_name
237237- "DATA_DIRS"
238238- "XDG_DATA_DIRS"
239239- [ "/usr/local/share"; "/usr/share" ]
240240- in
241241- ensure_dir config_dir;
242242- ensure_dir data_dir;
243243- ensure_dir cache_dir;
244244- ensure_dir state_dir;
245245- Option.iter (ensure_runtime_dir fs) runtime_dir;
246246- { app_name
247247- ; config_dir
248248- ; config_dir_source
249249- ; data_dir
250250- ; data_dir_source
251251- ; cache_dir
252252- ; cache_dir_source
253253- ; state_dir
254254- ; state_dir_source
255255- ; runtime_dir
256256- ; runtime_dir_source
257257- ; config_dirs
258258- ; data_dirs
259259- }
260260-;;
261261-262262-let app_name t = t.app_name
263263-let config_dir t = t.config_dir
264264-let data_dir t = t.data_dir
265265-let cache_dir t = t.cache_dir
266266-let state_dir t = t.state_dir
267267-let runtime_dir t = t.runtime_dir
268268-let config_dirs t = t.config_dirs
269269-let data_dirs t = t.data_dirs
270270-271271-(* File search following XDG specification *)
272272-let find_file_in_dirs dirs filename =
273273- let rec search_dirs = function
274274- | [] -> None
275275- | dir :: remaining_dirs ->
276276- let file_path = Eio.Path.(dir / filename) in
277277- (try
278278- (* Try to check if file exists and is readable *)
279279- let _ = Eio.Path.stat ~follow:true file_path in
280280- Some file_path
281281- with
282282- | _ ->
283283- (* File is inaccessible (non-existent, permissions, etc.)
284284- Skip and continue with next directory per XDG spec *)
285285- search_dirs remaining_dirs)
286286- in
287287- search_dirs dirs
288288-;;
289289-290290-let find_config_file t filename =
291291- (* Search user config dir first, then system config dirs *)
292292- find_file_in_dirs (t.config_dir :: t.config_dirs) filename
293293-;;
294294-295295-let find_data_file t filename =
296296- (* Search user data dir first, then system data dirs *)
297297- find_file_in_dirs (t.data_dir :: t.data_dirs) filename
298298-;;
299299-300300-let pp ?(brief = false) ?(sources = false) ppf t =
301301- let pp_source ppf = function
302302- | Default -> Fmt.(styled `Faint string) ppf "default"
303303- | Env var -> Fmt.pf ppf "%a" Fmt.(styled `Yellow string) ("env(" ^ var ^ ")")
304304- | Cmdline -> Fmt.(styled `Blue string) ppf "cmdline"
305305- in
306306- let pp_path_with_source ppf path source =
307307- if sources
308308- then
309309- Fmt.pf
310310- ppf
311311- "%a %a"
312312- Fmt.(styled `Green Eio.Path.pp)
313313- path
314314- Fmt.(styled `Faint (brackets pp_source))
315315- source
316316- else Fmt.(styled `Green Eio.Path.pp) ppf path
317317- in
318318- let pp_path_opt_with_source ppf path_opt source =
319319- match path_opt with
320320- | None ->
321321- if sources
322322- then
323323- Fmt.pf
324324- ppf
325325- "%a %a"
326326- Fmt.(styled `Red string)
327327- "<none>"
328328- Fmt.(styled `Faint (brackets pp_source))
329329- source
330330- else Fmt.(styled `Red string) ppf "<none>"
331331- | Some path -> pp_path_with_source ppf path source
332332- in
333333- let pp_paths ppf paths =
334334- Fmt.(list ~sep:(any ";@ ") (styled `Green Eio.Path.pp)) ppf paths
335335- in
336336- if brief
337337- then
338338- Fmt.pf
339339- ppf
340340- "%a config=%a data=%a>"
341341- Fmt.(styled `Cyan string)
342342- ("<xdg:" ^ t.app_name)
343343- (fun ppf (path, source) -> pp_path_with_source ppf path source)
344344- (t.config_dir, t.config_dir_source)
345345- (fun ppf (path, source) -> pp_path_with_source ppf path source)
346346- (t.data_dir, t.data_dir_source)
347347- else (
348348- Fmt.pf
349349- ppf
350350- "@[<v>%a@,"
351351- Fmt.(styled `Bold string)
352352- ("XDG directories for '" ^ t.app_name ^ "':");
353353- Fmt.pf ppf "@[<v 2>%a@," Fmt.(styled `Bold string) "User directories:";
354354- Fmt.pf
355355- ppf
356356- "%a %a@,"
357357- Fmt.(styled `Cyan string)
358358- "config:"
359359- (fun ppf (path, source) -> pp_path_with_source ppf path source)
360360- (t.config_dir, t.config_dir_source);
361361- Fmt.pf
362362- ppf
363363- "%a %a@,"
364364- Fmt.(styled `Cyan string)
365365- "data:"
366366- (fun ppf (path, source) -> pp_path_with_source ppf path source)
367367- (t.data_dir, t.data_dir_source);
368368- Fmt.pf
369369- ppf
370370- "%a %a@,"
371371- Fmt.(styled `Cyan string)
372372- "cache:"
373373- (fun ppf (path, source) -> pp_path_with_source ppf path source)
374374- (t.cache_dir, t.cache_dir_source);
375375- Fmt.pf
376376- ppf
377377- "%a %a@,"
378378- Fmt.(styled `Cyan string)
379379- "state:"
380380- (fun ppf (path, source) -> pp_path_with_source ppf path source)
381381- (t.state_dir, t.state_dir_source);
382382- Fmt.pf
383383- ppf
384384- "%a %a@]@,"
385385- Fmt.(styled `Cyan string)
386386- "runtime:"
387387- (fun ppf (path_opt, source) -> pp_path_opt_with_source ppf path_opt source)
388388- (t.runtime_dir, t.runtime_dir_source);
389389- Fmt.pf ppf "@[<v 2>%a@," Fmt.(styled `Bold string) "System directories:";
390390- Fmt.pf
391391- ppf
392392- "%a [@[<hov>%a@]]@,"
393393- Fmt.(styled `Cyan string)
394394- "config_dirs:"
395395- pp_paths
396396- t.config_dirs;
397397- Fmt.pf
398398- ppf
399399- "%a [@[<hov>%a@]]@]@]"
400400- Fmt.(styled `Cyan string)
401401- "data_dirs:"
402402- pp_paths
403403- t.data_dirs)
404404-;;
405405-406406-module Cmd = struct
407407- type xdg_t = t
408408-409409- type 'a with_source =
410410- { value : 'a option
411411- ; source : source
412412- }
413413-414414- type t =
415415- { config_dir : string with_source
416416- ; data_dir : string with_source
417417- ; cache_dir : string with_source
418418- ; state_dir : string with_source
419419- ; runtime_dir : string with_source
420420- }
421421-422422- let term app_name fs
423423- ?(dirs=[`Config; `Data; `Cache; `State; `Runtime]) () =
424424- let open Cmdliner in
425425- let app_upper = String.uppercase_ascii app_name in
426426- let show_paths =
427427- let doc = "Show only the resolved directory paths without formatting" in
428428- Arg.(value & flag & info [ "show-paths" ] ~doc)
429429- in
430430- let has_dir d = List.mem d dirs in
431431- let make_dir_arg ~enabled name env_suffix xdg_var default_path =
432432- if not enabled then
433433- (* Return a term that always gives the environment-only result *)
434434- Term.(const (fun () ->
435435- let app_env = app_upper ^ "_" ^ env_suffix in
436436- match Sys.getenv_opt app_env with
437437- | Some v when v <> "" -> { value = Some v; source = Env app_env }
438438- | Some _ | None ->
439439- (match Sys.getenv_opt xdg_var with
440440- | Some v -> { value = Some v; source = Env xdg_var }
441441- | None -> { value = None; source = Default }))
442442- $ const ())
443443- else
444444- let app_env = app_upper ^ "_" ^ env_suffix in
445445- let doc =
446446- match default_path with
447447- | Some path ->
448448- Printf.sprintf
449449- "Override %s directory. Can also be set with %s or %s. Default: %s"
450450- name
451451- app_env
452452- xdg_var
453453- path
454454- | None ->
455455- Printf.sprintf
456456- "Override %s directory. Can also be set with %s or %s. No default value."
457457- name
458458- app_env
459459- xdg_var
460460- in
461461- let arg =
462462- Arg.(value & opt (some string) None & info [ name ^ "-dir" ] ~docv:"DIR" ~doc)
463463- in
464464- Term.(
465465- const (fun cmdline_val ->
466466- match cmdline_val with
467467- | Some v -> { value = Some v; source = Cmdline }
468468- | None ->
469469- (match Sys.getenv_opt app_env with
470470- | Some v when v <> "" -> { value = Some v; source = Env app_env }
471471- | Some _ | None ->
472472- (match Sys.getenv_opt xdg_var with
473473- | Some v -> { value = Some v; source = Env xdg_var }
474474- | None -> { value = None; source = Default })))
475475- $ arg)
476476- in
477477- let home_prefix = "\\$HOME" in
478478- let config_dir =
479479- make_dir_arg
480480- ~enabled:(has_dir `Config)
481481- "config"
482482- "CONFIG_DIR"
483483- "XDG_CONFIG_HOME"
484484- (Some (home_prefix ^ "/.config/" ^ app_name))
485485- in
486486- let data_dir =
487487- make_dir_arg
488488- ~enabled:(has_dir `Data)
489489- "data"
490490- "DATA_DIR"
491491- "XDG_DATA_HOME"
492492- (Some (home_prefix ^ "/.local/share/" ^ app_name))
493493- in
494494- let cache_dir =
495495- make_dir_arg
496496- ~enabled:(has_dir `Cache)
497497- "cache"
498498- "CACHE_DIR"
499499- "XDG_CACHE_HOME"
500500- (Some (home_prefix ^ "/.cache/" ^ app_name))
501501- in
502502- let state_dir =
503503- make_dir_arg
504504- ~enabled:(has_dir `State)
505505- "state"
506506- "STATE_DIR"
507507- "XDG_STATE_HOME"
508508- (Some (home_prefix ^ "/.local/state/" ^ app_name))
509509- in
510510- let runtime_dir = make_dir_arg ~enabled:(has_dir `Runtime) "runtime" "RUNTIME_DIR" "XDG_RUNTIME_DIR" None in
511511- Term.(
512512- const
513513- (fun
514514- show_paths_flag
515515- config_dir_ws
516516- data_dir_ws
517517- cache_dir_ws
518518- state_dir_ws
519519- runtime_dir_ws
520520- ->
521521- let config =
522522- { config_dir = config_dir_ws
523523- ; data_dir = data_dir_ws
524524- ; cache_dir = cache_dir_ws
525525- ; state_dir = state_dir_ws
526526- ; runtime_dir = runtime_dir_ws
527527- }
528528- in
529529- let home_path = get_home_dir fs in
530530- (* First validate all standard XDG environment variables *)
531531- validate_standard_xdg_vars ();
532532- let xdg_ctx = Xdg.create ~env:Sys.getenv_opt () in
533533- (* Helper to resolve directory from config with source tracking *)
534534- let resolve_from_config config_ws xdg_getter =
535535- match config_ws.value with
536536- | Some dir -> resolve_path fs home_path dir, config_ws.source
537537- | None ->
538538- let xdg_base = xdg_getter xdg_ctx in
539539- let base_path = resolve_path fs home_path xdg_base in
540540- Eio.Path.(base_path / app_name), config_ws.source
541541- in
542542- (* User directories *)
543543- let config_dir, config_dir_source =
544544- resolve_from_config config.config_dir Xdg.config_dir
545545- in
546546- let data_dir, data_dir_source =
547547- resolve_from_config config.data_dir Xdg.data_dir
548548- in
549549- let cache_dir, cache_dir_source =
550550- resolve_from_config config.cache_dir Xdg.cache_dir
551551- in
552552- let state_dir, state_dir_source =
553553- resolve_from_config config.state_dir Xdg.state_dir
554554- in
555555- (* Runtime directory *)
556556- let runtime_dir, runtime_dir_source =
557557- match config.runtime_dir.value with
558558- | Some dir -> Some (resolve_path fs home_path dir), config.runtime_dir.source
559559- | None ->
560560- ( Option.map
561561- (fun base ->
562562- let base_path = resolve_path fs home_path base in
563563- Eio.Path.(base_path / app_name))
564564- (Xdg.runtime_dir xdg_ctx)
565565- , config.runtime_dir.source )
566566- in
567567- (* System directories - reuse shared helper *)
568568- let config_dirs =
569569- resolve_system_dirs
570570- fs
571571- home_path
572572- app_name
573573- "CONFIG_DIRS"
574574- "XDG_CONFIG_DIRS"
575575- [ "/etc/xdg" ]
576576- in
577577- let data_dirs =
578578- resolve_system_dirs
579579- fs
580580- home_path
581581- app_name
582582- "DATA_DIRS"
583583- "XDG_DATA_DIRS"
584584- [ "/usr/local/share"; "/usr/share" ]
585585- in
586586- ensure_dir config_dir;
587587- ensure_dir data_dir;
588588- ensure_dir cache_dir;
589589- ensure_dir state_dir;
590590- Option.iter (ensure_runtime_dir fs) runtime_dir;
591591- let xdg =
592592- { app_name
593593- ; config_dir
594594- ; config_dir_source
595595- ; data_dir
596596- ; data_dir_source
597597- ; cache_dir
598598- ; cache_dir_source
599599- ; state_dir
600600- ; state_dir_source
601601- ; runtime_dir
602602- ; runtime_dir_source
603603- ; config_dirs
604604- ; data_dirs
605605- }
606606- in
607607- (* Handle --show-paths option *)
608608- if show_paths_flag
609609- then (
610610- let print_path name path =
611611- match path with
612612- | None -> Printf.printf "%s: <none>\n" name
613613- | Some p -> Printf.printf "%s: %s\n" name (Eio.Path.native_exn p)
614614- in
615615- let print_paths name paths =
616616- match paths with
617617- | [] -> Printf.printf "%s: []\n" name
618618- | paths ->
619619- let paths_str = String.concat ":" (List.map Eio.Path.native_exn paths) in
620620- Printf.printf "%s: %s\n" name paths_str
621621- in
622622- print_path "config_dir" (Some config_dir);
623623- print_path "data_dir" (Some data_dir);
624624- print_path "cache_dir" (Some cache_dir);
625625- print_path "state_dir" (Some state_dir);
626626- print_path "runtime_dir" runtime_dir;
627627- print_paths "config_dirs" config_dirs;
628628- print_paths "data_dirs" data_dirs;
629629- Stdlib.exit 0);
630630- xdg, config)
631631- $ show_paths
632632- $ config_dir
633633- $ data_dir
634634- $ cache_dir
635635- $ state_dir
636636- $ runtime_dir)
637637- ;;
638638-639639- let cache_term app_name =
640640- let open Cmdliner in
641641- let app_upper = String.uppercase_ascii app_name in
642642- let app_env = app_upper ^ "_CACHE_DIR" in
643643- let xdg_var = "XDG_CACHE_HOME" in
644644- let home = Sys.getenv "HOME" in
645645- let default_path = home ^ "/.cache/" ^ app_name in
646646-647647- let doc =
648648- Printf.sprintf
649649- "Override cache directory. Can also be set with %s or %s. Default: %s"
650650- app_env xdg_var default_path
651651- in
652652-653653- let arg = Arg.(value & opt string default_path & info ["cache-dir"; "c"] ~docv:"DIR" ~doc) in
654654-655655- Term.(const (fun cmdline_val ->
656656- (* Check command line first *)
657657- if cmdline_val <> default_path then
658658- cmdline_val
659659- else
660660- (* Then check app-specific env var *)
661661- match Sys.getenv_opt app_env with
662662- | Some v when v <> "" -> v
663663- | _ ->
664664- (* Then check XDG env var *)
665665- match Sys.getenv_opt xdg_var with
666666- | Some v when v <> "" -> v ^ "/" ^ app_name
667667- | _ -> default_path
668668- ) $ arg)
669669- ;;
670670-671671- let env_docs app_name =
672672- let app_upper = String.uppercase_ascii app_name in
673673- Printf.sprintf
674674- {|
675675-Configuration Precedence (follows standard Unix conventions):
676676- 1. Command-line flags (e.g., --config-dir) - highest priority
677677- 2. Application-specific environment variable (e.g., %s_CONFIG_DIR)
678678- 3. XDG standard environment variable (e.g., XDG_CONFIG_HOME)
679679- 4. Default path (e.g., ~/.config/%s) - lowest priority
680680-681681- This allows per-application overrides without affecting other XDG-compliant programs.
682682- For example, setting %s_CONFIG_DIR only changes the config directory for %s,
683683- while XDG_CONFIG_HOME affects all XDG-compliant applications.
684684-685685-Application-specific variables:
686686- %s_CONFIG_DIR Override config directory for %s only
687687- %s_DATA_DIR Override data directory for %s only
688688- %s_CACHE_DIR Override cache directory for %s only
689689- %s_STATE_DIR Override state directory for %s only
690690- %s_RUNTIME_DIR Override runtime directory for %s only
691691-692692-XDG standard variables (shared by all XDG applications):
693693- XDG_CONFIG_HOME User configuration directory (default: ~/.config/%s)
694694- XDG_DATA_HOME User data directory (default: ~/.local/share/%s)
695695- XDG_CACHE_HOME User cache directory (default: ~/.cache/%s)
696696- XDG_STATE_HOME User state directory (default: ~/.local/state/%s)
697697- XDG_RUNTIME_DIR User runtime directory (no default)
698698- XDG_CONFIG_DIRS System configuration directories (default: /etc/xdg/%s)
699699- XDG_DATA_DIRS System data directories (default: /usr/local/share/%s:/usr/share/%s)
700700-|}
701701- app_upper
702702- app_name
703703- app_upper
704704- app_name
705705- app_upper
706706- app_name
707707- app_upper
708708- app_name
709709- app_upper
710710- app_name
711711- app_upper
712712- app_name
713713- app_upper
714714- app_name
715715- app_name
716716- app_name
717717- app_name
718718- app_name
719719- app_name
720720- app_name
721721- app_name
722722- ;;
723723-724724- let pp ppf config =
725725- let pp_source ppf = function
726726- | Default -> Fmt.(styled `Faint string) ppf "default"
727727- | Env var -> Fmt.pf ppf "%a" Fmt.(styled `Yellow string) ("env(" ^ var ^ ")")
728728- | Cmdline -> Fmt.(styled `Blue string) ppf "cmdline"
729729- in
730730- let pp_with_source name ppf ws =
731731- match ws.value with
732732- | None when ws.source = Default -> ()
733733- | None ->
734734- Fmt.pf
735735- ppf
736736- "@,%a %a %a"
737737- Fmt.(styled `Cyan string)
738738- (name ^ ":")
739739- Fmt.(styled `Red string)
740740- "<unset>"
741741- Fmt.(styled `Faint (brackets pp_source))
742742- ws.source
743743- | Some value ->
744744- Fmt.pf
745745- ppf
746746- "@,%a %a %a"
747747- Fmt.(styled `Cyan string)
748748- (name ^ ":")
749749- Fmt.(styled `Green string)
750750- value
751751- Fmt.(styled `Faint (brackets pp_source))
752752- ws.source
753753- in
754754- Fmt.pf
755755- ppf
756756- "@[<v>%a%a%a%a%a%a@]"
757757- Fmt.(styled `Bold string)
758758- "XDG config:"
759759- (pp_with_source "config_dir")
760760- config.config_dir
761761- (pp_with_source "data_dir")
762762- config.data_dir
763763- (pp_with_source "cache_dir")
764764- config.cache_dir
765765- (pp_with_source "state_dir")
766766- config.state_dir
767767- (pp_with_source "runtime_dir")
768768- config.runtime_dir
769769- ;;
770770-end
-415
stack/xdge/lib/xdge.mli
···11-(** XDG Base Directory Specification support with Eio capabilities
22-33- This library provides an OCaml implementation of the XDG Base Directory
44- Specification with Eio filesystem integration. The XDG specification defines
55- standard locations for user-specific and system-wide application files,
66- helping to keep user home directories clean and organized.
77-88- The specification is available at:
99- {{:https://specifications.freedesktop.org/basedir-spec/latest/} XDG Base Directory Specification}
1010-1111- {b Key Concepts:}
1212-1313- The XDG specification defines several types of directories:
1414- - {b User directories}: Store user-specific files (config, data, cache, state, runtime)
1515- - {b System directories}: Store system-wide files shared across users
1616- - {b Precedence}: User directories take precedence over system directories
1717- - {b Application isolation}: Each application gets its own subdirectory
1818-1919- {b Environment Variable Precedence:}
2020-2121- This library follows a three-level precedence system:
2222- + Application-specific variables (e.g., [MYAPP_CONFIG_DIR]) - highest priority
2323- + XDG standard variables (e.g., [XDG_CONFIG_HOME])
2424- + Default paths (e.g., [$HOME/.config]) - lowest priority
2525-2626- This allows fine-grained control over directory locations without affecting
2727- other XDG-compliant applications.
2828-2929- {b Directory Creation:}
3030-3131- All directories are automatically created with appropriate permissions (0o755)
3232- when accessed, except for runtime directories which require stricter permissions
3333- as per the specification.
3434-3535- @see <https://specifications.freedesktop.org/basedir-spec/latest/> XDG Base Directory Specification *)
3636-3737-(** The main XDG context type containing all directory paths for an application.
3838-3939- A value of type [t] represents the complete XDG directory structure for a
4040- specific application, including both user-specific and system-wide directories.
4141- All paths are resolved at creation time and are absolute paths within the
4242- Eio filesystem. *)
4343-type t
4444-4545-(** XDG directory types for specifying which directories an application needs.
4646-4747- These polymorphic variants allow applications to declare which XDG directories
4848- they use, enabling runtime systems to only provide the requested directories. *)
4949-type dir = [
5050- | `Config (** User configuration files *)
5151- | `Cache (** User-specific cached data *)
5252- | `Data (** User-specific application data *)
5353- | `State (** User-specific state data (logs, history, etc.) *)
5454- | `Runtime (** User-specific runtime files (sockets, pipes, etc.) *)
5555-]
5656-5757-(** {1 Exceptions} *)
5858-5959-(** Exception raised when XDG environment variables contain invalid paths.
6060-6161- The XDG specification requires all paths in environment variables to be
6262- absolute. This exception is raised when a relative path is found. *)
6363-exception Invalid_xdg_path of string
6464-6565-(** {1 Construction} *)
6666-6767-(** [create fs app_name] creates an XDG context for the given application.
6868-6969- This function initializes the complete XDG directory structure for your application,
7070- resolving all paths according to the environment variables and creating directories
7171- as needed.
7272-7373- @param fs The Eio filesystem providing filesystem access
7474- @param app_name The name of your application (used as subdirectory name)
7575-7676- {b Path Resolution:}
7777-7878- For each directory type, the following precedence is used:
7979- + Application-specific environment variable (e.g., [MYAPP_CONFIG_DIR])
8080- + XDG standard environment variable (e.g., [XDG_CONFIG_HOME])
8181- + Default path as specified in the XDG specification
8282-8383- {b Example:}
8484- {[
8585- let xdg = Xdge.create env#fs "myapp" in
8686- let config = Xdge.config_dir xdg in
8787- (* config is now <fs:$HOME/.config/myapp> or the overridden path *)
8888- ]}
8989-9090- All directories are created with permissions 0o755 if they don't exist,
9191- except for runtime directories which are created with 0o700 permissions and
9292- validated according to the XDG specification.
9393-9494- @raise Invalid_xdg_path if any environment variable contains a relative path *)
9595-val create : Eio.Fs.dir_ty Eio.Path.t -> string -> t
9696-9797-(** {1 Accessors} *)
9898-9999-(** [app_name t] returns the application name used when creating this XDG context.
100100-101101- This is the name that was passed to {!create} and is used as the subdirectory
102102- name within each XDG base directory. *)
103103-val app_name : t -> string
104104-105105-(** {1 Base Directories} *)
106106-107107-(** [config_dir t] returns the path to user-specific configuration files.
108108-109109- {b Purpose:} Store user preferences, settings, and configuration files.
110110- Configuration files should be human-readable when possible.
111111-112112- {b Environment Variables:}
113113- - [${APP_NAME}_CONFIG_DIR]: Application-specific override (highest priority)
114114- - [XDG_CONFIG_HOME]: XDG standard variable
115115- - Default: [$HOME/.config/{app_name}]
116116-117117- @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_CONFIG_HOME specification *)
118118-val config_dir : t -> Eio.Fs.dir_ty Eio.Path.t
119119-120120-(** [data_dir t] returns the path to user-specific data files.
121121-122122- {b Purpose:} Store persistent application data that should be preserved
123123- across application restarts and system reboots. This data is typically
124124- not modified by users directly.
125125-126126- {b Environment Variables:}
127127- - [${APP_NAME}_DATA_DIR]: Application-specific override (highest priority)
128128- - [XDG_DATA_HOME]: XDG standard variable
129129- - Default: [$HOME/.local/share/{app_name}]
130130-131131- {b Example Files:}
132132- - Application databases
133133- - User-generated content (documents, projects)
134134- - Downloaded resources
135135- - Application plugins or extensions
136136-137137- @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_DATA_HOME specification *)
138138-val data_dir : t -> Eio.Fs.dir_ty Eio.Path.t
139139-140140-(** [cache_dir t] returns the path to user-specific cache files.
141141-142142- {b Purpose:} Store non-essential cached data that can be regenerated
143143- if deleted. The application should remain functional if this directory
144144- is cleared, though performance may be temporarily impacted.
145145-146146- {b Environment Variables:}
147147- - [${APP_NAME}_CACHE_DIR]: Application-specific override (highest priority)
148148- - [XDG_CACHE_HOME]: XDG standard variable
149149- - Default: [$HOME/.cache/{app_name}]
150150-151151- {b Example Files:}
152152- - Downloaded thumbnails and previews
153153- - Compiled bytecode or object files
154154- - Network response caches
155155- - Temporary computation results
156156-157157- Users may clear cache directories to free disk space, so
158158- always check for cache validity and be prepared to regenerate data.
159159-160160- @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_CACHE_HOME specification *)
161161-val cache_dir : t -> Eio.Fs.dir_ty Eio.Path.t
162162-163163-(** [state_dir t] returns the path to user-specific state files.
164164-165165- {b Purpose:} Store persistent state data that should be preserved between
166166- application restarts but is not important enough to be user data. This
167167- includes application state that can be regenerated but would impact the
168168- user experience if lost.
169169-170170- {b Environment Variables:}
171171- - [${APP_NAME}_STATE_DIR]: Application-specific override (highest priority)
172172- - [XDG_STATE_HOME]: XDG standard variable
173173- - Default: [$HOME/.local/state/{app_name}]
174174-175175- {b Example Files:}
176176- - Application history (recently used files, command history)
177177- - Current application state (window positions, open tabs)
178178- - Logs and journal files
179179- - Undo/redo history
180180-181181- {b Comparison with other directories:}
182182- - Unlike cache: State should persist between reboots
183183- - Unlike data: State can be regenerated (though inconvenient)
184184- - Unlike config: State changes frequently during normal use
185185-186186- @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_STATE_HOME specification *)
187187-val state_dir : t -> Eio.Fs.dir_ty Eio.Path.t
188188-189189-(** [runtime_dir t] returns the path to user-specific runtime files.
190190-191191- {b Purpose:} Store runtime files such as sockets, named pipes, and
192192- process IDs. These files are only valid for the duration of the user's
193193- login session.
194194-195195- {b Environment Variables:}
196196- - [${APP_NAME}_RUNTIME_DIR]: Application-specific override (highest priority)
197197- - [XDG_RUNTIME_DIR]: XDG standard variable
198198- - Default: None (returns [None] if not set)
199199-200200- {b Required Properties (per specification):}
201201- - Owned by the user with access mode 0700
202202- - Bound to the user login session lifetime
203203- - Located on a local filesystem (not networked)
204204- - Fully-featured by the OS (supporting proper locking, etc.)
205205-206206- {b Example Files:}
207207- - Unix domain sockets
208208- - Named pipes (FIFOs)
209209- - Lock files
210210- - Small process communication files
211211-212212- This may return [None] if no suitable runtime directory
213213- is available. Applications should handle this gracefully, perhaps by
214214- falling back to [/tmp] with appropriate security measures.
215215-216216- @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_RUNTIME_DIR specification *)
217217-val runtime_dir : t -> Eio.Fs.dir_ty Eio.Path.t option
218218-219219-(** {1 System Directories} *)
220220-221221-(** [config_dirs t] returns search paths for system-wide configuration files.
222222-223223- {b Purpose:} Provide a search path for configuration files that are
224224- shared between multiple users. Files in user-specific {!config_dir}
225225- take precedence over these system directories.
226226-227227- {b Environment Variables:}
228228- - [${APP_NAME}_CONFIG_DIRS]: Application-specific override (highest priority)
229229- - [XDG_CONFIG_DIRS]: XDG standard variable (colon-separated list)
230230- - Default: [[/etc/xdg/{app_name}]]
231231-232232- {b Search Order:}
233233- Directories are ordered by preference, with earlier entries taking
234234- precedence over later ones. When looking for a configuration file,
235235- search {!config_dir} first, then each directory in this list.
236236-237237- @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_CONFIG_DIRS specification *)
238238-val config_dirs : t -> Eio.Fs.dir_ty Eio.Path.t list
239239-240240-(** [data_dirs t] returns search paths for system-wide data files.
241241-242242- {b Purpose:} Provide a search path for data files that are shared
243243- between multiple users. Files in user-specific {!data_dir} take
244244- precedence over these system directories.
245245-246246- {b Environment Variables:}
247247- - [${APP_NAME}_DATA_DIRS]: Application-specific override (highest priority)
248248- - [XDG_DATA_DIRS]: XDG standard variable (colon-separated list)
249249- - Default: [[/usr/local/share/{app_name}; /usr/share/{app_name}]]
250250-251251- {b Search Order:}
252252- Directories are ordered by preference, with earlier entries taking
253253- precedence over later ones. When looking for a data file, search
254254- {!data_dir} first, then each directory in this list.
255255-256256- {b Example Files:}
257257- - Application icons and themes
258258- - Desktop files
259259- - Shared application resources
260260- - Documentation files
261261- - Default templates
262262-263263- @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> XDG_DATA_DIRS specification *)
264264-val data_dirs : t -> Eio.Fs.dir_ty Eio.Path.t list
265265-266266-(** {1 File Search} *)
267267-268268-(** [find_config_file t filename] searches for a configuration file following XDG precedence.
269269-270270- This function searches for the given filename in the user configuration directory
271271- first, then in system configuration directories in order of preference.
272272- Files that are inaccessible (due to permissions, non-existence, etc.) are
273273- silently skipped as per the XDG specification.
274274-275275- @param t The XDG context
276276- @param filename The name of the file to search for
277277- @return [Some path] if found, [None] if not found in any directory
278278-279279- {b Search Order:}
280280- 1. User config directory ({!config_dir})
281281- 2. System config directories ({!config_dirs}) in preference order
282282-283283- *)
284284-val find_config_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option
285285-286286-(** [find_data_file t filename] searches for a data file following XDG precedence.
287287-288288- This function searches for the given filename in the user data directory
289289- first, then in system data directories in order of preference.
290290- Files that are inaccessible (due to permissions, non-existence, etc.) are
291291- silently skipped as per the XDG specification.
292292-293293- @param t The XDG context
294294- @param filename The name of the file to search for
295295- @return [Some path] if found, [None] if not found in any directory
296296-297297- {b Search Order:}
298298- 1. User data directory ({!data_dir})
299299- 2. System data directories ({!data_dirs}) in preference order
300300-301301- *)
302302-val find_data_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option
303303-304304-(** {1 Pretty Printing} *)
305305-306306-(** [pp ?brief ?sources ppf t] pretty prints the XDG directory configuration.
307307-308308- @param brief If [true], prints a compact one-line summary (default: [false])
309309- @param sources If [true], shows the source of each directory value,
310310- indicating whether it came from defaults, environment
311311- variables, or command line (default: [false])
312312- @param ppf The formatter to print to
313313- @param t The XDG context to print
314314-315315- {b Output formats:}
316316- - Normal: Multi-line detailed view of all directories
317317- - Brief: Single line showing app name and key directories
318318- - With sources: Adds annotations showing where each path came from
319319- *)
320320-val pp : ?brief:bool -> ?sources:bool -> Format.formatter -> t -> unit
321321-322322-(** {1 Cmdliner Integration} *)
323323-324324-module Cmd : sig
325325- (** The type of the outer XDG context *)
326326- type xdg_t = t
327327- (** Cmdliner integration for XDG directory configuration.
328328-329329- This module provides integration with the Cmdliner library,
330330- allowing XDG directories to be configured via command-line arguments
331331- while respecting the precedence of environment variables. *)
332332-333333- (** Type of XDG configuration gathered from command-line and environment.
334334-335335- This contains all XDG directory paths along with their sources,
336336- as determined by command-line arguments and environment variables. *)
337337- type t
338338-339339- (** [term app_name fs ?dirs ()] creates a Cmdliner term for XDG directory configuration.
340340-341341- This function generates a Cmdliner term that handles XDG directory
342342- configuration through both command-line flags and environment variables,
343343- and directly returns the XDG context. Only command-line flags for the
344344- requested directories are generated.
345345-346346- @param app_name The application name (used for environment variable prefixes)
347347- @param fs The Eio filesystem to use for path resolution
348348- @param dirs List of directories to include flags for (default: all directories)
349349-350350- {b Generated Command-line Flags:}
351351- Only the flags for requested directories are generated:
352352- - [--config-dir DIR]: Override configuration directory (if [`Config] in dirs)
353353- - [--data-dir DIR]: Override data directory (if [`Data] in dirs)
354354- - [--cache-dir DIR]: Override cache directory (if [`Cache] in dirs)
355355- - [--state-dir DIR]: Override state directory (if [`State] in dirs)
356356- - [--runtime-dir DIR]: Override runtime directory (if [`Runtime] in dirs)
357357-358358- {b Environment Variable Precedence:}
359359- For each directory type, the following precedence applies:
360360- + Command-line flag (e.g., [--config-dir]) - if enabled
361361- + Application-specific variable (e.g., [MYAPP_CONFIG_DIR])
362362- + XDG standard variable (e.g., [XDG_CONFIG_HOME])
363363- + Default value
364364- *)
365365- val term : string -> Eio.Fs.dir_ty Eio.Path.t ->
366366- ?dirs:dir list ->
367367- unit -> (xdg_t * t) Cmdliner.Term.t
368368-369369- (** [cache_term app_name] creates a Cmdliner term that provides just the cache
370370- directory path as a string, respecting XDG precedence.
371371-372372- This is a convenience function for applications that only need cache
373373- directory configuration. It returns the resolved cache directory path
374374- directly as a string, suitable for use in other Cmdliner terms.
375375-376376- @param app_name The application name (used for environment variable prefixes)
377377-378378- {b Generated Command-line Flag:}
379379- - [--cache-dir DIR]: Override cache directory
380380-381381- {b Environment Variable Precedence:}
382382- + Command-line flag ([--cache-dir])
383383- + Application-specific variable (e.g., [MYAPP_CACHE_DIR])
384384- + XDG standard variable ([XDG_CACHE_HOME])
385385- + Default value ([$HOME/.cache/{app_name}])
386386- *)
387387- val cache_term : string -> string Cmdliner.Term.t
388388-389389- (** [env_docs app_name] generates documentation for environment variables.
390390-391391- Returns a formatted string documenting all environment variables that
392392- affect XDG directory configuration for the given application. This is
393393- useful for generating man pages or help text.
394394-395395- @param app_name The application name
396396- @return A formatted documentation string
397397-398398- {b Included Information:}
399399- - Configuration precedence rules
400400- - Application-specific environment variables
401401- - XDG standard environment variables
402402- - Default values for each directory type
403403- *)
404404- val env_docs : string -> string
405405-406406- (** [pp ppf config] pretty prints a Cmdliner configuration.
407407-408408- This function formats the configuration showing each directory path
409409- along with its source, which is helpful for debugging configuration
410410- issues or displaying the current configuration to users.
411411-412412- @param ppf The formatter to print to
413413- @param config The configuration to print *)
414414- val pp : Format.formatter -> t -> unit
415415-end