Skip to main content

podbox/
xdg.rs

1use std::path::PathBuf;
2use std::process::Command;
3
4use anyhow::Result;
5
6use crate::config::{XdgDirConfig, XdgDirValue};
7
8pub struct ResolvedXdgDir {
9    pub path: PathBuf,
10    pub read_write: bool,
11}
12
13pub struct ResolvedXdgDirs {
14    pub documents: Option<ResolvedXdgDir>,
15    pub downloads: Option<ResolvedXdgDir>,
16    pub pictures: Option<ResolvedXdgDir>,
17    pub music: Option<ResolvedXdgDir>,
18    pub videos: Option<ResolvedXdgDir>,
19    pub desktop: Option<ResolvedXdgDir>,
20    pub projects: Option<ResolvedXdgDir>,
21}
22
23/// Resolve XDG user directories from the host.
24///
25/// For each enabled dir, tries in order:
26/// 1. Parse `~/.config/user-dirs.dirs`
27/// 2. Call `xdg-user-dir <NAME>`
28/// 3. Fall back to `~/DirName`
29/// 4. If path doesn't exist on disk: `None`
30pub fn resolve(config: &XdgDirConfig) -> Result<ResolvedXdgDirs> {
31    Ok(ResolvedXdgDirs {
32        documents: resolve_dir(&config.documents, "DOCUMENTS", "Documents"),
33        downloads: resolve_dir(&config.downloads, "DOWNLOADS", "Downloads"),
34        pictures: resolve_dir(&config.pictures, "PICTURES", "Pictures"),
35        music: resolve_dir(&config.music, "MUSIC", "Music"),
36        videos: resolve_dir(&config.videos, "VIDEOS", "Videos"),
37        desktop: resolve_dir(&config.desktop, "DESKTOP", "Desktop"),
38        projects: resolve_dir(&config.projects, "PROJECTS", "Projects"),
39    })
40}
41
42fn resolve_dir(value: &XdgDirValue, xdg_name: &str, fallback_name: &str) -> Option<ResolvedXdgDir> {
43    if !value.is_enabled() {
44        return None;
45    }
46    let read_write = value.is_read_write();
47
48    // Try xdg-user-dir
49    if let Ok(output) = Command::new("xdg-user-dir").arg(xdg_name).output() {
50        let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
51        if !path.is_empty() && path != home_dir_str() {
52            let p = PathBuf::from(path);
53            if p.exists() {
54                return Some(ResolvedXdgDir {
55                    path: p,
56                    read_write,
57                });
58            }
59        }
60    }
61
62    // Fallback to ~/DirName
63    if let Some(home) = dirs::home_dir() {
64        let p = home.join(fallback_name);
65        if p.exists() {
66            return Some(ResolvedXdgDir {
67                path: p,
68                read_write,
69            });
70        }
71    }
72
73    None
74}
75
76fn home_dir_str() -> String {
77    dirs::home_dir()
78        .map(|p| p.to_string_lossy().to_string())
79        .unwrap_or_default()
80}