From 81ed71a600dea4421a0fa99198d8df9fa61a8274 Mon Sep 17 00:00:00 2001 From: ReeseHatfield Date: Wed, 10 Jun 2026 18:24:47 -0400 Subject: [PATCH 1/6] add export prototype also add new run wrapper --- src/bin/oseda.rs | 15 ++++++------ src/cmd/export.rs | 61 +++++++++++++++++++++++++++++++++++++++++++++++ src/cmd/mod.rs | 1 + src/cmd/run.rs | 7 +++++- src/lib.rs | 2 ++ 5 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 src/cmd/export.rs diff --git a/src/bin/oseda.rs b/src/bin/oseda.rs index d8acece..d81c0b9 100644 --- a/src/bin/oseda.rs +++ b/src/bin/oseda.rs @@ -2,13 +2,9 @@ use std::{error::Error, process}; use clap::Parser; use oseda_cli::{ - cmd::{ - check, - deploy::{self}, - fork::{self}, - init, run, - }, - Cli, Commands, + Cli, Commands, cmd::{ + check, deploy::{self}, export::{self, export}, fork::{self}, init, run + } }; /// CLI entry point @@ -31,6 +27,11 @@ fn main() { println!("See deployment instructions..."); }), Commands::Fork => fork::fork(), + Commands::Export(options) => export::export(options.clone()) + .map(|_| { + println!("Successfully export project to {0}", options.port) + }) + .map_err(|e| e.into()), }; // little annoying, but makes the exit code match what users would expect diff --git a/src/cmd/export.rs b/src/cmd/export.rs new file mode 100644 index 0000000..bd08c46 --- /dev/null +++ b/src/cmd/export.rs @@ -0,0 +1,61 @@ +use std::{error::Error, process::Command, sync::{Arc, atomic::AtomicBool}}; + +use clap::Args; + +use crate::{cmd::run, net::kill_port}; + + +#[derive(Args, Debug, Clone)] +pub struct ExportOptions { + #[arg(long, default_value = "slides.pdf")] + pub output: String, + #[arg(long, default_value_t = 3000)] + pub port: u16, + +} +pub fn export(opts: ExportOptions) -> Result<(), Box> { + + + let output = Command::new("npm") + .args(["install", "decktape@3.15.0"]) + .current_dir(".") + .output()?; + + if !output.status.success() { + eprintln!( + "Decktape installation failure: {}", + String::from_utf8_lossy(&output.stderr) + ); + return Err("npm init failed".into()); + } + + // decktape automatic http://localhost:3000/ Desktop/IntroToRust/slides.pdf + + // let + + + let _run_handle = std::thread::spawn(run::run); + + + let addr = format!("http://localhost:{}", opts.port); + + let export_output = Command::new("decktape") + .args(["automatic", &addr, &opts.output]) + .output()?; + + if kill_port(opts.port).is_err() { + println!("Warning: could not kill process on port, project could still be running"); + } + if !export_output.status.success() { + eprintln!( + "Decktape PDF export failure: {}", + String::from_utf8_lossy(&export_output.stderr) + ); + return Err("npm init failed".into()); + } + + + + + Ok(()) +} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 531c54c..5e17d65 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -3,3 +3,4 @@ pub mod deploy; pub mod fork; pub mod init; pub mod run; +pub mod export; diff --git a/src/cmd/run.rs b/src/cmd/run.rs index d5c6de1..625bf15 100644 --- a/src/cmd/run.rs +++ b/src/cmd/run.rs @@ -1,4 +1,4 @@ -use std::{process::Command, sync::mpsc}; +use std::{process::Command, sync::{Arc, atomic::AtomicBool, mpsc}}; /// More in depth errors that could cause a project not to run #[derive(Debug)] @@ -28,6 +28,11 @@ impl std::fmt::Display for OsedaRunError { /// * `Ok(())` if both the build and serve steps succeed /// * `Err(OsedaRunError)` if any step fails (missing vite isn't installed, or `serve` fails to start) pub fn run() -> Result<(), OsedaRunError> { + // todo refactor the other check command to use this + run_with_shutdown(Arc::new(AtomicBool::new(false))) +} + +pub fn run_with_shutdown(shutdown_flag: Arc) -> Result<(), OsedaRunError> { // command run failure and command status are considered different, handled accordingly match Command::new("npx").arg("vite").arg("build").status() { Ok(status) => { diff --git a/src/lib.rs b/src/lib.rs index e337881..738a0c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,4 +33,6 @@ pub enum Commands { Deploy(cmd::deploy::DeployOptions), /// Fork the library repository to submit your course Fork, + /// Export the Oseda project to a PDF file + Export(cmd::export::ExportOptions), } From 081f73494e463de7044734d70eb76b708c641d8f Mon Sep 17 00:00:00 2001 From: ReeseHatfield Date: Wed, 10 Jun 2026 18:42:19 -0400 Subject: [PATCH 2/6] save value to pdf --- src/cmd/export.rs | 21 ++++++++++++++------- src/cmd/run.rs | 22 ++++++++++++++-------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/cmd/export.rs b/src/cmd/export.rs index bd08c46..055008e 100644 --- a/src/cmd/export.rs +++ b/src/cmd/export.rs @@ -1,4 +1,4 @@ -use std::{error::Error, process::Command, sync::{Arc, atomic::AtomicBool}}; +use std::{error::Error, process::Command, sync::{Arc, atomic::{AtomicBool, Ordering}}}; use clap::Args; @@ -33,19 +33,28 @@ pub fn export(opts: ExportOptions) -> Result<(), Box> { // let + let shutdown_flag = Arc::new(AtomicBool::new(false)); + let run_flag = shutdown_flag.clone(); - let _run_handle = std::thread::spawn(run::run); + let run_handle = std::thread::spawn(move || run::run_with_shutdown(run_flag)); + + // wait a moment for the localhost server to spin up + std::thread::sleep(std::time::Duration::from_millis(10000)); let addr = format!("http://localhost:{}", opts.port); + // run decktape, assuming the server has spun up by now let export_output = Command::new("decktape") .args(["automatic", &addr, &opts.output]) .output()?; - if kill_port(opts.port).is_err() { - println!("Warning: could not kill process on port, project could still be running"); - } + + // send shutdown flag, should signal to run_with_shutdown to kill the process + shutdown_flag.store(true, Ordering::SeqCst); + // wait to run to terminate (hopefully gracefully) and join the process to cur. thread + let _ = run_handle.join(); + if !export_output.status.success() { eprintln!( "Decktape PDF export failure: {}", @@ -55,7 +64,5 @@ pub fn export(opts: ExportOptions) -> Result<(), Box> { } - - Ok(()) } diff --git a/src/cmd/run.rs b/src/cmd/run.rs index 625bf15..55056b2 100644 --- a/src/cmd/run.rs +++ b/src/cmd/run.rs @@ -1,4 +1,4 @@ -use std::{process::Command, sync::{Arc, atomic::AtomicBool, mpsc}}; +use std::{process::Command, sync::{Arc, atomic::{AtomicBool, Ordering}, mpsc}, time::Duration}; /// More in depth errors that could cause a project not to run #[derive(Debug)] @@ -64,16 +64,22 @@ pub fn run_with_shutdown(shutdown_flag: Arc) -> Result<(), OsedaRunE // spawn will leave child running the background. Need to listen for ctrl+c, snatch it. Then kill subprocess // https://github.com/Detegr/rust-ctrlc - let (tx, rx) = mpsc::channel(); + // let (tx, rx) = mpsc::channel(); + let ctrlc_flag = shutdown_flag.clone(); ctrlc::set_handler(move || { println!("\nSIGINT received. Attempting graceful shutdown..."); - let _ = tx.send(()); - }) - .expect("Error setting Ctrl+C handler"); - - // block until ctrl+c - rx.recv().unwrap(); + ctrlc_flag.store(true, Ordering::SeqCst); + }).map_err(|e| { + println!("Error setting ctrl+c handler: {e}"); + OsedaRunError::ServeError("failed to set handler".into()) + })?; + + // block until ctrl+c or sigkill or flag set otherwise (e.g. via export) + while !shutdown_flag.load(Ordering::SeqCst) { + std::thread::sleep(Duration::from_millis(100)); + } + // attempt to kill the child process if let Err(e) = child.kill() { println!("Failed to kill `serve`: {e}"); From 5c2978eeb66237e79f6b51436047f341aea6a07d Mon Sep 17 00:00:00 2001 From: ReeseHatfield Date: Wed, 10 Jun 2026 18:48:09 -0400 Subject: [PATCH 3/6] move binary intro project directory in tests --- scripts/test-init.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/test-init.sh b/scripts/test-init.sh index ab26c40..8b5789e 100755 --- a/scripts/test-init.sh +++ b/scripts/test-init.sh @@ -13,3 +13,5 @@ cd test pwd ./oseda init --title ExampleProject --tags economics ComPuterScience --color red --template MaRKDoWN + +mv oseda ExampleProject \ No newline at end of file From 6933df3b28acc3252a2bbf8abeba80bdfe5b3490 Mon Sep 17 00:00:00 2001 From: ReeseHatfield Date: Wed, 10 Jun 2026 18:48:29 -0400 Subject: [PATCH 4/6] kill process on port in case it is still on from previous run --- src/cmd/export.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cmd/export.rs b/src/cmd/export.rs index 055008e..71f97cb 100644 --- a/src/cmd/export.rs +++ b/src/cmd/export.rs @@ -15,6 +15,10 @@ pub struct ExportOptions { } pub fn export(opts: ExportOptions) -> Result<(), Box> { + if kill_port(opts.port).is_err() { + eprintln!("Warning, could not kill value on desired port") + } + let output = Command::new("npm") .args(["install", "decktape@3.15.0"]) @@ -31,8 +35,6 @@ pub fn export(opts: ExportOptions) -> Result<(), Box> { // decktape automatic http://localhost:3000/ Desktop/IntroToRust/slides.pdf - // let - let shutdown_flag = Arc::new(AtomicBool::new(false)); let run_flag = shutdown_flag.clone(); From d035be6275c000c187d49e17977fd9f66570d009 Mon Sep 17 00:00:00 2001 From: ReeseHatfield Date: Wed, 10 Jun 2026 18:51:46 -0400 Subject: [PATCH 5/6] document export subcommand --- Usage.md | 19 +++++++++++++++++++ src/cmd/export.rs | 5 +++++ src/lib.rs | 2 ++ 3 files changed, 26 insertions(+) diff --git a/Usage.md b/Usage.md index 0316dc0..3dd995a 100644 --- a/Usage.md +++ b/Usage.md @@ -10,6 +10,7 @@ This document contains the help content for the `oseda` command-line program. * [`oseda check`↴](#oseda-check) * [`oseda deploy`↴](#oseda-deploy) * [`oseda fork`↴](#oseda-fork) +* [`oseda export`↴](#oseda-export) ## `oseda` @@ -24,6 +25,7 @@ oseda project scafolding CLI * `check` — Check the Oseda project in the working directory for common errors * `deploy` — Deploy your Oseda project to github to add to oseda.net * `fork` — Fork the library repository to submit your course +* `export` — Export the Oseda project to a PDF file @@ -84,6 +86,23 @@ Fork the library repository to submit your course +## `oseda export` + +Export the Oseda project to a PDF file. + +**Usage:** `oseda export [OPTIONS]` + +###### **Options:** + +* `--output ` + + Default value: `slides.pdf` +* `--port ` + + Default value: `3000` + + +
diff --git a/src/cmd/export.rs b/src/cmd/export.rs index 71f97cb..8adfa94 100644 --- a/src/cmd/export.rs +++ b/src/cmd/export.rs @@ -5,14 +5,19 @@ use clap::Args; use crate::{cmd::run, net::kill_port}; +/// Options struct for the export subcommand #[derive(Args, Debug, Clone)] pub struct ExportOptions { + /// String name of the output PDF file #[arg(long, default_value = "slides.pdf")] pub output: String, + /// Port the project runs on #[arg(long, default_value_t = 3000)] pub port: u16, } + +/// Export the current Oseda project to a PDF file via `decktape` pub fn export(opts: ExportOptions) -> Result<(), Box> { if kill_port(opts.port).is_err() { diff --git a/src/lib.rs b/src/lib.rs index 738a0c7..4cea8fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,5 +34,7 @@ pub enum Commands { /// Fork the library repository to submit your course Fork, /// Export the Oseda project to a PDF file + /// This will install the npm package `decktape` + /// This relies on a chromium backend, as a result, it may take a while to run Export(cmd::export::ExportOptions), } From b62481c30184c2f3e7d32f4c4d030c532ffe3214 Mon Sep 17 00:00:00 2001 From: ReeseHatfield Date: Wed, 10 Jun 2026 19:37:03 -0400 Subject: [PATCH 6/6] format --- Usage.md | 8 ++++---- src/bin/oseda.rs | 16 +++++++++------- src/cmd/export.rs | 24 ++++++++++++------------ src/cmd/mod.rs | 2 +- src/cmd/run.rs | 21 ++++++++++++++------- 5 files changed, 40 insertions(+), 31 deletions(-) diff --git a/Usage.md b/Usage.md index 3dd995a..aa4d748 100644 --- a/Usage.md +++ b/Usage.md @@ -25,7 +25,7 @@ oseda project scafolding CLI * `check` — Check the Oseda project in the working directory for common errors * `deploy` — Deploy your Oseda project to github to add to oseda.net * `fork` — Fork the library repository to submit your course -* `export` — Export the Oseda project to a PDF file +* `export` — Export the Oseda project to a PDF file This will install the npm package `decktape` This relies on a chromium backend, as a result, it may take a while to run @@ -88,16 +88,16 @@ Fork the library repository to submit your course ## `oseda export` -Export the Oseda project to a PDF file. +Export the Oseda project to a PDF file This will install the npm package `decktape` This relies on a chromium backend, as a result, it may take a while to run **Usage:** `oseda export [OPTIONS]` ###### **Options:** -* `--output ` +* `--output ` — String name of the output PDF file Default value: `slides.pdf` -* `--port ` +* `--port ` — Port the project runs on Default value: `3000` diff --git a/src/bin/oseda.rs b/src/bin/oseda.rs index d81c0b9..330bbd1 100644 --- a/src/bin/oseda.rs +++ b/src/bin/oseda.rs @@ -2,9 +2,14 @@ use std::{error::Error, process}; use clap::Parser; use oseda_cli::{ - Cli, Commands, cmd::{ - check, deploy::{self}, export::{self, export}, fork::{self}, init, run - } + cmd::{ + check, + deploy::{self}, + export::{self}, + fork::{self}, + init, run, + }, + Cli, Commands, }; /// CLI entry point @@ -28,10 +33,7 @@ fn main() { }), Commands::Fork => fork::fork(), Commands::Export(options) => export::export(options.clone()) - .map(|_| { - println!("Successfully export project to {0}", options.port) - }) - .map_err(|e| e.into()), + .map(|_| println!("Successfully export project to {0}", options.port)), }; // little annoying, but makes the exit code match what users would expect diff --git a/src/cmd/export.rs b/src/cmd/export.rs index 8adfa94..9687bbe 100644 --- a/src/cmd/export.rs +++ b/src/cmd/export.rs @@ -1,10 +1,16 @@ -use std::{error::Error, process::Command, sync::{Arc, atomic::{AtomicBool, Ordering}}}; +use std::{ + error::Error, + process::Command, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; use clap::Args; use crate::{cmd::run, net::kill_port}; - /// Options struct for the export subcommand #[derive(Args, Debug, Clone)] pub struct ExportOptions { @@ -14,21 +20,18 @@ pub struct ExportOptions { /// Port the project runs on #[arg(long, default_value_t = 3000)] pub port: u16, - } /// Export the current Oseda project to a PDF file via `decktape` pub fn export(opts: ExportOptions) -> Result<(), Box> { - if kill_port(opts.port).is_err() { eprintln!("Warning, could not kill value on desired port") } - let output = Command::new("npm") - .args(["install", "decktape@3.15.0"]) - .current_dir(".") - .output()?; + .args(["install", "decktape@3.15.0"]) + .current_dir(".") + .output()?; if !output.status.success() { eprintln!( @@ -43,9 +46,8 @@ pub fn export(opts: ExportOptions) -> Result<(), Box> { let shutdown_flag = Arc::new(AtomicBool::new(false)); let run_flag = shutdown_flag.clone(); - let run_handle = std::thread::spawn(move || run::run_with_shutdown(run_flag)); - + // wait a moment for the localhost server to spin up std::thread::sleep(std::time::Duration::from_millis(10000)); @@ -56,7 +58,6 @@ pub fn export(opts: ExportOptions) -> Result<(), Box> { .args(["automatic", &addr, &opts.output]) .output()?; - // send shutdown flag, should signal to run_with_shutdown to kill the process shutdown_flag.store(true, Ordering::SeqCst); // wait to run to terminate (hopefully gracefully) and join the process to cur. thread @@ -70,6 +71,5 @@ pub fn export(opts: ExportOptions) -> Result<(), Box> { return Err("npm init failed".into()); } - Ok(()) } diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 5e17d65..d63b2d2 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,6 +1,6 @@ pub mod check; pub mod deploy; +pub mod export; pub mod fork; pub mod init; pub mod run; -pub mod export; diff --git a/src/cmd/run.rs b/src/cmd/run.rs index 55056b2..a059aff 100644 --- a/src/cmd/run.rs +++ b/src/cmd/run.rs @@ -1,4 +1,11 @@ -use std::{process::Command, sync::{Arc, atomic::{AtomicBool, Ordering}, mpsc}, time::Duration}; +use std::{ + process::Command, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; /// More in depth errors that could cause a project not to run #[derive(Debug)] @@ -69,17 +76,17 @@ pub fn run_with_shutdown(shutdown_flag: Arc) -> Result<(), OsedaRunE ctrlc::set_handler(move || { println!("\nSIGINT received. Attempting graceful shutdown..."); ctrlc_flag.store(true, Ordering::SeqCst); - }).map_err(|e| { - println!("Error setting ctrl+c handler: {e}"); - OsedaRunError::ServeError("failed to set handler".into()) + }) + .map_err(|e| { + println!("Error setting ctrl+c handler: {e}"); + OsedaRunError::ServeError("failed to set handler".into()) })?; - // block until ctrl+c or sigkill or flag set otherwise (e.g. via export) while !shutdown_flag.load(Ordering::SeqCst) { - std::thread::sleep(Duration::from_millis(100)); + std::thread::sleep(Duration::from_millis(100)); } - + // attempt to kill the child process if let Err(e) = child.kill() { println!("Failed to kill `serve`: {e}");