diff --git a/Cargo.toml b/Cargo.toml index 1f23537..4b45e7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,8 @@ sha2 = "0.10.8" toml = "0.8" live-server = "0.10.0" notify = "8.0" -indoc = "2.0.6" +indoc = "2" +ignore = "0.4" [dev-dependencies] assert_cmd = "2" diff --git a/examples/first.typ b/examples/first.typ index bf575ee..ae0f4d9 100644 --- a/examples/first.typ +++ b/examples/first.typ @@ -19,9 +19,12 @@ #toolbox.pdfpc.speaker-note(" What if you could easily generate videos from text? + + I think that would be pretty cool. ") ] + #slide[ #set page(fill: black, margin: 3em) #set text(fill: white) @@ -35,7 +38,12 @@ ] #toolbox.pdfpc.speaker-note(" - That would be pretty cool. Here is a plan to make it happen. + + Step 1 is easy. Generate videos. + + Step 2 is for you to figure out. + + Step 3 is profit. ") ] diff --git a/src/main.rs b/src/main.rs index 7dc1752..c305d8f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -117,6 +117,10 @@ pub(crate) struct WatchArgs { /// Port to run the server on. #[arg(long, default_value = "8080")] port: u16, + + /// Command to run before compiling the Typst file. + #[arg(long)] + pre_typst: Option, } #[derive(Clone, Debug, clap::Subcommand)] @@ -124,7 +128,7 @@ enum Task { /// Build the video. Build(BuildArgs), - /// Watch the input file and rebuild the video when it changes. + /// Watch the current directory and rebuild the video on change. Watch(WatchArgs), } diff --git a/src/watch.rs b/src/watch.rs index 8776f26..e0b63eb 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -2,6 +2,7 @@ use crate::build; use crate::slide::Slide; use crate::Arguments; use crate::WatchArgs; +use ignore::Walk; use live_server::listen; use notify::recommended_watcher; use notify::Event; @@ -139,14 +140,47 @@ fn remove_old_files(args: &Arguments, timestamp: u64) { } } +#[derive(Clone, Debug, PartialEq)] +/// Status of the command. +/// +/// This can be used to avoid crashing the watch loop completely. Instead, +/// report an error and ignore further actions until the loop is called again. +/// This allows the user to fix the problem and continue without having to +/// manually restart the `trv watch`. +enum Status { + Success, + Failure, +} + +fn run_pre_typst(watch_args: &WatchArgs) -> Status { + if let Some(pre_typst) = &watch_args.pre_typst { + tracing::info!("Running pre-typst command..."); + let mut cmd = std::process::Command::new("/usr/bin/env"); + cmd.arg("bash"); + cmd.arg("-c"); + cmd.arg(pre_typst); + let output = cmd.output().expect("Failed to run pre-typst command"); + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + tracing::error!("pre-typst command failed: {}", stderr.trim()); + return Status::Failure; + } + } + Status::Success +} + async fn watch_build(watch_args: &WatchArgs, args: &Arguments) { let release = false; let input = watch_args.input.clone(); let audio_codec = None; - let slides = build(input.clone(), args, release, audio_codec).await; - let timestamp = move_files_into_public(args, &slides); - build_index(args, &slides, timestamp, false); - remove_old_files(args, timestamp); + + let status = run_pre_typst(watch_args); + if status == Status::Success { + let slides = build(input.clone(), args, release, audio_codec).await; + let timestamp = move_files_into_public(args, &slides); + build_index(args, &slides, timestamp, false); + remove_old_files(args, timestamp); + } } fn spawn_server(watch_args: &WatchArgs, args: &Arguments) { @@ -172,10 +206,20 @@ fn spawn_server(watch_args: &WatchArgs, args: &Arguments) { pub async fn watch(watch_args: &WatchArgs, args: &Arguments) { let (tx, rx) = mpsc::channel::>(); let mut watcher = recommended_watcher(tx).unwrap(); - let input = watch_args.input.clone(); - watcher - .watch(&input, notify::RecursiveMode::NonRecursive) - .expect("Failed to watch"); + let mode = notify::RecursiveMode::NonRecursive; + // Watch the current directory since that is probably the most intuitive + // path to watch. It also would allow watching scripts that are in a + // directory that is above the Typst file. For Typst, files have to be in + // the same directory, but allowing the current directory gives more + // flexibility. + // + // Flatten ignores the errors (e.g., permission errors). + for entry in Walk::new("./").flatten() { + let path = entry.path(); + if !path.is_dir() { + watcher.watch(path, mode).expect("Failed to watch"); + } + } let public_path = public_dir(args); if !public_path.exists() {