Skip to main content

oseda_cli/cmd/
export.rs

1use std::{
2    error::Error,
3    process::Command,
4    sync::{
5        atomic::{AtomicBool, Ordering},
6        Arc,
7    },
8};
9
10use clap::Args;
11
12use crate::{
13    cmd::run,
14    config::read_and_validate_config,
15    net::kill_port,
16    puppeteer::{is_puppeteer_chrome_installed, prompt_install_puppeteer_chrome},
17};
18
19/// Options struct for the export subcommand
20#[derive(Args, Debug, Clone)]
21pub struct ExportOptions {
22    /// String name of the output PDF file
23    #[arg(long, default_value_t = get_default_output())]
24    pub output: String,
25    /// Port the project runs on
26    #[arg(long, default_value_t = 3000)]
27    pub port: u16,
28}
29
30fn get_default_output() -> String {
31    let default = String::from("slides.pdf");
32
33    let Ok(config) = read_and_validate_config() else {
34        println!("Warning: Could not read oseda-config.json");
35        println!("Using slides.pdf as name instead");
36        return default;
37    };
38
39    format!("{}.pdf", config.title.trim())
40}
41
42/// Export the current Oseda project to a PDF file via `decktape`
43pub fn export(opts: ExportOptions) -> Result<(), Box<dyn Error>> {
44    println!("Cleaning any existing oseda processing...");
45    if kill_port(opts.port).is_err() {
46        eprintln!("Warning, could not kill value on desired port")
47    }
48
49    let output = Command::new("npm")
50        .args(["install", "decktape@3.15.0"])
51        .current_dir(".")
52        .output()?;
53
54    // prompt for puppeteer and confirm installation
55    if !is_puppeteer_chrome_installed() {
56        prompt_install_puppeteer_chrome()?;
57    }
58    println!("Puppeteer Chrome is installed, continuing...");
59
60    if !output.status.success() {
61        eprintln!(
62            "Decktape installation failure: {}",
63            String::from_utf8_lossy(&output.stderr)
64        );
65        return Err("npm init failed".into());
66    }
67
68    // decktape automatic http://localhost:3000/ Desktop/IntroToRust/slides.pdf
69
70    let shutdown_flag = Arc::new(AtomicBool::new(false));
71    let run_flag = shutdown_flag.clone();
72
73    let run_handle = std::thread::spawn(move || run::run_with_shutdown(run_flag));
74
75    // wait a moment for the localhost server to spin up
76    std::thread::sleep(std::time::Duration::from_millis(10000));
77
78    let addr = format!("http://localhost:{}", opts.port);
79
80    // run decktape, assuming the server has spun up by now
81    let export_output = Command::new("npm")
82        .args(["exec", "decktape", "reveal", &addr, &opts.output])
83        .output()?;
84
85    // send shutdown flag, should signal to run_with_shutdown to kill the process
86    shutdown_flag.store(true, Ordering::SeqCst);
87    // wait to run to terminate (hopefully gracefully) and join the process to cur. thread
88    let _ = run_handle.join();
89
90    if !export_output.status.success() {
91        eprintln!(
92            "Decktape PDF export failure: {}",
93            String::from_utf8_lossy(&export_output.stderr)
94        );
95        return Err("npm init failed".into());
96    }
97
98    Ok(())
99}