summaryrefslogtreecommitdiff
path: root/crates/typst-cli/src/watch.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-cli/src/watch.rs')
-rw-r--r--crates/typst-cli/src/watch.rs170
1 files changed, 170 insertions, 0 deletions
diff --git a/crates/typst-cli/src/watch.rs b/crates/typst-cli/src/watch.rs
new file mode 100644
index 00000000..2ad73f85
--- /dev/null
+++ b/crates/typst-cli/src/watch.rs
@@ -0,0 +1,170 @@
+use std::collections::HashSet;
+use std::io::{self, IsTerminal, Write};
+use std::path::{Path, PathBuf};
+
+use codespan_reporting::term::{self, termcolor};
+use notify::{RecommendedWatcher, RecursiveMode, Watcher};
+use same_file::is_same_file;
+use termcolor::WriteColor;
+use typst::diag::StrResult;
+use typst::eval::eco_format;
+
+use crate::args::CompileCommand;
+use crate::color_stream;
+use crate::compile::compile_once;
+use crate::world::SystemWorld;
+
+/// Execute a watching compilation command.
+pub fn watch(mut command: CompileCommand) -> StrResult<()> {
+ // Create the world that serves sources, files, and fonts.
+ let mut world = SystemWorld::new(&command)?;
+
+ // Perform initial compilation.
+ compile_once(&mut world, &mut command, true)?;
+
+ // Setup file watching.
+ let (tx, rx) = std::sync::mpsc::channel();
+ let mut watcher = RecommendedWatcher::new(tx, notify::Config::default())
+ .map_err(|_| "failed to setup file watching")?;
+
+ // Watch all the files that are used by the input file and its dependencies.
+ watch_dependencies(&mut world, &mut watcher, HashSet::new())?;
+
+ // Handle events.
+ let timeout = std::time::Duration::from_millis(100);
+ let output = command.output();
+ loop {
+ let mut recompile = false;
+ for event in rx
+ .recv()
+ .into_iter()
+ .chain(std::iter::from_fn(|| rx.recv_timeout(timeout).ok()))
+ {
+ let event = event.map_err(|_| "failed to watch directory")?;
+ recompile |= is_event_relevant(&event, &output);
+ }
+
+ if recompile {
+ // Retrieve the dependencies of the last compilation.
+ let previous: HashSet<PathBuf> =
+ world.dependencies().map(ToOwned::to_owned).collect();
+
+ // Recompile.
+ compile_once(&mut world, &mut command, true)?;
+ comemo::evict(10);
+
+ // Adjust the watching.
+ watch_dependencies(&mut world, &mut watcher, previous)?;
+ }
+ }
+}
+
+/// Adjust the file watching. Watches all new dependencies and unwatches
+/// all `previous` dependencies that are not relevant anymore.
+#[tracing::instrument(skip_all)]
+fn watch_dependencies(
+ world: &mut SystemWorld,
+ watcher: &mut dyn Watcher,
+ mut previous: HashSet<PathBuf>,
+) -> StrResult<()> {
+ // Watch new paths that weren't watched yet.
+ for path in world.dependencies() {
+ let watched = previous.remove(path);
+ if path.exists() && !watched {
+ tracing::info!("Watching {}", path.display());
+ watcher
+ .watch(path, RecursiveMode::NonRecursive)
+ .map_err(|_| eco_format!("failed to watch {path:?}"))?;
+ }
+ }
+
+ // Unwatch old paths that don't need to be watched anymore.
+ for path in previous {
+ tracing::info!("Unwatching {}", path.display());
+ watcher.unwatch(&path).ok();
+ }
+
+ Ok(())
+}
+
+/// Whether a watch event is relevant for compilation.
+fn is_event_relevant(event: &notify::Event, output: &Path) -> bool {
+ // Never recompile because the output file changed.
+ if event
+ .paths
+ .iter()
+ .all(|path| is_same_file(path, output).unwrap_or(false))
+ {
+ return false;
+ }
+
+ match &event.kind {
+ notify::EventKind::Any => true,
+ notify::EventKind::Access(_) => false,
+ notify::EventKind::Create(_) => true,
+ notify::EventKind::Modify(kind) => match kind {
+ notify::event::ModifyKind::Any => true,
+ notify::event::ModifyKind::Data(_) => true,
+ notify::event::ModifyKind::Metadata(_) => false,
+ notify::event::ModifyKind::Name(_) => true,
+ notify::event::ModifyKind::Other => false,
+ },
+ notify::EventKind::Remove(_) => true,
+ notify::EventKind::Other => false,
+ }
+}
+
+/// The status in which the watcher can be.
+pub enum Status {
+ Compiling,
+ Success(std::time::Duration),
+ Error,
+}
+
+impl Status {
+ /// Clear the terminal and render the status message.
+ pub fn print(&self, command: &CompileCommand) -> io::Result<()> {
+ let output = command.output();
+ let timestamp = chrono::offset::Local::now().format("%H:%M:%S");
+ let color = self.color();
+
+ let mut w = color_stream();
+ if std::io::stderr().is_terminal() {
+ // Clear the terminal.
+ let esc = 27 as char;
+ write!(w, "{esc}c{esc}[1;1H")?;
+ }
+
+ w.set_color(&color)?;
+ write!(w, "watching")?;
+ w.reset()?;
+ writeln!(w, " {}", command.input.display())?;
+
+ w.set_color(&color)?;
+ write!(w, "writing to")?;
+ w.reset()?;
+ writeln!(w, " {}", output.display())?;
+
+ writeln!(w)?;
+ writeln!(w, "[{timestamp}] {}", self.message())?;
+ writeln!(w)?;
+
+ w.flush()
+ }
+
+ fn message(&self) -> String {
+ match self {
+ Self::Compiling => "compiling ...".into(),
+ Self::Success(duration) => format!("compiled successfully in {duration:.2?}"),
+ Self::Error => "compiled with errors".into(),
+ }
+ }
+
+ fn color(&self) -> termcolor::ColorSpec {
+ let styles = term::Styles::default();
+ match self {
+ Self::Error => styles.header_error,
+ _ => styles.header_note,
+ }
+ }
+}