Files

211 lines
20 KiB
Markdown

# Project Research Summary
**Project:** bbs-md — Rust TUI Markdown Vault Reader / BBS System
**Domain:** Retro BBS-style login shell, serving a read-only markdown vault over SSH
**Researched:** 2026-02-28
**Confidence:** MEDIUM (stack HIGH, architecture HIGH, features MEDIUM, pitfalls MEDIUM)
## Executive Summary
bbs-md is a single-purpose Rust binary that serves as a login shell: SSH users connect and land inside a retro BBS-styled, keyboard-driven markdown vault reader. There is no embedded SSH server, no write capability, and no user session state — each process is fully independent, short-lived, and read-only. The domain is well-understood and shares patterns with mdbook, glow, and classic Obsidian vault viewers, but the BBS aesthetic and login-shell deployment context impose additional constraints that casual markdown viewers do not address.
The recommended approach is a synchronous ratatui Elm-architecture app: immutable `AppState` drives a pure render pass, and a single `App::update(action)` method handles all mutations. A vault subsystem (index + parser + resolver) operates at load time, not render time. Filesystem watching runs in a dedicated thread communicating via `std::sync::mpsc`. No tokio, no async runtime — the complexity does not justify the tradeoff for a process that reads local files and draws to a PTY. The stack is lightweight: ratatui 0.30 (already scaffolded), pulldown-cmark for markdown parsing, notify for filesystem watching, toml for configuration, and the tracing ecosystem for file-based logging.
The key risks are process-lifecycle issues specific to login-shell deployment: unhandled panics that leave SSH sessions with broken terminals, SIGHUP not being caught on normal disconnect, and ANSI escape or stderr writes corrupting the ratatui frame buffer. These must be addressed in Phase 1 before any feature work begins. Secondary risks include path traversal via wiki-links (security), per-process inotify exhaustion under concurrent users (scalability), and a currently invalid `edition = "2024"` in Cargo.toml that blocks compilation today.
## Key Findings
### Recommended Stack
The project already has a working ratatui 0.30 scaffold with crossterm, anyhow, thiserror, serde, and unicode-width confirmed in Cargo.lock — these are not decisions to make, only to build on. The new additions needed are narrow and well-justified. pulldown-cmark is preferred over comrak for its event-based (streaming, zero-copy) model that maps naturally to ratatui's `Line`/`Span` construction without a full AST allocation. Custom rendering from pulldown-cmark events (~300 lines) is explicitly recommended over pre-built markdown-to-ratatui crates, which impose their own styling and fight the retro BBS aesthetic.
Logging to files (not stderr/stdout) is non-optional and must be configured before any other code runs. The tracing + tracing-appender stack is the correct choice. Tokio must not be added — there is no async I/O in the hot path, and adding it would increase binary size and cognitive overhead for no benefit.
**Core technologies:**
- ratatui 0.30.0: TUI rendering — already scaffolded, sub-crate architecture (0.29+), do not downgrade
- crossterm 0.29.0: Terminal backend — ships with ratatui, do not add a second backend
- pulldown-cmark ^0.12: Markdown parsing — event-based, preferred over comrak for streaming render
- notify ^8 + notify-debouncer-full ^0.4: Filesystem watching — cross-platform, debounced for editor save storms
- toml ^0.8: Vault configuration (bbs.toml) — pairs with serde (already in tree), TOML is Rust-idiomatic
- tracing ^0.1 + tracing-subscriber ^0.3 + tracing-appender ^0.2: File-only logging — mandatory before TUI init
- regex (existing): Wiki-link pre-pass parsing — no new dependency needed for `[[Page Name]]` extraction
**Version caveat:** pulldown-cmark, notify, and notify-debouncer-full versions are from training knowledge (cutoff Aug 2025) and must be verified on crates.io before pinning. Run `cargo add [crate]` to get Cargo's current recommendation.
### Expected Features
The MVP is a fully functional vault browser: render all core markdown constructs, follow wiki-links and standard links, scroll, navigate back, show keyboard hints, and exit cleanly. Everything beyond that is a differentiator.
**Must have (table stakes):**
- Markdown rendering: H1-H6 headers, bold, italic, inline code, fenced code blocks, lists, blockquotes — without these it is not a markdown viewer
- Scrollable content with keyboard scroll (j/k, arrows, PgUp/PgDn) — content longer than the terminal is the normal case
- `[[wiki-link]]` following and standard `[text](path.md)` link following — core navigation model
- Back navigation with history stack — browsing without back is broken UX
- index.md landing page with graceful missing-file error — defined entry point
- Graceful exit (q, Ctrl+C) with terminal restoration — login shell requirement
- Keyboard hints in status bar — discoverability in a custom shell
- `[IMAGE: alt text]` placeholder rendering — stated requirement; images cannot render in standard terminals
- Terminal resize handling — SSH clients resize constantly
**Should have (differentiators):**
- Retro BBS ANSI art header / splash screen on index.md — creates the "BBS you SSH into" atmosphere
- Box-drawing borders on all panels — reinforces retro aesthetic (ratatui Block widgets, low effort)
- CGA-era color theme (cyan/magenta/bright-green on dark) — instant retro feel
- Forward navigation (forward after back) — makes browsing feel genuinely web-like
- Filesystem watching for live content updates — content updates without restarting sessions
- Vault-wide index / directory listing — discover documents beyond what links expose
- Inline link highlighting + Tab cycling — keyboard-first link navigation between links on a page
- "Last updated" file mtime on pages — adds BBS bulletin feel
- "You are here" breadcrumb in status bar — orientation in deep vaults
- Table rendering — common markdown construct, most TUI viewers skip it
**Defer (v2+):**
- Full history overlay UI — convenience after core navigation works
- ANSI art splash screen — aesthetic polish, implement after functional MVP
- Syntax highlighting in code blocks — syntect adds 5MB+ to binary, marginal value for retro aesthetic
- Search across vault — high complexity, not BBS-like
- Mouse click navigation — inconsistent SSH support, keyboard-only fits BBS aesthetic better
- Mermaid/PlantUML diagram rendering — poor terminal output, scope creep
- Write/edit capabilities — explicitly out of scope, hard guarantee
### Architecture Approach
The architecture is a layered single-process design with strict unidirectional data flow following the ratatui Elm-architecture pattern: `App::update(action)` mutates state; `Renderer::draw(&state)` is a pure read-only function. No mutation occurs in the render path. The vault subsystem (index, parser, resolver) is pre-built at startup and updated incrementally by the filesystem watcher. The watcher runs in a dedicated thread; its events enter the main event loop via `mpsc::try_recv()` between frame draws and terminal event polling.
The shared contract across all subsystems is `DocElement` — the typed AST node enum produced by the parser and consumed by the renderer. Both parser and renderer must agree on this type before either can be completed. Define `vault/types.rs` first.
**Major components:**
1. App (app.rs) — holds all mutable state, dispatches actions, orchestrates subsystems
2. Vault Engine (vault/mod.rs) — index + link resolver + markdown parser; load/cache lifecycle
3. Renderer (ui/mod.rs) — pure draw function, custom BBS widgets (ui/widgets.rs), theme constants (ui/theme.rs)
4. Input Handler (input.rs) — maps crossterm events to Action enum variants
5. Navigation (nav.rs) — back/forward history stack, stores path + scroll offset only (not rendered content)
6. Watcher (watcher.rs) — notify crate, dedicated thread, sends VaultChanged events via mpsc channel
7. Signal/panic hooks (main.rs) — restore terminal on panic, SIGHUP, SIGTERM, broken pipe
### Critical Pitfalls
1. **No panic hook = SSH session locked out** — Install a crossterm terminal-restore panic hook before the event loop. Also handle SIGHUP and SIGTERM via signal-hook to set a shutdown flag. This is the single highest-priority item. Address in Phase 1, day one.
2. **Logging to stderr/stdout corrupts TUI display** — Configure tracing-appender to write to a file before any other code. Zero `println!`/`eprintln!` after TUI init is a hard rule. Address in Phase 1.
3. **Path traversal via wiki-links**`PathBuf::join(user_input)` is not safe. Always canonicalize both vault root and resolved path, then verify the resolved path has the vault root as prefix. Silently reject traversal as "not found." Address in Phase 2 (link resolver), never retrofit.
4. **Per-process inotify exhaustion under concurrent users** — Each process independently watching the full vault will exhaust Linux kernel inotify watch limits at scale. Use Option C (watch only the currently viewed file) for the initial implementation; design the watcher interface so the strategy can be changed. Address in Phase 3.
5. **Invalid `edition = "2024"` blocks compilation** — Fix to `edition = "2021"` in Cargo.toml before any development. This is a pre-Phase-1 fix. (Note: STACK.md research observed that Rust 1.85 / Feb 2025 stabilized edition 2024 — verify whether the current toolchain accepts it before changing. If it builds, leave it; if it fails, change to 2021.)
## Implications for Roadmap
Based on the dependency graph from ARCHITECTURE.md and the phase warnings from PITFALLS.md, the natural build order is:
### Phase 0: Pre-flight (not a feature phase — mandatory fixes)
**Rationale:** Two issues block all work: the potential edition incompatibility and the absence of terminal restoration. Both must be resolved before writing a single line of feature code.
**Delivers:** A compilable project with a TUI event loop that does not lock out SSH users on crash.
**Addresses:** PITFALL-5 (Cargo edition), PITFALL-1 (panic hook), PITFALL-2 (SIGHUP/SIGTERM), PITFALL-7 (file-only logging), PITFALL-11 (broken pipe handling).
**Research flag:** No additional research needed — these are well-documented patterns with code provided in PITFALLS.md.
### Phase 1: Foundations and TUI Shell
**Rationale:** All other phases depend on a working event loop, the Action enum, navigation history, and the DocElement type contract. Build the skeleton before any rendering or vault logic.
**Delivers:** A running TUI shell that polls events, displays a placeholder, and exits cleanly (q/Ctrl+C). Also: Action enum, NavigationHistory (path-based, not content-based), and vault/types.rs (DocElement, RenderedPage, LinkTarget).
**Addresses:** FEATURE: graceful exit, keyboard hints scaffold, terminal resize skeleton.
**Avoids:** PITFALL-9 (history storing rendered content — use path+offset from day one).
**Research flag:** Standard ratatui patterns — skip research-phase.
### Phase 2: Vault Core — Parser, Resolver, Index
**Rationale:** The renderer cannot be built without DocElement. The navigator cannot load pages without VaultEngine. This phase produces the shared contract and the core data pipeline.
**Delivers:** Full markdown parse pipeline (pulldown-cmark → DocElement), VaultIndex (title→path map built at startup), LinkResolver (wiki-link + relative path + external classification), path traversal prevention baked in.
**Addresses:** FEATURE: `[[wiki-link]]` following, standard link following, index.md landing page.
**Avoids:** PITFALL-4 (path traversal — canonical prefix check from day one), PITFALL-13 (wiki-link grammar precedence), PITFALL-14 (case-sensitivity Linux vs macOS fallback), PITFALL-15 (missing index.md graceful error).
**Research flag:** Well-documented patterns (pulldown-cmark events, two-phase link resolution). Skip research-phase.
### Phase 3: Renderer — Markdown Display
**Rationale:** With DocElement defined and VaultEngine producing RenderedPage, the renderer can be built against a stable contract. This is the phase that makes the app visually useful.
**Delivers:** Full markdown rendering (headers with visual hierarchy, bold/italic/code, fenced code blocks, lists, blockquotes, image placeholders, horizontal rules). Scrollable content. Status bar. Box-drawing retro aesthetic. BBS color theme.
**Addresses:** FEATURES: all table-stakes markdown rendering, scrollable content, keyboard hints, `[IMAGE: alt text]` placeholders, terminal resize reflow.
**Avoids:** PITFALL-6 (unicode width — use unicode-width crate, let ratatui wrap), PITFALL-10 (scroll position on resize — decide representation upfront: fractional or reset-to-top), PITFALL-16 (strip ANSI from vault content before rendering).
**Research flag:** ratatui widget API for custom Widget impls may benefit from a quick research-phase pass to confirm 0.30 patterns before implementing custom BBS widgets.
### Phase 4: Navigation — Links and History
**Rationale:** With rendering working, wiring up actual navigation (follow links, back, forward) makes the app a true vault browser. Depends on resolver and navigation history from earlier phases.
**Delivers:** Live link following (Enter on focused link), back navigation, forward navigation, history-aware breadcrumb in status bar. Graceful "file not found" error page.
**Addresses:** FEATURES: back/forward navigation, breadcrumb, vault browsing.
**Avoids:** PITFALL-8 (blocking I/O on main thread — load files in background thread, show loading indicator).
**Research flag:** Background thread + channel file loading needs careful design. May benefit from a targeted research-phase session on the thread/channel pattern in ratatui apps.
### Phase 5: Filesystem Watching and Live Updates
**Rationale:** Watching can only be added after the render/navigation pipeline is stable, because it triggers re-parses that flow through the full pipeline.
**Delivers:** Vault content updates without restarting sessions. Auto-refresh of currently viewed page when its file changes. Silent index update for other changed files.
**Addresses:** FEATURE: filesystem watching for live content updates.
**Avoids:** PITFALL-3 (per-process inotify exhaustion — watch only current file, not full vault; use debouncer).
**Research flag:** notify 8.x API needs verification (notify 6.x had a different async model). This phase likely needs a research-phase session to confirm the correct watcher setup for per-file watching with event re-registration on navigation.
### Phase 6: BBS Polish and Discovery
**Rationale:** With a fully functional vault reader, aesthetic and discovery features elevate the product from "works" to "feels like a BBS."
**Delivers:** ANSI art splash screen on index.md, vault-wide directory listing (browsable file index), "last updated" mtime display, inline link highlighting + Tab cycling between links, page title in terminal title bar via OSC escape, optional bbs.toml configuration for vault path and theme.
**Addresses:** FEATURES: retro BBS differentiators, vault-wide index, link cycling.
**Avoids:** PITFALL-16 (ANSI art must go through ratatui, not raw escape writes).
**Research flag:** OSC escape sequences and terminal title setting are standard — no research needed. Table rendering (GFM tables) is complex enough to warrant a mini research-phase on ratatui Table widget limits.
### Phase Ordering Rationale
- Phase 0 is non-negotiable first: the edition and terminal-safety issues block everything else.
- Phases 1-2 establish the shared type contract (DocElement) that all later phases depend on. They must precede the renderer.
- Phase 3 (renderer) before Phase 4 (navigation) because link highlighting and focus state require the renderer to understand link positions.
- Phase 5 (watching) after Phase 4 because it triggers full navigation pipeline re-runs; that pipeline must be stable first.
- Phase 6 is additive polish and does not block any prior phase.
### Research Flags
Phases likely needing deeper research during planning:
- **Phase 3 (Renderer):** ratatui 0.30 custom Widget impl API — confirm the current trait signature and lifecycle, as this changed between 0.28 and 0.29.
- **Phase 4 (Navigation):** Background file loading pattern in a synchronous ratatui event loop — threading model and channel drain strategy.
- **Phase 5 (Watching):** notify 8.x API for per-file watch registration and re-registration on navigation — the API changed significantly between major versions.
- **Phase 6 (Tables):** ratatui Table widget constraints and text-wrapping limitations for GFM table rendering.
Phases with standard patterns (skip research-phase):
- **Phase 0:** Code provided in PITFALLS.md directly — no research needed.
- **Phase 1:** ratatui Elm-arch pattern is canonical and well-documented.
- **Phase 2:** pulldown-cmark event API is stable since 0.8; two-phase link resolution is the universal pattern.
## Confidence Assessment
| Area | Confidence | Notes |
|------|------------|-------|
| Stack | HIGH | Core stack confirmed from Cargo.lock. New additions (pulldown-cmark, notify, tracing) are well-established — only version numbers need crates.io verification. |
| Features | MEDIUM | Based on domain expertise (BBS systems, TUI markdown viewers), not competitive analysis of current live products. Web search unavailable during research. |
| Architecture | HIGH | ratatui Elm-arch is canonical and stable. Module structure and data flow are directly derived from established ratatui app patterns. pulldown-cmark → DocElement bridge is the universal approach in this space. |
| Pitfalls | MEDIUM | Login-shell and TUI pitfalls are well-established. notify version-specific behavior and ratatui 0.30 Drop guarantees should be verified against current docs. |
**Overall confidence:** MEDIUM-HIGH — enough to build a roadmap and start Phase 0 immediately. The architecture and stack decisions are solid. Versions need crates.io spot-checks before pinning.
### Gaps to Address
- **Rust edition 2024 status:** STACK.md research notes edition 2024 was stabilized in Rust 1.85 (Feb 2025). PITFALLS.md flags it as invalid. Run `cargo build` on the current toolchain to determine actual behavior before changing Cargo.toml. If it builds, leave it; if not, change to `edition = "2021"`.
- **notify 8.x exact API:** The watcher integration in Phase 5 depends on the current notify API. Verify `RecommendedWatcher` vs `PollWatcher` constructor signatures and `EventKind` variants at integration time.
- **pulldown-cmark wiki-link strategy:** The pre-pass regex substitution approach (replace `[[X]]` with `[X](X.md)` before parsing) versus the post-parse interception approach (intercept Code events containing `[[`) — both are described as viable in STACK.md. Choose and document one canonical approach at the start of Phase 2.
- **ratatui 0.30 scroll widget:** ARCHITECTURE.md mentions `ScrollView` widget as a candidate for scrolling. Verify this widget exists in 0.30 and has the expected API, or fall back to manual scroll offset state.
- **Concurrent user ceiling:** No load testing data exists for the inotify limit concern. The Phase 5 "watch only current file" strategy should be treated as correct until profiled under realistic concurrent session counts.
## Sources
### Primary (HIGH confidence)
- Cargo.lock (direct observation) — confirmed ratatui 0.30.0, crossterm 0.29.0, anyhow 1.0.102, thiserror 2.0.18, unicode-width 0.2.2, serde 1.0.228
- `.planning/PROJECT.md` — stated project requirements and deployment context
- POSIX standard — login shell SIGHUP behavior, argv[0] leading-dash convention
- Linux kernel documentation — inotify `max_user_watches` limit behavior
### Secondary (MEDIUM confidence)
- Training knowledge (cutoff Aug 2025) — ratatui 0.30 Elm-arch patterns, pulldown-cmark event API, notify 8.x architecture, tracing ecosystem
- Established tools: mdbook, Obsidian vault conventions, glow, lynx — feature and architecture patterns
- Classic BBS systems: PCBoard, TBBS, Telegard — aesthetic and UX references for the retro angle
### Tertiary (LOW confidence)
- pulldown-cmark ^0.12 current version — training knowledge, verify on crates.io before pinning
- notify ^8 / notify-debouncer-full ^0.4 versions — training knowledge, verify on crates.io
- ratatui ScrollView widget availability in 0.30 — needs direct docs verification
---
*Research completed: 2026-02-28*
*Ready for roadmap: yes*