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();
+ }
+}