diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index 525af037..cd9d5359 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -696,14 +696,27 @@ fn system_font_dirs() -> Vec { { v.push("/usr/share/fonts".into()); v.push("/usr/local/share/fonts".into()); - if let Some(h) = std::env::var_os("HOME") { - v.push(std::path::PathBuf::from(&h).join(".fonts")); - v.push(std::path::PathBuf::from(h).join(".local/share/fonts")); - } + v.extend(unix_user_font_dirs(std::env::var_os("HOME"), std::env::var_os("XDG_DATA_HOME"))); } v } +// Fontconfig's default configuration uses $XDG_DATA_HOME/fonts for user fonts. +// Ignore relative XDG paths, as required by the XDG Base Directory Specification. +#[cfg(all(unix, not(target_os = "macos")))] +fn unix_user_font_dirs(home: Option, data_home: Option) -> Vec { + let home = home.map(std::path::PathBuf::from); + let data = data_home.map(std::path::PathBuf::from).filter(|p| p.is_absolute()).or_else(|| home.as_ref().map(|h| h.join(".local/share"))); + let mut dirs = Vec::new(); + if let Some(home) = home { + dirs.push(home.join(".fonts")); + } + if let Some(data) = data { + dirs.push(data.join("fonts")); + } + dirs +} + fn scan_dir(dir: &std::path::Path, depth: usize, out: &mut BTreeMap)>) { if depth > 6 { return; diff --git a/crates/text/src/fontdb_tests.rs b/crates/text/src/fontdb_tests.rs index b9add9c2..46629866 100644 --- a/crates/text/src/fontdb_tests.rs +++ b/crates/text/src/fontdb_tests.rs @@ -67,3 +67,29 @@ fn the_scan_reads_names_like_a_whole_file_read() { assert_eq!(faces_in(&index_bytes(&p, whole.len()).unwrap()), faces_in(&whole), "{}", p.display()); } } + +#[cfg(all(unix, not(target_os = "macos")))] +#[test] +fn user_font_dirs_follow_xdg_data_home() { + use std::ffi::OsString; + use std::path::PathBuf; + let dirs = |home: Option<&str>, data: Option<&str>| unix_user_font_dirs(home.map(OsString::from), data.map(OsString::from)); + let paths = |items: &[&str]| items.iter().map(PathBuf::from).collect::>(); + assert_eq!(dirs(Some("/home/user"), Some("/custom user data")), paths(&["/home/user/.fonts", "/custom user data/fonts"])); + for data in [None, Some(""), Some("relative/data")] { + assert_eq!(dirs(Some("/home/user"), data), paths(&["/home/user/.fonts", "/home/user/.local/share/fonts"])); + } + assert_eq!(dirs(None, Some("/custom-data")), paths(&["/custom-data/fonts"])); + assert_eq!(dirs(None, None), paths(&[])); + assert_eq!(dirs(None, Some("relative")), paths(&[])); +} + +#[cfg(all(unix, not(target_os = "macos")))] +#[test] +fn user_font_dirs_preserve_non_utf8_paths() { + use std::ffi::OsString; + use std::os::unix::ffi::{OsStrExt, OsStringExt}; + let dirs = unix_user_font_dirs(Some(OsString::from_vec(b"/home/\xff".to_vec())), Some(OsString::from_vec(b"/data/\xfe".to_vec()))); + assert_eq!(dirs[0].as_os_str().as_bytes(), b"/home/\xff/.fonts"); + assert_eq!(dirs[1].as_os_str().as_bytes(), b"/data/\xfe/fonts"); +} diff --git a/docs/cli.md b/docs/cli.md index ab6c1ea8..80ccfa59 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -15,6 +15,10 @@ Every subcommand takes `--no-system-fonts` (or set `FIGCRAFT_NO_SYSTEM_FONTS=1`, app honours too) to use only the built-in fonts, for renders that come out the same on every machine, e.g. in CI. +On Linux and other Unix systems except macOS, user fonts are read from +`$XDG_DATA_HOME/fonts`, or `$HOME/.local/share/fonts` when `XDG_DATA_HOME` is unset, +empty or relative. The legacy `$HOME/.fonts` directory is also scanned. + The collaboration server is a binary of its own, `figcraft-server` (`cargo build --release -p figcraft-server`): it keeps files with share links and serves their live sessions, and its `key`, `files`, `links`, `reports`, `block-ip` and `check` subcommands manage them (a server can