Opening a .ppsx from the command line starts the slide show (#862)

This commit is contained in:
Marco Arnulfo
2026-10-16 05:44:02 +02:00
parent be2f10d8cb
commit e755d3eeb1
5 changed files with 119 additions and 5 deletions
+2
View File
@@ -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 |
+6 -4
View File
@@ -1,6 +1,7 @@
//! DeckCraft desktop app.
//!
//! Usage: `deckcraft [--control <port>] [--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 <port>` (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);
+20
View File
@@ -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
+89
View File
@@ -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();
}
}
+2 -1
View File
@@ -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 |