--- phase: 04-bbs-polish-and-live-content plan: 03 subsystem: app tags: [notify, filesystem-watcher, live-reload, debounce, mpsc, rust, event-loop] # Dependency graph requires: - phase: 04-01 provides: "notify = 6.1 in Cargo.toml" - phase: 04-02 provides: "__directory__ sentinel, navigate_to_directory(), list_vault_files(), rewatch target for virtual page" provides: - "FileWatcher pub struct with RecommendedWatcher, mpsc Receiver, rewatch() method" - "file_watcher and pending_reload_at fields on App struct" - "reload_current_document() preserving scroll position and link selection" - "rewatch_for_current_page() re-pointing watcher on every navigation event" - "try_recv() drain loop in event loop for non-blocking filesystem event polling" - "300ms debounce timer firing reload after last relevant event" - "FileWatcher initialized in main.rs with non-fatal failure handling" affects: [] # Tech tracking tech-stack: added: - "notify 6.1 RecommendedWatcher with mpsc channel — already in Cargo.toml from Plan 01" patterns: - "FileWatcher wraps RecommendedWatcher + Receiver in a struct with a rewatch() helper" - "Drain loop pattern: loop { match rx.try_recv() { ... Err(Empty) => break } } drains all queued events per poll iteration" - "Debounce pattern: pending_reload_at = Some(Instant::now()) on each event, reload fires 300ms after last event" - "Directory watch pattern: watch parent dir, not the file itself — survives atomic saves (vim/neovim rename-over)" - "Non-fatal watcher creation: watcher failure prints warning, app runs without live reload (None passed to App)" key-files: created: [] modified: - "src/app.rs — FileWatcher struct, file_watcher/pending_reload_at fields, App::new() new param, reload_current_document(), rewatch_for_current_page(), try_recv drain loop + debounce in run_event_loop(), rewatch calls in all navigate_* methods" - "src/main.rs — FileWatcher creation before App::new(), non-fatal error handling, updated App::new() call with file_watcher arg" key-decisions: - "Watch parent directory of current file (not the file itself) — survives atomic saves where editors write to temp file then rename" - "Drain loop pattern: loop { try_recv() ... break on Empty/Disconnected } — processes all queued events per poll so rapid saves do not queue lag" - "300ms debounce: pending_reload_at reset on each event — reload fires only after 300ms of silence from last event" - "File watcher initialized in main.rs before App::new() — avoids self-referential borrowing, keeps App::new() pure" - "Non-fatal watcher failure: eprintln warning only, None passed to App — app fully functional without live reload" patterns-established: - "notify mpsc pattern: RecommendedWatcher::new(tx, Config::default()) + Receiver> stored together" - "Debounce in event loop: check pending_reload_at after polling phase, not in key handler" requirements-completed: [LIVE-01] # Metrics duration: 7min completed: 2026-03-01 --- # Phase 4 Plan 03: Live Filesystem Watching Summary **FileWatcher struct using notify 6.1 mpsc pattern wired into App event loop with 300ms debounce, parent-directory watching, and non-fatal startup error handling** ## Performance - **Duration:** 7 min - **Started:** 2026-03-01T10:20:21Z - **Completed:** 2026-03-01T10:27:00Z - **Tasks:** 2 - **Files modified:** 2 ## Accomplishments - `FileWatcher` pub struct created in `src/app.rs` — wraps `RecommendedWatcher`, `Receiver>`, and `watched_dir: PathBuf` with a `rewatch()` helper that safely unwatches the old directory before watching the new one - `file_watcher: Option` and `pending_reload_at: Option` fields added to `App` struct - `App::new()` signature updated with `file_watcher: Option` as seventh parameter - `reload_current_document()` method added — handles both the `__directory__` virtual page (calls `list_vault_files()` + `build_directory_lines()`) and real documents (re-reads from disk, re-renders markdown with splash prepend for index.md), preserves scroll offset and link selection after reload - `rewatch_for_current_page()` method added — re-points watcher to parent directory of current file, or vault root for `__directory__`; called at the end of all four navigation methods - `try_recv()` drain loop added to `run_event_loop()` after event polling — non-blocking, drains all queued events per iteration, filters events by filename match (or `.md` extension for directory pages) - 300ms debounce timer integrated — `pending_reload_at` reset to `Some(Instant::now())` on each relevant event, reload fires only after 300ms of silence - `FileWatcher::new()` called in `main.rs` before `App::new()`, non-fatal: watcher creation failure prints a warning and passes `None` to `App` so the app runs normally without live reload ## Task Commits Each task was committed atomically: 1. **Task 1: FileWatcher struct and event loop integration** - `a44c9cc` (feat) 2. **Task 2: Initialize FileWatcher in main.rs and wire App constructor** - `f3133dd` (feat) ## Files Created/Modified - `src/app.rs` - `FileWatcher` struct; `file_watcher`/`pending_reload_at` fields on `App`; `App::new()` new param; `reload_current_document()`; `rewatch_for_current_page()`; try_recv drain loop and debounce in `run_event_loop()`; `rewatch_for_current_page()` calls in all navigate_* methods - `src/main.rs` - `FileWatcher` creation in step 3c; non-fatal error handling with `eprintln!`; updated `App::new()` call with `file_watcher` argument ## Decisions Made - Watch parent directory, not the file itself — survives atomic saves (vim/neovim write to a temp file then rename it over the target; inotify RENAME events on the directory catch this correctly) - Drain loop per poll iteration — ensures all queued events from a rapid burst are consumed; debounce collapses the burst into a single reload after 300ms of silence - `File_watcher` initialized in `main.rs` before `App::new()` — avoids self-referential ownership issues and keeps the constructor pure - Non-fatal failure path — watcher depends on OS filesystem notification support; platforms or permission configurations where it fails should not prevent the BBS from running ## Deviations from Plan None - plan executed exactly as written. The `notify` crate `Config` type required aliasing to `NotifyConfig` to avoid shadowing the existing `crate::config::Config` import, which is a natural Rust naming convention applied automatically. ## Issues Encountered None. The `notify 6.1` API was exactly as specified in the plan: `RecommendedWatcher::new(tx, Config::default())` and `mpsc::channel()`. Build passed on first attempt. The two pre-existing clippy warnings (`is_within_vault`, `init_logging`) and the two pre-existing renderer.rs style suggestions are out of scope for this plan. ## User Setup Required None - live reload is automatic. File changes to the currently displayed `.md` file are detected within ~800ms (300ms debounce + up to 500ms from two 250ms poll iterations). No configuration needed. ## Phase 4 Complete This is the final plan of Phase 4 and the final plan of the project. All requirements have been met: - **BBS-01**: Splash screen (`splash.txt` ANSI art) prepended to index.md (Plan 01) - **BBS-02**: File metadata (mtime, size) in status bar (Plan 01) - **LIVE-02**: Virtual `[[Directory]]` page with tree view and Tab-cycling (Plan 02) - **LIVE-01**: Live filesystem watching with auto-refresh and scroll preservation (Plan 03) --- *Phase: 04-bbs-polish-and-live-content* *Completed: 2026-03-01*