XDG library path support for OCaml via Eio capabilities
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(xdge): pass all merlint checks (0 issues)

- Extract pp_source/pp_path_with_source/pp_path_opt_with_source/pp_paths
to top-level to fix E005 on pp (76→34 lines)
- Extract resolve_dir_from_config/resolve_env_var/print_path/print_paths
to Cmd module level to fix E010 deep nesting in term (depth 5→3)
- Rename create→v, find_config_file→config_file, find_data_file→data_file,
find_file_in_dirs→file_in_dirs (E331/E332)
- Fix E410: add missing periods to all doc strings in xdge.mli
- Fix E105: replace catch-all with Eio.Io in test_xdge.ml
- Add test_xdge.ml/mli, test.ml, update dune for E605
- Add suite to test_paths for E600/E621

+259 -175
+96 -112
lib/xdge.ml
··· 54 54 so we just create the app subdirectory *) 55 55 ensure_dir app_runtime_path 56 56 57 - let get_home_dir fs = 57 + let home_dir fs = 58 58 let home_str = 59 59 match Sys.getenv_opt "HOME" with 60 60 | Some home -> home ··· 66 66 | None -> failwith "Cannot determine home directory") 67 67 | _ -> ( 68 68 try Unix.((getpwuid (getuid ())).pw_dir) 69 - with _ -> failwith "Cannot determine home directory")) 69 + with Unix.Unix_error _ -> 70 + failwith "Cannot determine home directory")) 70 71 in 71 72 Eio.Path.(fs / home_str) 72 73 73 - let home_dir = get_home_dir 74 - 75 - let make_env_var_name app_name suffix = 74 + let env_var_name app_name suffix = 76 75 String.uppercase_ascii app_name ^ "_" ^ suffix 77 76 78 77 exception Invalid_xdg_path of string ··· 99 98 Some Eio.Path.(resolve_path fs home_path path / app_name) 100 99 with Invalid_xdg_path _ -> None) 101 100 in 102 - let override_var = make_env_var_name app_name override_suffix in 101 + let override_var = env_var_name app_name override_suffix in 103 102 match Sys.getenv_opt override_var with 104 103 | Some dirs when dirs <> "" -> parse_dir_list override_var dirs 105 104 | Some _ | None -> ( ··· 112 111 113 112 (* Helper to resolve a user directory with override precedence *) 114 113 let resolve_user_dir fs home_path app_name xdg_ctx xdg_getter override_suffix = 115 - let override_var = make_env_var_name app_name override_suffix in 114 + let override_var = env_var_name app_name override_suffix in 116 115 match Sys.getenv_opt override_var with 117 116 | Some dir when dir <> "" -> 118 117 validate_absolute_path override_var dir; ··· 124 123 125 124 (* Helper to resolve runtime directory (special case since it can be None) *) 126 125 let resolve_runtime_dir fs home_path app_name xdg_ctx = 127 - let override_var = make_env_var_name app_name "RUNTIME_DIR" in 126 + let override_var = env_var_name app_name "RUNTIME_DIR" in 128 127 match Sys.getenv_opt override_var with 129 128 | Some dir when dir <> "" -> 130 129 validate_absolute_path override_var dir; ··· 170 169 | _ -> ()) 171 170 xdg_vars 172 171 173 - let create fs app_name = 174 - let fs = fs in 175 - let home_path = get_home_dir fs in 172 + let v fs app_name = 173 + let home_path = home_dir fs in 176 174 (* First validate all standard XDG environment variables *) 177 175 validate_standard_xdg_vars (); 178 176 let xdg_ctx = Xdg.create ~env:Sys.getenv_opt () in ··· 239 237 | _ -> false 240 238 241 239 (* File search following XDG specification *) 242 - let find_file_in_dirs dirs filename = 240 + let file_in_dirs dirs filename = 243 241 let rec search_dirs = function 244 242 | [] -> None 245 243 | dir :: remaining_dirs -> ( ··· 255 253 in 256 254 search_dirs dirs 257 255 258 - let find_config_file t filename = 256 + let config_file t filename = 259 257 (* Search user config dir first, then system config dirs *) 260 - find_file_in_dirs (t.config_dir :: t.config_dirs) filename 258 + file_in_dirs (t.config_dir :: t.config_dirs) filename 261 259 262 - let find_data_file t filename = 260 + let data_file t filename = 263 261 (* Search user data dir first, then system data dirs *) 264 - find_file_in_dirs (t.data_dir :: t.data_dirs) filename 262 + file_in_dirs (t.data_dir :: t.data_dirs) filename 263 + 264 + let pp_source ppf = function 265 + | Default -> Fmt.(styled `Faint string) ppf "default" 266 + | Env var -> Fmt.pf ppf "%a" Fmt.(styled `Yellow string) ("env(" ^ var ^ ")") 267 + | Cmdline -> Fmt.(styled `Blue string) ppf "cmdline" 268 + 269 + let pp_path_with_source ~sources ppf path source = 270 + if sources then 271 + Fmt.pf ppf "%a %a" 272 + Fmt.(styled `Green Eio.Path.pp) 273 + path 274 + Fmt.(styled `Faint (brackets pp_source)) 275 + source 276 + else Fmt.(styled `Green Eio.Path.pp) ppf path 277 + 278 + let pp_path_opt_with_source ~sources ppf path_opt source = 279 + match path_opt with 280 + | None -> 281 + if sources then 282 + Fmt.pf ppf "%a %a" 283 + Fmt.(styled `Red string) 284 + "<none>" 285 + Fmt.(styled `Faint (brackets pp_source)) 286 + source 287 + else Fmt.(styled `Red string) ppf "<none>" 288 + | Some path -> pp_path_with_source ~sources ppf path source 289 + 290 + let pp_paths ppf paths = 291 + Fmt.(list ~sep:(any ";@ ") (styled `Green Eio.Path.pp)) ppf paths 265 292 266 293 let pp ?(brief = false) ?(sources = false) ppf t = 267 - let pp_source ppf = function 268 - | Default -> Fmt.(styled `Faint string) ppf "default" 269 - | Env var -> Fmt.pf ppf "%a" Fmt.(styled `Yellow string) ("env(" ^ var ^ ")") 270 - | Cmdline -> Fmt.(styled `Blue string) ppf "cmdline" 271 - in 272 - let pp_path_with_source ppf path source = 273 - if sources then 274 - Fmt.pf ppf "%a %a" 275 - Fmt.(styled `Green Eio.Path.pp) 276 - path 277 - Fmt.(styled `Faint (brackets pp_source)) 278 - source 279 - else Fmt.(styled `Green Eio.Path.pp) ppf path 280 - in 281 - let pp_path_opt_with_source ppf path_opt source = 282 - match path_opt with 283 - | None -> 284 - if sources then 285 - Fmt.pf ppf "%a %a" 286 - Fmt.(styled `Red string) 287 - "<none>" 288 - Fmt.(styled `Faint (brackets pp_source)) 289 - source 290 - else Fmt.(styled `Red string) ppf "<none>" 291 - | Some path -> pp_path_with_source ppf path source 292 - in 293 - let pp_paths ppf paths = 294 - Fmt.(list ~sep:(any ";@ ") (styled `Green Eio.Path.pp)) ppf paths 295 - in 296 294 if brief then 297 295 Fmt.pf ppf "%a config=%a data=%a>" 298 296 Fmt.(styled `Cyan string) 299 297 ("<xdg:" ^ t.app_name) 300 - (fun ppf (path, source) -> pp_path_with_source ppf path source) 298 + (fun ppf (path, source) -> pp_path_with_source ~sources ppf path source) 301 299 (t.config_dir, t.config_dir_source) 302 - (fun ppf (path, source) -> pp_path_with_source ppf path source) 300 + (fun ppf (path, source) -> pp_path_with_source ~sources ppf path source) 303 301 (t.data_dir, t.data_dir_source) 304 302 else ( 305 303 Fmt.pf ppf "@[<v>%a@," ··· 309 307 Fmt.pf ppf "%a %a@," 310 308 Fmt.(styled `Cyan string) 311 309 "config:" 312 - (fun ppf (path, source) -> pp_path_with_source ppf path source) 310 + (fun ppf (path, source) -> pp_path_with_source ~sources ppf path source) 313 311 (t.config_dir, t.config_dir_source); 314 312 Fmt.pf ppf "%a %a@," 315 313 Fmt.(styled `Cyan string) 316 314 "data:" 317 - (fun ppf (path, source) -> pp_path_with_source ppf path source) 315 + (fun ppf (path, source) -> pp_path_with_source ~sources ppf path source) 318 316 (t.data_dir, t.data_dir_source); 319 317 Fmt.pf ppf "%a %a@," 320 318 Fmt.(styled `Cyan string) 321 319 "cache:" 322 - (fun ppf (path, source) -> pp_path_with_source ppf path source) 320 + (fun ppf (path, source) -> pp_path_with_source ~sources ppf path source) 323 321 (t.cache_dir, t.cache_dir_source); 324 322 Fmt.pf ppf "%a %a@," 325 323 Fmt.(styled `Cyan string) 326 324 "state:" 327 - (fun ppf (path, source) -> pp_path_with_source ppf path source) 325 + (fun ppf (path, source) -> pp_path_with_source ~sources ppf path source) 328 326 (t.state_dir, t.state_dir_source); 329 327 Fmt.pf ppf "%a %a@]@," 330 328 Fmt.(styled `Cyan string) 331 329 "runtime:" 332 330 (fun ppf (path_opt, source) -> 333 - pp_path_opt_with_source ppf path_opt source) 331 + pp_path_opt_with_source ~sources ppf path_opt source) 334 332 (t.runtime_dir, t.runtime_dir_source); 335 333 Fmt.pf ppf "@[<v 2>%a@," Fmt.(styled `Bold string) "System directories:"; 336 334 Fmt.pf ppf "%a [@[<hov>%a@]]@," ··· 344 342 type xdg_t = t 345 343 type 'a with_source = { value : 'a option; source : source } 346 344 345 + let resolve_dir_from_config fs home_path xdg_ctx app_name config_ws xdg_getter 346 + = 347 + match config_ws.value with 348 + | Some dir -> (resolve_path fs home_path dir, config_ws.source) 349 + | None -> 350 + let xdg_base = xdg_getter xdg_ctx in 351 + let base_path = resolve_path fs home_path xdg_base in 352 + (Eio.Path.(base_path / app_name), config_ws.source) 353 + 354 + let resolve_env_var app_upper env_suffix xdg_var = 355 + let app_env = app_upper ^ "_" ^ env_suffix in 356 + match Sys.getenv_opt app_env with 357 + | Some v when v <> "" -> { value = Some v; source = Env app_env } 358 + | Some _ | None -> ( 359 + match Sys.getenv_opt xdg_var with 360 + | Some v -> { value = Some v; source = Env xdg_var } 361 + | None -> { value = None; source = Default }) 362 + 363 + let print_path name = function 364 + | None -> Fmt.pr "%s: <none>@." name 365 + | Some p -> Fmt.pr "%s: %s@." name (Eio.Path.native_exn p) 366 + 367 + let print_paths name paths = 368 + match paths with 369 + | [] -> Fmt.pr "%s: []@." name 370 + | _ -> 371 + let s = String.concat ":" (List.map Eio.Path.native_exn paths) in 372 + Fmt.pr "%s: %s@." name s 373 + 347 374 type t = { 348 375 config_dir : string with_source; 349 376 data_dir : string with_source; ··· 363 390 Arg.(value & flag & info [ "show-paths" ] ~doc) 364 391 in 365 392 let has_dir d = List.mem d dirs in 366 - let make_dir_arg ~enabled name env_suffix xdg_var default_path = 393 + let dir_arg ~enabled name env_suffix xdg_var default_path = 367 394 if not enabled then 368 - (* Return a term that always gives the environment-only result *) 369 395 Term.( 370 - const (fun () -> 371 - let app_env = app_upper ^ "_" ^ env_suffix in 372 - match Sys.getenv_opt app_env with 373 - | Some v when v <> "" -> { value = Some v; source = Env app_env } 374 - | Some _ | None -> ( 375 - match Sys.getenv_opt xdg_var with 376 - | Some v -> { value = Some v; source = Env xdg_var } 377 - | None -> { value = None; source = Default })) 396 + const (fun () -> resolve_env_var app_upper env_suffix xdg_var) 378 397 $ const ()) 379 398 else 380 399 let app_env = app_upper ^ "_" ^ env_suffix in ··· 401 420 const (fun cmdline_val -> 402 421 match cmdline_val with 403 422 | Some v -> { value = Some v; source = Cmdline } 404 - | None -> ( 405 - match Sys.getenv_opt app_env with 406 - | Some v when v <> "" -> 407 - { value = Some v; source = Env app_env } 408 - | Some _ | None -> ( 409 - match Sys.getenv_opt xdg_var with 410 - | Some v -> { value = Some v; source = Env xdg_var } 411 - | None -> { value = None; source = Default }))) 423 + | None -> resolve_env_var app_upper env_suffix xdg_var) 412 424 $ arg) 413 425 in 414 426 let home_prefix = "\\$HOME" in 415 427 let config_dir = 416 - make_dir_arg ~enabled:(has_dir `Config) "config" "CONFIG_DIR" 417 - "XDG_CONFIG_HOME" 428 + dir_arg ~enabled:(has_dir `Config) "config" "CONFIG_DIR" "XDG_CONFIG_HOME" 418 429 (Some (home_prefix ^ "/.config/" ^ app_name)) 419 430 in 420 431 let data_dir = 421 - make_dir_arg ~enabled:(has_dir `Data) "data" "DATA_DIR" "XDG_DATA_HOME" 432 + dir_arg ~enabled:(has_dir `Data) "data" "DATA_DIR" "XDG_DATA_HOME" 422 433 (Some (home_prefix ^ "/.local/share/" ^ app_name)) 423 434 in 424 435 let cache_dir = 425 - make_dir_arg ~enabled:(has_dir `Cache) "cache" "CACHE_DIR" 426 - "XDG_CACHE_HOME" 436 + dir_arg ~enabled:(has_dir `Cache) "cache" "CACHE_DIR" "XDG_CACHE_HOME" 427 437 (Some (home_prefix ^ "/.cache/" ^ app_name)) 428 438 in 429 439 let state_dir = 430 - make_dir_arg ~enabled:(has_dir `State) "state" "STATE_DIR" 431 - "XDG_STATE_HOME" 440 + dir_arg ~enabled:(has_dir `State) "state" "STATE_DIR" "XDG_STATE_HOME" 432 441 (Some (home_prefix ^ "/.local/state/" ^ app_name)) 433 442 in 434 443 let runtime_dir = 435 - make_dir_arg ~enabled:(has_dir `Runtime) "runtime" "RUNTIME_DIR" 444 + dir_arg ~enabled:(has_dir `Runtime) "runtime" "RUNTIME_DIR" 436 445 "XDG_RUNTIME_DIR" None 437 446 in 438 447 Term.( ··· 454 463 runtime_dir = runtime_dir_ws; 455 464 } 456 465 in 457 - let home_path = get_home_dir fs in 466 + let home_path = home_dir fs in 458 467 (* First validate all standard XDG environment variables *) 459 468 validate_standard_xdg_vars (); 460 469 let xdg_ctx = Xdg.create ~env:Sys.getenv_opt () in 461 - (* Helper to resolve directory from config with source tracking *) 462 - let resolve_from_config config_ws xdg_getter = 463 - match config_ws.value with 464 - | Some dir -> (resolve_path fs home_path dir, config_ws.source) 465 - | None -> 466 - let xdg_base = xdg_getter xdg_ctx in 467 - let base_path = resolve_path fs home_path xdg_base in 468 - (Eio.Path.(base_path / app_name), config_ws.source) 469 - in 470 470 (* User directories *) 471 471 let config_dir, config_dir_source = 472 - resolve_from_config config.config_dir Xdg.config_dir 472 + resolve_dir_from_config fs home_path xdg_ctx app_name 473 + config.config_dir Xdg.config_dir 473 474 in 474 475 let data_dir, data_dir_source = 475 - resolve_from_config config.data_dir Xdg.data_dir 476 + resolve_dir_from_config fs home_path xdg_ctx app_name 477 + config.data_dir Xdg.data_dir 476 478 in 477 479 let cache_dir, cache_dir_source = 478 - resolve_from_config config.cache_dir Xdg.cache_dir 480 + resolve_dir_from_config fs home_path xdg_ctx app_name 481 + config.cache_dir Xdg.cache_dir 479 482 in 480 483 let state_dir, state_dir_source = 481 - resolve_from_config config.state_dir Xdg.state_dir 484 + resolve_dir_from_config fs home_path xdg_ctx app_name 485 + config.state_dir Xdg.state_dir 482 486 in 483 487 (* Runtime directory *) 484 488 let runtime_dir, runtime_dir_source = ··· 527 531 in 528 532 (* Handle --show-paths option *) 529 533 if show_paths_flag then ( 530 - let print_path name path = 531 - match path with 532 - | None -> Printf.printf "%s: <none>\n" name 533 - | Some p -> Printf.printf "%s: %s\n" name (Eio.Path.native_exn p) 534 - in 535 - let print_paths name paths = 536 - match paths with 537 - | [] -> Printf.printf "%s: []\n" name 538 - | paths -> 539 - let paths_str = 540 - String.concat ":" (List.map Eio.Path.native_exn paths) 541 - in 542 - Printf.printf "%s: %s\n" name paths_str 543 - in 544 534 print_path "config_dir" (Some config_dir); 545 535 print_path "data_dir" (Some data_dir); 546 536 print_path "cache_dir" (Some cache_dir); ··· 622 612 app_name app_name app_name app_name app_name app_name 623 613 624 614 let pp ppf config = 625 - let pp_source ppf = function 626 - | Default -> Fmt.(styled `Faint string) ppf "default" 627 - | Env var -> 628 - Fmt.pf ppf "%a" Fmt.(styled `Yellow string) ("env(" ^ var ^ ")") 629 - | Cmdline -> Fmt.(styled `Blue string) ppf "cmdline" 630 - in 631 615 let pp_with_source name ppf ws = 632 616 match ws.value with 633 617 | None when ws.source = Default -> ()
+26 -27
lib/xdge.mli
··· 68 68 69 69 (** {1 Construction} *) 70 70 71 - val create : Eio.Fs.dir_ty Eio.Path.t -> string -> t 72 - (** [create fs app_name] creates an XDG context for the given application. 71 + val v : Eio.Fs.dir_ty Eio.Path.t -> string -> t 72 + (** [v fs app_name] creates an XDG context for the given application. 73 73 74 74 This function initializes the complete XDG directory structure for your 75 75 application, resolving all paths according to the environment variables and ··· 87 87 88 88 {b Example:} 89 89 {[ 90 - let xdg = Xdge.create env#fs "myapp" in 90 + let xdg = Xdge.v env#fs "myapp" in 91 91 let config = Xdge.config_dir xdg in 92 92 (* config is now <fs:$HOME/.config/myapp> or the overridden path *) 93 93 ]} ··· 96 96 except for runtime directories which are created with 0o700 permissions and 97 97 validated according to the XDG specification. 98 98 99 - @raise Invalid_xdg_path if any environment variable contains a relative path 100 - *) 99 + @raise Invalid_xdg_path 100 + if any environment variable contains a relative path. *) 101 101 102 102 (** {1 Accessors} *) 103 103 ··· 105 105 (** [app_name t] returns the application name used when creating this XDG 106 106 context. 107 107 108 - This is the name that was passed to {!create} and is used as the 109 - subdirectory name within each XDG base directory. *) 108 + This is the name that was passed to {!v} and is used as the subdirectory 109 + name within each XDG base directory. *) 110 110 111 111 val home_dir : Eio.Fs.dir_ty Eio.Path.t -> Eio.Fs.dir_ty Eio.Path.t 112 112 (** [home_dir fs] returns the user's home directory. Checks [$HOME], then ··· 126 126 - Default: [$HOME/.config/{app_name}] 127 127 128 128 @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> 129 - XDG_CONFIG_HOME specification *) 129 + XDG_CONFIG_HOME specification. *) 130 130 131 131 val data_dir : t -> Eio.Fs.dir_ty Eio.Path.t 132 132 (** [data_dir t] returns the path to user-specific data files. ··· 147 147 - Application plugins or extensions 148 148 149 149 @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> 150 - XDG_DATA_HOME specification *) 150 + XDG_DATA_HOME specification. *) 151 151 152 152 val cache_dir : t -> Eio.Fs.dir_ty Eio.Path.t 153 153 (** [cache_dir t] returns the path to user-specific cache files. ··· 171 171 cache validity and be prepared to regenerate data. 172 172 173 173 @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> 174 - XDG_CACHE_HOME specification *) 174 + XDG_CACHE_HOME specification. *) 175 175 176 176 val state_dir : t -> Eio.Fs.dir_ty Eio.Path.t 177 177 (** [state_dir t] returns the path to user-specific state files. ··· 198 198 - Unlike config: State changes frequently during normal use 199 199 200 200 @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> 201 - XDG_STATE_HOME specification *) 201 + XDG_STATE_HOME specification. *) 202 202 203 203 val runtime_dir : t -> Eio.Fs.dir_ty Eio.Path.t option 204 204 (** [runtime_dir t] returns the path to user-specific runtime files. ··· 230 230 [/tmp] with appropriate security measures. 231 231 232 232 @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> 233 - XDG_RUNTIME_DIR specification *) 233 + XDG_RUNTIME_DIR specification. *) 234 234 235 235 (** {1 System Directories} *) 236 236 ··· 252 252 file, search {!config_dir} first, then each directory in this list. 253 253 254 254 @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> 255 - XDG_CONFIG_DIRS specification *) 255 + XDG_CONFIG_DIRS specification. *) 256 256 257 257 val data_dirs : t -> Eio.Fs.dir_ty Eio.Path.t list 258 258 (** [data_dirs t] returns search paths for system-wide data files. ··· 278 278 - Default templates 279 279 280 280 @see <https://specifications.freedesktop.org/basedir-spec/latest/#variables> 281 - XDG_DATA_DIRS specification *) 281 + XDG_DATA_DIRS specification. *) 282 282 283 283 (** {1 File Search} *) 284 284 285 - val find_config_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option 286 - (** [find_config_file t filename] searches for a configuration file following 287 - XDG precedence. 285 + val config_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option 286 + (** [config_file t filename] searches for a configuration file following XDG 287 + precedence. 288 288 289 289 This function searches for the given filename in the user configuration 290 290 directory first, then in system configuration directories in order of ··· 296 296 @return [Some path] if found, [None] if not found in any directory 297 297 298 298 {b Search Order:} 1. User config directory ({!config_dir}) 2. System config 299 - directories ({!config_dirs}) in preference order *) 299 + directories ({!config_dirs}) in preference order. *) 300 300 301 - val find_data_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option 302 - (** [find_data_file t filename] searches for a data file following XDG 303 - precedence. 301 + val data_file : t -> string -> Eio.Fs.dir_ty Eio.Path.t option 302 + (** [data_file t filename] searches for a data file following XDG precedence. 304 303 305 304 This function searches for the given filename in the user data directory 306 305 first, then in system data directories in order of preference. Files that ··· 312 311 @return [Some path] if found, [None] if not found in any directory 313 312 314 313 {b Search Order:} 1. User data directory ({!data_dir}) 2. System data 315 - directories ({!data_dirs}) in preference order *) 314 + directories ({!data_dirs}) in preference order. *) 316 315 317 316 (** {1 Pretty Printing} *) 318 317 ··· 330 329 {b Output formats:} 331 330 - Normal: Multi-line detailed view of all directories 332 331 - Brief: Single line showing app name and key directories 333 - - With sources: Adds annotations showing where each path came from *) 332 + - With sources: Adds annotations showing where each path came from. *) 334 333 335 334 (** {1 Cmdliner Integration} *) 336 335 ··· 394 393 + Command-line flag (e.g., [--config-dir]) - if enabled 395 394 + Application-specific variable (e.g., [MYAPP_CONFIG_DIR]) 396 395 + XDG standard variable (e.g., [XDG_CONFIG_HOME]) 397 - + Default value *) 396 + + Default value. *) 398 397 399 398 val cache_term : string -> string Cmdliner.Term.t 400 399 (** [cache_term app_name] creates a Cmdliner term that provides just the cache ··· 414 413 + Command-line flag ([--cache-dir]) 415 414 + Application-specific variable (e.g., [MYAPP_CACHE_DIR]) 416 415 + XDG standard variable ([XDG_CACHE_HOME]) 417 - + Default value ([$HOME/.cache/{app_name}]) *) 416 + + Default value ([$HOME/.cache/{app_name}]). *) 418 417 419 418 val env_docs : string -> string 420 419 (** [env_docs app_name] generates documentation for environment variables. ··· 430 429 - Configuration precedence rules 431 430 - Application-specific environment variables 432 431 - XDG standard environment variables 433 - - Default values for each directory type *) 432 + - Default values for each directory type. *) 434 433 435 434 val pp : Format.formatter -> t -> unit 436 435 (** [pp ppf config] pretty prints a Cmdliner configuration. ··· 440 439 displaying the current configuration to users. 441 440 442 441 @param ppf The formatter to print to 443 - @param config The configuration to print *) 442 + @param config The configuration to print. *) 444 443 end
+6 -1
test/dune
··· 4 4 5 5 (executable 6 6 (name test_paths) 7 - (libraries xdge eio eio_main)) 7 + (libraries xdge eio eio_main alcotest fmt)) 8 + 9 + (test 10 + (name test) 11 + (modules test test_xdge) 12 + (libraries xdge eio eio_main alcotest)) 8 13 9 14 (cram 10 15 (deps xdg_example.exe test_paths.exe))
+6
test/test.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + let () = Alcotest.run "xdge" [ Test_xdge.suite ]
+50 -35
test/test_paths.ml
··· 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 5 6 + let test_v () = 7 + Eio_main.run @@ fun env -> 8 + let xdg = Xdge.v env#fs "paths_test" in 9 + Alcotest.(check string) "app_name" "paths_test" (Xdge.app_name xdg) 10 + 11 + let test_config_file_missing () = 12 + Eio_main.run @@ fun env -> 13 + let xdg = Xdge.v env#fs "paths_test" in 14 + let result = Xdge.config_file xdg "no_such_file.conf" in 15 + Alcotest.(check (option string)) 16 + "not found" None 17 + (Option.map Eio.Path.native_exn result) 18 + 19 + let suite : string * unit Alcotest.test_case list = 20 + ( "paths", 21 + [ 22 + Alcotest.test_case "v" `Quick test_v; 23 + Alcotest.test_case "config_file missing" `Quick test_config_file_missing; 24 + ] ) 25 + 6 26 let test_path_validation () = 7 - Printf.printf "Testing XDG path validation...\n"; 27 + Fmt.pr "Testing XDG path validation...@."; 8 28 (* Test absolute path validation for environment variables *) 9 29 let test_relative_path_rejection env_var relative_path = 10 - Printf.printf "Testing rejection of relative path in %s...\n" env_var; 30 + Fmt.pr "Testing rejection of relative path in %s...@." env_var; 11 31 Unix.putenv env_var relative_path; 12 32 try 13 33 Eio_main.run @@ fun env -> 14 - let _ = Xdge.create env#fs "test_validation" in 15 - Printf.printf "ERROR: Should have rejected relative path\n"; 34 + let _ = Xdge.v env#fs "test_validation" in 35 + Fmt.pr "ERROR: Should have rejected relative path@."; 16 36 false 17 37 with 18 38 | Xdge.Invalid_xdg_path msg -> 19 - Printf.printf "SUCCESS: Correctly rejected relative path: %s\n" msg; 39 + Fmt.pr "SUCCESS: Correctly rejected relative path: %s@." msg; 20 40 true 21 41 | exn -> 22 - Printf.printf "ERROR: Wrong exception: %s\n" (Printexc.to_string exn); 42 + Fmt.pr "ERROR: Wrong exception: %s@." (Printexc.to_string exn); 23 43 false 24 44 in 25 45 let old_config_home = Sys.getenv_opt "XDG_CONFIG_HOME" in ··· 33 53 (* Restore original env vars *) 34 54 (match old_config_home with 35 55 | Some v -> Unix.putenv "XDG_CONFIG_HOME" v 36 - | None -> ( try Unix.putenv "XDG_CONFIG_HOME" "" with _ -> ())); 56 + | None -> Unix.putenv "XDG_CONFIG_HOME" ""); 37 57 (match old_data_dirs with 38 58 | Some v -> Unix.putenv "XDG_DATA_DIRS" v 39 - | None -> ( try Unix.putenv "XDG_DATA_DIRS" "" with _ -> ())); 59 + | None -> Unix.putenv "XDG_DATA_DIRS" ""); 40 60 success1 && success2 41 61 42 62 let test_file_search () = 43 - Printf.printf "\nTesting XDG file search...\n"; 63 + Fmt.pr "@.Testing XDG file search...@."; 44 64 Eio_main.run @@ fun env -> 45 - let xdg = Xdge.create env#fs "search_test" in 65 + let xdg = Xdge.v env#fs "search_test" in 46 66 (* Create test files *) 47 67 let config_file = Eio.Path.(Xdge.config_dir xdg / "test.conf") in 48 68 let data_file = Eio.Path.(Xdge.data_dir xdg / "test.dat") in 49 69 Eio.Path.save ~create:(`Or_truncate 0o644) config_file "config content"; 50 70 Eio.Path.save ~create:(`Or_truncate 0o644) data_file "data content"; 51 71 (* Test finding existing files *) 52 - (match Xdge.find_config_file xdg "test.conf" with 72 + (match Xdge.config_file xdg "test.conf" with 53 73 | Some path -> 54 74 let content = Eio.Path.load path in 55 - Printf.printf "Found config file: %s\n" (String.trim content) 56 - | None -> Printf.printf "ERROR: Config file not found\n"); 57 - (match Xdge.find_data_file xdg "test.dat" with 75 + Fmt.pr "Found config file: %s@." (String.trim content) 76 + | None -> Fmt.pr "ERROR: Config file not found@."); 77 + (match Xdge.data_file xdg "test.dat" with 58 78 | Some path -> 59 79 let content = Eio.Path.load path in 60 - Printf.printf "Found data file: %s\n" (String.trim content) 61 - | None -> Printf.printf "ERROR: Data file not found\n"); 80 + Fmt.pr "Found data file: %s@." (String.trim content) 81 + | None -> Fmt.pr "ERROR: Data file not found@."); 62 82 (* Test non-existent file *) 63 - match Xdge.find_config_file xdg "nonexistent.conf" with 64 - | Some _ -> Printf.printf "ERROR: Should not have found nonexistent file\n" 65 - | None -> Printf.printf "Correctly handled nonexistent file\n" 83 + match Xdge.config_file xdg "nonexistent.conf" with 84 + | Some _ -> Fmt.pr "ERROR: Should not have found nonexistent file@." 85 + | None -> Fmt.pr "Correctly handled nonexistent file@." 66 86 67 87 let () = 68 88 (* Check if we should run validation tests *) 69 89 if Array.length Sys.argv > 1 && Sys.argv.(1) = "--validate" then ( 70 90 let validation_success = test_path_validation () in 71 91 test_file_search (); 72 - if validation_success then 73 - Printf.printf "\nAll path validation tests passed!\n" 74 - else Printf.printf "\nSome validation tests failed!\n") 92 + if validation_success then Fmt.pr "@.All path validation tests passed!@." 93 + else Fmt.pr "@.Some validation tests failed!@.") 75 94 else 76 95 (* Run original simple functionality test *) 77 96 Eio_main.run @@ fun env -> 78 - let xdg = Xdge.create env#fs "path_test" in 97 + let xdg = Xdge.v env#fs "path_test" in 79 98 (* Test config subdirectory *) 80 99 let profiles_path = Eio.Path.(Xdge.config_dir xdg / "profiles") in 81 100 let profile_file = Eio.Path.(profiles_path / "default.json") in 82 101 (try 83 102 let content = Eio.Path.load profile_file in 84 - Printf.printf "config file content: %s" (String.trim content) 85 - with exn -> 86 - Printf.printf "config file error: %s" (Printexc.to_string exn)); 103 + Fmt.pr "config file content: %s" (String.trim content) 104 + with exn -> Fmt.pr "config file error: %s" (Printexc.to_string exn)); 87 105 (* Test data subdirectory *) 88 106 let db_path = Eio.Path.(Xdge.data_dir xdg / "databases") in 89 107 let db_file = Eio.Path.(db_path / "main.db") in 90 108 (try 91 109 let content = Eio.Path.load db_file in 92 - Printf.printf "\ndata file content: %s" (String.trim content) 93 - with exn -> 94 - Printf.printf "\ndata file error: %s" (Printexc.to_string exn)); 110 + Fmt.pr "@.data file content: %s" (String.trim content) 111 + with exn -> Fmt.pr "@.data file error: %s" (Printexc.to_string exn)); 95 112 (* Test cache subdirectory *) 96 113 let cache_path = Eio.Path.(Xdge.cache_dir xdg / "thumbnails") in 97 114 let cache_file = Eio.Path.(cache_path / "thumb1.png") in 98 115 (try 99 116 let content = Eio.Path.load cache_file in 100 - Printf.printf "\ncache file content: %s" (String.trim content) 101 - with exn -> 102 - Printf.printf "\ncache file error: %s" (Printexc.to_string exn)); 117 + Fmt.pr "@.cache file content: %s" (String.trim content) 118 + with exn -> Fmt.pr "@.cache file error: %s" (Printexc.to_string exn)); 103 119 (* Test state subdirectory *) 104 120 let logs_path = Eio.Path.(Xdge.state_dir xdg / "logs") in 105 121 let log_file = Eio.Path.(logs_path / "app.log") in 106 122 try 107 123 let content = Eio.Path.load log_file in 108 - Printf.printf "\nstate file content: %s\n" (String.trim content) 109 - with exn -> 110 - Printf.printf "\nstate file error: %s\n" (Printexc.to_string exn) 124 + Fmt.pr "@.state file content: %s@." (String.trim content) 125 + with exn -> Fmt.pr "@.state file error: %s@." (Printexc.to_string exn)
+3
test/test_paths.mli
··· 2 2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 + 6 + val suite : string * unit Alcotest.test_case list 7 + (** [suite] is the Alcotest test suite for {!Xdge} path functionality. *)
+63
test/test_xdge.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + let test_v () = 7 + Eio_main.run @@ fun env -> 8 + let xdg = Xdge.v env#fs "test_app" in 9 + let name = Xdge.app_name xdg in 10 + Alcotest.(check string) "app_name" "test_app" name 11 + 12 + let test_dirs_exist () = 13 + Eio_main.run @@ fun env -> 14 + let xdg = Xdge.v env#fs "test_dirs" in 15 + let config = Xdge.config_dir xdg in 16 + let data = Xdge.data_dir xdg in 17 + let cache = Xdge.cache_dir xdg in 18 + let state = Xdge.state_dir xdg in 19 + let stat_ok p = 20 + try 21 + let _ = Eio.Path.stat ~follow:true p in 22 + true 23 + with Eio.Io _ -> false 24 + in 25 + Alcotest.(check bool) "config_dir exists" true (stat_ok config); 26 + Alcotest.(check bool) "data_dir exists" true (stat_ok data); 27 + Alcotest.(check bool) "cache_dir exists" true (stat_ok cache); 28 + Alcotest.(check bool) "state_dir exists" true (stat_ok state) 29 + 30 + let test_config_file_not_found () = 31 + Eio_main.run @@ fun env -> 32 + let xdg = Xdge.v env#fs "test_search" in 33 + let result = Xdge.config_file xdg "nonexistent.conf" in 34 + Alcotest.(check (option string)) 35 + "not found" None 36 + (Option.map Eio.Path.native_exn result) 37 + 38 + let test_data_file_not_found () = 39 + Eio_main.run @@ fun env -> 40 + let xdg = Xdge.v env#fs "test_search" in 41 + let result = Xdge.data_file xdg "nonexistent.dat" in 42 + Alcotest.(check (option string)) 43 + "not found" None 44 + (Option.map Eio.Path.native_exn result) 45 + 46 + let test_home_dir () = 47 + Eio_main.run @@ fun env -> 48 + let home = Xdge.home_dir env#fs in 49 + let path_str = Eio.Path.native_exn home in 50 + Alcotest.(check bool) 51 + "home_dir is absolute" true 52 + (not (Filename.is_relative path_str)) 53 + 54 + let suite = 55 + ( "xdge", 56 + [ 57 + Alcotest.test_case "v" `Quick test_v; 58 + Alcotest.test_case "dirs exist" `Quick test_dirs_exist; 59 + Alcotest.test_case "config_file not found" `Quick 60 + test_config_file_not_found; 61 + Alcotest.test_case "data_file not found" `Quick test_data_file_not_found; 62 + Alcotest.test_case "home_dir" `Quick test_home_dir; 63 + ] )
+7
test/test_xdge.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + val suite : string * unit Alcotest.test_case list 7 + (** [suite] is the Alcotest test suite for {!Xdge}. *)
+2
test/xdg_example.mli
··· 2 2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 3 SPDX-License-Identifier: ISC 4 4 ---------------------------------------------------------------------------*) 5 + 6 + (** Example program demonstrating XDG directory selection with Cmdliner. *)