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, || {
56//!             println!("Files changed!");
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 takes no arguments since the specific paths are not tracked
828    /// during debouncing.
829    pub async fn run_debounced<F>(self, ms: u64, mut callback: F) -> Result<()>
830    where
831        F: FnMut(),
832    {
833        self.run_internal(|_, _| callback(), Some(Duration::from_millis(ms))).await
834    }
835
836    fn should_watch<F>(&self, state: &WatcherState<F>, relative_path: &Path, is_dir: bool) -> bool {
837        let segments = path_to_segments(relative_path);
838        
839        if state.exclude_patterns.iter().any(|p| p.check(&segments, false)) {
840            return false;
841        }
842
843        state.include_patterns.iter().any(|p| p.check(&segments, is_dir))
844    }
845
846    fn make_callback_path<F>(&self, state: &WatcherState<F>, rel_path: &Path) -> PathBuf {
847        if self.return_absolute {
848            if rel_path.as_os_str().is_empty() {
849                state.root.clone()
850            } else {
851                state.root.join(rel_path)
852            }
853        } else {
854            rel_path.to_path_buf()
855        }
856    }
857
858    fn emit_event<F>(
859        &self,
860        state: &mut WatcherState<F>,
861        event: WatchEvent,
862        rel_path: &Path,
863        is_dir: bool,
864    ) where
865        F: FnMut(WatchEvent, PathBuf),
866    {
867        if if is_dir { !self.match_dirs } else { !self.match_files } {
868            return;
869        }
870
871        if !self.should_watch(state, rel_path, false) {
872            return;
873        }
874
875        let path = self.make_callback_path(state, rel_path);
876        (state.callback)(event, path);
877    }
878
879    fn add_watch_recursive<F>(
880        &self,
881        state: &mut WatcherState<F>,
882        initial_path: PathBuf,
883        emit_initial: bool,
884    ) where
885        F: FnMut(WatchEvent, PathBuf),
886    {
887        if state.paths.contains(&initial_path) {
888            return;
889        }
890
891        let mut stack = vec![initial_path];
892        while let Some(rel_path) = stack.pop() {
893            if !self.should_watch(state, &rel_path, true) {
894                continue;
895            }
896
897            let full_path = if rel_path.as_os_str().is_empty() {
898                state.root.clone()
899            } else {
900                state.root.join(&rel_path)
901            };
902
903            if !full_path.is_dir() {
904                continue;
905            }
906
907            let wd = match state.inotify.add_watch(&full_path, INOTIFY_MASK) {
908                Ok(wd) => wd,
909                Err(e) => {
910                    eprintln!("Failed to add watch for {:?}: {}", full_path, e);
911                    continue;
912                }
913            };
914
915            state.paths.insert(rel_path.clone());
916            state.watches.insert(wd, rel_path.clone());
917
918            if self.debug_watches_enabled {
919                let path = self.make_callback_path(state, &rel_path);
920                (state.callback)(WatchEvent::DebugWatch, path);
921            }
922
923            if let Ok(entries) = std::fs::read_dir(&full_path) {
924                for entry in entries.flatten() {
925                    if let Ok(ft) = entry.file_type() {
926                        let child_rel_path = rel_path.join(entry.file_name());
927                        let is_dir = ft.is_dir();
928
929                        if emit_initial {
930                            self.emit_event(state, WatchEvent::Initial, &child_rel_path, is_dir);
931                        }
932
933                        if is_dir && !state.paths.contains(&child_rel_path) {
934                            stack.push(child_rel_path);
935                        }
936                    }
937                }
938            }
939        }
940    }
941
942    async fn run_internal<F>(self, callback: F, debounce: Option<Duration>) -> Result<()>
943    where
944        F: FnMut(WatchEvent, PathBuf),
945    {
946        // If no includes are specified, watch everything; if empty, sleep forever
947        let includes = if self.includes.is_empty() {
948            vec!["**".to_string()]
949        } else {
950            self.includes.clone()
951        };
952
953        // If no includes are specified, just sleep forever
954        if includes.is_empty() {
955            loop {
956                tokio::time::sleep(Duration::from_secs(3600)).await;
957            }
958        }
959
960        let root = if self.base_dir.is_absolute() {
961            self.base_dir.clone()
962        } else {
963            std::env::current_dir()
964                .unwrap_or_else(|_| PathBuf::from("/"))
965                .join(&self.base_dir)
966        };
967
968        let mut state = WatcherState {
969            root,
970            inotify: Inotify::new()?,
971            watches: HashMap::new(),
972            paths: HashSet::new(),
973            include_patterns: includes.iter().map(|p| Pattern::parse(p)).collect(),
974            exclude_patterns: self.excludes.iter().map(|p| Pattern::parse(p)).collect(),
975            callback,
976        };
977
978        // Initial scan for watches (and optionally emit Initial events)
979        let emit_initial = self.watch_initial && debounce.is_none();
980        self.add_watch_recursive(&mut state, PathBuf::new(), emit_initial);
981
982        // Debouncing state
983        let mut debounce_deadline: Option<tokio::time::Instant> = None;
984
985        // Event loop
986        let mut buffer = [0u8; 8192];
987        loop {
988            // Calculate timeout for debouncing
989            let read_future = state.inotify.read_events(&mut buffer);
990            
991            let read_result = if let Some(deadline) = debounce_deadline {
992                let now = tokio::time::Instant::now();
993                if deadline <= now {
994                    // Timer expired, fire callback and reset
995                    debounce_deadline = None;
996                    (state.callback)(WatchEvent::Update, PathBuf::new());
997                    continue;
998                }
999                // Wait with timeout
1000                match tokio::time::timeout(deadline - now, read_future).await {
1001                    Ok(result) => Some(result),
1002                    Err(_) => {
1003                        // Timeout expired, fire callback
1004                        debounce_deadline = None;
1005                        (state.callback)(WatchEvent::Update, PathBuf::new());
1006                        continue;
1007                    }
1008                }
1009            } else {
1010                Some(read_future.await)
1011            };
1012
1013            let Some(result) = read_result else { continue };
1014            
1015            match result {
1016                Ok(len) => {
1017                    let events = parse_inotify_events(&buffer, len);
1018                    let mut had_matching_event = false;
1019
1020                    for (wd, mask, name_str) in events {
1021                        if (mask & libc::IN_IGNORED as u32) != 0 {
1022                            if let Some(path) = state.watches.remove(&wd) {
1023                                state.paths.remove(&path);
1024                            }
1025                            continue;
1026                        }
1027
1028                        let rel_path = if let Some(dir_path) = state.watches.get(&wd) {
1029                            dir_path.join(&name_str)
1030                        } else {
1031                            println!("Warning: received event for unknown watch descriptor {}", wd);
1032                            continue;
1033                        };
1034
1035                        let is_dir = mask & libc::IN_ISDIR as u32 != 0;
1036                        let is_create = (mask & libc::IN_CREATE as u32) != 0
1037                            || (mask & libc::IN_MOVED_TO as u32) != 0;
1038                        let is_delete = (mask & libc::IN_DELETE as u32) != 0
1039                            || (mask & libc::IN_MOVED_FROM as u32) != 0;
1040                        let is_update = (mask & libc::IN_MODIFY as u32) != 0
1041                            || (mask & libc::IN_CLOSE_WRITE as u32) != 0;
1042
1043                        if is_dir && is_create {
1044                            // New directory created 
1045                            self.add_watch_recursive(&mut state, rel_path.clone(), false);
1046                        }
1047
1048                        let event_type = if is_create && self.watch_create {
1049                            WatchEvent::Create
1050                        } else if is_delete && self.watch_delete {
1051                            WatchEvent::Delete
1052                        } else if is_update && self.watch_update {
1053                            WatchEvent::Update
1054                        } else {
1055                            continue
1056                        };
1057
1058                        if if is_dir { !self.match_dirs } else { !self.match_files } {
1059                            continue;
1060                        }
1061
1062                        if !self.should_watch(&state, &rel_path, false) {
1063                            continue;
1064                        }
1065
1066                        had_matching_event = true;
1067                        
1068                        // Do callback if not in debounce mode
1069                        if debounce.is_none() {
1070                            let path = self.make_callback_path(&state, &rel_path);
1071                            (state.callback)(event_type, path);
1072                        }
1073                    }
1074                    
1075                    // If debouncing and we had events, reset the timer
1076                    if let Some(d) = debounce {
1077                        if had_matching_event {
1078                            debounce_deadline = Some(tokio::time::Instant::now() + d);
1079                        }
1080                    }
1081                }
1082                Err(e) => {
1083                    eprintln!("Error reading inotify events: {}", e);
1084                    tokio::time::sleep(Duration::from_millis(100)).await;
1085                }
1086            }
1087        }
1088    }
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093    use super::*;
1094    use std::collections::HashSet;
1095    use std::sync::{Arc, Mutex};
1096    use tokio::task::JoinHandle;
1097
1098    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1099    enum EventType {
1100        Create,
1101        Delete,
1102        Update,
1103        Initial,
1104        DebugWatch,
1105    }
1106
1107    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1108    struct Event {
1109        path: PathBuf,
1110        event_type: EventType,
1111    }
1112
1113    type EventTracker = Arc<Mutex<Vec<Event>>>;
1114
1115    struct TestInstance {
1116        test_dir: PathBuf,
1117        tracker: EventTracker,
1118        watcher_handle: Option<JoinHandle<()>>,
1119    }
1120
1121    impl TestInstance {
1122        async fn new<F>(test_name: &str, configure: F) -> Self
1123        where
1124            F: FnOnce(Watcher) -> Watcher + Send + 'static,
1125        {
1126            let test_dir = std::env::current_dir()
1127                .unwrap()
1128                .join(format!(".file-watcher-test-{}", test_name));
1129
1130            if test_dir.exists() {
1131                std::fs::remove_dir_all(&test_dir).unwrap();
1132            }
1133            std::fs::create_dir(&test_dir).unwrap();
1134
1135            let tracker = Arc::new(Mutex::new(Vec::new()));
1136
1137            let tracker_clone = tracker.clone();
1138            let test_dir_clone = test_dir.clone();
1139
1140            let watcher_handle = tokio::spawn(async move {
1141                let watcher = Watcher::new()
1142                    .set_base_dir(&test_dir_clone)
1143                    .debug_watches(true);
1144
1145                let watcher = configure(watcher);
1146
1147                let _ = watcher
1148                    .run(move |event_type, path| {
1149                        tracker_clone.lock().unwrap().push(Event {
1150                            path: path.clone(),
1151                            event_type: match event_type {
1152                                WatchEvent::Create => EventType::Create,
1153                                WatchEvent::Delete => EventType::Delete,
1154                                WatchEvent::Update => EventType::Update,
1155                                WatchEvent::Initial => EventType::Initial,
1156                                WatchEvent::DebugWatch => EventType::DebugWatch,
1157                            },
1158                        });
1159                    })
1160                    .await;
1161            });
1162
1163            tokio::time::sleep(Duration::from_millis(100)).await;
1164
1165            let instance = Self {
1166                test_dir,
1167                tracker,
1168                watcher_handle: Some(watcher_handle),
1169            };
1170
1171            instance.assert_events(&[], &[], &[], &[""]).await;
1172
1173            instance
1174        }
1175
1176        fn create_dir(&self, path: &str) {
1177            std::fs::create_dir_all(self.test_dir.join(path)).unwrap();
1178        }
1179
1180        fn write_file(&self, path: &str, content: &str) {
1181            let full_path = self.test_dir.join(path);
1182            if let Some(parent) = full_path.parent() {
1183                std::fs::create_dir_all(parent).unwrap();
1184            }
1185            std::fs::write(full_path, content).unwrap();
1186        }
1187
1188        fn remove_file(&self, path: &str) {
1189            std::fs::remove_file(self.test_dir.join(path)).unwrap();
1190        }
1191
1192        fn rename(&self, from: &str, to: &str) {
1193            std::fs::rename(self.test_dir.join(from), self.test_dir.join(to)).unwrap();
1194        }
1195
1196        async fn assert_events(
1197            &self,
1198            creates: &[&str],
1199            deletes: &[&str],
1200            updates: &[&str],
1201            watches: &[&str],
1202        ) {
1203            tokio::time::sleep(Duration::from_millis(200)).await;
1204
1205            let events = self.tracker.lock().unwrap().clone();
1206            let mut expected = HashSet::new();
1207
1208            for create in creates {
1209                expected.insert(Event {
1210                    path: PathBuf::from(create),
1211                    event_type: EventType::Create,
1212                });
1213            }
1214
1215            for delete in deletes {
1216                expected.insert(Event {
1217                    path: PathBuf::from(delete),
1218                    event_type: EventType::Delete,
1219                });
1220            }
1221
1222            for update in updates {
1223                expected.insert(Event {
1224                    path: PathBuf::from(update),
1225                    event_type: EventType::Update,
1226                });
1227            }
1228
1229            for watch in watches {
1230                expected.insert(Event {
1231                    path: PathBuf::from(watch),
1232                    event_type: EventType::DebugWatch,
1233                });
1234            }
1235
1236            let actual: HashSet<Event> = events.iter().cloned().collect();
1237
1238            for event in &actual {
1239                if !expected.contains(event) {
1240                    panic!("Unexpected event: {:?}", event);
1241                }
1242            }
1243
1244            for event in &expected {
1245                if !actual.contains(event) {
1246                    panic!(
1247                        "Missing expected event: {:?}\nActual events: {:?}",
1248                        event, actual
1249                    );
1250                }
1251            }
1252
1253            self.tracker.lock().unwrap().clear();
1254        }
1255
1256        async fn assert_no_events(&self) {
1257            tokio::time::sleep(Duration::from_millis(500)).await;
1258            let events = self.tracker.lock().unwrap();
1259            assert_eq!(
1260                events.len(),
1261                0,
1262                "Expected no events, but got: {:?}",
1263                events
1264            );
1265        }
1266    }
1267
1268    impl Drop for TestInstance {
1269        fn drop(&mut self) {
1270            if let Some(handle) = self.watcher_handle.take() {
1271                handle.abort();
1272            }
1273            if self.test_dir.exists() {
1274                let _ = std::fs::remove_dir_all(&self.test_dir);
1275            }
1276        }
1277    }
1278
1279    #[tokio::test]
1280    async fn test_file_create_update_delete() {
1281        let test = TestInstance::new("create_update_delete", |b| b.add_include("**/*")).await;
1282
1283        test.write_file("test.txt", "");
1284        test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1285            .await;
1286
1287        test.write_file("test.txt", "hello");
1288        test.assert_events(&[], &[], &["test.txt"], &[]).await;
1289
1290        test.remove_file("test.txt");
1291        test.assert_events(&[], &["test.txt"], &[], &[]).await;
1292    }
1293
1294    #[tokio::test]
1295    async fn test_directory_operations() {
1296        let test = TestInstance::new("directory_operations", |b| b.add_include("**/*")).await;
1297
1298        test.create_dir("subdir");
1299        test.assert_events(&["subdir"], &[], &[], &["subdir"]).await;
1300
1301        test.write_file("subdir/file.txt", "");
1302        test.assert_events(&["subdir/file.txt"], &[], &["subdir/file.txt"], &[])
1303            .await;
1304    }
1305
1306    #[tokio::test]
1307    async fn test_move_operations() {
1308        let test = TestInstance::new("move_operations", |b| b.add_include("**/*")).await;
1309
1310        test.write_file("old.txt", "content");
1311        test.assert_events(&["old.txt"], &[], &["old.txt"], &[])
1312            .await;
1313
1314        test.rename("old.txt", "new.txt");
1315        test.assert_events(&["new.txt"], &["old.txt"], &[], &[])
1316            .await;
1317    }
1318
1319    #[tokio::test]
1320    async fn test_event_filtering() {
1321        let test = TestInstance::new("event_filtering", |b| {
1322            b.add_include("**/*")
1323                .watch_create(true)
1324                .watch_delete(false)
1325                .watch_update(false)
1326        })
1327        .await;
1328
1329        test.write_file("test.txt", "");
1330        test.assert_events(&["test.txt"], &[], &[], &[]).await;
1331
1332        test.write_file("test.txt", "hello");
1333        test.assert_no_events().await;
1334
1335        test.remove_file("test.txt");
1336        test.assert_no_events().await;
1337    }
1338
1339    #[tokio::test]
1340    async fn test_pattern_matching() {
1341        let test = TestInstance::new("pattern_matching", |b| b.add_include("**/*.txt")).await;
1342
1343        test.write_file("test.txt", "");
1344        test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1345            .await;
1346
1347        test.write_file("test.rs", "");
1348        test.assert_no_events().await;
1349    }
1350
1351    #[tokio::test]
1352    async fn test_matching_stops_at_depth() {
1353        let test = TestInstance::new("matching_stops_at_depth", |b| b.add_include("*/xyz/*.*")).await;
1354
1355        test.write_file("test.txt", "");
1356        test.assert_no_events().await;
1357
1358        test.create_dir("abc/xyz");
1359        test.assert_events(&[], &[], &[], &["abc", "abc/xyz"]).await;
1360
1361        test.create_dir("abc/hjk/a.b");
1362        test.assert_no_events().await;
1363
1364        test.create_dir("abc/xyz/a.b");
1365        test.assert_events(&["abc/xyz/a.b"], &[], &[], &[]).await; // Should not watch the a.b dir
1366
1367        test.create_dir("abc/xyz/a.b/x.y");
1368        test.assert_events(&[], &[], &[], &[]).await;
1369    }
1370
1371    #[tokio::test]
1372    async fn test_exclude_prevents_watching() {
1373        let test = TestInstance::new("exclude_prevents_watch", |b| {
1374            b.add_include("**/*").add_exclude("node_modules/**")
1375        })
1376        .await;
1377
1378        test.create_dir("node_modules");
1379        tokio::time::sleep(Duration::from_millis(200)).await;
1380
1381        test.write_file("node_modules/package.json", "");
1382        test.assert_no_events().await;
1383
1384        test.write_file("test.txt", "");
1385        test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1386            .await;
1387    }
1388
1389    #[tokio::test]
1390    async fn test_pattern_file() {
1391        // Setup: create test directory manually and write pattern file first
1392        let test_dir = std::env::current_dir()
1393            .unwrap()
1394            .join(".file-watcher-test-pattern_file");
1395
1396        if test_dir.exists() {
1397            std::fs::remove_dir_all(&test_dir).unwrap();
1398        }
1399        std::fs::create_dir(&test_dir).unwrap();
1400
1401        // Write pattern file before starting watcher
1402        std::fs::write(
1403            test_dir.join(".watchignore"),
1404            "# Comment line\nignored/**\n",
1405        )
1406        .unwrap();
1407
1408        // Now create watcher with pattern file
1409        let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1410        let tracker_clone = tracker.clone();
1411        let test_dir_clone = test_dir.clone();
1412
1413        let watcher_handle = tokio::spawn(async move {
1414            let _ = Watcher::new()
1415                .set_base_dir(&test_dir_clone)
1416                .debug_watches(true)
1417                .add_include("**/*")
1418                .add_ignore_file(".watchignore")
1419                .run(move |event_type, path| {
1420                    tracker_clone.lock().unwrap().push(Event {
1421                        path: path.clone(),
1422                        event_type: match event_type {
1423                            WatchEvent::Create => EventType::Create,
1424                            WatchEvent::Delete => EventType::Delete,
1425                            WatchEvent::Update => EventType::Update,
1426                            WatchEvent::Initial => EventType::Initial,
1427                            WatchEvent::DebugWatch => EventType::DebugWatch,
1428                        },
1429                    });
1430                })
1431                .await;
1432        });
1433
1434        tokio::time::sleep(Duration::from_millis(100)).await;
1435        tracker.lock().unwrap().clear(); // Clear initial watch event
1436
1437        // Create ignored directory
1438        std::fs::create_dir(test_dir.join("ignored")).unwrap();
1439        tokio::time::sleep(Duration::from_millis(200)).await;
1440
1441        // Files in ignored/ should not trigger events (because of exclude)
1442        std::fs::write(test_dir.join("ignored/test.txt"), "").unwrap();
1443        tokio::time::sleep(Duration::from_millis(200)).await;
1444
1445        // Check no events for ignored files
1446        {
1447            let events = tracker.lock().unwrap();
1448            let has_ignored_events = events.iter().any(|e| {
1449                e.path.to_string_lossy().contains("ignored")
1450                    && e.event_type != EventType::DebugWatch
1451            });
1452            assert!(
1453                !has_ignored_events,
1454                "Expected no events for ignored files, but got: {:?}",
1455                events
1456            );
1457        }
1458        tracker.lock().unwrap().clear();
1459
1460        // Normal files should still work
1461        std::fs::write(test_dir.join("normal.txt"), "").unwrap();
1462        tokio::time::sleep(Duration::from_millis(200)).await;
1463
1464        {
1465            let events = tracker.lock().unwrap();
1466            let has_normal = events
1467                .iter()
1468                .any(|e| e.path == PathBuf::from("normal.txt"));
1469            assert!(has_normal, "Expected event for normal.txt, got: {:?}", events);
1470        }
1471
1472        // Cleanup
1473        watcher_handle.abort();
1474        let _ = std::fs::remove_dir_all(&test_dir);
1475    }
1476
1477    #[tokio::test]
1478    async fn test_watch_initial() {
1479        // Setup: create test directory and files before starting watcher
1480        let test_dir = std::env::current_dir()
1481            .unwrap()
1482            .join(".file-watcher-test-watch_initial");
1483
1484        if test_dir.exists() {
1485            std::fs::remove_dir_all(&test_dir).unwrap();
1486        }
1487        std::fs::create_dir(&test_dir).unwrap();
1488
1489        // Create some files before starting the watcher
1490        std::fs::write(test_dir.join("existing1.txt"), "content1").unwrap();
1491        std::fs::write(test_dir.join("existing2.txt"), "content2").unwrap();
1492        std::fs::create_dir(test_dir.join("subdir")).unwrap();
1493        std::fs::write(test_dir.join("subdir/nested.txt"), "nested").unwrap();
1494        std::fs::write(test_dir.join("ignored.rs"), "should be ignored").unwrap();
1495
1496        let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1497        let tracker_clone = tracker.clone();
1498        let test_dir_clone = test_dir.clone();
1499
1500        let watcher_handle = tokio::spawn(async move {
1501            let _ = Watcher::new()
1502                .set_base_dir(&test_dir_clone)
1503                .add_include("**/*.txt")
1504                .watch_initial(true)
1505                .run(move |event_type, path| {
1506                    tracker_clone.lock().unwrap().push(Event {
1507                        path: path.clone(),
1508                        event_type: match event_type {
1509                            WatchEvent::Create => EventType::Create,
1510                            WatchEvent::Delete => EventType::Delete,
1511                            WatchEvent::Update => EventType::Update,
1512                            WatchEvent::Initial => EventType::Initial,
1513                            WatchEvent::DebugWatch => EventType::DebugWatch,
1514                        },
1515                    });
1516                })
1517                .await;
1518        });
1519
1520        tokio::time::sleep(Duration::from_millis(200)).await;
1521
1522        // Check that Initial events were emitted for preexisting .txt files
1523        {
1524            let events = tracker.lock().unwrap();
1525            let initial_events: Vec<_> = events
1526                .iter()
1527                .filter(|e| e.event_type == EventType::Initial)
1528                .collect();
1529
1530            assert_eq!(
1531                initial_events.len(),
1532                3,
1533                "Expected 3 Initial events, got: {:?}",
1534                initial_events
1535            );
1536
1537            let paths: HashSet<_> = initial_events.iter().map(|e| e.path.clone()).collect();
1538            assert!(paths.contains(&PathBuf::from("existing1.txt")));
1539            assert!(paths.contains(&PathBuf::from("existing2.txt")));
1540            assert!(paths.contains(&PathBuf::from("subdir/nested.txt")));
1541
1542            // .rs file should not have Initial event
1543            assert!(!events.iter().any(|e| e.path.to_string_lossy().contains("ignored.rs")));
1544        }
1545
1546        tracker.lock().unwrap().clear();
1547
1548        // Verify normal events still work after initial scan
1549        std::fs::write(test_dir.join("new.txt"), "new content").unwrap();
1550        tokio::time::sleep(Duration::from_millis(200)).await;
1551
1552        {
1553            let events = tracker.lock().unwrap();
1554            let has_create = events
1555                .iter()
1556                .any(|e| e.path == PathBuf::from("new.txt") && e.event_type == EventType::Create);
1557            assert!(has_create, "Expected Create event for new.txt, got: {:?}", events);
1558        }
1559
1560        // Cleanup
1561        watcher_handle.abort();
1562        let _ = std::fs::remove_dir_all(&test_dir);
1563    }
1564
1565    #[tokio::test]
1566    async fn test_watch_initial_with_dirs() {
1567        // Test that watch_initial respects match_dirs setting
1568        let test_dir = std::env::current_dir()
1569            .unwrap()
1570            .join(".file-watcher-test-watch_initial_dirs");
1571
1572        if test_dir.exists() {
1573            std::fs::remove_dir_all(&test_dir).unwrap();
1574        }
1575        std::fs::create_dir(&test_dir).unwrap();
1576
1577        // Create files and directories
1578        std::fs::write(test_dir.join("file.txt"), "content").unwrap();
1579        std::fs::create_dir(test_dir.join("mydir")).unwrap();
1580
1581        let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1582        let tracker_clone = tracker.clone();
1583        let test_dir_clone = test_dir.clone();
1584
1585        let watcher_handle = tokio::spawn(async move {
1586            let _ = Watcher::new()
1587                .set_base_dir(&test_dir_clone)
1588                .add_include("**/*")
1589                .watch_initial(true)
1590                .match_files(true)
1591                .match_dirs(false)  // Only files, not directories
1592                .run(move |event_type, path| {
1593                    tracker_clone.lock().unwrap().push(Event {
1594                        path: path.clone(),
1595                        event_type: match event_type {
1596                            WatchEvent::Create => EventType::Create,
1597                            WatchEvent::Delete => EventType::Delete,
1598                            WatchEvent::Update => EventType::Update,
1599                            WatchEvent::Initial => EventType::Initial,
1600                            WatchEvent::DebugWatch => EventType::DebugWatch,
1601                        },
1602                    });
1603                })
1604                .await;
1605        });
1606
1607        tokio::time::sleep(Duration::from_millis(200)).await;
1608
1609        {
1610            let events = tracker.lock().unwrap();
1611            let initial_events: Vec<_> = events
1612                .iter()
1613                .filter(|e| e.event_type == EventType::Initial)
1614                .collect();
1615
1616            // Should only have Initial for file.txt, not mydir
1617            assert_eq!(
1618                initial_events.len(),
1619                1,
1620                "Expected 1 Initial event (file only), got: {:?}",
1621                initial_events
1622            );
1623            assert_eq!(initial_events[0].path, PathBuf::from("file.txt"));
1624        }
1625
1626        // Cleanup
1627        watcher_handle.abort();
1628        let _ = std::fs::remove_dir_all(&test_dir);
1629    }
1630
1631    #[tokio::test]
1632    async fn test_watch_initial_disabled_by_default() {
1633        // Test that watch_initial is disabled by default
1634        let test_dir = std::env::current_dir()
1635            .unwrap()
1636            .join(".file-watcher-test-watch_initial_disabled");
1637
1638        if test_dir.exists() {
1639            std::fs::remove_dir_all(&test_dir).unwrap();
1640        }
1641        std::fs::create_dir(&test_dir).unwrap();
1642
1643        // Create a file before starting watcher
1644        std::fs::write(test_dir.join("existing.txt"), "content").unwrap();
1645
1646        let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1647        let tracker_clone = tracker.clone();
1648        let test_dir_clone = test_dir.clone();
1649
1650        let watcher_handle = tokio::spawn(async move {
1651            let _ = Watcher::new()
1652                .set_base_dir(&test_dir_clone)
1653                .add_include("**/*.txt")
1654                // watch_initial not enabled (default is false)
1655                .run(move |event_type, path| {
1656                    tracker_clone.lock().unwrap().push(Event {
1657                        path: path.clone(),
1658                        event_type: match event_type {
1659                            WatchEvent::Create => EventType::Create,
1660                            WatchEvent::Delete => EventType::Delete,
1661                            WatchEvent::Update => EventType::Update,
1662                            WatchEvent::Initial => EventType::Initial,
1663                            WatchEvent::DebugWatch => EventType::DebugWatch,
1664                        },
1665                    });
1666                })
1667                .await;
1668        });
1669
1670        tokio::time::sleep(Duration::from_millis(200)).await;
1671
1672        {
1673            let events = tracker.lock().unwrap();
1674            let initial_events: Vec<_> = events
1675                .iter()
1676                .filter(|e| e.event_type == EventType::Initial)
1677                .collect();
1678
1679            // Should have no Initial events since watch_initial is disabled
1680            assert_eq!(
1681                initial_events.len(),
1682                0,
1683                "Expected no Initial events when watch_initial is disabled, got: {:?}",
1684                initial_events
1685            );
1686        }
1687
1688        // Cleanup
1689        watcher_handle.abort();
1690        let _ = std::fs::remove_dir_all(&test_dir);
1691    }
1692}