1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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: ¬ify::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,
}
}
}
|