watchdiff_tui/core/
watcher.rs

1use 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        // Create the notify watcher
25        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        // Spawn background thread to process notify events
35        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                        // Debounce rapid events on the same path
43                        let now = std::time::Instant::now();
44                        
45                        for path in event.paths {
46                            // Filter out ignored files
47                            if !filter_clone.should_watch(&path) {
48                                continue;
49                            }
50                            
51                            // Debounce: ignore events that happen too quickly after the previous one
52                            if let Some(last_time) = last_event_time.get(&path) {
53                                if now.duration_since(*last_time) < Duration::from_millis(100) {
54                                    continue;  // Skip this event as it's too soon
55                                }
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                                    // For new files, read content for preview
64                                    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                                    // Generate diff for modified files
81                                    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                                                // Skip if content hasn't actually changed
85                                                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                                                // First time seeing this file - show a preview instead of empty diff
92                                                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; // Receiver dropped, exit thread
114                                }
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}