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