Technology Stack
Project: bbs-md — Rust TUI Markdown Vault Reader / BBS System
Researched: 2026-02-28
Confidence Note: Web and docs access was unavailable during this research session. Versions are derived from: (1) the project's existing Cargo.lock for already-pinned deps, and (2) training knowledge for new additions. All new library versions are flagged with their confidence level and must be verified against crates.io before pinning.
Existing Stack (Confirmed from Cargo.lock)
These are locked and do not need verification — they are already in the dependency tree.
| Library |
Locked Version |
Role |
| ratatui |
0.30.0 |
TUI framework — keep as-is |
| ratatui-core |
0.1.0 |
Core types (transitive) |
| ratatui-crossterm |
0.1.0 |
Crossterm backend (transitive) |
| ratatui-macros |
0.7.0 |
Layout macros (transitive) |
| ratatui-widgets |
0.3.0 |
Built-in widgets (transitive) |
| crossterm |
0.29.0 |
Terminal I/O (transitive via ratatui) |
| anyhow |
1.0.102 |
Error handling — keep |
| thiserror |
2.0.18 |
Structured errors (transitive, available) |
| termwiz |
0.23.3 |
Terminal capabilities (transitive) |
| regex |
1.12.3 |
Regex (transitive, available to use) |
| unicode-width |
0.2.2 |
Display width (transitive) |
| serde |
1.0.228 |
Serialization (transitive, available) |
| serde_json |
1.0.149 |
JSON (transitive, available) |
Recommended Stack — New Additions
Core Framework (already decided)
| Technology |
Version |
Purpose |
Why |
| ratatui |
0.30.0 (existing) |
TUI rendering and layout |
Already scaffolded; ratatui is the dominant Rust TUI framework. 0.30.x is current stable. The split into ratatui-core/ratatui-widgets is the new architecture introduced in 0.29+. Do not downgrade. |
| crossterm |
0.29.0 (existing) |
Terminal backend |
Ships with ratatui. Provides raw mode, cursor, event loop. Do not add a second backend. |
Markdown Parsing
| Technology |
Version |
Purpose |
Why |
| pulldown-cmark |
^0.12 |
Parse CommonMark markdown into events |
Use pulldown-cmark, not comrak. pulldown-cmark is event-based (streaming, zero-copy), not a full AST allocator. For a TUI renderer that walks nodes once to produce styled spans, the event model maps perfectly to ratatui's Line/Span construction. comrak builds a full AST tree, consumes more memory, and adds GFM extension dependencies you do not need. pulldown-cmark supports tables and footnotes via feature flags. Confidence: MEDIUM — version 0.12 was the 2024/2025 major; verify against crates.io. |
Why not tui-markdown or similar? Pre-built markdown-to-ratatui renderers exist (e.g., ratatui-markdown, tui-markdown) but they impose their own styling decisions and fight the retro BBS aesthetic you need. Writing the pulldown-cmark → ratatui spans bridge yourself gives complete control over box-drawing, color palettes, and header decoration. Total code for a basic bridge is ~300 lines. Worth it.
Filesystem Watching
| Technology |
Version |
Purpose |
Why |
| notify |
^8 |
Watch vault directory for .md file changes |
Use notify, not inotify directly. notify is the de-facto cross-platform file watching crate in Rust — uses kqueue on macOS, inotify on Linux, FSEvents on macOS (optional), ReadDirectoryChangesW on Windows. For a Linux SSH server deployment, this is fine. The notify::recommended_watcher function picks the best backend automatically. Version 8.x introduced the EventKind redesign. Confidence: MEDIUM — notify 8.x is the current major; verify on crates.io. |
| notify-debouncer-full |
^0.4 |
Debounce rapid file change events |
Markdown editors (Obsidian, vim) fire multiple events per save (write + metadata update). Without debouncing you rebuild the file index on every partial write. notify-debouncer-full batches events within a configurable window (200ms is good). Ships alongside notify in the same repo. Confidence: MEDIUM — same version caveat as notify. |
Threading strategy with notify: Run the watcher in a dedicated std::thread, send notify::Event through a std::sync::mpsc::channel into the main event loop. This avoids tokio entirely — the ratatui event loop already has a crossterm event poller, so you integrate watcher events into the same crossterm::event::poll loop with a mpsc::try_recv() check each frame. Do not add tokio just for filesystem watching.
Async Runtime
| Decision |
Verdict |
| tokio |
Do not add tokio. There is no async I/O here. Each SSH session is its own OS process. The ratatui event loop is synchronous. File watching runs in a thread. Adding tokio buys nothing and adds ~2MB binary size and compilation complexity. If a future feature needs async (e.g., fetching remote content), add tokio then. |
Configuration
| Technology |
Version |
Purpose |
Why |
| toml |
^0.8 |
Parse bbs.toml vault configuration file |
The vault should support a config file for: vault root path, color theme, index.md filename, disabled link types, etc. TOML is the Rust-idiomatic config format (Cargo uses it). toml crate pairs with serde (already in dep tree) via #[derive(Deserialize)]. Confidence: HIGH — toml 0.8 is established stable. |
Wiki-Link Parsing
| Decision |
Verdict |
| regex |
Use the existing regex crate already in the dep tree. Wiki-links ([[Page Name]]) are not CommonMark — pulldown-cmark will not parse them. A pre-pass over raw markdown text with a regex (\[\[([^\]]+)\]\]) before handing off to pulldown-cmark is the correct approach. Replace [[Page Name]] with a standard link [Page Name](Page_Name.md) before parsing, OR parse pulldown-cmark events and intercept Code events that contain [[. The pre-pass substitution approach is simpler. No new dependency needed. |
Logging
| Technology |
Version |
Purpose |
Why |
| tracing |
^0.1 |
Structured logging |
Critical requirement: a TUI app cannot write to stderr/stdout — it corrupts the terminal. tracing with tracing-appender writes logs to a rotating file. This is non-optional for any debugging after the "Hello world" stage. Confidence: HIGH — tracing 0.1 is the Rust async/structured logging standard. |
| tracing-subscriber |
^0.3 |
Configure tracing output |
Pairs with tracing. Use EnvFilter so RUST_LOG=debug controls log level. Confidence: HIGH. |
| tracing-appender |
^0.2 |
Non-blocking file log sink |
Ships with the tracing ecosystem. Writes to /tmp/bbs-md.log or a configured path without blocking the render loop. Confidence: HIGH. |
State Management
| Decision |
Verdict |
| No external state library |
Ratatui's model is: immutable App struct → render → event → mutate App → repeat. This is sufficient. Use a single App struct holding current_doc: Arc<Document>, history: Vec<PathBuf>, scroll: usize. No Redux/signals/observable libraries needed. |
Error Handling
| Technology |
Version |
Purpose |
Why |
| anyhow |
1.0.102 (existing) |
Application-level errors |
Already present. Use anyhow::Result at the top level. For the main event loop, handle errors by displaying them as a TUI overlay rather than crashing — SSH users get a helpful error panel instead of a dead process. |
| thiserror |
2.0.18 (existing) |
Domain error types |
Use for VaultError, ParseError, NavigationError. These convert into anyhow automatically. |
Alternatives Considered
| Category |
Recommended |
Alternative |
Why Not |
| Markdown parser |
pulldown-cmark |
comrak |
comrak builds a full AST in memory; heavier allocation model for what is essentially a streaming render task. pulldown-cmark is faster for single-pass rendering. |
| Markdown parser |
pulldown-cmark |
markdown-it (JS via WASM) |
Absurd. This is a Rust project. |
| Markdown parser |
pulldown-cmark |
tui-markdown / ratatui-markdown |
Canned styling; incompatible with retro BBS aesthetic requirements. |
| File watching |
notify |
inotify (Linux-only) |
notify wraps inotify and adds macOS/Windows fallbacks. Since vault might be developed on macOS and deployed to Linux, cross-platform matters during development. |
| File watching |
notify |
fs-watch |
Unmaintained as of 2024. |
| Config format |
toml |
serde_json |
JSON has no comments. Vault authors need to annotate config. TOML is the Rust community default. |
| Config format |
toml |
ron |
RON is Rust-specific; vault authors are likely from the Obsidian/markdown world, not Rust devs. |
| Async runtime |
(none) |
tokio |
Zero async I/O in the hot path. Single-process per SSH session. tokio adds compile time, binary size, and cognitive overhead for no benefit. |
| Logging |
tracing |
env_logger |
env_logger writes to stderr which corrupts TUI rendering. tracing-appender writes to a file. |
| Logging |
tracing |
println! / eprintln! |
Writes to stderr/stdout — immediate TUI corruption. |
| SSH server |
(none — OS daemon) |
russh / thrussh |
Out of scope per project decisions. The binary is a login shell, not an SSH server. Do not add embedded SSH. |
Installation
Version pinning note: All versions above for new additions (pulldown-cmark, notify, notify-debouncer-full, toml, tracing*) should be verified against crates.io before committing. The train cutoff for this research is August 2025; patch versions may have advanced. Use cargo add [crate] to get the latest compatible version recommended by Cargo.
Sources
| Claim |
Confidence |
Basis |
| ratatui 0.30.0 with sub-crate architecture |
HIGH |
Cargo.lock direct observation |
| crossterm 0.29.0 as backend |
HIGH |
Cargo.lock direct observation |
| anyhow 1.0.102, thiserror 2.0.18 |
HIGH |
Cargo.lock direct observation |
| pulldown-cmark is event-based, preferred for rendering |
HIGH |
Training knowledge — well-established in Rust ecosystem |
| pulldown-cmark ~0.12 as current major |
MEDIUM |
Training knowledge — verify on crates.io |
| notify 8.x as current major, kqueue/inotify backends |
MEDIUM |
Training knowledge — verify on crates.io |
| notify-debouncer-full 0.4 |
MEDIUM |
Training knowledge — verify on crates.io |
| toml 0.8 stable |
HIGH |
Training knowledge — used by Cargo itself |
| tracing/tracing-subscriber/tracing-appender 0.1/0.3/0.2 |
HIGH |
Training knowledge — standard Rust logging ecosystem |
| tokio not needed |
HIGH |
Architectural analysis — no async I/O in hot path |
| Wiki-link regex pre-pass pattern |
HIGH |
Architectural analysis — CommonMark spec does not include wiki-links |
Key Constraints for Downstream Phases
-
No tokio. Thread-based concurrency with std::sync::mpsc only. This keeps the event loop simple and avoids async executor confusion.
-
ratatui 0.30 API uses the new sub-crate structure. Imports come from ratatui::prelude::* and ratatui::widgets::*. The split into ratatui-core/ratatui-widgets happened in 0.29 — do not follow pre-0.29 tutorials.
-
Rust edition 2024 is valid. The CONCERNS.md flags this as invalid, but Rust 1.85 (February 2025) stabilized edition 2024. This is not a bug. No change needed.
-
Log to file, never to stderr. Any eprintln!, println!, or logger that writes to a terminal fd will corrupt the ratatui-rendered output in the connected SSH session.
-
pulldown-cmark features must be explicit. With default-features = false, you opt into only what you need. Tables and footnotes are the relevant extras for an Obsidian-style vault.