Files
bbs-md/.planning/phases/04-bbs-polish-and-live-content/04-03-SUMMARY.md
T
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

7.5 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
04-bbs-polish-and-live-content 03 app
notify
filesystem-watcher
live-reload
debounce
mpsc
rust
event-loop
phase provides
04-01 notify = 6.1 in Cargo.toml
phase provides
04-02 __directory__ sentinel, navigate_to_directory(), list_vault_files(), rewatch target for virtual page
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
added patterns
notify 6.1 RecommendedWatcher with mpsc channel — already in Cargo.toml from Plan 01
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)
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
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
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
LIVE-01
7min 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