From 18e47d1af765855d164938d10145070462fef12f Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 13 Oct 2026 11:55:00 +0300 Subject: [PATCH] CLI: say that render --slide counts from 0 (#359) --all names files slide-01.png onward, so --slide 8 on an 8-slide deck looked like an off-by-one. The index stays 0-based, matching slide.go, and the error now gives the range and the file names. Co-authored-by: Cursor --- apps/deckcraft-cli/src/main.rs | 53 +++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/apps/deckcraft-cli/src/main.rs b/apps/deckcraft-cli/src/main.rs index c450393f..bad8087c 100644 --- a/apps/deckcraft-cli/src/main.rs +++ b/apps/deckcraft-cli/src/main.rs @@ -3,6 +3,7 @@ //! ```text //! deckcraft-cli info FILE # slides, titles, layouts (JSON) //! deckcraft-cli render (FILE | --sample) [--slide N | --all] [--scale S] OUT.png|OUT_DIR +//! N starts at 0. --all writes slide-01.png, slide-02.png, … //! deckcraft-cli convert IN OUT # .deckcraft ⇄ .pptx, outline .txt, .png (first slide) //! deckcraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--save OUT] [--export OUT] [--print] //! deckcraft-cli commands [FILTER] # list commands (JSON) @@ -34,6 +35,7 @@ macro_rules! outln { const USAGE: &str = "usage: deckcraft-cli info FILE deckcraft-cli render (FILE | --sample) [--slide N | --all] [--scale S] [--edit] OUT + N starts at 0 (the first slide). --all writes slide-01.png, slide-02.png, … deckcraft-cli convert IN OUT deckcraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--save OUT] [--export OUT] [--print] deckcraft-cli commands [FILTER] @@ -145,14 +147,11 @@ fn render(args: &[String]) -> Result<(), String> { open(&mut s, input.as_deref(), sample)?; let st = s.doc().map_err(|e| e.to_string())?; let n = st.doc.slides.len(); - let list: Vec = if all { (0..n).collect() } else { vec![slide.unwrap_or(0)] }; + let list = slide_indices(n, all, slide)?; if all { std::fs::create_dir_all(&out).map_err(|e| format!("{out}: {e}"))?; } for i in list { - if i >= n { - return Err(format!("no slide {i} (the deck has {n})")); - } let (png, w, h) = deckcraft_engine::cmd::file::render_png(&st.doc, i, scale, edit); let path = if all { format!("{out}/slide-{:02}.png", i + 1) } else { out.clone() }; std::fs::write(&path, png).map_err(|e| format!("{path}: {e}"))?; @@ -161,6 +160,32 @@ fn render(args: &[String]) -> Result<(), String> { Ok(()) } +/// Slides `--slide` asks for. `N` is a 0-based index, the same one `slide.go` uses. +/// `--all` still names files from 1 (`slide-01.png`), which is PowerPoint's slide number. +fn slide_indices(n: usize, all: bool, slide: Option) -> Result, String> { + if all { + return Ok((0..n).collect()); + } + let i = slide.unwrap_or(0); + if i >= n { + return Err(missing_slide(i, n)); + } + Ok(vec![i]) +} + +fn missing_slide(i: usize, n: usize) -> String { + match n { + 0 => format!("no slide index {i} (this deck has no slides)"), + 1 => format!("no slide index {i} (this deck has 1 slide, index 0, which --all names slide-01.png)"), + n => { + let last = n - 1; + format!( + "no slide index {i} (this deck has {n} slides, numbered 0 through {last}; --all names them slide-01.png through slide-{n:02}.png)" + ) + } + } +} + fn convert(args: &[String]) -> Result<(), String> { let (Some(input), Some(out)) = (args.first(), args.get(1)) else { return Err("convert needs IN and OUT".into()) }; let mut s = Session::new(); @@ -278,3 +303,23 @@ fn mcp(args: &[String]) -> Result<(), String> { let stdin = std::io::stdin(); server.serve(stdin.lock(), std::io::stdout()).map_err(|e| e.to_string()) } + +#[cfg(test)] +mod tests { + use super::{missing_slide, slide_indices}; + + #[test] + fn slide_numbers_start_at_zero_and_files_at_one() { + assert_eq!(slide_indices(8, false, None).ok().as_deref(), Some(&[0][..])); + assert_eq!(slide_indices(8, false, Some(0)).ok().as_deref(), Some(&[0][..])); + assert_eq!(slide_indices(8, false, Some(7)).ok().as_deref(), Some(&[7][..])); + assert_eq!(slide_indices(8, true, None).ok().as_deref(), Some(&[0, 1, 2, 3, 4, 5, 6, 7][..])); + let err = slide_indices(8, false, Some(8)).err().unwrap_or_default(); + assert!(!err.is_empty(), "index 8 was accepted"); + assert!(err.contains("numbered 0 through 7"), "{err}"); + assert!(err.contains("slide-01.png"), "{err}"); + assert!(err.contains("slide-08.png"), "{err}"); + assert!(missing_slide(0, 0).contains("no slides")); + assert!(missing_slide(1, 1).contains("index 0")); + } +}