Files
ruohki 2e0c9d2277 docs(04-03): complete live reload plan — project complete
- Create 04-03-SUMMARY.md documenting FileWatcher integration
- Update STATE.md: Phase 4 COMPLETE, project COMPLETE, 100% progress
- Update ROADMAP.md: 04-03 checked, Phase 4 3/3 Complete with date
- Update REQUIREMENTS.md: LIVE-01, BBS-01, BBS-02 marked complete
2026-03-01 11:30:37 +01:00

124 lines
7.5 KiB
Markdown

---
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<notify::Result<notify::Event>> 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<notify::Result<notify::Event>>`, and `watched_dir: PathBuf` with a `rewatch()` helper that safely unwatches the old directory before watching the new one
- `file_watcher: Option<FileWatcher>` and `pending_reload_at: Option<Instant>` fields added to `App` struct
- `App::new()` signature updated with `file_watcher: Option<FileWatcher>` 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*