//! Files the viewer makes for you (report PDFs). They go to the //! Downloads folder, never into the books: the viewer stays read-only. use std::path::{Path, PathBuf}; /// Where saved files go: `$XDG_DOWNLOAD_DIR`, else `~/Downloads`, else /// the home directory. pub fn downloads_dir() -> PathBuf { if let Some(d) = std::env::var_os("XDG_DOWNLOAD_DIR").map(PathBuf::from) && d.is_dir() { return d; } let home = std::env::var_os("HOME") .or_else(|| std::env::var_os("USERPROFILE")) .map(PathBuf::from) .unwrap_or_else(|| PathBuf::from(".")); let d = home.join("Downloads"); if d.is_dir() { d } else { home } } /// `name` made safe as a file name on every platform. pub fn file_name(name: &str, ext: &str) -> String { let clean: String = name .chars() .map(|c| match c { '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '-', c if c.is_control() => ' ', c => c, }) .collect(); format!("{}.{ext}", clean.trim()) } /// Write `bytes` to the downloads folder as `name.ext` (replacing a file /// of that name: the same report renders the same bytes) and open it with /// the system's viewer. pub fn save_and_open(name: &str, ext: &str, bytes: &[u8]) -> Result { let path = downloads_dir().join(file_name(name, ext)); std::fs::write(&path, bytes).map_err(|e| format!("{}: {e}", path.display()))?; open(&path); Ok(path) } /// Open a file with whatever the desktop uses for its type. Best effort: /// the file is saved either way. fn open(path: &Path) { #[cfg(target_os = "macos")] let cmd = std::process::Command::new("open").arg(path).spawn(); #[cfg(target_os = "windows")] let cmd = std::process::Command::new("cmd") .args(["/C", "start", ""]) .arg(path) .spawn(); #[cfg(not(any(target_os = "macos", target_os = "windows")))] let cmd = std::process::Command::new("xdg-open").arg(path).spawn(); let _ = cmd; } #[cfg(test)] mod tests { use super::*; #[test] fn file_names_are_safe() { assert_eq!( file_name("Acme LLC — Profit and loss — Jan 1/2026: draft", "pdf"), "Acme LLC — Profit and loss — Jan 1-2026- draft.pdf" ); } }