1use std::collections::HashMap;
10use std::path::PathBuf;
11use std::sync::Mutex;
12use std::time::Instant;
13
14static LAST_CHANGE: Mutex<Option<HashMap<PathBuf, Instant>>> = Mutex::new(None);
16
17fn with_state<R>(f: impl FnOnce(&mut HashMap<PathBuf, Instant>) -> R) -> R {
18 let mut guard = LAST_CHANGE.lock().unwrap();
19 let map = guard.get_or_insert_with(HashMap::new);
20 f(map)
21}
22
23pub fn document_changed(file: &str) {
27 let path = PathBuf::from(file);
28 with_state(|map| {
29 map.insert(path, Instant::now());
30 });
31}
32
33pub fn is_idle(file: &str, debounce_ms: u64) -> bool {
39 let path = PathBuf::from(file);
40 with_state(|map| {
41 match map.get(&path) {
42 None => true, Some(last) => last.elapsed().as_millis() >= debounce_ms as u128,
44 }
45 })
46}
47
48pub fn is_tracked(file: &str) -> bool {
53 let path = PathBuf::from(file);
54 with_state(|map| map.contains_key(&path))
55}
56
57pub fn await_idle(file: &str, debounce_ms: u64, timeout_ms: u64) -> bool {
63 let start = Instant::now();
64 let timeout = std::time::Duration::from_millis(timeout_ms);
65 let poll_interval = std::time::Duration::from_millis(100);
66
67 loop {
68 if is_idle(file, debounce_ms) {
69 return true;
70 }
71 if start.elapsed() >= timeout {
72 return false;
73 }
74 std::thread::sleep(poll_interval);
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn idle_when_no_changes() {
84 assert!(is_idle("/tmp/test-no-changes.md", 1500));
85 }
86
87 #[test]
88 fn not_idle_after_change() {
89 document_changed("/tmp/test-just-changed.md");
90 assert!(!is_idle("/tmp/test-just-changed.md", 1500));
91 }
92
93 #[test]
94 fn idle_after_debounce_period() {
95 document_changed("/tmp/test-debounce.md");
96 std::thread::sleep(std::time::Duration::from_millis(50));
98 assert!(is_idle("/tmp/test-debounce.md", 10));
99 }
100
101 #[test]
102 fn await_idle_returns_immediately_when_idle() {
103 let start = Instant::now();
104 assert!(await_idle("/tmp/test-await-idle.md", 100, 5000));
105 assert!(start.elapsed().as_millis() < 200);
106 }
107
108 #[test]
109 fn await_idle_waits_for_settle() {
110 document_changed("/tmp/test-await-settle.md");
111 let start = Instant::now();
112 assert!(await_idle("/tmp/test-await-settle.md", 200, 5000));
113 assert!(start.elapsed().as_millis() >= 200);
114 }
115}