My agentic slop goes here. Not intended for anyone else!
1type source =
2 | Default
3 | Env of string
4 | Cmdline
5
6type t =
7 { app_name : string
8 ; config_dir : Eio.Fs.dir_ty Eio.Path.t
9 ; config_dir_source : source
10 ; data_dir : Eio.Fs.dir_ty Eio.Path.t
11 ; data_dir_source : source
12 ; cache_dir : Eio.Fs.dir_ty Eio.Path.t
13 ; cache_dir_source : source
14 ; state_dir : Eio.Fs.dir_ty Eio.Path.t
15 ; state_dir_source : source
16 ; runtime_dir : Eio.Fs.dir_ty Eio.Path.t option
17 ; runtime_dir_source : source
18 ; config_dirs : Eio.Fs.dir_ty Eio.Path.t list
19 ; data_dirs : Eio.Fs.dir_ty Eio.Path.t list
20 }
21
22type dir = [
23 | `Config
24 | `Cache
25 | `Data
26 | `State
27 | `Runtime
28]
29
30let ensure_dir ?(perm = 0o755) path = Eio.Path.mkdirs ~exists_ok:true ~perm path
31
32let validate_runtime_base_dir base_path =
33 (* Validate the base XDG_RUNTIME_DIR has correct permissions per spec *)
34 try
35 let path_str = Eio.Path.native_exn base_path in
36 let stat = Eio.Path.stat ~follow:true base_path in
37 let current_perm = stat.perm land 0o777 in
38 if current_perm <> 0o700
39 then
40 failwith
41 (Printf.sprintf
42 "XDG_RUNTIME_DIR base directory %s has incorrect permissions: %o (must be \
43 0700)"
44 path_str
45 current_perm);
46 (* Check ownership - directory should be owned by current user *)
47 let uid = Unix.getuid () in
48 if stat.uid <> Int64.of_int uid
49 then
50 failwith
51 (Printf.sprintf
52 "XDG_RUNTIME_DIR base directory %s not owned by current user (uid %d, owner \
53 %Ld)"
54 path_str
55 uid
56 stat.uid)
57 (* TODO: Check that directory is on local filesystem (not networked).
58 This would require filesystem type detection which is OS-specific. *)
59 with
60 | exn ->
61 failwith
62 (Printf.sprintf "Cannot validate XDG_RUNTIME_DIR: %s" (Printexc.to_string exn))
63;;
64
65let ensure_runtime_dir _fs app_runtime_path =
66 (* Base directory validation is done in resolve_runtime_dir,
67 so we just create the app subdirectory *)
68 ensure_dir app_runtime_path
69;;
70
71let get_home_dir fs =
72 let home_str =
73 match Sys.getenv_opt "HOME" with
74 | Some home -> home
75 | None ->
76 (match Sys.os_type with
77 | "Win32" | "Cygwin" ->
78 (match Sys.getenv_opt "USERPROFILE" with
79 | Some profile -> profile
80 | None -> failwith "Cannot determine home directory")
81 | _ ->
82 (try Unix.((getpwuid (getuid ())).pw_dir) with
83 | _ -> failwith "Cannot determine home directory"))
84 in
85 Eio.Path.(fs / home_str)
86;;
87
88let make_env_var_name app_name suffix = String.uppercase_ascii app_name ^ "_" ^ suffix
89
90exception Invalid_xdg_path of string
91
92let validate_absolute_path context path =
93 if Filename.is_relative path
94 then
95 raise
96 (Invalid_xdg_path
97 (Printf.sprintf "%s must be an absolute path, got: %s" context path))
98;;
99
100let resolve_path fs home_path base_path =
101 if Filename.is_relative base_path
102 then Eio.Path.(home_path / base_path)
103 else Eio.Path.(fs / base_path)
104;;
105
106(* Helper to resolve system directories (config_dirs or data_dirs) *)
107let resolve_system_dirs fs home_path app_name override_suffix xdg_var default_paths =
108 let override_var = make_env_var_name app_name override_suffix in
109 match Sys.getenv_opt override_var with
110 | Some dirs when dirs <> "" ->
111 String.split_on_char ':' dirs
112 |> List.filter (fun s -> s <> "")
113 |> List.filter_map (fun path ->
114 try
115 validate_absolute_path override_var path;
116 Some Eio.Path.(resolve_path fs home_path path / app_name)
117 with
118 | Invalid_xdg_path _ -> None)
119 | Some _ | None ->
120 (match Sys.getenv_opt xdg_var with
121 | Some dirs when dirs <> "" ->
122 String.split_on_char ':' dirs
123 |> List.filter (fun s -> s <> "")
124 |> List.filter_map (fun path ->
125 try
126 validate_absolute_path xdg_var path;
127 Some Eio.Path.(resolve_path fs home_path path / app_name)
128 with
129 | Invalid_xdg_path _ -> None)
130 | Some _ | None ->
131 List.map
132 (fun path -> Eio.Path.(resolve_path fs home_path path / app_name))
133 default_paths)
134;;
135
136(* Helper to resolve a user directory with override precedence *)
137let resolve_user_dir fs home_path app_name xdg_ctx xdg_getter override_suffix =
138 let override_var = make_env_var_name app_name override_suffix in
139 match Sys.getenv_opt override_var with
140 | Some dir when dir <> "" ->
141 validate_absolute_path override_var dir;
142 Eio.Path.(fs / dir / app_name), Env override_var
143 | Some _ | None ->
144 let xdg_base = xdg_getter xdg_ctx in
145 let base_path = resolve_path fs home_path xdg_base in
146 Eio.Path.(base_path / app_name), Default
147;;
148
149(* Helper to resolve runtime directory (special case since it can be None) *)
150let resolve_runtime_dir fs home_path app_name xdg_ctx =
151 let override_var = make_env_var_name app_name "RUNTIME_DIR" in
152 match Sys.getenv_opt override_var with
153 | Some dir when dir <> "" ->
154 validate_absolute_path override_var dir;
155 (* Validate the base runtime directory has correct permissions *)
156 let base_runtime_dir = resolve_path fs home_path dir in
157 validate_runtime_base_dir base_runtime_dir;
158 Some Eio.Path.(base_runtime_dir / app_name), Env override_var
159 | Some _ | None ->
160 ( (match Xdg.runtime_dir xdg_ctx with
161 | Some base ->
162 (* Validate the base runtime directory has correct permissions *)
163 let base_runtime_dir = resolve_path fs home_path base in
164 validate_runtime_base_dir base_runtime_dir;
165 Some Eio.Path.(base_runtime_dir / app_name)
166 | None -> None)
167 , Default )
168;;
169
170let validate_standard_xdg_vars () =
171 (* Validate standard XDG environment variables for absolute paths *)
172 let xdg_vars =
173 [ "XDG_CONFIG_HOME"
174 ; "XDG_DATA_HOME"
175 ; "XDG_CACHE_HOME"
176 ; "XDG_STATE_HOME"
177 ; "XDG_RUNTIME_DIR"
178 ; "XDG_CONFIG_DIRS"
179 ; "XDG_DATA_DIRS"
180 ]
181 in
182 List.iter
183 (fun var ->
184 match Sys.getenv_opt var with
185 | Some value when value <> "" ->
186 if String.contains value ':'
187 then
188 (* Colon-separated list - validate each part *)
189 String.split_on_char ':' value
190 |> List.filter (fun s -> s <> "")
191 |> List.iter (fun path -> validate_absolute_path var path)
192 else
193 (* Single path *)
194 validate_absolute_path var value
195 | _ -> ())
196 xdg_vars
197;;
198
199let create fs app_name =
200 let fs = fs in
201 let home_path = get_home_dir fs in
202 (* First validate all standard XDG environment variables *)
203 validate_standard_xdg_vars ();
204 let xdg_ctx = Xdg.create ~env:Sys.getenv_opt () in
205 (* User directories *)
206 let config_dir, config_dir_source =
207 resolve_user_dir fs home_path app_name xdg_ctx Xdg.config_dir "CONFIG_DIR"
208 in
209 let data_dir, data_dir_source =
210 resolve_user_dir fs home_path app_name xdg_ctx Xdg.data_dir "DATA_DIR"
211 in
212 let cache_dir, cache_dir_source =
213 resolve_user_dir fs home_path app_name xdg_ctx Xdg.cache_dir "CACHE_DIR"
214 in
215 let state_dir, state_dir_source =
216 resolve_user_dir fs home_path app_name xdg_ctx Xdg.state_dir "STATE_DIR"
217 in
218 (* Runtime directory *)
219 let runtime_dir, runtime_dir_source =
220 resolve_runtime_dir fs home_path app_name xdg_ctx
221 in
222 (* System directories *)
223 let config_dirs =
224 resolve_system_dirs
225 fs
226 home_path
227 app_name
228 "CONFIG_DIRS"
229 "XDG_CONFIG_DIRS"
230 [ "/etc/xdg" ]
231 in
232 let data_dirs =
233 resolve_system_dirs
234 fs
235 home_path
236 app_name
237 "DATA_DIRS"
238 "XDG_DATA_DIRS"
239 [ "/usr/local/share"; "/usr/share" ]
240 in
241 ensure_dir config_dir;
242 ensure_dir data_dir;
243 ensure_dir cache_dir;
244 ensure_dir state_dir;
245 Option.iter (ensure_runtime_dir fs) runtime_dir;
246 { app_name
247 ; config_dir
248 ; config_dir_source
249 ; data_dir
250 ; data_dir_source
251 ; cache_dir
252 ; cache_dir_source
253 ; state_dir
254 ; state_dir_source
255 ; runtime_dir
256 ; runtime_dir_source
257 ; config_dirs
258 ; data_dirs
259 }
260;;
261
262let app_name t = t.app_name
263let config_dir t = t.config_dir
264let data_dir t = t.data_dir
265let cache_dir t = t.cache_dir
266let state_dir t = t.state_dir
267let runtime_dir t = t.runtime_dir
268let config_dirs t = t.config_dirs
269let data_dirs t = t.data_dirs
270
271(* File search following XDG specification *)
272let find_file_in_dirs dirs filename =
273 let rec search_dirs = function
274 | [] -> None
275 | dir :: remaining_dirs ->
276 let file_path = Eio.Path.(dir / filename) in
277 (try
278 (* Try to check if file exists and is readable *)
279 let _ = Eio.Path.stat ~follow:true file_path in
280 Some file_path
281 with
282 | _ ->
283 (* File is inaccessible (non-existent, permissions, etc.)
284 Skip and continue with next directory per XDG spec *)
285 search_dirs remaining_dirs)
286 in
287 search_dirs dirs
288;;
289
290let find_config_file t filename =
291 (* Search user config dir first, then system config dirs *)
292 find_file_in_dirs (t.config_dir :: t.config_dirs) filename
293;;
294
295let find_data_file t filename =
296 (* Search user data dir first, then system data dirs *)
297 find_file_in_dirs (t.data_dir :: t.data_dirs) filename
298;;
299
300let pp ?(brief = false) ?(sources = false) ppf t =
301 let pp_source ppf = function
302 | Default -> Fmt.(styled `Faint string) ppf "default"
303 | Env var -> Fmt.pf ppf "%a" Fmt.(styled `Yellow string) ("env(" ^ var ^ ")")
304 | Cmdline -> Fmt.(styled `Blue string) ppf "cmdline"
305 in
306 let pp_path_with_source ppf path source =
307 if sources
308 then
309 Fmt.pf
310 ppf
311 "%a %a"
312 Fmt.(styled `Green Eio.Path.pp)
313 path
314 Fmt.(styled `Faint (brackets pp_source))
315 source
316 else Fmt.(styled `Green Eio.Path.pp) ppf path
317 in
318 let pp_path_opt_with_source ppf path_opt source =
319 match path_opt with
320 | None ->
321 if sources
322 then
323 Fmt.pf
324 ppf
325 "%a %a"
326 Fmt.(styled `Red string)
327 "<none>"
328 Fmt.(styled `Faint (brackets pp_source))
329 source
330 else Fmt.(styled `Red string) ppf "<none>"
331 | Some path -> pp_path_with_source ppf path source
332 in
333 let pp_paths ppf paths =
334 Fmt.(list ~sep:(any ";@ ") (styled `Green Eio.Path.pp)) ppf paths
335 in
336 if brief
337 then
338 Fmt.pf
339 ppf
340 "%a config=%a data=%a>"
341 Fmt.(styled `Cyan string)
342 ("<xdg:" ^ t.app_name)
343 (fun ppf (path, source) -> pp_path_with_source ppf path source)
344 (t.config_dir, t.config_dir_source)
345 (fun ppf (path, source) -> pp_path_with_source ppf path source)
346 (t.data_dir, t.data_dir_source)
347 else (
348 Fmt.pf
349 ppf
350 "@[<v>%a@,"
351 Fmt.(styled `Bold string)
352 ("XDG directories for '" ^ t.app_name ^ "':");
353 Fmt.pf ppf "@[<v 2>%a@," Fmt.(styled `Bold string) "User directories:";
354 Fmt.pf
355 ppf
356 "%a %a@,"
357 Fmt.(styled `Cyan string)
358 "config:"
359 (fun ppf (path, source) -> pp_path_with_source ppf path source)
360 (t.config_dir, t.config_dir_source);
361 Fmt.pf
362 ppf
363 "%a %a@,"
364 Fmt.(styled `Cyan string)
365 "data:"
366 (fun ppf (path, source) -> pp_path_with_source ppf path source)
367 (t.data_dir, t.data_dir_source);
368 Fmt.pf
369 ppf
370 "%a %a@,"
371 Fmt.(styled `Cyan string)
372 "cache:"
373 (fun ppf (path, source) -> pp_path_with_source ppf path source)
374 (t.cache_dir, t.cache_dir_source);
375 Fmt.pf
376 ppf
377 "%a %a@,"
378 Fmt.(styled `Cyan string)
379 "state:"
380 (fun ppf (path, source) -> pp_path_with_source ppf path source)
381 (t.state_dir, t.state_dir_source);
382 Fmt.pf
383 ppf
384 "%a %a@]@,"
385 Fmt.(styled `Cyan string)
386 "runtime:"
387 (fun ppf (path_opt, source) -> pp_path_opt_with_source ppf path_opt source)
388 (t.runtime_dir, t.runtime_dir_source);
389 Fmt.pf ppf "@[<v 2>%a@," Fmt.(styled `Bold string) "System directories:";
390 Fmt.pf
391 ppf
392 "%a [@[<hov>%a@]]@,"
393 Fmt.(styled `Cyan string)
394 "config_dirs:"
395 pp_paths
396 t.config_dirs;
397 Fmt.pf
398 ppf
399 "%a [@[<hov>%a@]]@]@]"
400 Fmt.(styled `Cyan string)
401 "data_dirs:"
402 pp_paths
403 t.data_dirs)
404;;
405
406module Cmd = struct
407 type xdg_t = t
408
409 type 'a with_source =
410 { value : 'a option
411 ; source : source
412 }
413
414 type t =
415 { config_dir : string with_source
416 ; data_dir : string with_source
417 ; cache_dir : string with_source
418 ; state_dir : string with_source
419 ; runtime_dir : string with_source
420 }
421
422 let term app_name fs
423 ?(dirs=[`Config; `Data; `Cache; `State; `Runtime]) () =
424 let open Cmdliner in
425 let app_upper = String.uppercase_ascii app_name in
426 let show_paths =
427 let doc = "Show only the resolved directory paths without formatting" in
428 Arg.(value & flag & info [ "show-paths" ] ~doc)
429 in
430 let has_dir d = List.mem d dirs in
431 let make_dir_arg ~enabled name env_suffix xdg_var default_path =
432 if not enabled then
433 (* Return a term that always gives the environment-only result *)
434 Term.(const (fun () ->
435 let app_env = app_upper ^ "_" ^ env_suffix in
436 match Sys.getenv_opt app_env with
437 | Some v when v <> "" -> { value = Some v; source = Env app_env }
438 | Some _ | None ->
439 (match Sys.getenv_opt xdg_var with
440 | Some v -> { value = Some v; source = Env xdg_var }
441 | None -> { value = None; source = Default }))
442 $ const ())
443 else
444 let app_env = app_upper ^ "_" ^ env_suffix in
445 let doc =
446 match default_path with
447 | Some path ->
448 Printf.sprintf
449 "Override %s directory. Can also be set with %s or %s. Default: %s"
450 name
451 app_env
452 xdg_var
453 path
454 | None ->
455 Printf.sprintf
456 "Override %s directory. Can also be set with %s or %s. No default value."
457 name
458 app_env
459 xdg_var
460 in
461 let arg =
462 Arg.(value & opt (some string) None & info [ name ^ "-dir" ] ~docv:"DIR" ~doc)
463 in
464 Term.(
465 const (fun cmdline_val ->
466 match cmdline_val with
467 | Some v -> { value = Some v; source = Cmdline }
468 | None ->
469 (match Sys.getenv_opt app_env with
470 | Some v when v <> "" -> { value = Some v; source = Env app_env }
471 | Some _ | None ->
472 (match Sys.getenv_opt xdg_var with
473 | Some v -> { value = Some v; source = Env xdg_var }
474 | None -> { value = None; source = Default })))
475 $ arg)
476 in
477 let home_prefix = "\\$HOME" in
478 let config_dir =
479 make_dir_arg
480 ~enabled:(has_dir `Config)
481 "config"
482 "CONFIG_DIR"
483 "XDG_CONFIG_HOME"
484 (Some (home_prefix ^ "/.config/" ^ app_name))
485 in
486 let data_dir =
487 make_dir_arg
488 ~enabled:(has_dir `Data)
489 "data"
490 "DATA_DIR"
491 "XDG_DATA_HOME"
492 (Some (home_prefix ^ "/.local/share/" ^ app_name))
493 in
494 let cache_dir =
495 make_dir_arg
496 ~enabled:(has_dir `Cache)
497 "cache"
498 "CACHE_DIR"
499 "XDG_CACHE_HOME"
500 (Some (home_prefix ^ "/.cache/" ^ app_name))
501 in
502 let state_dir =
503 make_dir_arg
504 ~enabled:(has_dir `State)
505 "state"
506 "STATE_DIR"
507 "XDG_STATE_HOME"
508 (Some (home_prefix ^ "/.local/state/" ^ app_name))
509 in
510 let runtime_dir = make_dir_arg ~enabled:(has_dir `Runtime) "runtime" "RUNTIME_DIR" "XDG_RUNTIME_DIR" None in
511 Term.(
512 const
513 (fun
514 show_paths_flag
515 config_dir_ws
516 data_dir_ws
517 cache_dir_ws
518 state_dir_ws
519 runtime_dir_ws
520 ->
521 let config =
522 { config_dir = config_dir_ws
523 ; data_dir = data_dir_ws
524 ; cache_dir = cache_dir_ws
525 ; state_dir = state_dir_ws
526 ; runtime_dir = runtime_dir_ws
527 }
528 in
529 let home_path = get_home_dir fs in
530 (* First validate all standard XDG environment variables *)
531 validate_standard_xdg_vars ();
532 let xdg_ctx = Xdg.create ~env:Sys.getenv_opt () in
533 (* Helper to resolve directory from config with source tracking *)
534 let resolve_from_config config_ws xdg_getter =
535 match config_ws.value with
536 | Some dir -> resolve_path fs home_path dir, config_ws.source
537 | None ->
538 let xdg_base = xdg_getter xdg_ctx in
539 let base_path = resolve_path fs home_path xdg_base in
540 Eio.Path.(base_path / app_name), config_ws.source
541 in
542 (* User directories *)
543 let config_dir, config_dir_source =
544 resolve_from_config config.config_dir Xdg.config_dir
545 in
546 let data_dir, data_dir_source =
547 resolve_from_config config.data_dir Xdg.data_dir
548 in
549 let cache_dir, cache_dir_source =
550 resolve_from_config config.cache_dir Xdg.cache_dir
551 in
552 let state_dir, state_dir_source =
553 resolve_from_config config.state_dir Xdg.state_dir
554 in
555 (* Runtime directory *)
556 let runtime_dir, runtime_dir_source =
557 match config.runtime_dir.value with
558 | Some dir -> Some (resolve_path fs home_path dir), config.runtime_dir.source
559 | None ->
560 ( Option.map
561 (fun base ->
562 let base_path = resolve_path fs home_path base in
563 Eio.Path.(base_path / app_name))
564 (Xdg.runtime_dir xdg_ctx)
565 , config.runtime_dir.source )
566 in
567 (* System directories - reuse shared helper *)
568 let config_dirs =
569 resolve_system_dirs
570 fs
571 home_path
572 app_name
573 "CONFIG_DIRS"
574 "XDG_CONFIG_DIRS"
575 [ "/etc/xdg" ]
576 in
577 let data_dirs =
578 resolve_system_dirs
579 fs
580 home_path
581 app_name
582 "DATA_DIRS"
583 "XDG_DATA_DIRS"
584 [ "/usr/local/share"; "/usr/share" ]
585 in
586 ensure_dir config_dir;
587 ensure_dir data_dir;
588 ensure_dir cache_dir;
589 ensure_dir state_dir;
590 Option.iter (ensure_runtime_dir fs) runtime_dir;
591 let xdg =
592 { app_name
593 ; config_dir
594 ; config_dir_source
595 ; data_dir
596 ; data_dir_source
597 ; cache_dir
598 ; cache_dir_source
599 ; state_dir
600 ; state_dir_source
601 ; runtime_dir
602 ; runtime_dir_source
603 ; config_dirs
604 ; data_dirs
605 }
606 in
607 (* Handle --show-paths option *)
608 if show_paths_flag
609 then (
610 let print_path name path =
611 match path with
612 | None -> Printf.printf "%s: <none>\n" name
613 | Some p -> Printf.printf "%s: %s\n" name (Eio.Path.native_exn p)
614 in
615 let print_paths name paths =
616 match paths with
617 | [] -> Printf.printf "%s: []\n" name
618 | paths ->
619 let paths_str = String.concat ":" (List.map Eio.Path.native_exn paths) in
620 Printf.printf "%s: %s\n" name paths_str
621 in
622 print_path "config_dir" (Some config_dir);
623 print_path "data_dir" (Some data_dir);
624 print_path "cache_dir" (Some cache_dir);
625 print_path "state_dir" (Some state_dir);
626 print_path "runtime_dir" runtime_dir;
627 print_paths "config_dirs" config_dirs;
628 print_paths "data_dirs" data_dirs;
629 Stdlib.exit 0);
630 xdg, config)
631 $ show_paths
632 $ config_dir
633 $ data_dir
634 $ cache_dir
635 $ state_dir
636 $ runtime_dir)
637 ;;
638
639 let cache_term app_name =
640 let open Cmdliner in
641 let app_upper = String.uppercase_ascii app_name in
642 let app_env = app_upper ^ "_CACHE_DIR" in
643 let xdg_var = "XDG_CACHE_HOME" in
644 let home = Sys.getenv "HOME" in
645 let default_path = home ^ "/.cache/" ^ app_name in
646
647 let doc =
648 Printf.sprintf
649 "Override cache directory. Can also be set with %s or %s. Default: %s"
650 app_env xdg_var default_path
651 in
652
653 let arg = Arg.(value & opt string default_path & info ["cache-dir"; "c"] ~docv:"DIR" ~doc) in
654
655 Term.(const (fun cmdline_val ->
656 (* Check command line first *)
657 if cmdline_val <> default_path then
658 cmdline_val
659 else
660 (* Then check app-specific env var *)
661 match Sys.getenv_opt app_env with
662 | Some v when v <> "" -> v
663 | _ ->
664 (* Then check XDG env var *)
665 match Sys.getenv_opt xdg_var with
666 | Some v when v <> "" -> v ^ "/" ^ app_name
667 | _ -> default_path
668 ) $ arg)
669 ;;
670
671 let env_docs app_name =
672 let app_upper = String.uppercase_ascii app_name in
673 Printf.sprintf
674 {|
675Configuration Precedence (follows standard Unix conventions):
676 1. Command-line flags (e.g., --config-dir) - highest priority
677 2. Application-specific environment variable (e.g., %s_CONFIG_DIR)
678 3. XDG standard environment variable (e.g., XDG_CONFIG_HOME)
679 4. Default path (e.g., ~/.config/%s) - lowest priority
680
681 This allows per-application overrides without affecting other XDG-compliant programs.
682 For example, setting %s_CONFIG_DIR only changes the config directory for %s,
683 while XDG_CONFIG_HOME affects all XDG-compliant applications.
684
685Application-specific variables:
686 %s_CONFIG_DIR Override config directory for %s only
687 %s_DATA_DIR Override data directory for %s only
688 %s_CACHE_DIR Override cache directory for %s only
689 %s_STATE_DIR Override state directory for %s only
690 %s_RUNTIME_DIR Override runtime directory for %s only
691
692XDG standard variables (shared by all XDG applications):
693 XDG_CONFIG_HOME User configuration directory (default: ~/.config/%s)
694 XDG_DATA_HOME User data directory (default: ~/.local/share/%s)
695 XDG_CACHE_HOME User cache directory (default: ~/.cache/%s)
696 XDG_STATE_HOME User state directory (default: ~/.local/state/%s)
697 XDG_RUNTIME_DIR User runtime directory (no default)
698 XDG_CONFIG_DIRS System configuration directories (default: /etc/xdg/%s)
699 XDG_DATA_DIRS System data directories (default: /usr/local/share/%s:/usr/share/%s)
700|}
701 app_upper
702 app_name
703 app_upper
704 app_name
705 app_upper
706 app_name
707 app_upper
708 app_name
709 app_upper
710 app_name
711 app_upper
712 app_name
713 app_upper
714 app_name
715 app_name
716 app_name
717 app_name
718 app_name
719 app_name
720 app_name
721 app_name
722 ;;
723
724 let pp ppf config =
725 let pp_source ppf = function
726 | Default -> Fmt.(styled `Faint string) ppf "default"
727 | Env var -> Fmt.pf ppf "%a" Fmt.(styled `Yellow string) ("env(" ^ var ^ ")")
728 | Cmdline -> Fmt.(styled `Blue string) ppf "cmdline"
729 in
730 let pp_with_source name ppf ws =
731 match ws.value with
732 | None when ws.source = Default -> ()
733 | None ->
734 Fmt.pf
735 ppf
736 "@,%a %a %a"
737 Fmt.(styled `Cyan string)
738 (name ^ ":")
739 Fmt.(styled `Red string)
740 "<unset>"
741 Fmt.(styled `Faint (brackets pp_source))
742 ws.source
743 | Some value ->
744 Fmt.pf
745 ppf
746 "@,%a %a %a"
747 Fmt.(styled `Cyan string)
748 (name ^ ":")
749 Fmt.(styled `Green string)
750 value
751 Fmt.(styled `Faint (brackets pp_source))
752 ws.source
753 in
754 Fmt.pf
755 ppf
756 "@[<v>%a%a%a%a%a%a@]"
757 Fmt.(styled `Bold string)
758 "XDG config:"
759 (pp_with_source "config_dir")
760 config.config_dir
761 (pp_with_source "data_dir")
762 config.data_dir
763 (pp_with_source "cache_dir")
764 config.cache_dir
765 (pp_with_source "state_dir")
766 config.state_dir
767 (pp_with_source "runtime_dir")
768 config.runtime_dir
769 ;;
770end