1use std::path::{Component, Path};
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::mpsc::{Receiver, RecvTimeoutError};
5use std::time::Duration;
6
7use notify::{Event, EventKind};
8
9use crate::error::Error;
10use crate::workspace::SKIP_NAMES;
11
12pub(crate) const STOP_TICK: Duration = Duration::from_millis(100);
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum WatchEvent {
19 Started,
22 Snapshotted {
24 snapshot: String,
26 },
27}
28
29#[derive(Debug, Clone, Default)]
32pub struct WatchStop(Arc<AtomicBool>);
33
34impl WatchStop {
35 #[must_use]
37 pub fn new() -> Self {
38 Self::default()
39 }
40
41 pub fn stop(&self) {
43 self.0.store(true, Ordering::Release);
44 }
45
46 pub(crate) fn stopped(&self) -> bool {
47 self.0.load(Ordering::Acquire)
48 }
49}
50
51pub(crate) type Pulse = Result<(), notify::Error>;
54
55pub(crate) fn settle(
58 pulses: &Receiver<Pulse>,
59 debounce: Duration,
60 stop: &WatchStop,
61) -> Result<(), Error> {
62 loop {
63 if stop.stopped() {
64 return Ok(());
65 }
66 match pulses.recv_timeout(debounce) {
67 Ok(Ok(())) => {}
68 Ok(Err(error)) => return Err(watcher_failed(&error)),
69 Err(RecvTimeoutError::Timeout) => return Ok(()),
70 Err(RecvTimeoutError::Disconnected) => return Err(watcher_gone()),
71 }
72 }
73}
74
75pub(crate) fn watcher_failed(error: ¬ify::Error) -> Error {
76 Error::Engine(format!("the file watcher failed: {error}"))
77}
78
79pub(crate) fn watcher_gone() -> Error {
80 Error::Engine("the file watcher stopped delivering events".to_owned())
81}
82
83pub(crate) fn event_is_content(root: &Path, event: &Event) -> bool {
87 if matches!(event.kind, EventKind::Access(_)) {
88 return false;
89 }
90 if event.paths.is_empty() {
93 return true;
94 }
95 event.paths.iter().any(|path| is_content_path(root, path))
96}
97
98fn is_content_path(root: &Path, path: &Path) -> bool {
99 let Ok(relative) = path.strip_prefix(root) else {
100 return true;
101 };
102 match relative.components().next() {
103 Some(Component::Normal(name)) => !SKIP_NAMES.iter().any(|skip| name == *skip),
104 _ => true,
105 }
106}