watchdiff_tui/core/
watcher.rs1use std::path::{Path, PathBuf};
2use std::sync::mpsc::{self, Receiver, Sender};
3use std::thread;
4use std::time::Duration;
5use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher};
6use anyhow::{Result, Context};
7use super::{FileEvent, FileEventKind, filter::FileFilter};
8use super::events::AppEvent;
9
10pub struct FileWatcher {
11 _watcher: RecommendedWatcher,
12 event_rx: Receiver<AppEvent>,
13 filter: FileFilter,
14}
15
16impl FileWatcher {
17 pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
18 let path = path.as_ref();
19 let filter = FileFilter::new(path)?;
20
21 let (tx, rx) = mpsc::channel::<notify::Result<Event>>();
22 let (event_tx, event_rx) = mpsc::channel::<AppEvent>();
23
24 let mut watcher = notify::recommended_watcher(tx)
26 .context("Failed to create file system watcher")?;
27
28 watcher
29 .watch(path, RecursiveMode::Recursive)
30 .context("Failed to start watching directory")?;
31
32 let filter_clone = FileFilter::new(path)?;
33
34 thread::spawn(move || {
36 let mut previous_contents = std::collections::HashMap::<PathBuf, String>::new();
37 let mut last_event_time = std::collections::HashMap::<PathBuf, std::time::Instant>::new();
38
39 while let Ok(result) = rx.recv() {
40 match result {
41 Ok(event) => {
42 let now = std::time::Instant::now();
44
45 for path in event.paths {
46 if !filter_clone.should_watch(&path) {
48 continue;
49 }
50
51 if let Some(last_time) = last_event_time.get(&path) {
53 if now.duration_since(*last_time) < Duration::from_millis(100) {
54 continue; }
56 }
57 last_event_time.insert(path.clone(), now);
58
59 let file_event = match event.kind {
60 notify::EventKind::Create(_) => {
61 let mut fe = FileEvent::new(path.clone(), FileEventKind::Created);
62
63 if filter_clone.is_text_file(&path) {
65 if let Ok(content) = std::fs::read_to_string(&path) {
66 let preview = if content.len() > 200 {
67 format!("{}...", &content[..200])
68 } else {
69 content.clone()
70 };
71 fe = fe.with_preview(preview);
72 previous_contents.insert(path.clone(), content);
73 }
74 }
75 Some(fe)
76 }
77 notify::EventKind::Modify(_) => {
78 let mut fe = FileEvent::new(path.clone(), FileEventKind::Modified);
79
80 if filter_clone.is_text_file(&path) {
82 if let Ok(new_content) = std::fs::read_to_string(&path) {
83 if let Some(old_content) = previous_contents.get(&path) {
84 if *old_content == new_content {
86 continue;
87 }
88 let diff = crate::diff::generate_unified_diff(old_content, &new_content, &path, &path);
89 fe = fe.with_diff(diff);
90 } else {
91 let preview = if new_content.len() > 200 {
93 format!("{}...", &new_content[..200])
94 } else {
95 new_content.clone()
96 };
97 fe = fe.with_preview(preview);
98 }
99 previous_contents.insert(path.clone(), new_content);
100 }
101 }
102 Some(fe)
103 }
104 notify::EventKind::Remove(_) => {
105 previous_contents.remove(&path);
106 Some(FileEvent::new(path.clone(), FileEventKind::Deleted))
107 }
108 _ => None,
109 };
110
111 if let Some(fe) = file_event {
112 if event_tx.send(AppEvent::FileChanged(fe)).is_err() {
113 break; }
115 }
116 }
117 }
118 Err(err) => {
119 tracing::error!("File watcher error: {}", err);
120 }
121 }
122 }
123 });
124
125 Ok(Self {
126 _watcher: watcher,
127 event_rx,
128 filter,
129 })
130 }
131
132 pub fn try_recv(&self) -> Result<AppEvent, std::sync::mpsc::TryRecvError> {
133 self.event_rx.try_recv()
134 }
135
136 pub fn recv(&self) -> Result<AppEvent, std::sync::mpsc::RecvError> {
137 self.event_rx.recv()
138 }
139
140 pub fn recv_timeout(&self, timeout: Duration) -> Result<AppEvent, std::sync::mpsc::RecvTimeoutError> {
141 self.event_rx.recv_timeout(timeout)
142 }
143
144 pub fn get_initial_files(&self) -> Result<Vec<PathBuf>> {
145 self.filter.get_watchable_files()
146 }
147}
148
149pub fn start_ticker(sender: Sender<AppEvent>) {
150 thread::spawn(move || {
151 loop {
152 thread::sleep(Duration::from_millis(100));
153 if sender.send(AppEvent::Tick).is_err() {
154 break;
155 }
156 }
157 });
158}