include_exclude_watcher/
lib.rs

1//! Async file watcher with glob-based include/exclude patterns.
2//!
3//! This crate provides an efficient recursive file watcher using Linux's inotify,
4//! with built-in support for glob patterns to filter events. Unlike most file
5//! watchers that require you to filter events after receiving them, this watcher
6//! only sets up watches on directories that could potentially match your patterns,
7//! reducing resource usage on large directory trees.
8//!
9//! # Features
10//!
11//! - **Selective directory watching**: Only watches directories that could match your include patterns
12//! - **Glob patterns**: Supports `*`, `**`, `?`, and character classes like `[a-z]`
13//! - **Include/exclude filtering**: Gitignore-style pattern matching with exclude taking precedence
14//! - **Pattern files**: Load patterns from `.gitignore`-style files
15//! - **Event filtering**: Watch only creates, deletes, updates, or any combination
16//! - **Type filtering**: Match only files, only directories, or both
17//! - **Debouncing**: Built-in debounce support to batch rapid changes
18//! - **Async/await**: Native tokio integration
19//!
20//! # Platform Support
21//!
22//! **Linux only** (uses inotify directly). PRs welcome for other platforms.
23//!
24//! # Quick Start
25//!
26//! ```no_run
27//! use include_exclude_watcher::{Watcher, WatchEvent};
28//!
29//! #[tokio::main]
30//! async fn main() -> std::io::Result<()> {
31//!     Watcher::new()
32//!         .set_base_dir("./src")
33//!         .add_include("**/*.rs")
34//!         .add_exclude("**/target/**")
35//!         .run(|event, path| {
36//!             println!("{:?}: {}", event, path.display());
37//!         })
38//!         .await
39//! }
40//! ```
41//!
42//! # Debounced Watching
43//!
44//! When files change rapidly (e.g., during a build), you often want to wait
45//! for changes to settle before taking action:
46//!
47//! ```no_run
48//! use include_exclude_watcher::Watcher;
49//!
50//! #[tokio::main]
51//! async fn main() -> std::io::Result<()> {
52//!     Watcher::new()
53//!         .set_base_dir("./src")
54//!         .add_include("**/*.rs")
55//!         .run_debounced(500, |first_changed_path| {
56//!             println!("Files changed! First: {}", first_changed_path.display());
57//!         })
58//!         .await
59//! }
60//! ```
61//!
62//! # Pattern Syntax
63//!
64//! Patterns use glob syntax similar to `.gitignore`:
65//!
66//! | Pattern | Matches |
67//! |---------|---------|
68//! | `*` | Any characters except `/` |
69//! | `**` | Any characters including `/` (matches across directories) |
70//! | `?` | Any single character except `/` |
71//! | `[abc]` | Any character in the set |
72//! | `[a-z]` | Any character in the range |
73//!
74//! ## Pattern Behavior
75//!
76//! - Patterns **without** `/` match anywhere in the tree (like gitignore).
77//!   For example, `*.rs` matches `foo.rs` and `src/bar.rs`.
78//! - Patterns **with** `/` are anchored to the base directory.
79//!   For example, `src/*.rs` matches `src/main.rs` but not `src/sub/lib.rs`.
80//!
81//! ## Examples
82//!
83//! | Pattern | Description |
84//! |---------|-------------|
85//! | `*.rs` | All Rust files anywhere |
86//! | `src/*.rs` | Rust files directly in `src/` |
87//! | `**/test_*.rs` | Test files anywhere |
88//! | `target/**` | Everything under `target/` |
89//! | `*.{rs,toml}` | Rust and TOML files (character class) |
90//!
91//! # Loading Patterns from Files
92//!
93//! You can load exclude patterns from gitignore-style files:
94//!
95//! ```no_run
96//! use include_exclude_watcher::Watcher;
97//!
98//! #[tokio::main]
99//! async fn main() -> std::io::Result<()> {
100//!     Watcher::new()
101//!         .set_base_dir("/project")
102//!         .add_include("**/*")
103//!         .add_ignore_file(".gitignore")
104//!         .add_ignore_file(".watchignore")
105//!         .run(|event, path| {
106//!             println!("{:?}: {}", event, path.display());
107//!         })
108//!         .await
109//! }
110//! ```
111//!
112//! Pattern file format:
113//! - Lines starting with `#` are comments
114//! - Empty lines are ignored
115//! - All other lines are exclude patterns
116//! - **Note**: `!` negation patterns are not supported (excludes always take precedence)
117//!
118//! # Filtering Events
119//!
120//! You can filter which events to receive and what types to match:
121//!
122//! ```no_run
123//! use include_exclude_watcher::Watcher;
124//!
125//! # async fn example() -> std::io::Result<()> {
126//! Watcher::new()
127//!     .add_include("**/*.rs")
128//!     .watch_create(true)   // Receive create events
129//!     .watch_delete(true)   // Receive delete events
130//!     .watch_update(false)  // Ignore modifications
131//!     .match_files(true)    // Match regular files
132//!     .match_dirs(false)    // Ignore directories
133//!     .run(|event, path| {
134//!         // Only file creates and deletes
135//!     })
136//!     .await
137//! # }
138//! ```
139
140use std::collections::{HashMap, HashSet};
141use std::ffi::CString;
142use std::fs;
143use std::io::{BufRead, BufReader, Result};
144use std::os::unix::ffi::OsStrExt;
145use std::os::unix::io::AsRawFd;
146use std::path::{Path, PathBuf};
147use std::time::Duration;
148use tokio::io::unix::AsyncFd;
149
150// --- Pattern Parsing ---
151
152/// Simple glob pattern matcher for a single path component.
153/// Supports: * (any chars), ? (single char), [abc] (char class), [a-z] (range)
154#[derive(Debug, Clone)]
155struct GlobPattern {
156    pattern: String,
157}
158
159impl PartialEq for GlobPattern {
160    fn eq(&self, other: &Self) -> bool {
161        self.pattern == other.pattern
162    }
163}
164
165impl GlobPattern {
166    fn new(pattern: &str) -> Self {
167        Self {
168            pattern: pattern.to_string(),
169        }
170    }
171
172    fn matches(&self, text: &str) -> bool {
173        Self::match_recursive(self.pattern.as_bytes(), text.as_bytes())
174    }
175
176    fn match_recursive(pattern: &[u8], text: &[u8]) -> bool {
177        let mut p = 0;
178        let mut t = 0;
179
180        // For backtracking on '*'
181        let mut star_p = None;
182        let mut star_t = None;
183
184        while t < text.len() {
185            if p < pattern.len() {
186                match pattern[p] {
187                    b'*' => {
188                        // '*' matches zero or more characters
189                        star_p = Some(p);
190                        star_t = Some(t);
191                        p += 1;
192                        continue;
193                    }
194                    b'?' => {
195                        // '?' matches exactly one character
196                        p += 1;
197                        t += 1;
198                        continue;
199                    }
200                    b'[' => {
201                        // Character class
202                        if let Some((matched, end_pos)) =
203                            Self::match_char_class(&pattern[p..], text[t])
204                        {
205                            if matched {
206                                p += end_pos;
207                                t += 1;
208                                continue;
209                            }
210                        }
211                        // Fall through to backtrack
212                    }
213                    c => {
214                        // Literal character match
215                        if c == text[t] {
216                            p += 1;
217                            t += 1;
218                            continue;
219                        }
220                        // Fall through to backtrack
221                    }
222                }
223            }
224
225            // No match at current position, try backtracking
226            if let (Some(sp), Some(st)) = (star_p, star_t) {
227                // Backtrack: make '*' match one more character
228                p = sp + 1;
229                star_t = Some(st + 1);
230                t = st + 1;
231            } else {
232                return false;
233            }
234        }
235
236        // Consume any trailing '*' in pattern
237        while p < pattern.len() && pattern[p] == b'*' {
238            p += 1;
239        }
240
241        p == pattern.len()
242    }
243
244    /// Match a character class like [abc] or [a-z] or [!abc]
245    /// Returns (matched, bytes_consumed) if valid class, None if invalid
246    fn match_char_class(pattern: &[u8], ch: u8) -> Option<(bool, usize)> {
247        if pattern.is_empty() || pattern[0] != b'[' {
248            return None;
249        }
250
251        let mut i = 1;
252        let mut matched = false;
253        let negated = i < pattern.len() && (pattern[i] == b'!' || pattern[i] == b'^');
254        if negated {
255            i += 1;
256        }
257
258        while i < pattern.len() {
259            if pattern[i] == b']' && i > 1 + (negated as usize) {
260                // End of character class
261                return Some((matched != negated, i + 1));
262            }
263
264            // Check for range: a-z
265            if i + 2 < pattern.len() && pattern[i + 1] == b'-' && pattern[i + 2] != b']' {
266                let start = pattern[i];
267                let end = pattern[i + 2];
268                if ch >= start && ch <= end {
269                    matched = true;
270                }
271                i += 3;
272            } else {
273                // Single character
274                if pattern[i] == ch {
275                    matched = true;
276                }
277                i += 1;
278            }
279        }
280
281        // No closing bracket found
282        None
283    }
284}
285
286#[derive(Debug, Clone, PartialEq)]
287enum Segment {
288    Exact(String),
289    Wildcard(GlobPattern),
290    DoubleWildcard, // **
291}
292
293#[derive(Debug, Clone)]
294struct Pattern {
295    segments: Vec<Segment>,
296}
297
298impl Pattern {
299    fn parse(pattern: &str) -> Self {
300        let mut segments = Vec::new();
301
302        // Patterns without / match anywhere in the tree (like gitignore)
303        let effective_pattern = if !pattern.contains('/') {
304            format!("**/{}", pattern)
305        } else {
306            pattern.trim_start_matches('/').to_string()
307        };
308
309        let normalized = effective_pattern.replace("//", "/");
310
311        for part in normalized.split('/') {
312            if part.is_empty() || part == "." {
313                continue;
314            }
315
316            if part == "**" {
317                segments.push(Segment::DoubleWildcard);
318            } else if part.contains('*') || part.contains('?') || part.contains('[') {
319                segments.push(Segment::Wildcard(GlobPattern::new(part)));
320            } else {
321                segments.push(Segment::Exact(part.to_string()));
322            }
323        }
324
325        Pattern { segments }
326    }
327
328    fn check(&self, path_segments: &[String], allow_prefix: bool) -> bool {
329        let pattern_segments = &self.segments;
330        let mut path_index = 0;
331
332        for pattern_index in 0..pattern_segments.len() {
333            let pattern_segment = &pattern_segments[pattern_index];
334
335            if path_index >= path_segments.len() {
336                // We ran out of path elements
337                if pattern_segment == &Segment::DoubleWildcard && pattern_index == pattern_segments.len() - 1
338                {
339                    // The only pattern segment we still need to match is **. We'll consider that a match for the parent.
340                    return true;
341                }
342                // Something within this path could potentially match.
343                return allow_prefix;
344            }
345
346            match &pattern_segment {
347                Segment::Exact(s) => {
348                    if s != &path_segments[path_index] {
349                        return false;
350                    }
351                    path_index += 1;
352                }
353                Segment::Wildcard(p) => {
354                    if !p.matches(&path_segments[path_index]) {
355                        return false;
356                    }
357                    path_index += 1;
358                }
359                Segment::DoubleWildcard => {
360                    if allow_prefix {
361                        // If we're matching a **, there can always be some deeply nested dir structure that
362                        // will match the rest of our pattern. So for prefix matching, the answer is always true.
363                        return true;
364                    }
365
366                    let patterns_left = pattern_segments.len() - (pattern_index + 1);
367                    let next_path_index = path_segments.len() - patterns_left;
368                    if next_path_index < path_index {
369                        return false;
370                    }
371                    path_index = next_path_index;
372                }
373            }
374        }
375
376        // If there are spurious path elements, this is not a match.
377        if path_index < path_segments.len() {
378            return false;
379        }
380
381        // We have an exact match. However when in allow_prefix mode, that means this directory is the target
382        // and its contents does not need to be watched.
383        return !allow_prefix;
384    }
385}
386
387// --- Inotify Wrapper ---
388
389struct Inotify {
390    fd: AsyncFd<i32>,
391}
392
393impl Inotify {
394    fn new() -> Result<Self> {
395        let fd = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) };
396        if fd < 0 {
397            return Err(std::io::Error::last_os_error());
398        }
399        Ok(Self {
400            fd: AsyncFd::new(fd)?,
401        })
402    }
403
404    fn add_watch(&self, path: &Path, mask: u32) -> Result<i32> {
405        let c_path = CString::new(path.as_os_str().as_bytes())
406            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
407        let wd = unsafe { libc::inotify_add_watch(self.fd.as_raw_fd(), c_path.as_ptr(), mask) };
408        if wd < 0 {
409            return Err(std::io::Error::last_os_error());
410        }
411        Ok(wd)
412    }
413
414    async fn read_events(&self, buffer: &mut [u8]) -> Result<usize> {
415        loop {
416            let mut guard = self.fd.readable().await?;
417            match guard.try_io(|inner| {
418                let res = unsafe {
419                    libc::read(
420                        inner.as_raw_fd(),
421                        buffer.as_mut_ptr() as *mut _,
422                        buffer.len(),
423                    )
424                };
425                if res < 0 {
426                    Err(std::io::Error::last_os_error())
427                } else {
428                    Ok(res as usize)
429                }
430            }) {
431                Ok(Ok(len)) => return Ok(len),
432                Ok(Err(e)) => {
433                    if e.kind() == std::io::ErrorKind::WouldBlock {
434                        continue;
435                    }
436                    return Err(e);
437                }
438                Err(_) => continue,
439            }
440        }
441    }
442}
443
444impl Drop for Inotify {
445    fn drop(&mut self) {
446        unsafe { libc::close(self.fd.as_raw_fd()) };
447    }
448}
449
450// --- Helper Functions ---
451
452fn path_to_segments(path: &Path) -> Vec<String> {
453    let path_str = path.to_string_lossy();
454    let path_str = path_str.replace("//", "/");
455    path_str
456        .split('/')
457        .filter(|s| !s.is_empty())
458        .map(|s| s.to_string())
459        .collect()
460}
461
462const INOTIFY_MASK: u32 = libc::IN_MODIFY
463    | libc::IN_CLOSE_WRITE
464    | libc::IN_CREATE
465    | libc::IN_DELETE
466    | libc::IN_MOVED_FROM
467    | libc::IN_MOVED_TO
468    | libc::IN_DONT_FOLLOW;
469
470
471fn parse_inotify_events(buffer: &[u8], len: usize) -> Vec<(i32, u32, String)> {
472    let mut events = Vec::new();
473    let mut ptr = buffer.as_ptr();
474    let end = unsafe { ptr.add(len) };
475
476    while ptr < end {
477        let event = unsafe { &*(ptr as *const libc::inotify_event) };
478        let name_len = event.len as usize;
479
480        if name_len > 0 {
481            let name_ptr = unsafe { ptr.add(std::mem::size_of::<libc::inotify_event>()) };
482            let name_slice =
483                unsafe { std::slice::from_raw_parts(name_ptr as *const u8, name_len) };
484            let name_str = String::from_utf8_lossy(name_slice)
485                .trim_matches(char::from(0))
486                .to_string();
487            events.push((event.wd, event.mask, name_str));
488        }
489
490        ptr = unsafe { ptr.add(std::mem::size_of::<libc::inotify_event>() + name_len) };
491    }
492
493    events
494}
495
496/// Type of file system event.
497///
498/// These events correspond to inotify events, but are simplified into three
499/// categories that cover most use cases.
500#[derive(Debug, Clone, Copy, PartialEq, Eq)]
501pub enum WatchEvent {
502    /// File or directory was created.
503    ///
504    /// Also triggered when a file/directory is moved *into* a watched directory.
505    Create,
506    /// File or directory was deleted.
507    ///
508    /// Also triggered when a file/directory is moved *out of* a watched directory.
509    Delete,
510    /// File content was modified.
511    ///
512    /// Triggered on `IN_MODIFY` (content changed) or `IN_CLOSE_WRITE` (file
513    /// opened for writing was closed). Directory content changes (files added/removed)
514    /// are reported as [`Create`](WatchEvent::Create)/[`Delete`](WatchEvent::Delete) instead.
515    Update,
516    /// Initial event for preexisting files/directories.
517    ///
518    /// Only emitted when [`Watcher::watch_initial`] is enabled. Fired once
519    /// for each file or directory that matches the patterns at the time the
520    /// watcher starts, before any file system events are processed.
521    Initial,
522    /// Debug event: a watch was added on this directory.
523    ///
524    /// Only emitted when [`Watcher::debug_watches`] is enabled. Useful for
525    /// understanding which directories are being watched based on your patterns.
526    DebugWatch,
527}
528
529/// Builder for configuring and running a file watcher.
530///
531/// Use method chaining to configure the watcher, then call [`run`](Watcher::run)
532/// or [`run_debounced`](Watcher::run_debounced) to start watching.
533///
534/// # Example
535///
536/// ```no_run
537/// use include_exclude_watcher::Watcher;
538///
539/// # async fn example() -> std::io::Result<()> {
540/// Watcher::new()
541///     .set_base_dir("/project")
542///     .add_include("src/**/*.rs")
543///     .add_include("Cargo.toml")
544///     .add_exclude("**/target/**")
545///     .run(|event, path| {
546///         println!("{:?}: {}", event, path.display());
547///     })
548///     .await
549/// # }
550/// ```
551pub struct Watcher {
552    includes: Vec<String>,
553    excludes: Vec<String>,
554    base_dir: PathBuf,
555    watch_create: bool,
556    watch_delete: bool,
557    watch_update: bool,
558    watch_initial: bool,
559    match_files: bool,
560    match_dirs: bool,
561    return_absolute: bool,
562    debug_watches_enabled: bool,
563}
564
565/// Backwards compatibility alias for [`Watcher`].
566#[deprecated(since = "0.1.2", note = "Renamed to Watcher")]
567pub type WatchBuilder = Watcher;
568
569impl Default for Watcher {
570    fn default() -> Self {
571        Self::new()
572    }
573}
574
575// Runtime state for the watcher
576struct WatcherState<F> {
577    root: PathBuf,
578    inotify: Inotify,
579    watches: HashMap<i32, PathBuf>,
580    paths: HashSet<PathBuf>,
581    include_patterns: Vec<Pattern>,
582    exclude_patterns: Vec<Pattern>,
583    callback: F,
584}
585
586impl Watcher {
587    /// Create a new file watcher with default settings.
588    ///
589    /// Defaults:
590    /// - Base directory: current working directory
591    /// - Includes: none (must be added, or watches everything)
592    /// - Excludes: none
593    /// - Event types: create, delete, update all enabled; initial disabled
594    /// - Match types: both files and directories
595    /// - Path format: relative paths
596    pub fn new() -> Self {
597        Watcher {
598            includes: Vec::new(),
599            excludes: Vec::new(),
600            base_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
601            watch_create: true,
602            watch_delete: true,
603            watch_update: true,
604            watch_initial: false,
605            match_files: true,
606            match_dirs: true,
607            return_absolute: false,
608            debug_watches_enabled: false,
609        }
610    }
611
612    /// Enable debug watch events.
613    ///
614    /// When enabled, [`WatchEvent::DebugWatch`] events will be emitted for each
615    /// directory that is watched. Useful for debugging pattern matching.
616    pub fn debug_watches(mut self, enabled: bool) -> Self {
617        self.debug_watches_enabled = enabled;
618        self
619    }
620
621    /// Add a single include pattern.
622    ///
623    /// Patterns use glob syntax:
624    /// - `*` matches any sequence of characters except `/`
625    /// - `**` matches any sequence of characters including `/`
626    /// - `?` matches any single character except `/`
627    /// - `[abc]` matches any character in the set
628    ///
629    /// Patterns without a `/` match anywhere in the tree (like gitignore).
630    /// For example, `*.rs` is equivalent to `**/*.rs`.
631    pub fn add_include(mut self, pattern: impl Into<String>) -> Self {
632        self.includes.push(pattern.into());
633        self
634    }
635
636    /// Add multiple include patterns.
637    pub fn add_includes(mut self, patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
638        self.includes.extend(patterns.into_iter().map(|p| p.into()));
639        self
640    }
641
642    /// Add a single exclude pattern.
643    ///
644    /// Excludes take precedence over includes. Uses the same glob syntax as includes.
645    pub fn add_exclude(mut self, pattern: impl Into<String>) -> Self {
646        self.excludes.push(pattern.into());
647        self
648    }
649
650    /// Add multiple exclude patterns.
651    pub fn add_excludes(mut self, patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
652        self.excludes.extend(patterns.into_iter().map(|p| p.into()));
653        self
654    }
655
656    /// Add patterns from a gitignore-style file.
657    ///
658    /// Lines starting with `#` are comments. All other non-empty lines are
659    /// exclude patterns. Note: `!` negation patterns are not supported (a
660    /// warning will be printed) because excludes always take precedence over
661    /// includes in this library.
662    ///
663    /// If the file doesn't exist, this method does nothing (no error).
664    ///
665    /// # Example
666    ///
667    /// ```no_run
668    /// use include_exclude_watcher::Watcher;
669    ///
670    /// # async fn example() -> std::io::Result<()> {
671    /// Watcher::new()
672    ///     .set_base_dir("/project")
673    ///     .add_include("*")
674    ///     .add_ignore_file(".gitignore")
675    ///     .add_ignore_file(".watchignore")
676    ///     .run(|event, path| {
677    ///         println!("{:?}: {}", event, path.display());
678    ///     })
679    ///     .await
680    /// # }
681    /// ```
682    pub fn add_ignore_file(mut self, path: impl AsRef<Path>) -> Self {
683        let path = path.as_ref();
684
685        // Resolve relative to base_dir
686        let full_path = if path.is_absolute() {
687            path.to_path_buf()
688        } else {
689            self.base_dir.join(path)
690        };
691
692        if let Ok(file) = fs::File::open(&full_path) {
693            let reader = BufReader::new(file);
694            let mut has_negation = false;
695            for line in reader.lines().map_while(Result::ok) {
696                let trimmed = line.trim();
697
698                // Skip empty lines and comments
699                if trimmed.is_empty() || trimmed.starts_with('#') {
700                    continue;
701                }
702
703                // Lines starting with ! are negations - not supported
704                if trimmed.starts_with('!') {
705                    has_negation = true;
706                } else {
707                    // Regular lines are exclude patterns
708                    self.excludes.push(trimmed.to_string());
709                }
710            }
711            if has_negation {
712                println!("Warning: negation patterns (!) in {} are ignored; excludes always take precedence over includes in this library", full_path.display());
713            }
714        }
715
716        self
717    }
718
719    /// Set the base directory for watching.
720    ///
721    /// All patterns are relative to this directory. Defaults to the current
722    /// working directory.
723    pub fn set_base_dir(mut self, base_dir: impl Into<PathBuf>) -> Self {
724        self.base_dir = base_dir.into();
725        self
726    }
727
728    /// Set whether to watch for file/directory creation events.
729    ///
730    /// Default: `true`
731    pub fn watch_create(mut self, enabled: bool) -> Self {
732        self.watch_create = enabled;
733        self
734    }
735
736    /// Set whether to watch for file/directory deletion events.
737    ///
738    /// Default: `true`
739    pub fn watch_delete(mut self, enabled: bool) -> Self {
740        self.watch_delete = enabled;
741        self
742    }
743
744    /// Set whether to watch for file modification events.
745    ///
746    /// Default: `true`
747    pub fn watch_update(mut self, enabled: bool) -> Self {
748        self.watch_update = enabled;
749        self
750    }
751
752    /// Set whether to emit initial events for preexisting files/directories.
753    ///
754    /// When enabled, [`WatchEvent::Initial`] events will be emitted for all
755    /// files and directories that match the patterns at startup, before any
756    /// file system events are processed. This is useful for building an initial
757    /// inventory of matching files.
758    ///
759    /// Default: `false`
760    ///
761    /// # Example
762    ///
763    /// ```no_run
764    /// use include_exclude_watcher::Watcher;
765    ///
766    /// # async fn example() -> std::io::Result<()> {
767    /// Watcher::new()
768    ///     .add_include("**/*.rs")
769    ///     .watch_initial(true)
770    ///     .run(|event, path| {
771    ///         // First receives Initial events for all existing .rs files,
772    ///         // then receives Create/Update/Delete events for changes
773    ///     })
774    ///     .await
775    /// # }
776    /// ```
777    pub fn watch_initial(mut self, enabled: bool) -> Self {
778        self.watch_initial = enabled;
779        self
780    }
781
782    /// Set whether to match regular files.
783    ///
784    /// Default: `true`
785    pub fn match_files(mut self, enabled: bool) -> Self {
786        self.match_files = enabled;
787        self
788    }
789
790    /// Set whether to match directories.
791    ///
792    /// Default: `true`
793    pub fn match_dirs(mut self, enabled: bool) -> Self {
794        self.match_dirs = enabled;
795        self
796    }
797
798    /// Set whether to return absolute paths.
799    ///
800    /// When `false` (default), paths passed to the callback are relative to
801    /// the base directory. When `true`, paths are absolute.
802    pub fn return_absolute(mut self, enabled: bool) -> Self {
803        self.return_absolute = enabled;
804        self
805    }
806
807    /// Run the watcher with the provided callback.
808    ///
809    /// This method runs forever, calling the callback for each matching event.
810    /// The callback receives the event type and the path (relative or absolute
811    /// depending on configuration).
812    ///
813    /// If no include patterns are specified, watches everything.
814    pub async fn run<F>(self, callback: F) -> Result<()>
815    where
816        F: FnMut(WatchEvent, PathBuf),
817    {
818        self.run_internal(callback, None).await
819    }
820
821    /// Run the watcher with debouncing.
822    ///
823    /// Waits for file changes, then waits until no changes have occurred for
824    /// at least `ms` milliseconds before calling the callback. This is useful
825    /// for batching rapid changes (like when a build tool writes many files).
826    ///
827    /// The callback receives the path of the first file that changed.
828    pub async fn run_debounced<F>(self, ms: u64, mut callback: F) -> Result<()>
829    where
830        F: FnMut(PathBuf),
831    {
832        self.run_internal(|_, path| callback(path), Some(Duration::from_millis(ms))).await
833    }
834
835    fn should_watch<F>(&self, state: &WatcherState<F>, relative_path: &Path, is_dir: bool) -> bool {
836        let segments = path_to_segments(relative_path);
837        
838        if state.exclude_patterns.iter().any(|p| p.check(&segments, false)) {
839            return false;
840        }
841
842        state.include_patterns.iter().any(|p| p.check(&segments, is_dir))
843    }
844
845    fn check_event<F>(&self, state: &WatcherState<F>, rel_path: &Path, is_dir: bool) -> bool {
846        if if is_dir { !self.match_dirs } else { !self.match_files } {
847            return false;
848        }
849        self.should_watch(state, rel_path, false)
850    }
851
852    fn emit_event<F>(
853        &self,
854        state: &mut WatcherState<F>,
855        event: WatchEvent,
856        rel_path: &Path,
857    ) where
858        F: FnMut(WatchEvent, PathBuf),
859    {
860        let path = if self.return_absolute {
861            if rel_path.as_os_str().is_empty() {
862                state.root.clone()
863            } else {
864                state.root.join(rel_path)
865            }
866        } else {
867            rel_path.to_path_buf()
868        };
869        (state.callback)(event, path);
870    }
871
872    fn add_watch_recursive<F>(
873        &self,
874        state: &mut WatcherState<F>,
875        initial_path: PathBuf,
876        emit_initial: bool,
877    ) where
878        F: FnMut(WatchEvent, PathBuf),
879    {
880        if state.paths.contains(&initial_path) {
881            return;
882        }
883
884        let mut stack = vec![initial_path];
885        while let Some(rel_path) = stack.pop() {
886            if !self.should_watch(state, &rel_path, true) {
887                continue;
888            }
889
890            let full_path = if rel_path.as_os_str().is_empty() {
891                state.root.clone()
892            } else {
893                state.root.join(&rel_path)
894            };
895
896            if !full_path.is_dir() {
897                continue;
898            }
899
900            let wd = match state.inotify.add_watch(&full_path, INOTIFY_MASK) {
901                Ok(wd) => wd,
902                Err(e) => {
903                    eprintln!("Failed to add watch for {:?}: {}", full_path, e);
904                    continue;
905                }
906            };
907
908            state.paths.insert(rel_path.clone());
909            state.watches.insert(wd, rel_path.clone());
910
911            if self.debug_watches_enabled {
912                (state.callback)(WatchEvent::DebugWatch, rel_path.clone());
913            }
914
915            if let Ok(entries) = std::fs::read_dir(&full_path) {
916                for entry in entries.flatten() {
917                    if let Ok(ft) = entry.file_type() {
918                        let child_rel_path = rel_path.join(entry.file_name());
919                        let is_dir = ft.is_dir();
920
921                        if emit_initial && self.check_event(state, &child_rel_path, is_dir) {
922                            self.emit_event(state, WatchEvent::Initial, &child_rel_path);
923                        }
924
925                        if is_dir && !state.paths.contains(&child_rel_path) {
926                            stack.push(child_rel_path);
927                        }
928                    }
929                }
930            }
931        }
932    }
933
934    async fn run_internal<F>(self, callback: F, debounce: Option<Duration>) -> Result<()>
935    where
936        F: FnMut(WatchEvent, PathBuf),
937    {
938        // If no includes are specified, watch everything; if empty, sleep forever
939        let includes = if self.includes.is_empty() {
940            vec!["**".to_string()]
941        } else {
942            self.includes.clone()
943        };
944
945        // If no includes are specified, just sleep forever
946        if includes.is_empty() {
947            loop {
948                tokio::time::sleep(Duration::from_secs(3600)).await;
949            }
950        }
951
952        let root = if self.base_dir.is_absolute() {
953            self.base_dir.clone()
954        } else {
955            std::env::current_dir()
956                .unwrap_or_else(|_| PathBuf::from("/"))
957                .join(&self.base_dir)
958        };
959
960        let mut state = WatcherState {
961            root,
962            inotify: Inotify::new()?,
963            watches: HashMap::new(),
964            paths: HashSet::new(),
965            include_patterns: includes.iter().map(|p| Pattern::parse(p)).collect(),
966            exclude_patterns: self.excludes.iter().map(|p| Pattern::parse(p)).collect(),
967            callback,
968        };
969
970        // Initial scan for watches (and optionally emit Initial events)
971        let emit_initial = self.watch_initial && debounce.is_none();
972        self.add_watch_recursive(&mut state, PathBuf::new(), emit_initial);
973
974        // Debouncing state
975        let mut debounce_deadline: Option<tokio::time::Instant> = None;
976        let mut debounce_first_path: Option<PathBuf> = None;
977
978        // Event loop
979        let mut buffer = [0u8; 8192];
980        loop {
981            // Calculate timeout for debouncing
982            let read_future = state.inotify.read_events(&mut buffer);
983            
984            let read_result = if let Some(deadline) = debounce_deadline {
985                let now = tokio::time::Instant::now();
986                if deadline <= now {
987                    // Timer expired, fire callback and reset
988                    debounce_deadline = None;
989                    (state.callback)(WatchEvent::Update, debounce_first_path.take().unwrap_or_default());
990                    continue;
991                }
992                // Wait with timeout
993                match tokio::time::timeout(deadline - now, read_future).await {
994                    Ok(result) => Some(result),
995                    Err(_) => {
996                        // Timeout expired, fire callback
997                        debounce_deadline = None;
998                        (state.callback)(WatchEvent::Update, debounce_first_path.take().unwrap_or_default());
999                        continue;
1000                    }
1001                }
1002            } else {
1003                Some(read_future.await)
1004            };
1005
1006            let Some(result) = read_result else { continue };
1007            
1008            match result {
1009                Ok(len) => {
1010                    let events = parse_inotify_events(&buffer, len);
1011                    let mut first_matching_path: Option<PathBuf> = None;
1012
1013                    for (wd, mask, name_str) in events {
1014                        if (mask & libc::IN_IGNORED as u32) != 0 {
1015                            if let Some(path) = state.watches.remove(&wd) {
1016                                state.paths.remove(&path);
1017                            }
1018                            continue;
1019                        }
1020
1021                        let rel_path = if let Some(dir_path) = state.watches.get(&wd) {
1022                            dir_path.join(&name_str)
1023                        } else {
1024                            println!("Warning: received event for unknown watch descriptor {}", wd);
1025                            continue;
1026                        };
1027
1028                        let is_dir = mask & libc::IN_ISDIR as u32 != 0;
1029                        let is_create = (mask & libc::IN_CREATE as u32) != 0
1030                            || (mask & libc::IN_MOVED_TO as u32) != 0;
1031                        let is_delete = (mask & libc::IN_DELETE as u32) != 0
1032                            || (mask & libc::IN_MOVED_FROM as u32) != 0;
1033                        let is_update = (mask & libc::IN_MODIFY as u32) != 0
1034                            || (mask & libc::IN_CLOSE_WRITE as u32) != 0;
1035
1036                        if is_dir && is_create {
1037                            // New directory created 
1038                            self.add_watch_recursive(&mut state, rel_path.clone(), false);
1039                        }
1040
1041                        let event_type = if is_create && self.watch_create {
1042                            WatchEvent::Create
1043                        } else if is_delete && self.watch_delete {
1044                            WatchEvent::Delete
1045                        } else if is_update && self.watch_update {
1046                            WatchEvent::Update
1047                        } else {
1048                            continue
1049                        };
1050
1051                        if !self.check_event(&state, &rel_path, is_dir) {
1052                            continue;
1053                        }
1054
1055                        if first_matching_path.is_none() {
1056                            first_matching_path = Some(rel_path.clone());
1057                        }
1058
1059                        // Emit event if not in debounce mode
1060                        if debounce.is_none() {
1061                            self.emit_event(&mut state, event_type, &rel_path);
1062                        }
1063                    }
1064                    
1065                    // If debouncing and we had events, reset the timer
1066                    if let Some(d) = debounce {
1067                        if let Some(path) = first_matching_path {
1068                            if debounce_first_path.is_none() {
1069                                debounce_first_path = Some(path);
1070                            }
1071                            debounce_deadline = Some(tokio::time::Instant::now() + d);
1072                        }
1073                    }
1074                }
1075                Err(e) => {
1076                    eprintln!("Error reading inotify events: {}", e);
1077                    tokio::time::sleep(Duration::from_millis(100)).await;
1078                }
1079            }
1080        }
1081    }
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086    use super::*;
1087    use std::collections::HashSet;
1088    use std::sync::{Arc, Mutex};
1089    use tokio::task::JoinHandle;
1090
1091    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1092    enum EventType {
1093        Create,
1094        Delete,
1095        Update,
1096        Initial,
1097        DebugWatch,
1098    }
1099
1100    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1101    struct Event {
1102        path: PathBuf,
1103        event_type: EventType,
1104    }
1105
1106    type EventTracker = Arc<Mutex<Vec<Event>>>;
1107
1108    struct TestInstance {
1109        test_dir: PathBuf,
1110        tracker: EventTracker,
1111        watcher_handle: Option<JoinHandle<()>>,
1112    }
1113
1114    impl TestInstance {
1115        async fn new<F>(test_name: &str, configure: F) -> Self
1116        where
1117            F: FnOnce(Watcher) -> Watcher + Send + 'static,
1118        {
1119            let test_dir = std::env::current_dir()
1120                .unwrap()
1121                .join(format!(".file-watcher-test-{}", test_name));
1122
1123            if test_dir.exists() {
1124                std::fs::remove_dir_all(&test_dir).unwrap();
1125            }
1126            std::fs::create_dir(&test_dir).unwrap();
1127
1128            let tracker = Arc::new(Mutex::new(Vec::new()));
1129
1130            let tracker_clone = tracker.clone();
1131            let test_dir_clone = test_dir.clone();
1132
1133            let watcher_handle = tokio::spawn(async move {
1134                let watcher = Watcher::new()
1135                    .set_base_dir(&test_dir_clone)
1136                    .debug_watches(true);
1137
1138                let watcher = configure(watcher);
1139
1140                let _ = watcher
1141                    .run(move |event_type, path| {
1142                        tracker_clone.lock().unwrap().push(Event {
1143                            path: path.clone(),
1144                            event_type: match event_type {
1145                                WatchEvent::Create => EventType::Create,
1146                                WatchEvent::Delete => EventType::Delete,
1147                                WatchEvent::Update => EventType::Update,
1148                                WatchEvent::Initial => EventType::Initial,
1149                                WatchEvent::DebugWatch => EventType::DebugWatch,
1150                            },
1151                        });
1152                    })
1153                    .await;
1154            });
1155
1156            tokio::time::sleep(Duration::from_millis(100)).await;
1157
1158            let instance = Self {
1159                test_dir,
1160                tracker,
1161                watcher_handle: Some(watcher_handle),
1162            };
1163
1164            instance.assert_events(&[], &[], &[], &[""]).await;
1165
1166            instance
1167        }
1168
1169        fn create_dir(&self, path: &str) {
1170            std::fs::create_dir_all(self.test_dir.join(path)).unwrap();
1171        }
1172
1173        fn write_file(&self, path: &str, content: &str) {
1174            let full_path = self.test_dir.join(path);
1175            if let Some(parent) = full_path.parent() {
1176                std::fs::create_dir_all(parent).unwrap();
1177            }
1178            std::fs::write(full_path, content).unwrap();
1179        }
1180
1181        fn remove_file(&self, path: &str) {
1182            std::fs::remove_file(self.test_dir.join(path)).unwrap();
1183        }
1184
1185        fn rename(&self, from: &str, to: &str) {
1186            std::fs::rename(self.test_dir.join(from), self.test_dir.join(to)).unwrap();
1187        }
1188
1189        async fn assert_events(
1190            &self,
1191            creates: &[&str],
1192            deletes: &[&str],
1193            updates: &[&str],
1194            watches: &[&str],
1195        ) {
1196            tokio::time::sleep(Duration::from_millis(200)).await;
1197
1198            let events = self.tracker.lock().unwrap().clone();
1199            let mut expected = HashSet::new();
1200
1201            for create in creates {
1202                expected.insert(Event {
1203                    path: PathBuf::from(create),
1204                    event_type: EventType::Create,
1205                });
1206            }
1207
1208            for delete in deletes {
1209                expected.insert(Event {
1210                    path: PathBuf::from(delete),
1211                    event_type: EventType::Delete,
1212                });
1213            }
1214
1215            for update in updates {
1216                expected.insert(Event {
1217                    path: PathBuf::from(update),
1218                    event_type: EventType::Update,
1219                });
1220            }
1221
1222            for watch in watches {
1223                expected.insert(Event {
1224                    path: PathBuf::from(watch),
1225                    event_type: EventType::DebugWatch,
1226                });
1227            }
1228
1229            let actual: HashSet<Event> = events.iter().cloned().collect();
1230
1231            for event in &actual {
1232                if !expected.contains(event) {
1233                    panic!("Unexpected event: {:?}", event);
1234                }
1235            }
1236
1237            for event in &expected {
1238                if !actual.contains(event) {
1239                    panic!(
1240                        "Missing expected event: {:?}\nActual events: {:?}",
1241                        event, actual
1242                    );
1243                }
1244            }
1245
1246            self.tracker.lock().unwrap().clear();
1247        }
1248
1249        async fn assert_no_events(&self) {
1250            tokio::time::sleep(Duration::from_millis(500)).await;
1251            let events = self.tracker.lock().unwrap();
1252            assert_eq!(
1253                events.len(),
1254                0,
1255                "Expected no events, but got: {:?}",
1256                events
1257            );
1258        }
1259    }
1260
1261    impl Drop for TestInstance {
1262        fn drop(&mut self) {
1263            if let Some(handle) = self.watcher_handle.take() {
1264                handle.abort();
1265            }
1266            if self.test_dir.exists() {
1267                let _ = std::fs::remove_dir_all(&self.test_dir);
1268            }
1269        }
1270    }
1271
1272    #[tokio::test]
1273    async fn test_file_create_update_delete() {
1274        let test = TestInstance::new("create_update_delete", |b| b.add_include("**/*")).await;
1275
1276        test.write_file("test.txt", "");
1277        test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1278            .await;
1279
1280        test.write_file("test.txt", "hello");
1281        test.assert_events(&[], &[], &["test.txt"], &[]).await;
1282
1283        test.remove_file("test.txt");
1284        test.assert_events(&[], &["test.txt"], &[], &[]).await;
1285    }
1286
1287    #[tokio::test]
1288    async fn test_directory_operations() {
1289        let test = TestInstance::new("directory_operations", |b| b.add_include("**/*")).await;
1290
1291        test.create_dir("subdir");
1292        test.assert_events(&["subdir"], &[], &[], &["subdir"]).await;
1293
1294        test.write_file("subdir/file.txt", "");
1295        test.assert_events(&["subdir/file.txt"], &[], &["subdir/file.txt"], &[])
1296            .await;
1297    }
1298
1299    #[tokio::test]
1300    async fn test_move_operations() {
1301        let test = TestInstance::new("move_operations", |b| b.add_include("**/*")).await;
1302
1303        test.write_file("old.txt", "content");
1304        test.assert_events(&["old.txt"], &[], &["old.txt"], &[])
1305            .await;
1306
1307        test.rename("old.txt", "new.txt");
1308        test.assert_events(&["new.txt"], &["old.txt"], &[], &[])
1309            .await;
1310    }
1311
1312    #[tokio::test]
1313    async fn test_event_filtering() {
1314        let test = TestInstance::new("event_filtering", |b| {
1315            b.add_include("**/*")
1316                .watch_create(true)
1317                .watch_delete(false)
1318                .watch_update(false)
1319        })
1320        .await;
1321
1322        test.write_file("test.txt", "");
1323        test.assert_events(&["test.txt"], &[], &[], &[]).await;
1324
1325        test.write_file("test.txt", "hello");
1326        test.assert_no_events().await;
1327
1328        test.remove_file("test.txt");
1329        test.assert_no_events().await;
1330    }
1331
1332    #[tokio::test]
1333    async fn test_pattern_matching() {
1334        let test = TestInstance::new("pattern_matching", |b| b.add_include("**/*.txt")).await;
1335
1336        test.write_file("test.txt", "");
1337        test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1338            .await;
1339
1340        test.write_file("test.rs", "");
1341        test.assert_no_events().await;
1342    }
1343
1344    #[tokio::test]
1345    async fn test_matching_stops_at_depth() {
1346        let test = TestInstance::new("matching_stops_at_depth", |b| b.add_include("*/xyz/*.*")).await;
1347
1348        test.write_file("test.txt", "");
1349        test.assert_no_events().await;
1350
1351        test.create_dir("abc/xyz");
1352        test.assert_events(&[], &[], &[], &["abc", "abc/xyz"]).await;
1353
1354        test.create_dir("abc/hjk/a.b");
1355        test.assert_no_events().await;
1356
1357        test.create_dir("abc/xyz/a.b");
1358        test.assert_events(&["abc/xyz/a.b"], &[], &[], &[]).await; // Should not watch the a.b dir
1359
1360        test.create_dir("abc/xyz/a.b/x.y");
1361        test.assert_events(&[], &[], &[], &[]).await;
1362    }
1363
1364    #[tokio::test]
1365    async fn test_exclude_prevents_watching() {
1366        let test = TestInstance::new("exclude_prevents_watch", |b| {
1367            b.add_include("**/*").add_exclude("node_modules/**")
1368        })
1369        .await;
1370
1371        test.create_dir("node_modules");
1372        tokio::time::sleep(Duration::from_millis(200)).await;
1373
1374        test.write_file("node_modules/package.json", "");
1375        test.assert_no_events().await;
1376
1377        test.write_file("test.txt", "");
1378        test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1379            .await;
1380    }
1381
1382    #[tokio::test]
1383    async fn test_pattern_file() {
1384        // Setup: create test directory manually and write pattern file first
1385        let test_dir = std::env::current_dir()
1386            .unwrap()
1387            .join(".file-watcher-test-pattern_file");
1388
1389        if test_dir.exists() {
1390            std::fs::remove_dir_all(&test_dir).unwrap();
1391        }
1392        std::fs::create_dir(&test_dir).unwrap();
1393
1394        // Write pattern file before starting watcher
1395        std::fs::write(
1396            test_dir.join(".watchignore"),
1397            "# Comment line\nignored/**\n",
1398        )
1399        .unwrap();
1400
1401        // Now create watcher with pattern file
1402        let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1403        let tracker_clone = tracker.clone();
1404        let test_dir_clone = test_dir.clone();
1405
1406        let watcher_handle = tokio::spawn(async move {
1407            let _ = Watcher::new()
1408                .set_base_dir(&test_dir_clone)
1409                .debug_watches(true)
1410                .add_include("**/*")
1411                .add_ignore_file(".watchignore")
1412                .run(move |event_type, path| {
1413                    tracker_clone.lock().unwrap().push(Event {
1414                        path: path.clone(),
1415                        event_type: match event_type {
1416                            WatchEvent::Create => EventType::Create,
1417                            WatchEvent::Delete => EventType::Delete,
1418                            WatchEvent::Update => EventType::Update,
1419                            WatchEvent::Initial => EventType::Initial,
1420                            WatchEvent::DebugWatch => EventType::DebugWatch,
1421                        },
1422                    });
1423                })
1424                .await;
1425        });
1426
1427        tokio::time::sleep(Duration::from_millis(100)).await;
1428        tracker.lock().unwrap().clear(); // Clear initial watch event
1429
1430        // Create ignored directory
1431        std::fs::create_dir(test_dir.join("ignored")).unwrap();
1432        tokio::time::sleep(Duration::from_millis(200)).await;
1433
1434        // Files in ignored/ should not trigger events (because of exclude)
1435        std::fs::write(test_dir.join("ignored/test.txt"), "").unwrap();
1436        tokio::time::sleep(Duration::from_millis(200)).await;
1437
1438        // Check no events for ignored files
1439        {
1440            let events = tracker.lock().unwrap();
1441            let has_ignored_events = events.iter().any(|e| {
1442                e.path.to_string_lossy().contains("ignored")
1443                    && e.event_type != EventType::DebugWatch
1444            });
1445            assert!(
1446                !has_ignored_events,
1447                "Expected no events for ignored files, but got: {:?}",
1448                events
1449            );
1450        }
1451        tracker.lock().unwrap().clear();
1452
1453        // Normal files should still work
1454        std::fs::write(test_dir.join("normal.txt"), "").unwrap();
1455        tokio::time::sleep(Duration::from_millis(200)).await;
1456
1457        {
1458            let events = tracker.lock().unwrap();
1459            let has_normal = events
1460                .iter()
1461                .any(|e| e.path == PathBuf::from("normal.txt"));
1462            assert!(has_normal, "Expected event for normal.txt, got: {:?}", events);
1463        }
1464
1465        // Cleanup
1466        watcher_handle.abort();
1467        let _ = std::fs::remove_dir_all(&test_dir);
1468    }
1469
1470    #[tokio::test]
1471    async fn test_watch_initial() {
1472        // Setup: create test directory and files before starting watcher
1473        let test_dir = std::env::current_dir()
1474            .unwrap()
1475            .join(".file-watcher-test-watch_initial");
1476
1477        if test_dir.exists() {
1478            std::fs::remove_dir_all(&test_dir).unwrap();
1479        }
1480        std::fs::create_dir(&test_dir).unwrap();
1481
1482        // Create some files before starting the watcher
1483        std::fs::write(test_dir.join("existing1.txt"), "content1").unwrap();
1484        std::fs::write(test_dir.join("existing2.txt"), "content2").unwrap();
1485        std::fs::create_dir(test_dir.join("subdir")).unwrap();
1486        std::fs::write(test_dir.join("subdir/nested.txt"), "nested").unwrap();
1487        std::fs::write(test_dir.join("ignored.rs"), "should be ignored").unwrap();
1488
1489        let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1490        let tracker_clone = tracker.clone();
1491        let test_dir_clone = test_dir.clone();
1492
1493        let watcher_handle = tokio::spawn(async move {
1494            let _ = Watcher::new()
1495                .set_base_dir(&test_dir_clone)
1496                .add_include("**/*.txt")
1497                .watch_initial(true)
1498                .run(move |event_type, path| {
1499                    tracker_clone.lock().unwrap().push(Event {
1500                        path: path.clone(),
1501                        event_type: match event_type {
1502                            WatchEvent::Create => EventType::Create,
1503                            WatchEvent::Delete => EventType::Delete,
1504                            WatchEvent::Update => EventType::Update,
1505                            WatchEvent::Initial => EventType::Initial,
1506                            WatchEvent::DebugWatch => EventType::DebugWatch,
1507                        },
1508                    });
1509                })
1510                .await;
1511        });
1512
1513        tokio::time::sleep(Duration::from_millis(200)).await;
1514
1515        // Check that Initial events were emitted for preexisting .txt files
1516        {
1517            let events = tracker.lock().unwrap();
1518            let initial_events: Vec<_> = events
1519                .iter()
1520                .filter(|e| e.event_type == EventType::Initial)
1521                .collect();
1522
1523            assert_eq!(
1524                initial_events.len(),
1525                3,
1526                "Expected 3 Initial events, got: {:?}",
1527                initial_events
1528            );
1529
1530            let paths: HashSet<_> = initial_events.iter().map(|e| e.path.clone()).collect();
1531            assert!(paths.contains(&PathBuf::from("existing1.txt")));
1532            assert!(paths.contains(&PathBuf::from("existing2.txt")));
1533            assert!(paths.contains(&PathBuf::from("subdir/nested.txt")));
1534
1535            // .rs file should not have Initial event
1536            assert!(!events.iter().any(|e| e.path.to_string_lossy().contains("ignored.rs")));
1537        }
1538
1539        tracker.lock().unwrap().clear();
1540
1541        // Verify normal events still work after initial scan
1542        std::fs::write(test_dir.join("new.txt"), "new content").unwrap();
1543        tokio::time::sleep(Duration::from_millis(200)).await;
1544
1545        {
1546            let events = tracker.lock().unwrap();
1547            let has_create = events
1548                .iter()
1549                .any(|e| e.path == PathBuf::from("new.txt") && e.event_type == EventType::Create);
1550            assert!(has_create, "Expected Create event for new.txt, got: {:?}", events);
1551        }
1552
1553        // Cleanup
1554        watcher_handle.abort();
1555        let _ = std::fs::remove_dir_all(&test_dir);
1556    }
1557
1558    #[tokio::test]
1559    async fn test_watch_initial_with_dirs() {
1560        // Test that watch_initial respects match_dirs setting
1561        let test_dir = std::env::current_dir()
1562            .unwrap()
1563            .join(".file-watcher-test-watch_initial_dirs");
1564
1565        if test_dir.exists() {
1566            std::fs::remove_dir_all(&test_dir).unwrap();
1567        }
1568        std::fs::create_dir(&test_dir).unwrap();
1569
1570        // Create files and directories
1571        std::fs::write(test_dir.join("file.txt"), "content").unwrap();
1572        std::fs::create_dir(test_dir.join("mydir")).unwrap();
1573
1574        let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1575        let tracker_clone = tracker.clone();
1576        let test_dir_clone = test_dir.clone();
1577
1578        let watcher_handle = tokio::spawn(async move {
1579            let _ = Watcher::new()
1580                .set_base_dir(&test_dir_clone)
1581                .add_include("**/*")
1582                .watch_initial(true)
1583                .match_files(true)
1584                .match_dirs(false)  // Only files, not directories
1585                .run(move |event_type, path| {
1586                    tracker_clone.lock().unwrap().push(Event {
1587                        path: path.clone(),
1588                        event_type: match event_type {
1589                            WatchEvent::Create => EventType::Create,
1590                            WatchEvent::Delete => EventType::Delete,
1591                            WatchEvent::Update => EventType::Update,
1592                            WatchEvent::Initial => EventType::Initial,
1593                            WatchEvent::DebugWatch => EventType::DebugWatch,
1594                        },
1595                    });
1596                })
1597                .await;
1598        });
1599
1600        tokio::time::sleep(Duration::from_millis(200)).await;
1601
1602        {
1603            let events = tracker.lock().unwrap();
1604            let initial_events: Vec<_> = events
1605                .iter()
1606                .filter(|e| e.event_type == EventType::Initial)
1607                .collect();
1608
1609            // Should only have Initial for file.txt, not mydir
1610            assert_eq!(
1611                initial_events.len(),
1612                1,
1613                "Expected 1 Initial event (file only), got: {:?}",
1614                initial_events
1615            );
1616            assert_eq!(initial_events[0].path, PathBuf::from("file.txt"));
1617        }
1618
1619        // Cleanup
1620        watcher_handle.abort();
1621        let _ = std::fs::remove_dir_all(&test_dir);
1622    }
1623
1624    #[tokio::test]
1625    async fn test_watch_initial_disabled_by_default() {
1626        // Test that watch_initial is disabled by default
1627        let test_dir = std::env::current_dir()
1628            .unwrap()
1629            .join(".file-watcher-test-watch_initial_disabled");
1630
1631        if test_dir.exists() {
1632            std::fs::remove_dir_all(&test_dir).unwrap();
1633        }
1634        std::fs::create_dir(&test_dir).unwrap();
1635
1636        // Create a file before starting watcher
1637        std::fs::write(test_dir.join("existing.txt"), "content").unwrap();
1638
1639        let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1640        let tracker_clone = tracker.clone();
1641        let test_dir_clone = test_dir.clone();
1642
1643        let watcher_handle = tokio::spawn(async move {
1644            let _ = Watcher::new()
1645                .set_base_dir(&test_dir_clone)
1646                .add_include("**/*.txt")
1647                // watch_initial not enabled (default is false)
1648                .run(move |event_type, path| {
1649                    tracker_clone.lock().unwrap().push(Event {
1650                        path: path.clone(),
1651                        event_type: match event_type {
1652                            WatchEvent::Create => EventType::Create,
1653                            WatchEvent::Delete => EventType::Delete,
1654                            WatchEvent::Update => EventType::Update,
1655                            WatchEvent::Initial => EventType::Initial,
1656                            WatchEvent::DebugWatch => EventType::DebugWatch,
1657                        },
1658                    });
1659                })
1660                .await;
1661        });
1662
1663        tokio::time::sleep(Duration::from_millis(200)).await;
1664
1665        {
1666            let events = tracker.lock().unwrap();
1667            let initial_events: Vec<_> = events
1668                .iter()
1669                .filter(|e| e.event_type == EventType::Initial)
1670                .collect();
1671
1672            // Should have no Initial events since watch_initial is disabled
1673            assert_eq!(
1674                initial_events.len(),
1675                0,
1676                "Expected no Initial events when watch_initial is disabled, got: {:?}",
1677                initial_events
1678            );
1679        }
1680
1681        // Cleanup
1682        watcher_handle.abort();
1683        let _ = std::fs::remove_dir_all(&test_dir);
1684    }
1685}