Support XDG directory spec on macOS by switching app_dirs2 to etcetera (#467)

Closes #311

Similar to how `bat` does it, we now have a global struct which holds
all the queried system directories. We prefer the XDG directories, but
use the "native" directories as a fallback. This should ensure that when
MacOs users upgrade from an older version, their existing config and
cache are still used. After deleting the old directories, the new ones
should be used by tealdeer automatically.
This commit is contained in:
Niklas Mohrin 2026-08-06 00:37:37 +02:00 committed by GitHub
commit 37b0dee39f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 83 additions and 75 deletions

View file

@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
toolchain: [stable, 1.85.0]
toolchain: [stable, 1.87.0] # MSRV
include:
- platform: windows-latest
exe_suffix: .exe

36
Cargo.lock generated
View file

@ -73,18 +73,6 @@ version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "app_dirs2"
version = "2.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7e7b35733e3a8c1ccb90385088dd5b6eaa61325cb4d1ad56e683b5224ff352e"
dependencies = [
"jni",
"ndk-context",
"winapi",
"xdg",
]
[[package]]
name = "arbitrary"
version = "1.4.2"
@ -380,6 +368,16 @@ dependencies = [
"serde_json",
]
[[package]]
name = "etcetera"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96"
dependencies = [
"cfg-if",
"windows-sys 0.61.1",
]
[[package]]
name = "fastrand"
version = "2.3.0"
@ -635,12 +633,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "ndk-context"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
@ -1117,11 +1109,11 @@ name = "tealdeer"
version = "1.8.1"
dependencies = [
"anyhow",
"app_dirs2",
"assert_cmd",
"clap",
"env_logger",
"escargot",
"etcetera",
"filetime",
"log",
"pager",
@ -1634,12 +1626,6 @@ version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "xdg"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546"
[[package]]
name = "yansi"
version = "1.0.1"

View file

@ -12,7 +12,7 @@ repository = "https://github.com/tealdeer-rs/tealdeer/"
documentation = "https://tealdeer-rs.github.io/tealdeer/"
version = "1.8.1"
include = ["/src/**/*", "/tests/**/*", "/Cargo.toml", "/README.md", "/LICENSE-*", "/screenshot.png", "completion/*"]
rust-version = "1.85"
rust-version = "1.87" # MSRV
edition = "2021"
[[bin]]
@ -21,9 +21,9 @@ path = "src/main.rs"
[dependencies]
anyhow = "1"
app_dirs = { version = "2", package = "app_dirs2" }
clap = { version = "4", features = ["std", "derive", "help", "usage", "cargo", "error-context", "color", "wrap_help"], default-features = false }
env_logger = { version = "0.11", optional = true }
etcetera = "0.11.0"
log = "0.4"
serde = "1.0.21"
serde_derive = "1.0.21"

View file

@ -8,8 +8,7 @@ use std::{
time::Duration,
};
use anyhow::{anyhow, bail, ensure, Context, Result};
use app_dirs::{get_app_root, AppDataType};
use anyhow::{anyhow, ensure, Context, Result};
use clap::ValueEnum;
use log::info;
use serde::Serialize as _;
@ -33,6 +32,50 @@ const SUPPORTED_TLS_BACKENDS: &[RawTlsBackend] = &[
RawTlsBackend::RustlsWithNativeRoots,
];
struct SystemDirectories {
config: PathBuf,
cache: PathBuf,
data: PathBuf,
}
impl SystemDirectories {
fn discover() -> Result<Self> {
use etcetera::{
app_strategy::choose_native_strategy, choose_app_strategy, AppStrategy, AppStrategyArgs,
};
let args = AppStrategyArgs {
top_level_domain: String::new(),
author: String::new(),
app_name: crate::NAME.to_string(),
};
// The app strategy prefers XDG on MacOs, whereas the native strategy returns paths which
// are used by installed applications. On Linux and Windows, the strategies are the same.
let app_dirs = choose_app_strategy(args.clone())?;
let native_dirs = choose_native_strategy(args)?;
// We prefer the XDG paths, but before tealdeer 1.9, we used only the native paths on MacOs.
// So if we find files in these locations, we keep using them.
let fallback = |app_dir: PathBuf, native_dir: PathBuf| {
if !app_dir.exists() && native_dir.exists() {
native_dir
} else {
app_dir
}
};
Ok(Self {
config: fallback(app_dirs.config_dir(), native_dirs.config_dir()),
cache: fallback(app_dirs.cache_dir(), native_dirs.cache_dir()),
data: fallback(app_dirs.data_dir(), native_dirs.data_dir()),
})
}
}
static SYSTEM_DIRECTORIES: LazyLock<SystemDirectories> = LazyLock::new(|| {
SystemDirectories::discover().expect("Failed to initialize system directories.")
});
pub(crate) fn supported_tls_backends_string() -> String {
SUPPORTED_TLS_BACKENDS
.iter()
@ -619,15 +662,11 @@ impl<'a> Config<'a> {
path: resolved_path,
source: PathSource::ConfigFile,
}
} else if let Ok(default_dir) = get_app_root(AppDataType::UserCache, &crate::APP_INFO) {
// Otherwise, fall back to the default user cache directory.
} else {
PathWithSource {
path: default_dir,
path: SYSTEM_DIRECTORIES.cache.clone(),
source: PathSource::OsConvention,
}
} else {
// If everything fails, give up
bail!("Could not determine user cache directory");
};
let custom_pages_dir = raw_config
.directories
@ -646,15 +685,11 @@ impl<'a> Config<'a> {
})
.transpose()?
.or_else(|| {
get_app_root(AppDataType::UserData, &crate::APP_INFO)
.map(|path| {
// Note: The `join("")` call ensures that there's a trailing slash
PathWithSource {
path: path.join("pages").join(""),
Some(PathWithSource {
path: SYSTEM_DIRECTORIES.data.join("pages").join(""),
source: PathSource::OsConvention,
}
})
.ok()
});
let directories = DirectoriesConfig {
cache_dir,
@ -743,7 +778,7 @@ impl ConfigLoader {
/// Create a loader that uses the default config file location. If no file is present at the default location, the
/// default configuration is used.
pub fn read_default_path() -> Result<Self> {
let path = get_default_config_path().context("Could not determine default config path.")?;
let path = get_default_config_path();
Self::read_internal(path, true)
}
@ -761,30 +796,24 @@ impl ConfigLoader {
///
/// Note that this function does not verify whether the directory at that
/// location exists, or is a directory.
pub fn get_config_dir() -> Result<(PathBuf, PathSource)> {
pub fn get_config_dir() -> (PathBuf, PathSource) {
// Allow overriding the config directory by setting the
// $TEALDEER_CONFIG_DIR env variable.
if let Ok(value) = env::var("TEALDEER_CONFIG_DIR") {
return Ok((PathBuf::from(value), PathSource::EnvVar));
return (PathBuf::from(value), PathSource::EnvVar);
}
// Otherwise, fall back to the user config directory.
let dirs = get_app_root(AppDataType::UserConfig, &crate::APP_INFO)
.context("Failed to determine the user config directory")?;
Ok((dirs, PathSource::OsConvention))
(SYSTEM_DIRECTORIES.config.clone(), PathSource::OsConvention)
}
/// Return the path to the config file.
///
/// Note that this function does not verify whether the file at that location
/// exists, or is a file.
pub fn get_default_config_path() -> Result<PathWithSource> {
let (config_dir, source) = get_config_dir()?;
let config_file_path = config_dir.join(CONFIG_FILE_NAME);
Ok(PathWithSource {
path: config_file_path,
source,
})
pub fn get_default_config_path() -> PathWithSource {
let (mut path, source) = get_config_dir();
path.push(CONFIG_FILE_NAME);
PathWithSource { path, source }
}
/// Create default config file.
@ -794,7 +823,7 @@ pub fn make_default_config(path: Option<&Path>) -> Result<PathBuf> {
let config_file_path = if let Some(p) = path {
p.into()
} else {
let (config_dir, _) = get_config_dir()?;
let (config_dir, _) = get_config_dir();
// Ensure that config directory exists
if config_dir.exists() {

View file

@ -36,7 +36,6 @@ use std::{
};
use anyhow::{anyhow, Context, Result};
use app_dirs::AppInfo;
use cache::{CacheConfig, TLDR_OLD_PAGES_DIR};
use clap::Parser;
use config::{ConfigLoader, Language, StyleConfig, TlsBackend};
@ -65,10 +64,6 @@ use crate::{
};
const NAME: &str = "tealdeer";
const APP_INFO: AppInfo = AppInfo {
name: NAME,
author: NAME,
};
static TEALDEER_PAGE: &str =
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/pages/tealdeer.md"));
@ -110,16 +105,14 @@ fn update_cache(
/// Show file paths
fn show_paths(config: &Config) {
let config_dir = get_config_dir().map_or_else(
|e| format!("[Error: {e}]"),
|(mut path, source)| {
let config_dir = {
let (mut path, source) = get_config_dir();
path.push(""); // Trailing path separator
match path.to_str() {
Some(path) => format!("{path} ({source})"),
None => "[Invalid]".to_string(),
}
},
);
};
let config_path = config.file_path.to_string();
let cache_dir = config.directories.cache_dir.to_string();
let pages_dir = {