From e755d3eeb1955a1cd7e78928ded009b58a7b689e Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Fri, 16 Oct 2026 05:44:02 +0200 Subject: [PATCH] Opening a .ppsx from the command line starts the slide show (#862) --- ROADMAP.md | 2 + apps/deckcraft/src/main.rs | 10 ++-- crates/ui-egui/src/lib.rs | 20 ++++++++ crates/ui-egui/src/opening.rs | 89 +++++++++++++++++++++++++++++++++++ docs/file-format-parity.md | 3 +- 5 files changed, 119 insertions(+), 5 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 8ea976e6..b371127b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -131,6 +131,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-16 | A PowerPoint Show (`.ppsx`) opened from the command line or the file association starts its slide show, as in PowerPoint; File › Open still opens it for editing (#862) | | 2026-10-15 | PDF export and rendering: `anchorCtr` centres the drawn text block — right-aligned text no longer ran past the box edge (#736) | | 2026-10-14 | connected MCP rejects unrelated, malformed, incomplete and oversized replies without replay; buffered unsolicited bytes force a fresh connection before sending (#718) | | 2026-10-14 | Empty bulleted and numbered paragraphs show gray markers while editing, including on first click and after Enter. Markers use the list style and stay out of slide shows and exports. | @@ -226,6 +227,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | Change | Summary | |---|---|---| +| 2026-10-16 | trivial | Progress log: a `.ppsx` from the system starts the slide show (#862) | | 2026-10-14 | trivial | remote MCP checks reply IDs, envelopes and bounded newline framing (#718) | | 2026-10-14 | minor | Gray list markers on empty paragraphs while editing; caret keeps the list indent before typing | | 2026-10-14 | minor | Community PR wave folded: headline breadth 84% over 191 features (P0 92%, P1 82%, P2 55%) and the feature-area breadths from `cargo xtask parity`; one row per area; Upcoming item 2 merged; per-PR count notes dropped from the progress log. Ready stays ~55%, stage alpha | diff --git a/apps/deckcraft/src/main.rs b/apps/deckcraft/src/main.rs index ef80daaa..e40804b6 100644 --- a/apps/deckcraft/src/main.rs +++ b/apps/deckcraft/src/main.rs @@ -1,6 +1,7 @@ //! DeckCraft desktop app. //! //! Usage: `deckcraft [--control ] [--sample] [--show] [files…]` +//! A PowerPoint Show (`.ppsx`) given last starts its slide show, as a double-click does in PowerPoint. //! `deckcraft --help` prints the flags and exits. `deckcraft --version` prints the version and exits. //! //! `--control ` (or `DECKCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server: @@ -134,6 +135,8 @@ Usage: deckcraft [--control PORT] [--sample] [--show] [FILE...] --show start the slide show --help, -h print this help and exit --version, -V print the version and exit + +A PowerPoint Show (.ppsx) given last starts its slide show. "; enum Startup { @@ -254,10 +257,9 @@ fn main() -> eframe::Result { if sample && let Err(e) = deckcraft_engine::sample::open_sample(&mut app.session) { log::error!("sample: {e}"); } - for f in files { - if let Err(e) = app.open_path(&f) { - log::error!("{f}: {e}"); - } + // A .ppsx opened last starts its slide show (#862). + for (f, e) in app.open_from_system(&files) { + log::error!("{f}: {e}"); } if show { app.start_show(0, false); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 88674070..f0270945 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1184,6 +1184,26 @@ impl SlideApp { Ok(()) } + /// Files the system hands over (the command line, a double-click through the file + /// association), opened in order. When the last one is a PowerPoint Show (`.ppsx`), its slide + /// show starts at once from where Play from Start begins, as PowerPoint does; anything else + /// opens for editing, and File › Open opens a `.ppsx` for editing too. Returns the files that + /// couldn't be opened, with the reason. + pub fn open_from_system(&mut self, paths: &[String]) -> Vec<(String, String)> { + let mut failed = Vec::new(); + let mut last = None; + for p in paths { + match self.open_path(p) { + Ok(()) => last = Some(p), + Err(e) => failed.push((p.clone(), e)), + } + } + if last.is_some_and(|p| p.to_ascii_lowercase().ends_with(".ppsx")) { + self.start_show(self.show_start_index(), false); + } + failed + } + /// A presentation was opened from `path`: first in the recent list, shown in Normal view. pub(crate) fn note_recent(&mut self, path: &str) { self.status = None; // an older error (a missing file just before this one) must not linger diff --git a/crates/ui-egui/src/opening.rs b/crates/ui-egui/src/opening.rs index 37d20a4e..318cacfb 100644 --- a/crates/ui-egui/src/opening.rs +++ b/crates/ui-egui/src/opening.rs @@ -199,4 +199,93 @@ mod tests { assert!(harness.state().opening.is_none()); assert_eq!(harness.state().session.documents().len(), 1); } + + /// A deck of `slides` slides saved as `file` (its extension picks the format). + fn saved_as(file: &str, slides: usize) -> String { + let mut s = Session::with_new(); + for _ in 1..slides { + s.execute("slide.new", &serde_json::json!({})).unwrap(); + } + let dir = std::env::temp_dir().join(format!("deckcraft-opening-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join(file).to_string_lossy().to_string(); + s.execute("file.saveAs", &serde_json::json!({"path": path})).unwrap(); + path + } + + /// #862: a PowerPoint Show handed over by the system (command line, double-click) starts its + /// slide show at once, full screen and from the first slide. + #[test] + fn a_ppsx_from_the_system_starts_the_show() { + let path = saved_as("Talk.PPSX", 3); + let mut app = SlideApp::new(Session::new(), crate::Services::default()); + assert!(app.open_from_system(std::slice::from_ref(&path)).is_empty()); + assert_eq!(app.session.documents().len(), 1); + let show = app.show.as_ref().expect("the slide show started"); + assert_eq!(show.state.slide, 0); + assert!(!show.reading, "full screen, not Reading View"); + std::fs::remove_file(&path).ok(); + } + + /// #862: a `.pptx` from the system opens for editing, and so does a `.ppsx` followed by a + /// `.pptx`: the show would otherwise play the deck opened last. + #[test] + fn a_pptx_from_the_system_opens_for_editing() { + let pptx = saved_as("edit.pptx", 3); + let ppsx = saved_as("first.ppsx", 2); + let mut app = SlideApp::new(Session::new(), crate::Services::default()); + assert!(app.open_from_system(std::slice::from_ref(&pptx)).is_empty()); + assert!(app.show.is_none()); + assert!(app.open_from_system(&[ppsx.clone(), pptx.clone()]).is_empty()); + assert_eq!(app.session.documents().len(), 3); + assert!(app.show.is_none(), "the last file is a .pptx"); + std::fs::remove_file(&pptx).ok(); + std::fs::remove_file(&ppsx).ok(); + } + + /// #862: File › Open opens a `.ppsx` for editing, as PowerPoint does. + #[test] + fn file_open_opens_a_ppsx_for_editing() { + let path = saved_as("open.ppsx", 2); + let mut app = SlideApp::new(Session::new(), crate::Services::default()); + app.open_path_in_background(&path); + wait(&mut app); + assert_eq!(app.session.active().unwrap().doc.slides.len(), 2); + assert!(app.show.is_none()); + assert_eq!(app.ui.view, crate::ViewMode::Normal); + std::fs::remove_file(&path).ok(); + } + + /// #862: Esc, or a click past the end screen, ends the `.ppsx` show and leaves the deck open in + /// the editor (PowerPoint closes it instead). + #[test] + fn leaving_the_ppsx_show_keeps_the_deck_open() { + let path = saved_as("leave.ppsx", 2); + for leave in [egui::Key::Escape, egui::Key::ArrowRight] { + let mut app = SlideApp::new(Session::new(), crate::Services::default()); + assert!(app.open_from_system(std::slice::from_ref(&path)).is_empty()); + let mut harness = egui_kittest::Harness::builder().with_size(egui::vec2(1000.0, 700.0)).build_ui_state( + |ui, app: &mut SlideApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + app, + ); + harness.run_steps(3); + assert!(harness.state().show.is_some(), "the show is running"); + if leave == egui::Key::ArrowRight { + for _ in 0..2 { + harness.key_press(egui::Key::ArrowRight); + harness.run_steps(2); + } + assert!(harness.state().show.as_ref().is_some_and(|s| s.ended), "the end screen"); + } + harness.key_press(leave); + harness.run_steps(2); + assert!(harness.state().show.is_none(), "{leave:?} ends the show"); + assert_eq!(harness.state().session.documents().len(), 1, "{leave:?}: the deck stays open"); + assert_eq!(harness.state().ui.view, crate::ViewMode::Normal); + } + std::fs::remove_file(&path).ok(); + } } diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index a5f72fe7..468c1d0e 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -18,7 +18,7 @@ PowerPoint opens and saves; Viewer = opens only). |---|---|---|---|---|---| | `.pptx` presentation | Editor | yes | yes | Broad (see below); an owned corpus of decks we author round-trips tables, charts, animations, media, sections, masters and layouts, comments, links and actions, custom shows, rotated groups, inherited placeholders, text effects and body layout, theme effects, fills, bevels, connectors, 3-D models and animation bounce end. Not measured against PowerPoint renders or decks from outside this repo | `crates/pptx/tests/corpus.rs`, `corpus_kinds.rs`, `corpus_elements.rs`, `corpus_style.rs`, `corpus_themes.rs`, `corpus_charts.rs`, `corpus_media.rs`, `roundtrip.rs`, `sample.rs`, `malformed.rs`, `sniff.rs`; opened in PowerPoint by hand 2026-10-06 | | `.potx` template | Editor | yes (as a presentation) | no: written with the `.pptx` content type | `file.saveTemplate` is Save As under another name | none | -| `.ppsx` slide show | Editor | yes (as a presentation) | no: written as `.pptx` content | Doesn't start in show mode | none | +| `.ppsx` slide show | Editor | yes (as a presentation) | no: written as `.pptx` content | Handed over by the system (command line, file association) it starts the slide show, as in PowerPoint; File › Open opens it for editing. The macOS Finder event arrives with #432 | `crates/ui-egui/src/opening.rs` tests | | `.pptm` / `.potm` / `.ppsm` macro-enabled | Editor | no | no | Should open and keep `vbaProject.bin` without running it | none | | `.ppt` / `.pot` / `.pps` 97–2003 binary | Editor | no | no | Owner decision; import only would be enough | none | | `.odp` OpenDocument | Editor | no | no | | none | @@ -103,6 +103,7 @@ the round trip even though DeckCraft doesn't model it. | Date | Change | Summary | |---|---|---| +| 2026-10-16 | trivial | A `.ppsx` opened from the command line or the file association starts the slide show (#862) | | 2026-10-14 | minor | Community PR wave folded: one row each for `.pptx` (seven owned corpus files), outline, 3-D models, embedded fonts, text, effects, 3-D (extrusion sides draw now), SmartArt (written as a diagram) and VBA (tags and custom XML are kept) | | 2026-10-12 | minor | SmartArt import lays each shape's text out on its `dsp:txXfrm` text rectangle instead of the shape box | | 2026-10-13 | minor | EMF/WMF rectangle clip regions limit later drawing; EMF+ records remain |