diff --git a/Cargo.lock b/Cargo.lock index 80a7276d9d..0e5017f347 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -404,6 +404,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "executable-path" version = "1.0.0" @@ -535,6 +546,7 @@ dependencies = [ "dirs", "dotenvy", "edit-distance", + "etcetera", "executable-path", "heck", "lexiclean", diff --git a/Cargo.toml b/Cargo.toml index f74367d02f..af44567f4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ derivative = "2.0.0" dirs = "5.0.1" dotenvy = "0.15" edit-distance = "2.0.0" +etcetera = "0.8.0" heck = "0.5.0" lexiclean = "0.0.1" libc = "0.2.0" diff --git a/README.md b/README.md index 535b2bc402..f935148aba 100644 --- a/README.md +++ b/README.md @@ -1816,13 +1816,10 @@ for details. `requirement`, e.g., `">=0.1.0"`, returning `"true"` if so and `"false"` otherwise. -##### XDG Directories1.23.0 +##### Directories1.23.0 -These functions return paths to user-specific directories for things like -configuration, data, caches, executables, and the user's home directory. These -functions follow the -[XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html), -and are implemented with the +These functions return paths to standard operating system directories for things like +configuration, data, caches, executables, and the user's home directory. These are implemented with the [`dirs`](https://docs.rs/dirs/latest/dirs/index.html) crate. - `cache_directory()` - The user-specific cache directory. @@ -1833,6 +1830,23 @@ and are implemented with the - `executable_directory()` - The user-specific executable directory. - `home_directory()` - The user's home directory. +##### XDG Directories + +These functions return paths to XDG directories for things like +configuration, data, caches, executables, and the user's home directory. These +functions follow the +[XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) and fall back to the operating system standard directories, +and are implemented with the +[`etcetera`](https://docs.rs/etcetera/latest/etcetera/index.html) crate. + +- `xdg_cache_directory()` - The XDG cache directory. +- `xdg_config_directory()` - The XDG configuration directory. +- `xdg_data_directory()` - The XDG data directory. +- `xdg_home_directory()` - The user's home directory. +- `xdg_runtime_directory()` - The local XDG runtime directory. +- `xdg_state_directory()` - The XDG state directory. + + ### Constants A number of constants are predefined: diff --git a/src/function.rs b/src/function.rs index a714a8d0fd..3a92968c7e 100644 --- a/src/function.rs +++ b/src/function.rs @@ -1,5 +1,6 @@ use { super::*, + etcetera::{choose_base_strategy, BaseStrategy}, heck::{ ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToUpperCamelCase, @@ -46,15 +47,15 @@ pub(crate) fn get(name: &str) -> Option { "arch" => Nullary(arch), "blake3" => Unary(blake3), "blake3_file" => Unary(blake3_file), - "cache_directory" => Nullary(|_| dir("cache", dirs::cache_dir)), + "cache_directory" => Nullary(|_| dir("cache", dirs::cache_dir())), "canonicalize" => Unary(canonicalize), "capitalize" => Unary(capitalize), "choose" => Binary(choose), "clean" => Unary(clean), - "config_directory" => Nullary(|_| dir("config", dirs::config_dir)), - "config_local_directory" => Nullary(|_| dir("local config", dirs::config_local_dir)), - "data_directory" => Nullary(|_| dir("data", dirs::data_dir)), - "data_local_directory" => Nullary(|_| dir("local data", dirs::data_local_dir)), + "config_directory" => Nullary(|_| dir("config", dirs::config_dir())), + "config_local_directory" => Nullary(|_| dir("local config", dirs::config_local_dir())), + "data_directory" => Nullary(|_| dir("data", dirs::data_dir())), + "data_local_directory" => Nullary(|_| dir("local data", dirs::data_local_dir())), "datetime" => Unary(datetime), "datetime_utc" => Unary(datetime_utc), "encode_uri_component" => Unary(encode_uri_component), @@ -62,11 +63,11 @@ pub(crate) fn get(name: &str) -> Option { "env_var" => Unary(env_var), "env_var_or_default" => Binary(env_var_or_default), "error" => Unary(error), - "executable_directory" => Nullary(|_| dir("executable", dirs::executable_dir)), + "executable_directory" => Nullary(|_| dir("executable", dirs::executable_dir())), "extension" => Unary(extension), "file_name" => Unary(file_name), "file_stem" => Unary(file_stem), - "home_directory" => Nullary(|_| dir("home", dirs::home_dir)), + "home_directory" => Nullary(|_| dir("home", dirs::home_dir())), "invocation_directory" => Nullary(invocation_directory), "invocation_directory_native" => Nullary(invocation_directory_native), "is_dependency" => Nullary(is_dependency), @@ -110,6 +111,25 @@ pub(crate) fn get(name: &str) -> Option { "uppercase" => Unary(uppercase), "uuid" => Nullary(uuid), "without_extension" => Unary(without_extension), + "xdg_cache_directory" => { + Nullary(|_| dir("cache", Some(choose_base_strategy().unwrap().cache_dir()))) + } + "xdg_config_directory" => { + Nullary(|_| dir("config", Some(choose_base_strategy().unwrap().config_dir()))) + } + "xdg_data_directory" => { + Nullary(|_| dir("data", Some(choose_base_strategy().unwrap().data_dir()))) + } + "xdg_home_directory" => Nullary(|_| { + dir( + "home", + Some(choose_base_strategy().unwrap().home_dir().to_path_buf()), + ) + }), + "xdg_runtime_directory" => { + Nullary(|_| dir("runtime", choose_base_strategy().unwrap().runtime_dir())) + } + "xdg_state_directory" => Nullary(|_| dir("state", choose_base_strategy().unwrap().state_dir())), _ => return None, }; Some(function) @@ -223,8 +243,8 @@ fn clean(_context: Context, path: &str) -> FunctionResult { Ok(Path::new(path).lexiclean().to_str().unwrap().to_owned()) } -fn dir(name: &'static str, f: fn() -> Option) -> FunctionResult { - match f() { +fn dir(name: &'static str, path: Option) -> FunctionResult { + match path { Some(path) => path .as_os_str() .to_str() @@ -701,7 +721,7 @@ mod tests { #[test] fn dir_not_found() { - assert_eq!(dir("foo", || None).unwrap_err(), "foo directory not found"); + assert_eq!(dir("foo", None).unwrap_err(), "foo directory not found"); } #[cfg(unix)] @@ -709,9 +729,10 @@ mod tests { fn dir_not_unicode() { use std::os::unix::ffi::OsStrExt; assert_eq!( - dir("foo", || Some( - std::ffi::OsStr::from_bytes(b"\xe0\x80\x80").into() - )) + dir( + "foo", + Some(std::ffi::OsStr::from_bytes(b"\xe0\x80\x80").into()) + ) .unwrap_err(), "unable to convert foo directory path to string: ���", ); diff --git a/tests/directories.rs b/tests/directories.rs index 41a05cd6c1..8b36ca65e7 100644 --- a/tests/directories.rs +++ b/tests/directories.rs @@ -1,4 +1,5 @@ use super::*; +use etcetera::BaseStrategy; #[test] fn cache_directory() { @@ -83,3 +84,111 @@ fn home_directory() { .stdout(dirs::home_dir().unwrap_or_default().to_string_lossy()) .run(); } + +#[test] +fn xdg_cache_directory() { + Test::new() + .justfile("x := xdg_cache_directory()") + .args(["--evaluate", "x"]) + .stdout( + etcetera::choose_base_strategy() + .unwrap() + .cache_dir() + .to_string_lossy(), + ) + .run(); +} + +#[test] +fn xdg_config_directory() { + Test::new() + .justfile("x := xdg_config_directory()") + .args(["--evaluate", "x"]) + .stdout( + etcetera::choose_base_strategy() + .unwrap() + .config_dir() + .to_string_lossy(), + ) + .run(); +} + +#[test] +fn xdg_data_directory() { + Test::new() + .justfile("x := xdg_data_directory()") + .args(["--evaluate", "x"]) + .stdout( + etcetera::choose_base_strategy() + .unwrap() + .data_dir() + .to_string_lossy(), + ) + .run(); +} + +#[test] +fn xdg_home_directory() { + Test::new() + .justfile("x := xdg_home_directory()") + .args(["--evaluate", "x"]) + .stdout( + etcetera::choose_base_strategy() + .unwrap() + .home_dir() + .to_string_lossy(), + ) + .run(); +} + +#[test] +fn xdg_runtime_directory() { + if let Some(runtime_dir) = etcetera::choose_base_strategy().unwrap().runtime_dir() { + Test::new() + .justfile("x := xdg_runtime_directory()") + .args(["--evaluate", "x"]) + .stdout(runtime_dir.to_string_lossy()) + .run(); + } else { + Test::new() + .justfile("x := xdg_runtime_directory()") + .args(["--evaluate", "x"]) + .stderr( + " + error: Call to function `xdg_runtime_directory` failed: runtime directory not found + ——▶ justfile:1:6 + │ + 1 │ x := xdg_runtime_directory() + │ ^^^^^^^^^^^^^^^^^^^^^ + ", + ) + .status(EXIT_FAILURE) + .run(); + } +} + +#[test] +fn xdg_state_directory() { + if let Some(state_dir) = etcetera::choose_base_strategy().unwrap().state_dir() { + Test::new() + .justfile("x := xdg_state_directory()") + .args(["--evaluate", "x"]) + .stdout(state_dir.to_string_lossy()) + .run(); + } else { + Test::new() + .justfile("x := xdg_state_directory()") + .args(["--evaluate", "x"]) + .stderr( + " + error: Call to function `xdg_state_directory` failed: state directory not found + ——▶ justfile:1:6 + │ + 1 │ x := xdg_state_directory() + │ ^^^^^^^^^^^^^^^^^^^ + ", + ) + .status(EXIT_FAILURE) + .run(); + } +}