
Monitor filesystem changes with debouncing and glob-based ignore patterns:

```vary-snippet
import fs

let watcher = fs.watch("./src")
mut changes = watcher.next_batch()
while changes is not None {
    for c in changes {
        print(f"{c.kind()} {c.path()}")
    }
    changes = watcher.next_batch()
}
```

| **Function** | **Returns** | **Description** |
|----------|---------|-------------|
| `fs.watch(directory, recursive?, settle_ms?, ignore?)` | `FileWatcher` | Create a persistent file watcher |
| `fs.watch_once(directory)` | `List[FileChange]` | Wait for a single batch of changes |

Defaults: `recursive=True`, `settle_ms=100`, `ignore=[".git/", ".DS_Store", "*.swp", "*.swo", "*~", "#*#", ".#*"]`.

`FileWatcher` methods:

| **Method** | **Returns** | **Description** |
|--------|---------|-------------|
| `.next_batch(timeout_ms?)` | `List[FileChange]` | Wait for next batch of changes (None = block forever) |
| `.close()` | `None` | Stop watching |
| `.is_closed()` | `Bool` | Check if watcher is closed |

`FileChange` methods:

| **Method** | **Returns** | **Description** |
|--------|---------|-------------|
| `.kind()` | `Str` | `"created"`, `"modified"`, `"deleted"`, `"overflow"` |
| `.path()` | `Str` | Path relative to watch root |
| `.absolute_path()` | `Str` | Absolute filesystem path |
| `.is_dir()` | `Bool` | True if the changed path is a directory |
