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
171
172
173
174
175
use {
crate::*,
crossbeam::channel,
notify::{
EventKind,
RecommendedWatcher,
RecursiveMode,
Watcher,
event::{
AccessKind,
AccessMode,
DataChange,
ModifyKind,
RenameMode,
},
},
std::{
path::PathBuf,
sync::{
Arc,
atomic::{
AtomicBool,
Ordering,
},
},
thread,
},
termimad::crossterm::style::Stylize,
};
const DEBOUNCE_DELAY_MS: u64 = 40;
#[derive(Debug)]
pub enum FileChange {
/// Creation, move inside, edition, etc.
Write(PathBuf),
/// Removal, move outside, etc. but also first part of a move inside
Removal(PathBuf),
/// Anything else looking relevant (e.g. multiple files written)
/// A full rebuild is probably needed
Other,
}
/// Something that should be watched for changes to trigger a rebuild of the project
pub struct WatchTarget {
pub path: PathBuf,
pub recursive: bool, // ie directory or not
}
impl WatchTarget {
pub fn new_dir<P: Into<PathBuf>>(path: P) -> Self {
Self {
path: path.into(),
recursive: true,
}
}
pub fn new_file<P: Into<PathBuf>>(path: P) -> Self {
Self {
path: path.into(),
recursive: false,
}
}
}
/// watch for file changes to keep a project up to date
///
/// Caller should keep the returned watcher alive (e.g., by storing it in a variable)
/// as watching stops when the watcher is dropped.
pub fn rebuild_on_change(
mut project: Project,
base_url: String, // to display the modified page URL
) -> Result<RecommendedWatcher, notify::Error> {
let skip = Arc::new(AtomicBool::new(false));
let snd_skip = skip.clone();
//let (snd, rcv) = mpsc::sync_channel::<FileChange>(100);
let (snd, rcv) = channel::unbounded::<FileChange>();
let mut watcher =
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
Ok(we) => {
// Filter to get events which are relevant for a rebuild
// (not a cleaning) of the project:
// - file being modified
// - file being created
// - file being renamed
// - file being removed (matters when the file is linked from the head)
let mut is_removal = false;
match we.kind {
EventKind::Modify(ModifyKind::Metadata(_)) => {
return; // useless event
}
EventKind::Modify(ModifyKind::Data(DataChange::Content)) => {
debug!("modify content event: {we:?}");
}
EventKind::Modify(ModifyKind::Data(DataChange::Any)) => {
return; // probably useless event with no real change
}
EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {
debug!("rename from event: {we:?}");
is_removal = true;
}
EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {
debug!("rename to event: {we:?}");
}
EventKind::Access(AccessKind::Close(AccessMode::Write)) => {
// file was created or modified
debug!("close write event: {we:?}");
}
EventKind::Access(_) => {
return; // probably useless event
}
_ => {
// probably useless, log just in case a user has missing rebuilds
debug!("skipped notify event: {we:?}");
return;
}
}
if snd_skip.load(Ordering::SeqCst) {
debug!("skipping event due to skip flag: {we:?}");
return;
}
snd_skip.store(true, Ordering::SeqCst);
let path = if we.paths.len() == 1 {
Some(we.paths[0].clone())
} else {
None // several paths changed
};
let change = match (path, is_removal) {
(Some(p), true) => FileChange::Removal(p),
(Some(p), false) => FileChange::Write(p),
(None, _) => FileChange::Other,
};
let _ = snd.send(change);
}
Err(e) => warn!("watch error: {e:?}"),
})?;
for target in project.watch_targets() {
watcher.watch(
&target.path,
if target.recursive {
RecursiveMode::Recursive
} else {
RecursiveMode::NonRecursive
},
)?;
}
// start the build thread
thread::spawn(move || {
let debounce_delay = std::time::Duration::from_millis(DEBOUNCE_DELAY_MS);
loop {
match rcv.recv() {
Ok(change) => {
thread::sleep(debounce_delay);
info!("rebuilding site due to {change:?}");
let start = std::time::Instant::now();
match project.update(change, &base_url) {
Ok(true) => eprintln!("Site rebuilt in {}", duration_since(start)),
Ok(false) => debug!("No rebuild needed"),
Err(e) => eprintln!("{}{}", "Error rebuilding site: ".red().bold(), e),
}
skip.store(false, Ordering::SeqCst);
}
Err(e) => {
warn!("rebuild_on_change channel error: {e:?}");
break;
}
}
}
});
Ok(watcher)
}
pub fn duration_since(start: std::time::Instant) -> String {
let millis = start.elapsed().as_secs_f32() / 1000.0;
format!("{:.3}ms", millis)
}