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