linesmith_core/data_context/
xdg.rs1use std::ffi::OsString;
17use std::path::PathBuf;
18
19#[derive(Debug, Clone, Default)]
29#[non_exhaustive]
30pub struct XdgEnv {
31 pub(crate) xdg_cache_home: Option<OsString>,
32 pub(crate) xdg_config_home: Option<OsString>,
33 pub(crate) home: Option<OsString>,
34}
35
36impl XdgEnv {
37 #[must_use]
42 pub fn from_process_env() -> Self {
43 Self::from_os_options(
44 std::env::var_os("XDG_CACHE_HOME"),
45 std::env::var_os("XDG_CONFIG_HOME"),
46 std::env::var_os("HOME"),
47 )
48 }
49
50 #[must_use]
54 pub fn from_os_options(
55 xdg_cache_home: Option<OsString>,
56 xdg_config_home: Option<OsString>,
57 home: Option<OsString>,
58 ) -> Self {
59 fn nonempty(v: Option<OsString>) -> Option<OsString> {
60 v.filter(|s| !s.is_empty())
61 }
62 Self {
63 xdg_cache_home: nonempty(xdg_cache_home),
64 xdg_config_home: nonempty(xdg_config_home),
65 home: nonempty(home),
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72#[non_exhaustive]
73pub enum XdgScope {
74 Cache,
76 Config,
78}
79
80#[must_use]
91pub fn resolve_subdir(env: &XdgEnv, scope: XdgScope, sub: &str) -> Option<PathBuf> {
92 let (xdg, home_sub) = match scope {
93 XdgScope::Cache => (env.xdg_cache_home.as_deref(), ".cache"),
94 XdgScope::Config => (env.xdg_config_home.as_deref(), ".config"),
95 };
96 if let Some(x) = xdg {
97 return Some(linesmith_subdir(PathBuf::from(x), sub));
98 }
99 env.home
100 .as_deref()
101 .map(|h| linesmith_subdir(PathBuf::from(h).join(home_sub), sub))
102}
103
104fn linesmith_subdir(base: PathBuf, sub: &str) -> PathBuf {
105 let with_app = base.join("linesmith");
106 if sub.is_empty() {
107 with_app
108 } else {
109 with_app.join(sub)
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 fn os(s: &str) -> Option<OsString> {
118 Some(OsString::from(s))
119 }
120
121 #[test]
122 fn from_os_options_filters_empty_strings_to_none() {
123 let env = XdgEnv::from_os_options(Some(OsString::new()), os("/x"), Some(OsString::new()));
124 assert_eq!(env.xdg_cache_home, None);
125 assert_eq!(env.xdg_config_home, os("/x"));
126 assert_eq!(env.home, None);
127 }
128
129 #[test]
130 fn cache_scope_prefers_xdg_cache_home_over_home() {
131 let env = XdgEnv::from_os_options(os("/xdg"), None, os("/home"));
132 assert_eq!(
133 resolve_subdir(&env, XdgScope::Cache, ""),
134 Some(PathBuf::from("/xdg/linesmith"))
135 );
136 }
137
138 #[test]
139 fn cache_scope_falls_back_to_home_dot_cache() {
140 let env = XdgEnv::from_os_options(None, None, os("/home/user"));
141 assert_eq!(
142 resolve_subdir(&env, XdgScope::Cache, ""),
143 Some(PathBuf::from("/home/user/.cache/linesmith"))
144 );
145 }
146
147 #[test]
148 fn config_scope_uses_xdg_config_home() {
149 let env = XdgEnv::from_os_options(None, os("/conf"), None);
150 assert_eq!(
151 resolve_subdir(&env, XdgScope::Config, "segments"),
152 Some(PathBuf::from("/conf/linesmith/segments"))
153 );
154 }
155
156 #[test]
157 fn config_scope_falls_back_to_home_dot_config() {
158 let env = XdgEnv::from_os_options(None, None, os("/home/user"));
159 assert_eq!(
160 resolve_subdir(&env, XdgScope::Config, "themes"),
161 Some(PathBuf::from("/home/user/.config/linesmith/themes"))
162 );
163 }
164
165 #[test]
166 fn config_scope_does_not_borrow_xdg_cache_home() {
167 let env = XdgEnv::from_os_options(os("/xdg-cache"), None, None);
171 assert_eq!(resolve_subdir(&env, XdgScope::Config, ""), None);
172 }
173
174 #[test]
175 fn returns_none_when_neither_xdg_nor_home_is_set() {
176 let env = XdgEnv::default();
177 assert_eq!(resolve_subdir(&env, XdgScope::Cache, "x"), None);
178 assert_eq!(resolve_subdir(&env, XdgScope::Config, "y"), None);
179 }
180
181 #[test]
182 fn empty_sub_does_not_append_trailing_slash() {
183 let env = XdgEnv::from_os_options(os("/xdg"), None, None);
184 let path = resolve_subdir(&env, XdgScope::Cache, "").unwrap();
185 assert_eq!(path, PathBuf::from("/xdg/linesmith"));
186 assert_eq!(path.components().count(), 3); }
188
189 #[cfg(unix)]
190 #[test]
191 fn preserves_non_utf8_xdg_cache_home() {
192 use std::os::unix::ffi::OsStringExt;
199 let bytes = b"/srv/caf\xe9-bin".to_vec();
201 let xdg = OsString::from_vec(bytes.clone());
202 let env = XdgEnv::from_os_options(Some(xdg), None, os("/home/user"));
203 let resolved = resolve_subdir(&env, XdgScope::Cache, "").unwrap();
204 assert!(
207 resolved
208 .as_os_str()
209 .as_encoded_bytes()
210 .starts_with(b"/srv/caf\xe9-bin/linesmith"),
211 "expected non-UTF-8 XDG to be preserved: got {:?}",
212 resolved
213 );
214 }
215}