gui: Save PDF on every report; short and narrow windows hold up
Reports get a Save PDF button beside Copy for a spreadsheet. It typesets the report as it's set up on screen (range, months/quarters/years, comparison) through ledger-import's Typst statement template, the same one the CPA package uses, in the background, writes it to the Downloads folder (never the books: the viewer stays read-only), and opens it. - ledger-import: Statement.percent_from marks percentage columns (comparison %, % of income), typeset as 12.5% with no dollar sign; wide statements turn the page, and past seven columns set smaller so a year of months fits; statement() is public for other callers. - Compared columns that read the same (Jan 1 - Oct 12 vs a year earlier) now say which year, on screen and on paper. - Short windows: the sidebar's and rail's links scroll above the status box instead of under it. The search palette fits the window. - A test typesets every table-backed report from the demo books, plain, by month, and compared (PDF_DIR keeps the files). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
6fb594ed28
commit
b582d3d8a0
9 files changed
+444
-101
No files matched your search
@@ -0,0 +1,71 @@
|
||||
//! 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<PathBuf, String> {
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user