···11+(*
22+ * ISC License
33+ *
44+ * Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>
55+ *
66+ * Permission to use, copy, modify, and distribute this software for any
77+ * purpose with or without fee is hereby granted, provided that the above
88+ * copyright notice and this permission notice appear in all copies.
99+ *
1010+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
1111+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1212+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1313+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1414+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1515+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1616+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1717+ *
1818+ *)
+30
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/xdge")
1010+(maintainers "Anil Madhavapeddy <anil@recoil.org>")
1111+(bug_reports "https://tangled.sh/@anil.recoil.org/xgde/issues")
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 provide safe access to configuration, \
2020+ data, cache, state, and runtime directories. The library exposes \
2121+ Cmdliner terms that allow for proper environment variable overrides \
2222+ and command-line flags.")
2323+ (depends
2424+ (ocaml (>= 5.1.0))
2525+ (eio (>= 1.1))
2626+ (cmdliner (>= 1.2.0))
2727+ (fmt (>= 0.11.0))
2828+ (odoc :with-doc)
2929+ (eio_main :with-test)
3030+ (alcotest (and :with-test (>= 1.7.0)))))
···11+type source = Default | Env of string | Cmdline
22+33+type t = {
44+ app_name : string;
55+ config_dir : Eio.Fs.dir_ty Eio.Path.t;
66+ config_dir_source : source;
77+ data_dir : Eio.Fs.dir_ty Eio.Path.t;
88+ data_dir_source : source;
99+ cache_dir : Eio.Fs.dir_ty Eio.Path.t;
1010+ cache_dir_source : source;
1111+ state_dir : Eio.Fs.dir_ty Eio.Path.t;
1212+ state_dir_source : source;
1313+ runtime_dir : Eio.Fs.dir_ty Eio.Path.t option;
1414+ runtime_dir_source : source;
1515+ config_dirs : Eio.Fs.dir_ty Eio.Path.t list;
1616+ data_dirs : Eio.Fs.dir_ty Eio.Path.t list;
1717+}
1818+1919+let ensure_dir ?(perm = 0o755) path = Eio.Path.mkdirs ~exists_ok:true ~perm path
2020+2121+let validate_runtime_base_dir base_path =
2222+ (* Validate the base XDG_RUNTIME_DIR has correct permissions per spec *)
2323+ try
2424+ let path_str = Eio.Path.native_exn base_path in
2525+ let stat = Eio.Path.stat ~follow:true base_path in
2626+ let current_perm = stat.perm land 0o777 in
2727+ if current_perm <> 0o700 then
2828+ failwith
2929+ (Printf.sprintf
3030+ "XDG_RUNTIME_DIR base directory %s has incorrect permissions: %o \
3131+ (must be 0700)"
3232+ path_str current_perm);
3333+ (* Check ownership - directory should be owned by current user *)
3434+ let uid = Unix.getuid () in
3535+ if stat.uid <> Int64.of_int uid then
3636+ failwith
3737+ (Printf.sprintf
3838+ "XDG_RUNTIME_DIR base directory %s not owned by current user (uid \
3939+ %d, owner %Ld)"
4040+ path_str uid stat.uid)
4141+ (* TODO: Check that directory is on local filesystem (not networked).
4242+ This would require filesystem type detection which is OS-specific. *)
4343+ with exn ->
4444+ failwith
4545+ (Printf.sprintf "Cannot validate XDG_RUNTIME_DIR: %s"
4646+ (Printexc.to_string exn))
4747+4848+let ensure_runtime_dir _fs app_runtime_path =
4949+ (* Base directory validation is done in resolve_runtime_dir,
5050+ so we just create the app subdirectory *)
5151+ ensure_dir app_runtime_path
5252+5353+let get_home_dir fs =
5454+ let home_str =
5555+ match Sys.getenv_opt "HOME" with
5656+ | Some home -> home
5757+ | None -> (
5858+ match Sys.os_type with
5959+ | "Win32" | "Cygwin" -> (
6060+ match Sys.getenv_opt "USERPROFILE" with
6161+ | Some profile -> profile
6262+ | None -> failwith "Cannot determine home directory")
6363+ | _ -> (
6464+ try Unix.((getpwuid (getuid ())).pw_dir)
6565+ with _ -> failwith "Cannot determine home directory"))
6666+ in
6767+ Eio.Path.(fs / home_str)
6868+6969+let make_env_var_name app_name suffix =
7070+ String.uppercase_ascii app_name ^ "_" ^ suffix
7171+7272+exception Invalid_xdg_path of string
7373+7474+let validate_absolute_path context path =
7575+ if Filename.is_relative path then
7676+ raise
7777+ (Invalid_xdg_path
7878+ (Printf.sprintf "%s must be an absolute path, got: %s" context path))
7979+8080+let resolve_path fs home_path base_path =
8181+ if Filename.is_relative base_path then Eio.Path.(home_path / base_path)
8282+ else Eio.Path.(fs / base_path)
8383+8484+(* Helper to resolve system directories (config_dirs or data_dirs) *)
8585+let resolve_system_dirs fs home_path app_name override_suffix xdg_var
8686+ default_paths =
8787+ let override_var = make_env_var_name app_name override_suffix in
8888+ match Sys.getenv_opt override_var with
8989+ | Some dirs when dirs <> "" ->
9090+ String.split_on_char ':' dirs
9191+ |> List.filter (fun s -> s <> "")
9292+ |> List.filter_map (fun path ->
9393+ try
9494+ validate_absolute_path override_var path;
9595+ Some Eio.Path.(resolve_path fs home_path path / app_name)
9696+ with Invalid_xdg_path _ -> None)
9797+ | Some _ | None -> (
9898+ match Sys.getenv_opt xdg_var with
9999+ | Some dirs when dirs <> "" ->
100100+ String.split_on_char ':' dirs
101101+ |> List.filter (fun s -> s <> "")
102102+ |> List.filter_map (fun path ->
103103+ try
104104+ validate_absolute_path xdg_var path;
105105+ Some Eio.Path.(resolve_path fs home_path path / app_name)
106106+ with Invalid_xdg_path _ -> None)
107107+ | Some _ | None ->
108108+ List.map
109109+ (fun path -> Eio.Path.(resolve_path fs home_path path / app_name))
110110+ default_paths)
111111+112112+(* Helper to resolve a user directory with override precedence *)
113113+let resolve_user_dir fs home_path app_name xdg_ctx xdg_getter override_suffix =
114114+ let override_var = make_env_var_name app_name override_suffix in
115115+ match Sys.getenv_opt override_var with
116116+ | Some dir when dir <> "" ->
117117+ validate_absolute_path override_var dir;
118118+ (Eio.Path.(fs / dir / app_name), Env override_var)
119119+ | Some _ | None ->
120120+ let xdg_base = xdg_getter xdg_ctx in
121121+ let base_path = resolve_path fs home_path xdg_base in
122122+ (Eio.Path.(base_path / app_name), Default)
123123+124124+(* Helper to resolve runtime directory (special case since it can be None) *)
125125+let resolve_runtime_dir fs home_path app_name xdg_ctx =
126126+ let override_var = make_env_var_name app_name "RUNTIME_DIR" in
127127+ match Sys.getenv_opt override_var with
128128+ | Some dir when dir <> "" ->
129129+ validate_absolute_path override_var dir;
130130+ (* Validate the base runtime directory has correct permissions *)
131131+ let base_runtime_dir = resolve_path fs home_path dir in
132132+ validate_runtime_base_dir base_runtime_dir;
133133+ (Some Eio.Path.(base_runtime_dir / app_name), Env override_var)
134134+ | Some _ | None ->
135135+ ( (match Xdg.runtime_dir xdg_ctx with
136136+ | Some base ->
137137+ (* Validate the base runtime directory has correct permissions *)
138138+ let base_runtime_dir = resolve_path fs home_path base in
139139+ validate_runtime_base_dir base_runtime_dir;
140140+ Some Eio.Path.(base_runtime_dir / app_name)
141141+ | None -> None),
142142+ Default )
143143+144144+let validate_standard_xdg_vars () =
145145+ (* Validate standard XDG environment variables for absolute paths *)
146146+ let xdg_vars =
147147+ [
148148+ "XDG_CONFIG_HOME";
149149+ "XDG_DATA_HOME";
150150+ "XDG_CACHE_HOME";
151151+ "XDG_STATE_HOME";
152152+ "XDG_RUNTIME_DIR";
153153+ "XDG_CONFIG_DIRS";
154154+ "XDG_DATA_DIRS";
155155+ ]
156156+ in
157157+ List.iter
158158+ (fun var ->
159159+ match Sys.getenv_opt var with
160160+ | Some value when value <> "" ->
161161+ if String.contains value ':' then
162162+ (* Colon-separated list - validate each part *)
163163+ String.split_on_char ':' value
164164+ |> List.filter (fun s -> s <> "")
165165+ |> List.iter (fun path -> validate_absolute_path var path)
166166+ else
167167+ (* Single path *)
168168+ validate_absolute_path var value
169169+ | _ -> ())
170170+ xdg_vars
171171+172172+let create fs app_name =
173173+ let fs = fs in
174174+ let home_path = get_home_dir fs in
175175+ (* First validate all standard XDG environment variables *)
176176+ validate_standard_xdg_vars ();
177177+ let xdg_ctx = Xdg.create ~env:Sys.getenv_opt () in
178178+ (* User directories *)
179179+ let config_dir, config_dir_source =
180180+ resolve_user_dir fs home_path app_name xdg_ctx Xdg.config_dir "CONFIG_DIR"
181181+ in
182182+ let data_dir, data_dir_source =
183183+ resolve_user_dir fs home_path app_name xdg_ctx Xdg.data_dir "DATA_DIR"
184184+ in
185185+ let cache_dir, cache_dir_source =
186186+ resolve_user_dir fs home_path app_name xdg_ctx Xdg.cache_dir "CACHE_DIR"
187187+ in
188188+ let state_dir, state_dir_source =
189189+ resolve_user_dir fs home_path app_name xdg_ctx Xdg.state_dir "STATE_DIR"
190190+ in
191191+ (* Runtime directory *)
192192+ let runtime_dir, runtime_dir_source =
193193+ resolve_runtime_dir fs home_path app_name xdg_ctx
194194+ in
195195+ (* System directories *)
196196+ let config_dirs =
197197+ resolve_system_dirs fs home_path app_name "CONFIG_DIRS" "XDG_CONFIG_DIRS"
198198+ [ "/etc/xdg" ]
199199+ in
200200+ let data_dirs =
201201+ resolve_system_dirs fs home_path app_name "DATA_DIRS" "XDG_DATA_DIRS"
202202+ [ "/usr/local/share"; "/usr/share" ]
203203+ in
204204+ ensure_dir config_dir;
205205+ ensure_dir data_dir;
206206+ ensure_dir cache_dir;
207207+ ensure_dir state_dir;
208208+ Option.iter (ensure_runtime_dir fs) runtime_dir;
209209+ {
210210+ app_name;
211211+ config_dir;
212212+ config_dir_source;
213213+ data_dir;
214214+ data_dir_source;
215215+ cache_dir;
216216+ cache_dir_source;
217217+ state_dir;
218218+ state_dir_source;
219219+ runtime_dir;
220220+ runtime_dir_source;
221221+ config_dirs;
222222+ data_dirs;
223223+ }
224224+225225+let app_name t = t.app_name
226226+let config_dir t = t.config_dir
227227+let data_dir t = t.data_dir
228228+let cache_dir t = t.cache_dir
229229+let state_dir t = t.state_dir
230230+let runtime_dir t = t.runtime_dir
231231+let config_dirs t = t.config_dirs
232232+let data_dirs t = t.data_dirs
233233+234234+(* File search following XDG specification *)
235235+let find_file_in_dirs dirs filename =
236236+ let rec search_dirs = function
237237+ | [] -> None
238238+ | dir :: remaining_dirs -> (
239239+ let file_path = Eio.Path.(dir / filename) in
240240+ try
241241+ (* Try to check if file exists and is readable *)
242242+ let _ = Eio.Path.stat ~follow:true file_path in
243243+ Some file_path
244244+ with _ ->
245245+ (* File is inaccessible (non-existent, permissions, etc.)
246246+ Skip and continue with next directory per XDG spec *)
247247+ search_dirs remaining_dirs)
248248+ in
249249+ search_dirs dirs
250250+251251+let find_config_file t filename =
252252+ (* Search user config dir first, then system config dirs *)
253253+ find_file_in_dirs (t.config_dir :: t.config_dirs) filename
254254+255255+let find_data_file t filename =
256256+ (* Search user data dir first, then system data dirs *)
257257+ find_file_in_dirs (t.data_dir :: t.data_dirs) filename
258258+259259+let pp ?(brief = false) ?(sources = false) ppf t =
260260+ let pp_source ppf = function
261261+ | Default -> Fmt.(styled `Faint string) ppf "default"
262262+ | Env var -> Fmt.pf ppf "%a" Fmt.(styled `Yellow string) ("env(" ^ var ^ ")")
263263+ | Cmdline -> Fmt.(styled `Blue string) ppf "cmdline"
264264+ in
265265+ let pp_path_with_source ppf path source =
266266+ if sources then
267267+ Fmt.pf ppf "%a %a"
268268+ Fmt.(styled `Green Eio.Path.pp)
269269+ path
270270+ Fmt.(styled `Faint (brackets pp_source))
271271+ source
272272+ else Fmt.(styled `Green Eio.Path.pp) ppf path
273273+ in
274274+ let pp_path_opt_with_source ppf path_opt source =
275275+ match path_opt with
276276+ | None ->
277277+ if sources then
278278+ Fmt.pf ppf "%a %a"
279279+ Fmt.(styled `Red string)
280280+ "<none>"
281281+ Fmt.(styled `Faint (brackets pp_source))
282282+ source
283283+ else Fmt.(styled `Red string) ppf "<none>"
284284+ | Some path -> pp_path_with_source ppf path source
285285+ in
286286+ let pp_paths ppf paths =
287287+ Fmt.(list ~sep:(any ";@ ") (styled `Green Eio.Path.pp)) ppf paths
288288+ in
289289+ if brief then
290290+ Fmt.pf ppf "%a config=%a data=%a>"
291291+ Fmt.(styled `Cyan string)
292292+ ("<xdg:" ^ t.app_name)
293293+ (fun ppf (path, source) -> pp_path_with_source ppf path source)
294294+ (t.config_dir, t.config_dir_source)
295295+ (fun ppf (path, source) -> pp_path_with_source ppf path source)
296296+ (t.data_dir, t.data_dir_source)
297297+ else (
298298+ Fmt.pf ppf "@[<v>%a@,"
299299+ Fmt.(styled `Bold string)
300300+ ("XDG directories for '" ^ t.app_name ^ "':");
301301+ Fmt.pf ppf "@[<v 2>%a@," Fmt.(styled `Bold string) "User directories:";
302302+ Fmt.pf ppf "%a %a@,"
303303+ Fmt.(styled `Cyan string)
304304+ "config:"
305305+ (fun ppf (path, source) -> pp_path_with_source ppf path source)
306306+ (t.config_dir, t.config_dir_source);
307307+ Fmt.pf ppf "%a %a@,"
308308+ Fmt.(styled `Cyan string)
309309+ "data:"
310310+ (fun ppf (path, source) -> pp_path_with_source ppf path source)
311311+ (t.data_dir, t.data_dir_source);
312312+ Fmt.pf ppf "%a %a@,"
313313+ Fmt.(styled `Cyan string)
314314+ "cache:"
315315+ (fun ppf (path, source) -> pp_path_with_source ppf path source)
316316+ (t.cache_dir, t.cache_dir_source);
317317+ Fmt.pf ppf "%a %a@,"
318318+ Fmt.(styled `Cyan string)
319319+ "state:"
320320+ (fun ppf (path, source) -> pp_path_with_source ppf path source)
321321+ (t.state_dir, t.state_dir_source);
322322+ Fmt.pf ppf "%a %a@]@,"
323323+ Fmt.(styled `Cyan string)
324324+ "runtime:"
325325+ (fun ppf (path_opt, source) ->
326326+ pp_path_opt_with_source ppf path_opt source)
327327+ (t.runtime_dir, t.runtime_dir_source);
328328+ Fmt.pf ppf "@[<v 2>%a@," Fmt.(styled `Bold string) "System directories:";
329329+ Fmt.pf ppf "%a [@[<hov>%a@]]@,"
330330+ Fmt.(styled `Cyan string)
331331+ "config_dirs:" pp_paths t.config_dirs;
332332+ Fmt.pf ppf "%a [@[<hov>%a@]]@]@]"
333333+ Fmt.(styled `Cyan string)
334334+ "data_dirs:" pp_paths t.data_dirs)
335335+336336+module Cmd = struct
337337+ type xdg_t = t
338338+ type 'a with_source = { value : 'a option; source : source }
339339+340340+ type t = {
341341+ config_dir : string with_source;
342342+ data_dir : string with_source;
343343+ cache_dir : string with_source;
344344+ state_dir : string with_source;
345345+ runtime_dir : string with_source;
346346+ }
347347+348348+ type dir = [ `Config | `Cache | `Data | `State | `Runtime ]
349349+350350+ let term app_name fs ?(dirs = [ `Config; `Data; `Cache; `State; `Runtime ]) ()
351351+ =
352352+ let open Cmdliner in
353353+ let app_upper = String.uppercase_ascii app_name in
354354+ let show_paths =
355355+ let doc = "Show only the resolved directory paths without formatting" in
356356+ Arg.(value & flag & info [ "show-paths" ] ~doc)
357357+ in
358358+ let has_dir d = List.mem d dirs in
359359+ let make_dir_arg ~enabled name env_suffix xdg_var default_path =
360360+ if not enabled then
361361+ (* Return a term that always gives the environment-only result *)
362362+ Term.(
363363+ const (fun () ->
364364+ let app_env = app_upper ^ "_" ^ env_suffix in
365365+ match Sys.getenv_opt app_env with
366366+ | Some v when v <> "" -> { value = Some v; source = Env app_env }
367367+ | Some _ | None -> (
368368+ match Sys.getenv_opt xdg_var with
369369+ | Some v -> { value = Some v; source = Env xdg_var }
370370+ | None -> { value = None; source = Default }))
371371+ $ const ())
372372+ else
373373+ let app_env = app_upper ^ "_" ^ env_suffix in
374374+ let doc =
375375+ match default_path with
376376+ | Some path ->
377377+ Printf.sprintf
378378+ "Override %s directory. Can also be set with %s or %s. \
379379+ Default: %s"
380380+ name app_env xdg_var path
381381+ | None ->
382382+ Printf.sprintf
383383+ "Override %s directory. Can also be set with %s or %s. No \
384384+ default value."
385385+ name app_env xdg_var
386386+ in
387387+ let arg =
388388+ Arg.(
389389+ value
390390+ & opt (some string) None
391391+ & info [ name ^ "-dir" ] ~docv:"DIR" ~doc)
392392+ in
393393+ Term.(
394394+ const (fun cmdline_val ->
395395+ match cmdline_val with
396396+ | Some v -> { value = Some v; source = Cmdline }
397397+ | None -> (
398398+ match Sys.getenv_opt app_env with
399399+ | Some v when v <> "" ->
400400+ { value = Some v; source = Env app_env }
401401+ | Some _ | None -> (
402402+ match Sys.getenv_opt xdg_var with
403403+ | Some v -> { value = Some v; source = Env xdg_var }
404404+ | None -> { value = None; source = Default })))
405405+ $ arg)
406406+ in
407407+ let home_prefix = "\\$HOME" in
408408+ let config_dir =
409409+ make_dir_arg ~enabled:(has_dir `Config) "config" "CONFIG_DIR"
410410+ "XDG_CONFIG_HOME"
411411+ (Some (home_prefix ^ "/.config/" ^ app_name))
412412+ in
413413+ let data_dir =
414414+ make_dir_arg ~enabled:(has_dir `Data) "data" "DATA_DIR" "XDG_DATA_HOME"
415415+ (Some (home_prefix ^ "/.local/share/" ^ app_name))
416416+ in
417417+ let cache_dir =
418418+ make_dir_arg ~enabled:(has_dir `Cache) "cache" "CACHE_DIR"
419419+ "XDG_CACHE_HOME"
420420+ (Some (home_prefix ^ "/.cache/" ^ app_name))
421421+ in
422422+ let state_dir =
423423+ make_dir_arg ~enabled:(has_dir `State) "state" "STATE_DIR"
424424+ "XDG_STATE_HOME"
425425+ (Some (home_prefix ^ "/.local/state/" ^ app_name))
426426+ in
427427+ let runtime_dir =
428428+ make_dir_arg ~enabled:(has_dir `Runtime) "runtime" "RUNTIME_DIR"
429429+ "XDG_RUNTIME_DIR" None
430430+ in
431431+ Term.(
432432+ const
433433+ (fun
434434+ show_paths_flag
435435+ config_dir_ws
436436+ data_dir_ws
437437+ cache_dir_ws
438438+ state_dir_ws
439439+ runtime_dir_ws
440440+ ->
441441+ let config =
442442+ {
443443+ config_dir = config_dir_ws;
444444+ data_dir = data_dir_ws;
445445+ cache_dir = cache_dir_ws;
446446+ state_dir = state_dir_ws;
447447+ runtime_dir = runtime_dir_ws;
448448+ }
449449+ in
450450+ let home_path = get_home_dir fs in
451451+ (* First validate all standard XDG environment variables *)
452452+ validate_standard_xdg_vars ();
453453+ let xdg_ctx = Xdg.create ~env:Sys.getenv_opt () in
454454+ (* Helper to resolve directory from config with source tracking *)
455455+ let resolve_from_config config_ws xdg_getter =
456456+ match config_ws.value with
457457+ | Some dir -> (resolve_path fs home_path dir, config_ws.source)
458458+ | None ->
459459+ let xdg_base = xdg_getter xdg_ctx in
460460+ let base_path = resolve_path fs home_path xdg_base in
461461+ (Eio.Path.(base_path / app_name), config_ws.source)
462462+ in
463463+ (* User directories *)
464464+ let config_dir, config_dir_source =
465465+ resolve_from_config config.config_dir Xdg.config_dir
466466+ in
467467+ let data_dir, data_dir_source =
468468+ resolve_from_config config.data_dir Xdg.data_dir
469469+ in
470470+ let cache_dir, cache_dir_source =
471471+ resolve_from_config config.cache_dir Xdg.cache_dir
472472+ in
473473+ let state_dir, state_dir_source =
474474+ resolve_from_config config.state_dir Xdg.state_dir
475475+ in
476476+ (* Runtime directory *)
477477+ let runtime_dir, runtime_dir_source =
478478+ match config.runtime_dir.value with
479479+ | Some dir ->
480480+ (Some (resolve_path fs home_path dir), config.runtime_dir.source)
481481+ | None ->
482482+ ( Option.map
483483+ (fun base ->
484484+ let base_path = resolve_path fs home_path base in
485485+ Eio.Path.(base_path / app_name))
486486+ (Xdg.runtime_dir xdg_ctx),
487487+ config.runtime_dir.source )
488488+ in
489489+ (* System directories - reuse shared helper *)
490490+ let config_dirs =
491491+ resolve_system_dirs fs home_path app_name "CONFIG_DIRS"
492492+ "XDG_CONFIG_DIRS" [ "/etc/xdg" ]
493493+ in
494494+ let data_dirs =
495495+ resolve_system_dirs fs home_path app_name "DATA_DIRS"
496496+ "XDG_DATA_DIRS"
497497+ [ "/usr/local/share"; "/usr/share" ]
498498+ in
499499+ ensure_dir config_dir;
500500+ ensure_dir data_dir;
501501+ ensure_dir cache_dir;
502502+ ensure_dir state_dir;
503503+ Option.iter (ensure_runtime_dir fs) runtime_dir;
504504+ let xdg =
505505+ {
506506+ app_name;
507507+ config_dir;
508508+ config_dir_source;
509509+ data_dir;
510510+ data_dir_source;
511511+ cache_dir;
512512+ cache_dir_source;
513513+ state_dir;
514514+ state_dir_source;
515515+ runtime_dir;
516516+ runtime_dir_source;
517517+ config_dirs;
518518+ data_dirs;
519519+ }
520520+ in
521521+ (* Handle --show-paths option *)
522522+ if show_paths_flag then (
523523+ let print_path name path =
524524+ match path with
525525+ | None -> Printf.printf "%s: <none>\n" name
526526+ | Some p -> Printf.printf "%s: %s\n" name (Eio.Path.native_exn p)
527527+ in
528528+ let print_paths name paths =
529529+ match paths with
530530+ | [] -> Printf.printf "%s: []\n" name
531531+ | paths ->
532532+ let paths_str =
533533+ String.concat ":" (List.map Eio.Path.native_exn paths)
534534+ in
535535+ Printf.printf "%s: %s\n" name paths_str
536536+ in
537537+ print_path "config_dir" (Some config_dir);
538538+ print_path "data_dir" (Some data_dir);
539539+ print_path "cache_dir" (Some cache_dir);
540540+ print_path "state_dir" (Some state_dir);
541541+ print_path "runtime_dir" runtime_dir;
542542+ print_paths "config_dirs" config_dirs;
543543+ print_paths "data_dirs" data_dirs;
544544+ Stdlib.exit 0);
545545+ (xdg, config))
546546+ $ show_paths $ config_dir $ data_dir $ cache_dir $ state_dir $ runtime_dir)
547547+548548+ let cache_term app_name =
549549+ let open Cmdliner in
550550+ let app_upper = String.uppercase_ascii app_name in
551551+ let app_env = app_upper ^ "_CACHE_DIR" in
552552+ let xdg_var = "XDG_CACHE_HOME" in
553553+ let home = Sys.getenv "HOME" in
554554+ let default_path = home ^ "/.cache/" ^ app_name in
555555+556556+ let doc =
557557+ Printf.sprintf
558558+ "Override cache directory. Can also be set with %s or %s. Default: %s"
559559+ app_env xdg_var default_path
560560+ in
561561+562562+ let arg =
563563+ Arg.(
564564+ value & opt string default_path
565565+ & info [ "cache-dir"; "c" ] ~docv:"DIR" ~doc)
566566+ in
567567+568568+ Term.(
569569+ const (fun cmdline_val ->
570570+ (* Check command line first *)
571571+ if cmdline_val <> default_path then cmdline_val
572572+ else
573573+ (* Then check app-specific env var *)
574574+ match Sys.getenv_opt app_env with
575575+ | Some v when v <> "" -> v
576576+ | _ -> (
577577+ (* Then check XDG env var *)
578578+ match Sys.getenv_opt xdg_var with
579579+ | Some v when v <> "" -> v ^ "/" ^ app_name
580580+ | _ -> default_path))
581581+ $ arg)
582582+583583+ let env_docs app_name =
584584+ let app_upper = String.uppercase_ascii app_name in
585585+ Printf.sprintf
586586+ {|
587587+Configuration Precedence (follows standard Unix conventions):
588588+ 1. Command-line flags (e.g., --config-dir) - highest priority
589589+ 2. Application-specific environment variable (e.g., %s_CONFIG_DIR)
590590+ 3. XDG standard environment variable (e.g., XDG_CONFIG_HOME)
591591+ 4. Default path (e.g., ~/.config/%s) - lowest priority
592592+593593+ This allows per-application overrides without affecting other XDG-compliant programs.
594594+ For example, setting %s_CONFIG_DIR only changes the config directory for %s,
595595+ while XDG_CONFIG_HOME affects all XDG-compliant applications.
596596+597597+Application-specific variables:
598598+ %s_CONFIG_DIR Override config directory for %s only
599599+ %s_DATA_DIR Override data directory for %s only
600600+ %s_CACHE_DIR Override cache directory for %s only
601601+ %s_STATE_DIR Override state directory for %s only
602602+ %s_RUNTIME_DIR Override runtime directory for %s only
603603+604604+XDG standard variables (shared by all XDG applications):
605605+ XDG_CONFIG_HOME User configuration directory (default: ~/.config/%s)
606606+ XDG_DATA_HOME User data directory (default: ~/.local/share/%s)
607607+ XDG_CACHE_HOME User cache directory (default: ~/.cache/%s)
608608+ XDG_STATE_HOME User state directory (default: ~/.local/state/%s)
609609+ XDG_RUNTIME_DIR User runtime directory (no default)
610610+ XDG_CONFIG_DIRS System configuration directories (default: /etc/xdg/%s)
611611+ XDG_DATA_DIRS System data directories (default: /usr/local/share/%s:/usr/share/%s)
612612+|}
613613+ app_upper app_name app_upper app_name app_upper app_name app_upper
614614+ app_name app_upper app_name app_upper app_name app_upper app_name app_name
615615+ app_name app_name app_name app_name app_name app_name
616616+617617+ let pp ppf config =
618618+ let pp_source ppf = function
619619+ | Default -> Fmt.(styled `Faint string) ppf "default"
620620+ | Env var ->
621621+ Fmt.pf ppf "%a" Fmt.(styled `Yellow string) ("env(" ^ var ^ ")")
622622+ | Cmdline -> Fmt.(styled `Blue string) ppf "cmdline"
623623+ in
624624+ let pp_with_source name ppf ws =
625625+ match ws.value with
626626+ | None when ws.source = Default -> ()
627627+ | None ->
628628+ Fmt.pf ppf "@,%a %a %a"
629629+ Fmt.(styled `Cyan string)
630630+ (name ^ ":")
631631+ Fmt.(styled `Red string)
632632+ "<unset>"
633633+ Fmt.(styled `Faint (brackets pp_source))
634634+ ws.source
635635+ | Some value ->
636636+ Fmt.pf ppf "@,%a %a %a"
637637+ Fmt.(styled `Cyan string)
638638+ (name ^ ":")
639639+ Fmt.(styled `Green string)
640640+ value
641641+ Fmt.(styled `Faint (brackets pp_source))
642642+ ws.source
643643+ in
644644+ Fmt.pf ppf "@[<v>%a%a%a%a%a%a@]"
645645+ Fmt.(styled `Bold string)
646646+ "XDG config:"
647647+ (pp_with_source "config_dir")
648648+ config.config_dir
649649+ (pp_with_source "data_dir")
650650+ config.data_dir
651651+ (pp_with_source "cache_dir")
652652+ config.cache_dir
653653+ (pp_with_source "state_dir")
654654+ config.state_dir
655655+ (pp_with_source "runtime_dir")
656656+ config.runtime_dir
657657+end
+428
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
1010+ Directory Specification}
1111+1212+ {b Key Concepts:}
1313+1414+ The XDG specification defines several types of directories:
1515+ - {b User directories}: Store user-specific files (config, data, cache,
1616+ state, runtime)
1717+ - {b System directories}: Store system-wide files shared across users
1818+ - {b Precedence}: User directories take precedence over system directories
1919+ - {b Application isolation}: Each application gets its own subdirectory
2020+2121+ {b Environment Variable Precedence:}
2222+2323+ This library follows a three-level precedence system:
2424+ + Application-specific variables (e.g., [MYAPP_CONFIG_DIR]) - highest
2525+ priority
2626+ + XDG standard variables (e.g., [XDG_CONFIG_HOME])
2727+ + Default paths (e.g., [$HOME/.config]) - lowest priority
2828+2929+ This allows fine-grained control over directory locations without affecting
3030+ other XDG-compliant applications.
3131+3232+ {b Directory Creation:}
3333+3434+ All directories are automatically created with appropriate permissions
3535+ (0o755) when accessed, except for runtime directories which require stricter
3636+ permissions as per the specification.
3737+3838+ @see <https://specifications.freedesktop.org/basedir-spec/latest/>
3939+ XDG Base Directory Specification *)
4040+4141+type t
4242+(** The main XDG context type containing all directory paths for an application.
4343+4444+ A value of type [t] represents the complete XDG directory structure for a
4545+ specific application, including both user-specific and system-wide
4646+ directories. All paths are resolved at creation time and are absolute paths
4747+ within the Eio filesystem. *)
4848+4949+(** {1 Exceptions} *)
5050+5151+exception Invalid_xdg_path of string
5252+(** Exception raised when XDG environment variables contain invalid paths.
5353+5454+ The XDG specification requires all paths in environment variables to be
5555+ absolute. This exception is raised when a relative path is found. *)
5656+5757+(** {1 Construction} *)
5858+5959+val create : Eio.Fs.dir_ty Eio.Path.t -> string -> t
6060+(** [create fs app_name] creates an XDG context for the given application.
6161+6262+ This function initializes the complete XDG directory structure for your
6363+ application, resolving all paths according to the environment variables and
6464+ creating directories as needed.
6565+6666+ @param fs The Eio filesystem providing filesystem access
6767+ @param app_name The name of your application (used as subdirectory name)
6868+6969+ {b Path Resolution:}
7070+7171+ For each directory type, the following precedence is used:
7272+ + Application-specific environment variable (e.g., [MYAPP_CONFIG_DIR])
7373+ + XDG standard environment variable (e.g., [XDG_CONFIG_HOME])
7474+ + Default path as specified in the XDG specification
7575+7676+ {b Example:}
7777+ {[
7878+ let xdg = Xdge.create env#fs "myapp" in
7979+ let config = Xdge.config_dir xdg in
8080+ (* config is now <fs:$HOME/.config/myapp> or the overridden path *)
8181+ ]}
8282+8383+ All directories are created with permissions 0o755 if they don't exist,
8484+ except for runtime directories which are created with 0o700 permissions and
8585+ validated according to the XDG specification.
8686+8787+ @raise Invalid_xdg_path if any environment variable contains a relative path
8888+*)
8989+9090+(** {1 Accessors} *)
9191+9292+val app_name : t -> string
9393+(** [app_name t] returns the application name used when creating this XDG
9494+ context.
9595+9696+ This is the name that was passed to {!create} and is used as the
9797+ subdirectory name within each XDG base directory. *)
9898+9999+(** {1 Base Directories} *)
100100+101101+val config_dir : t -> Eio.Fs.dir_ty Eio.Path.t
102102+(** [config_dir t] returns the path to user-specific configuration files.
103103+104104+ {b Purpose:} Store user preferences, settings, and configuration files.
105105+ Configuration files should be human-readable when possible.
106106+107107+ {b Environment Variables:}
108108+ - [${APP_NAME}_CONFIG_DIR]: Application-specific override (highest priority)
109109+ - [XDG_CONFIG_HOME]: XDG standard variable
110110+ - Default: [$HOME/.config/{app_name}]
111111+112112+ @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables>
113113+ XDG_CONFIG_HOME specification *)
114114+115115+val data_dir : t -> Eio.Fs.dir_ty Eio.Path.t
116116+(** [data_dir t] returns the path to user-specific data files.
117117+118118+ {b Purpose:} Store persistent application data that should be preserved
119119+ across application restarts and system reboots. This data is typically not
120120+ modified by users directly.
121121+122122+ {b Environment Variables:}
123123+ - [${APP_NAME}_DATA_DIR]: Application-specific override (highest priority)
124124+ - [XDG_DATA_HOME]: XDG standard variable
125125+ - Default: [$HOME/.local/share/{app_name}]
126126+127127+ {b Example Files:}
128128+ - Application databases
129129+ - User-generated content (documents, projects)
130130+ - Downloaded resources
131131+ - Application plugins or extensions
132132+133133+ @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables>
134134+ XDG_DATA_HOME specification *)
135135+136136+val cache_dir : t -> Eio.Fs.dir_ty Eio.Path.t
137137+(** [cache_dir t] returns the path to user-specific cache files.
138138+139139+ {b Purpose:} Store non-essential cached data that can be regenerated if
140140+ deleted. The application should remain functional if this directory is
141141+ cleared, though performance may be temporarily impacted.
142142+143143+ {b Environment Variables:}
144144+ - [${APP_NAME}_CACHE_DIR]: Application-specific override (highest priority)
145145+ - [XDG_CACHE_HOME]: XDG standard variable
146146+ - Default: [$HOME/.cache/{app_name}]
147147+148148+ {b Example Files:}
149149+ - Downloaded thumbnails and previews
150150+ - Compiled bytecode or object files
151151+ - Network response caches
152152+ - Temporary computation results
153153+154154+ Users may clear cache directories to free disk space, so always check for
155155+ cache validity and be prepared to regenerate data.
156156+157157+ @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables>
158158+ XDG_CACHE_HOME specification *)
159159+160160+val state_dir : t -> Eio.Fs.dir_ty Eio.Path.t
161161+(** [state_dir t] returns the path to user-specific state files.
162162+163163+ {b Purpose:} Store persistent state data that should be preserved between
164164+ application restarts but is not important enough to be user data. This
165165+ includes application state that can be regenerated but would impact the user
166166+ experience if lost.
167167+168168+ {b Environment Variables:}
169169+ - [${APP_NAME}_STATE_DIR]: Application-specific override (highest priority)
170170+ - [XDG_STATE_HOME]: XDG standard variable
171171+ - Default: [$HOME/.local/state/{app_name}]
172172+173173+ {b Example Files:}
174174+ - Application history (recently used files, command history)
175175+ - Current application state (window positions, open tabs)
176176+ - Logs and journal files
177177+ - Undo/redo history
178178+179179+ {b Comparison with other directories:}
180180+ - Unlike cache: State should persist between reboots
181181+ - Unlike data: State can be regenerated (though inconvenient)
182182+ - Unlike config: State changes frequently during normal use
183183+184184+ @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables>
185185+ XDG_STATE_HOME specification *)
186186+187187+val runtime_dir : t -> Eio.Fs.dir_ty Eio.Path.t option
188188+(** [runtime_dir t] returns the path to user-specific runtime files.
189189+190190+ {b Purpose:} Store runtime files such as sockets, named pipes, and process
191191+ IDs. These files are only valid for the duration of the user's login
192192+ session.
193193+194194+ {b Environment Variables:}
195195+ - [${APP_NAME}_RUNTIME_DIR]: Application-specific override (highest
196196+ 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 is available.
213213+ Applications should handle this gracefully, perhaps by falling back to
214214+ [/tmp] with appropriate security measures.
215215+216216+ @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables>
217217+ XDG_RUNTIME_DIR specification *)
218218+219219+(** {1 System Directories} *)
220220+221221+val config_dirs : t -> Eio.Fs.dir_ty Eio.Path.t list
222222+(** [config_dirs t] returns search paths for system-wide configuration files.
223223+224224+ {b Purpose:} Provide a search path for configuration files that are shared
225225+ between multiple users. Files in user-specific {!config_dir} take precedence
226226+ over these system directories.
227227+228228+ {b Environment Variables:}
229229+ - [${APP_NAME}_CONFIG_DIRS]: Application-specific override (highest
230230+ priority)
231231+ - [XDG_CONFIG_DIRS]: XDG standard variable (colon-separated list)
232232+ - Default: [[/etc/xdg/{app_name}]]
233233+234234+ {b Search Order:} Directories are ordered by preference, with earlier
235235+ entries taking precedence over later ones. When looking for a configuration
236236+ file, search {!config_dir} first, then each directory in this list.
237237+238238+ @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables>
239239+ XDG_CONFIG_DIRS specification *)
240240+241241+val data_dirs : t -> Eio.Fs.dir_ty Eio.Path.t list
242242+(** [data_dirs t] returns search paths for system-wide data files.
243243+244244+ {b Purpose:} Provide a search path for data files that are shared between
245245+ multiple users. Files in user-specific {!data_dir} take precedence over
246246+ these system directories.
247247+248248+ {b Environment Variables:}
249249+ - [${APP_NAME}_DATA_DIRS]: Application-specific override (highest priority)
250250+ - [XDG_DATA_DIRS]: XDG standard variable (colon-separated list)
251251+ - Default: [[/usr/local/share/{app_name}; /usr/share/{app_name}]]
252252+253253+ {b Search Order:} Directories are ordered by preference, with earlier
254254+ entries taking precedence over later ones. When looking for a data file,
255255+ search {!data_dir} first, then each directory in this list.
256256+257257+ {b Example Files:}
258258+ - Application icons and themes
259259+ - Desktop files
260260+ - Shared application resources
261261+ - Documentation files
262262+ - Default templates
263263+264264+ @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables>
265265+ XDG_DATA_DIRS specification *)
266266+267267+(** {1 File Search} *)
268268+269269+val find_config_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option
270270+(** [find_config_file t filename] searches for a configuration file following
271271+ XDG precedence.
272272+273273+ This function searches for the given filename in the user configuration
274274+ directory first, then in system configuration directories in order of
275275+ preference. Files that are inaccessible (due to permissions, non-existence,
276276+ etc.) are silently skipped as per the XDG specification.
277277+278278+ @param t The XDG context
279279+ @param filename The name of the file to search for
280280+ @return [Some path] if found, [None] if not found in any directory
281281+282282+ {b Search Order:} 1. User config directory ({!config_dir}) 2. System config
283283+ directories ({!config_dirs}) in preference order *)
284284+285285+val find_data_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option
286286+(** [find_data_file t filename] searches for a data file following XDG
287287+ precedence.
288288+289289+ This function searches for the given filename in the user data directory
290290+ first, then in system data directories in order of preference. Files that
291291+ are inaccessible (due to permissions, non-existence, etc.) are silently
292292+ skipped as per the XDG specification.
293293+294294+ @param t The XDG context
295295+ @param filename The name of the file to search for
296296+ @return [Some path] if found, [None] if not found in any directory
297297+298298+ {b Search Order:} 1. User data directory ({!data_dir}) 2. System data
299299+ directories ({!data_dirs}) in preference order *)
300300+301301+(** {1 Pretty Printing} *)
302302+303303+val pp : ?brief:bool -> ?sources:bool -> Format.formatter -> t -> unit
304304+(** [pp ?brief ?sources ppf t] pretty prints the XDG directory configuration.
305305+306306+ @param brief If [true], prints a compact one-line summary (default: [false])
307307+ @param sources
308308+ If [true], shows the source of each directory value, indicating whether it
309309+ came from defaults, environment variables, or command line (default:
310310+ [false])
311311+ @param ppf The formatter to print to
312312+ @param t The XDG context to print
313313+314314+ {b Output formats:}
315315+ - Normal: Multi-line detailed view of all directories
316316+ - Brief: Single line showing app name and key directories
317317+ - With sources: Adds annotations showing where each path came from *)
318318+319319+(** {1 Cmdliner Integration} *)
320320+321321+module Cmd : sig
322322+ (** The type of the outer XDG context *)
323323+ type xdg_t = t
324324+ (** Cmdliner integration for XDG directory configuration.
325325+326326+ This module provides integration with the Cmdliner library, allowing XDG
327327+ directories to be configured via command-line arguments while respecting
328328+ the precedence of environment variables. *)
329329+330330+ type t
331331+ (** Type of XDG configuration gathered from command-line and environment.
332332+333333+ This contains all XDG directory paths along with their sources, as
334334+ determined by command-line arguments and environment variables. *)
335335+336336+ type dir =
337337+ [ `Config (** User configuration files *)
338338+ | `Cache (** User-specific cached data *)
339339+ | `Data (** User-specific application data *)
340340+ | `State (** User-specific state data (logs, history, etc.) *)
341341+ | `Runtime (** User-specific runtime files (sockets, pipes, etc.) *) ]
342342+ (** XDG directory types for specifying which directories an application needs.
343343+344344+ These allow applications to declare which XDG directories they use,
345345+ enabling runtime systems to only provide the requested directories. *)
346346+347347+ val term :
348348+ string ->
349349+ Eio.Fs.dir_ty Eio.Path.t ->
350350+ ?dirs:dir list ->
351351+ unit ->
352352+ (xdg_t * t) Cmdliner.Term.t
353353+ (** [term app_name fs ?dirs ()] creates a Cmdliner term for XDG directory
354354+ configuration.
355355+356356+ This function generates a Cmdliner term that handles XDG directory
357357+ configuration through both command-line flags and environment variables,
358358+ and directly returns the XDG context. Only command-line flags for the
359359+ requested directories are generated.
360360+361361+ @param app_name
362362+ The application name (used for environment variable prefixes)
363363+ @param fs The Eio filesystem to use for path resolution
364364+ @param dirs
365365+ List of directories to include flags for (default: all directories)
366366+367367+ {b Generated Command-line Flags:} Only the flags for requested directories
368368+ are generated:
369369+ - [--config-dir DIR]: Override configuration directory (if [`Config] in
370370+ dirs)
371371+ - [--data-dir DIR]: Override data directory (if [`Data] in dirs)
372372+ - [--cache-dir DIR]: Override cache directory (if [`Cache] in dirs)
373373+ - [--state-dir DIR]: Override state directory (if [`State] in dirs)
374374+ - [--runtime-dir DIR]: Override runtime directory (if [`Runtime] in dirs)
375375+376376+ {b Environment Variable Precedence:} For each directory type, the
377377+ following precedence applies:
378378+ + Command-line flag (e.g., [--config-dir]) - if enabled
379379+ + Application-specific variable (e.g., [MYAPP_CONFIG_DIR])
380380+ + XDG standard variable (e.g., [XDG_CONFIG_HOME])
381381+ + Default value *)
382382+383383+ val cache_term : string -> string Cmdliner.Term.t
384384+ (** [cache_term app_name] creates a Cmdliner term that provides just the cache
385385+ directory path as a string, respecting XDG precedence.
386386+387387+ This is a convenience function for applications that only need cache
388388+ directory configuration. It returns the resolved cache directory path
389389+ directly as a string, suitable for use in other Cmdliner terms.
390390+391391+ @param app_name
392392+ The application name (used for environment variable prefixes)
393393+394394+ {b Generated Command-line Flag:}
395395+ - [--cache-dir DIR]: Override cache directory
396396+397397+ {b Environment Variable Precedence:}
398398+ + Command-line flag ([--cache-dir])
399399+ + Application-specific variable (e.g., [MYAPP_CACHE_DIR])
400400+ + XDG standard variable ([XDG_CACHE_HOME])
401401+ + Default value ([$HOME/.cache/{app_name}]) *)
402402+403403+ val env_docs : string -> string
404404+ (** [env_docs app_name] generates documentation for environment variables.
405405+406406+ Returns a formatted string documenting all environment variables that
407407+ affect XDG directory configuration for the given application. This is
408408+ useful for generating man pages or help text.
409409+410410+ @param app_name The application name
411411+ @return A formatted documentation string
412412+413413+ {b Included Information:}
414414+ - Configuration precedence rules
415415+ - Application-specific environment variables
416416+ - XDG standard environment variables
417417+ - Default values for each directory type *)
418418+419419+ val pp : Format.formatter -> t -> unit
420420+ (** [pp ppf config] pretty prints a Cmdliner configuration.
421421+422422+ This function formats the configuration showing each directory path along
423423+ with its source, which is helpful for debugging configuration issues or
424424+ displaying the current configuration to users.
425425+426426+ @param ppf The formatter to print to
427427+ @param config The configuration to print *)
428428+end