From 33b6e11726206967b2172b5bf1b7612880366f54 Mon Sep 17 00:00:00 2001 From: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:48:37 -0500 Subject: [PATCH] Use private directories for print previews --- ROADMAP.md | 3 +- crates/ui-egui/src/app.rs | 3 +- crates/ui-egui/src/app/files.rs | 102 +++++++++++++++++++++++++- crates/ui-egui/src/backstage/tests.rs | 15 +++- crates/ui-egui/src/lib.rs | 1 + 5 files changed, 116 insertions(+), 8 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index b04c4b69..39c1c6ba 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~20 points and ~120–210 h away -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-24 · **Change:** trivial (progress log: Edit Points handle drags with Shift, Ctrl/Cmd or Alt/Option set the point type; previous: trivial, progress log: scroll bars on the zoomed slide pane, #1157; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-28 · **Change:** trivial (print previews use unique private temporary directories and are cleaned up with the UI app; parity scores unchanged; previous: trivial, progress log: Edit Points handle drags with Shift, Ctrl/Cmd or Alt/Option set the point type; previous: trivial, progress log: scroll bars on the zoomed slide pane, #1157; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-24 · **Change:** trivial (progress log: Ctrl+wheel zoom during the slide show, #1286; previous: trivial, scroll bars on the zoomed slide pane, #1157; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-24 · **Change:** trivial (progress log: picture and embedded object fills; previous: trivial, scroll bars on the zoomed slide pane, #1157; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-22 · **Change:** trivial (fill and outline edits reach group members, #1079; stage and scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) @@ -137,6 +137,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-28 | Print previews use unique temporary directories, owner-only on Unix, and are removed when the UI app closes; parity scores unchanged | | 2026-10-25 | Windows installer: the Start menu shortcut takes the icon embedded in `deckcraft.exe` instead of an MSI ``, so a taskbar pin keeps its icon after an update (#1114); a pin that already lost it has to be unpinned and pinned again once | | 2026-10-24 | AutoFormat as you type: a paragraph that starts with `*`, `-`, `--`, `>`, `<>`, `1.`, `1)`, `I.`, `i.`, `A.`, `a.` or `a)` and a space or tab becomes a bulleted or numbered list in that style; Backspace right after puts the typed text back, and Tools ▸ AutoCorrect and `text.autocorrect {lists}` turn it off | | 2026-10-24 | Tables: while the caret is in a cell, dragging into another cell selects that range of cells (shaded, merged cells taken in whole), and Shift+arrow grows or shrinks it; formatting applies to the selected cells only, and Delete clears their text and keeps the table | diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index 8496d0ed..3b3da592 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -13,7 +13,7 @@ use super::{ mod clipboard; mod commands; mod control; -mod files; +pub(crate) mod files; mod frame; mod ime; mod input; @@ -77,6 +77,7 @@ impl SlideApp { integrated_titlebar: false, backstage: None, pending_urls: vec![], + print_previews: files::PrintPreviews::default(), #[cfg(not(target_arch = "wasm32"))] updates_rt: updates_ui::Runtime::default(), pending_copy: None, diff --git a/crates/ui-egui/src/app/files.rs b/crates/ui-egui/src/app/files.rs index 0f3c944f..310956a8 100644 --- a/crates/ui-egui/src/app/files.rs +++ b/crates/ui-egui/src/app/files.rs @@ -3,6 +3,93 @@ use super::{ json, media, }; +#[derive(Default)] +pub(crate) struct PrintPreviews(Vec); + +impl PrintPreviews { + fn create(&mut self, title: &str) -> std::io::Result { + self.create_in(&std::env::temp_dir(), title) + } + + fn create_in(&mut self, base: &std::path::Path, title: &str) -> std::io::Result { + let dir = create_private_print_dir(base)?; + self.0.push(dir.clone()); + let safe: String = title.chars().map(|c| if c.is_alphanumeric() || c == ' ' || c == '-' { c } else { '_' }).collect(); + Ok(dir.join(format!("{safe} (print).pdf"))) + } + + fn discard(&mut self, path: &std::path::Path) { + if let Some(dir) = path.parent() { + let _ = std::fs::remove_dir_all(dir); + self.0.retain(|owned| owned != dir); + } + } +} + +#[cfg(test)] +mod print_preview_tests { + use super::PrintPreviews; + use std::time::{SystemTime, UNIX_EPOCH}; + + #[test] + fn same_title_gets_distinct_private_paths_and_drop_cleans_them() { + let stamp = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or_default(); + let base = std::env::temp_dir().join(format!("deckcraft-print-test-{}-{stamp}", std::process::id())); + std::fs::create_dir(&base).unwrap(); + let (first, second) = { + let mut previews = PrintPreviews::default(); + let first = previews.create_in(&base, "Same title").unwrap(); + let second = previews.create_in(&base, "Same title").unwrap(); + assert_ne!(first, second); + assert_eq!(first.file_name(), second.file_name()); + assert!(first.parent().is_some_and(|dir| dir.is_dir())); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = std::fs::metadata(first.parent().unwrap()).unwrap().permissions().mode(); + assert_eq!(mode & 0o777, 0o700); + } + (first, second) + }; + assert!(!first.parent().is_some_and(|dir| dir.exists())); + assert!(!second.parent().is_some_and(|dir| dir.exists())); + std::fs::remove_dir(base).unwrap(); + } +} + +impl Drop for PrintPreviews { + fn drop(&mut self) { + for dir in &self.0 { + let _ = std::fs::remove_dir_all(dir); + } + } +} + +fn create_private_print_dir(base: &std::path::Path) -> std::io::Result { + use std::sync::atomic::{AtomicU64, Ordering}; + static NEXT_DIR: AtomicU64 = AtomicU64::new(0); + + for _ in 0..128 { + let id = NEXT_DIR.fetch_add(1, Ordering::Relaxed); + let path = base.join(format!("deckcraft-print-{}-{id}", std::process::id())); + #[cfg(unix)] + let mut builder = std::fs::DirBuilder::new(); + #[cfg(not(unix))] + let builder = std::fs::DirBuilder::new(); + #[cfg(unix)] + { + use std::os::unix::fs::DirBuilderExt; + builder.mode(0o700); + } + match builder.create(&path) { + Ok(()) => return Ok(path), + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(e) => return Err(e), + } + } + Err(std::io::Error::new(std::io::ErrorKind::AlreadyExists, "could not allocate a unique print preview directory")) +} + fn purpose_of(purpose: &str) -> FilePurpose { match purpose { "open" => FilePurpose::Open, @@ -207,8 +294,14 @@ impl SlideApp { let bytes = deckcraft_engine::cmd::file::pdf_bytes(&doc, ¶ms); return self.download(&format!("{title}.pdf"), bytes); } - let safe: String = title.chars().map(|c| if c.is_alphanumeric() || c == ' ' || c == '-' { c } else { '_' }).collect(); - let path = std::env::temp_dir().join(format!("{safe} (print).pdf")).to_string_lossy().to_string(); + let preview_path = match self.print_previews.create(&title) { + Ok(path) => path, + Err(e) => { + self.set_status(format!("Couldn't create a private print preview: {e}")); + return false; + } + }; + let path = preview_path.to_string_lossy().to_string(); if let Some(o) = params.as_object_mut() { o.insert("path".into(), json!(path)); o.insert("format".into(), json!("pdf")); @@ -219,7 +312,10 @@ impl SlideApp { self.set_status("Opened the presentation as a PDF to print"); true } - Err(_) => false, + Err(_) => { + self.print_previews.discard(&preview_path); + false + } } } diff --git a/crates/ui-egui/src/backstage/tests.rs b/crates/ui-egui/src/backstage/tests.rs index 78330b1b..30577797 100644 --- a/crates/ui-egui/src/backstage/tests.rs +++ b/crates/ui-egui/src/backstage/tests.rs @@ -618,10 +618,19 @@ fn print_previews_pages_and_prints_a_pdf() { h.run_steps(2); let url = h.state().pending_urls.first().cloned().unwrap_or_default(); // The URL was handed to egui at the end of the frame; check what it pointed at. - let printed = std::env::temp_dir().join(format!("{} (print).pdf", h.state().session.active().unwrap().title())); - assert!(printed.is_file(), "the PDF to print was written: {url}"); + assert!(url.contains("deckcraft-print-"), "print preview uses its private directory: {url}"); + let title = h.state().session.active().unwrap().title(); + let safe: String = title.chars().map(|c| if c.is_alphanumeric() || c == ' ' || c == '-' { c } else { '_' }).collect(); + let file_name = format!("{safe} (print).pdf"); + let printed = std::fs::read_dir(std::env::temp_dir()) + .unwrap() + .filter_map(Result::ok) + .filter(|entry| entry.file_name().to_string_lossy().starts_with("deckcraft-print-")) + .map(|entry| entry.path().join(&file_name)) + .find(|path| path.is_file()) + .expect("the PDF to print was written in its private directory"); assert!(std::fs::read(&printed).unwrap().starts_with(b"%PDF")); - let _ = std::fs::remove_file(printed); + let _ = std::fs::remove_dir_all(printed.parent().unwrap()); let _ = std::fs::remove_dir_all(&dir); } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 30b5a0cd..fd3deaa4 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -278,6 +278,7 @@ pub struct SlideApp { /// creating or closing a presentation leaves it. pub backstage: Option, pending_urls: Vec, + print_previews: app::files::PrintPreviews, #[cfg(not(target_arch = "wasm32"))] updates_rt: updates_ui::Runtime, /// Text for the system clipboard from a Copy or Cut command, handed over on the next frame.