# How Quarkdown's Directory Watcher Detects File Changes for Live Preview

> Discover how Quarkdown leverages Directory Watcher and io methvin watcher to detect file changes, filter paths, and recompile for live preview.

- Repository: [Giorgio Garofalo/quarkdown](https://github.com/iamgio/quarkdown)
- Tags: internals
- Published: 2026-04-29

---

**Quarkdown uses a `DirectoryWatcher` wrapper around the `io.methvin.watcher` library to monitor source directories for changes, filter out excluded paths, and trigger automatic recompilation when files are modified.**

Quarkdown's live preview feature relies on a robust file system monitoring system that detects changes in real time and reloads the compiled output without manual intervention. The implementation centers on a Kotlin-based directory watcher that integrates with the CLI's command execution pipeline. This article examines the source code of the `iamgio/quarkdown` repository to explain how file change detection works, from the low-level watcher implementation to the browser auto-reload mechanism.

## Core Architecture of the Directory Watcher

### The DirectoryWatcher Wrapper Class

The monitoring capability is encapsulated in `com.quarkdown.cli.watcher.DirectoryWatcher`, located at [`quarkdown-cli/src/main/kotlin/com/quarkdown/cli/watcher/DirectoryWatcher.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-cli/src/main/kotlin/com/quarkdown/cli/watcher/DirectoryWatcher.kt). This class provides a thin abstraction over the third-party `io.methvin.watcher.DirectoryWatcher` library, handling Kotlin-specific concurrency patterns and path filtering.

The companion object exposes a `create()` factory method that accepts three parameters: the target `directory` to watch, an optional `exclude` predicate (defaulting to a filter that skips temporary files ending with `~`), and an `onChange` listener that receives a `DirectoryChangeEvent`. The wrapper constructs the underlying native watcher and attaches a custom listener that performs preliminary filtering before invoking the user-supplied callback.

### Event Filtering and Exclusion Logic

Before forwarding any filesystem event, the watcher validates the changed path against two exclusion criteria. As implemented in lines 58-63 of [`DirectoryWatcher.kt`](https://github.com/iamgio/quarkdown/blob/main/DirectoryWatcher.kt), the logic checks both a path-based predicate and a list of excluded file objects:

```kotlin
val acceptByPath = !exclude(it.path())
val acceptByFiles = excludeFiles.none { file -> it.path().startsWith(file.absolutePath) }
if (acceptByPath && acceptByFiles) onChange(it)

```

This dual-filter approach ensures that temporary editor files and the output directory do not trigger unnecessary recompilation cycles. The `excludeFiles` list specifically prevents the watcher from reacting to changes in the directory where Quarkdown writes its generated HTML, avoiding infinite loops where compilation output would trigger further compilation.

### Running Modes and Threading

The wrapper provides two execution modes for different consumption patterns. The `watchBlocking()` method occupies the current thread indefinitely, directly delegating to the underlying library's `watch()` method. Alternatively, `watch()` spawns a background thread using `kotlin.concurrent.thread` that internally calls `watchBlocking()`, allowing the CLI to continue with other initialization tasks while monitoring proceeds asynchronously. Cleanup is handled through the `stop()` method, which simply closes the native watcher instance.

## CLI Integration and Live Preview Triggering

### The --watch Flag in ExecuteCommand

The decision to enable file watching occurs in `ExecuteCommand`, the abstract base class for `CompileCommand` and `ReplCommand` found at [`quarkdown-cli/src/main/kotlin/com/quarkdown/cli/exec/ExecuteCommand.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-cli/src/main/kotlin/com/quarkdown/cli/exec/ExecuteCommand.kt). The CLI exposes the `-w`/`--watch` flag as a Boolean property (lines 65-68), which the `run()` method checks after building the CLI options.

When the watch flag is active, the command identifies the parent directory of the source file and initializes a watcher that excludes the output directory:

```kotlin
cliOptions.takeIf { watch }?.source?.absoluteFile?.parentFile?.let { sourceDirectory ->
    Log.info("Watching for file changes in source directory: $sourceDirectory")
    DirectoryWatcher
        .create(sourceDirectory, exclude = cliOptions.outputDirectory) { event ->
            Log.info("File changed: ${event.path()}. Launching.")
            execute(cliOptions, pipelineOptions)
        }.watch()
}

```

This code (lines 65-74) watches the `sourceDirectory` (the parent of the input `.qd` file), excludes `cliOptions.outputDirectory` from monitoring, and invokes `execute()`—which runs the full compilation pipeline—whenever a valid change event occurs.

### Preview Mode and Auto-Reload Mechanism

When the `-p`/`--preview` flag accompanies `--watch`, the pipeline executes with `isPreview` set to true, causing the generated HTML to be served by an embedded web server. The preview system uses a fixed resource name derived from a hash of the source path (see `resolveResourceName`, lines 19-23), meaning each recompilation overwrites the same URL. The browser client, implemented in [`live-preview.ts`](https://github.com/iamgio/quarkdown/blob/main/live-preview.ts), receives server-sent events or polling updates and automatically refreshes the page when the resource changes, completing the live-reload cycle without requiring manual browser interaction.

## End-to-End File Change Detection Flow

The complete workflow from file modification to browser refresh follows these steps:

1.  **Initialization**: User executes `quarkdown compile -p -w mydoc.qd`, triggering `ExecuteCommand.run()`.
2.  **Watcher Setup**: The system creates a `DirectoryWatcher` for the directory containing `mydoc.qd`, configured to ignore the `quarkdown-output` directory.
3.  **Monitoring**: A background thread begins listening for filesystem events via the native `io.methvin.watcher` implementation.
4.  **Change Detection**: When the user saves changes to `mydoc.qd`, the OS notifies the watcher, which passes the path through the exclusion filters.
5.  **Recompilation**: The listener callback logs the change and invokes `execute(cliOptions, pipelineOptions)`, re-running the document compilation.
6.  **Browser Update**: The new HTML overwrites the preview resource, and the frontend TypeScript client detects the update and reloads the browser tab.

## Testing the Watcher Implementation

The reliability of the directory watcher is verified by [`WatcherTest.kt`](https://github.com/iamgio/quarkdown/blob/main/WatcherTest.kt) in [`quarkdown-cli/src/test/kotlin/com/quarkdown/cli/WatcherTest.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-cli/src/test/kotlin/com/quarkdown/cli/WatcherTest.kt). These unit tests instantiate the `DirectoryWatcher` with temporary directories, programmatically create and modify files, and assert that the callback listener receives the expected events. The test suite specifically validates the exclusion logic (ensuring filtered paths do not trigger callbacks) and confirms proper asynchronous start/stop behavior, preventing resource leaks during CLI operation.

## Summary

-   **Quarkdown's directory watcher** is a Kotlin wrapper around `io.methvin.watcher.DirectoryWatcher` that handles threading and path filtering.
-   **Dual exclusion logic** prevents temporary files and the output directory from triggering recompilation, avoiding infinite loops.
-   **The `--watch` flag** in `ExecuteCommand` activates the watcher on the source file's parent directory, calling `execute()` on every valid change.
-   **Live preview integration** relies on fixed resource naming and an embedded server, allowing the browser to auto-reload when the pipeline regenerates HTML.
-   **Unit tests in [`WatcherTest.kt`](https://github.com/iamgio/quarkdown/blob/main/WatcherTest.kt)** verify the watcher's event handling and exclusion capabilities.

## Frequently Asked Questions

### How does Quarkdown avoid infinite loops when the output directory changes?

The watcher explicitly excludes `cliOptions.outputDirectory` from monitoring by passing it as an `excludeFiles` parameter during initialization. When the compilation pipeline writes new HTML files, the exclusion logic in [`DirectoryWatcher.kt`](https://github.com/iamgio/quarkdown/blob/main/DirectoryWatcher.kt) (lines 58-63) checks `excludeFiles.none { file -> it.path().startsWith(file.absolutePath) }`, preventing these write operations from triggering additional compilation cycles.

### What library does Quarkdown use for file system monitoring?

Quarkdown utilizes the `io.methvin.watcher.DirectoryWatcher` library, a high-performance native file system watcher for the JVM. The `com.quarkdown.cli.watcher.DirectoryWatcher` class serves as a Kotlin-friendly wrapper that manages threading, event filtering, and lifecycle management while delegating the actual OS-level monitoring to this underlying library.

### Can I exclude specific files from triggering a reload?

Yes, the `DirectoryWatcher.create()` method accepts a customizable `exclude` predicate function that filters paths based on custom logic. By default, it ignores files ending with `~` (common temporary file patterns), but you can supply your own lambda—such as `{ path -> path.extension == "tmp" }`—to ignore specific file extensions or patterns during initialization.

### How does the browser know when to refresh during live preview?

The preview system uses a fixed resource name generated from a hash of the source path, causing each recompilation to overwrite the same URL. The TypeScript client in [`live-preview.ts`](https://github.com/iamgio/quarkdown/blob/main/live-preview.ts) establishes a connection to the embedded server and listens for update notifications. When `ExecuteCommand` triggers `execute()` after detecting a file change, the regenerated HTML replaces the previous version, and the frontend client automatically reloads the browser to display the updated content.