13 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-vault-core-and-rendering | 03 | execute | 3 |
|
|
true |
|
|
Purpose: This is the integration plan that turns the separate modules (vault, renderer, highlighter) into a working content viewer.
Output: Running cargo run displays index.md from the vault with full markdown styling, scrolling, and a status bar.
<execution_context> @/Users/ruohki/.claude/get-shit-done/workflows/execute-plan.md @/Users/ruohki/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-vault-core-and-rendering/02-RESEARCH.md @.planning/phases/02-vault-core-and-rendering/02-CONTEXT.md @.planning/phases/02-vault-core-and-rendering/02-01-SUMMARY.md @.planning/phases/02-vault-core-and-rendering/02-02-SUMMARY.md @src/app.rs @src/main.rs Task 1: Rework app.rs with document state, scrolling, status bar, and error screen src/app.rs **Extend the App struct** — add new fields while preserving ALL Phase 1 fields and behavior:pub struct App {
// Phase 1 fields (keep exactly as-is)
is_login_shell: bool,
ctrl_c_pressed_at: Option<Instant>,
show_quit_prompt: bool,
should_quit: bool,
config: Config,
// Phase 2 additions
document: DocumentState,
scroll_offset: u16,
}
Add DocumentState enum:
pub enum DocumentState {
Loaded {
filename: String,
lines: Vec<Line<'static>>,
},
Missing {
path: PathBuf,
},
Error {
path: PathBuf,
reason: String,
},
}
Update App::new() to accept the initial document state:
Change signature to pub fn new(is_login_shell: bool, config: Config, document: DocumentState) -> Self with scroll_offset: 0.
Rework draw() — replace the Phase 1 placeholder UI entirely:
-
Split frame into two areas using Layout:
let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(0), Constraint::Length(1)]) .split(frame.area()); let content_area = chunks[0]; let status_area = chunks[1]; -
Content area rendering — match on
self.document:DocumentState::Loaded { filename, lines }:- Create
Paragraph::new(lines.clone()).scroll((self.scroll_offset, 0)) - Do NOT enable Wrap — lines are pre-rendered
- Render into
content_area
- Create
DocumentState::Missing { path }:- Call
draw_error_screen(frame, content_area, path)(see below)
- Call
DocumentState::Error { path, reason }:- Call
draw_error_screen_with_reason(frame, content_area, path, reason)
- Call
-
Status bar rendering (NAV-08):
- Determine filename text: from DocumentState::Loaded filename, or "ERROR" for missing/error states
- Determine hints:
"q:Quit j/k:Scroll PgUp/PgDn:Page"(if not login shell), or"Ctrl+C×2:Quit j/k:Scroll PgUp/PgDn:Page"(if login shell) - Build the status bar as a single Line with the filename left-aligned and hints right-aligned:
- Calculate padding to fill the full status_area.width between filename and hints
- Style:
Style::default().add_modifier(Modifier::REVERSED)on the entire Paragraph
- Render
Paragraph::new(status_line).style(Style::default().add_modifier(Modifier::REVERSED))intostatus_area
-
Quit prompt overlay — if
self.show_quit_promptis true, render the "Press Ctrl+C again..." message. Options:- Render it as part of the status bar (replace hints with the warning), OR
- Overlay it on the bottom of content_area
- Recommended: replace the status bar content with the quit prompt in yellow bold reverse video
Error screen widget (NAV-07):
Create a private method draw_error_screen(frame: &mut Frame, area: Rect, path: &Path):
Use ratatui's Block widget with BorderType::Plain (┌─┐│└─┘) borders:
┌─────────────────────────────────────────┐
│ *** SYSTEM ERROR *** │
│ │
│ No index.md found in vault: │
│ /path/to/vault │
│ │
│ Create index.md to begin. │
└─────────────────────────────────────────┘
- Block border:
Color::Red - "SYSTEM ERROR" text:
Color::LightRed + Modifier::BOLD - Path:
Color::Yellow - Hint:
Color::DarkGray - Center the block in the content area (calculate a centered Rect)
Also create draw_error_screen_with_reason() for ReadError states — same layout but shows the error reason instead of "No index.md found".
Extend handle_key() for scrolling (NAV-05):
Add new key bindings BEFORE the existing _ catch-all:
KeyCode::Char('j') | KeyCode::Down => {
self.scroll_down(1);
}
KeyCode::Char('k') | KeyCode::Up => {
self.scroll_up(1);
}
KeyCode::PageDown => {
self.scroll_down(self.page_height());
}
KeyCode::PageUp => {
self.scroll_up(self.page_height());
}
Helper methods:
scroll_down(n: u16):self.scroll_offset = self.scroll_offset.saturating_add(n).min(self.max_scroll())scroll_up(n: u16):self.scroll_offset = self.scroll_offset.saturating_sub(n)max_scroll() -> u16: if Loaded,lines.len() as u16 - page_height, else 0. Usesaturating_sub.page_height() -> u16: store the content area height from the last draw, or default to 24. Add alast_content_height: u16field to App, update it in draw().
IMPORTANT: The scroll keys (j/k/arrows/PgUp/PgDn) must NOT trigger the quit prompt dismissal. Currently the _ => branch dismisses the prompt. The scroll keys should be handled before the _ catch-all, and only dismiss the prompt for truly unrelated keys.
Resize handling (NAV-09):
- App needs to store the raw markdown content so it can re-render on resize. Add a
raw_content: Option<String>field to App (populated when document is loaded). - In
run_event_loop(), handleEvent::Resize(w, _h):- If
raw_contentis Some, re-render:let lines = renderer::render_markdown(&content, w); self.document = DocumentState::Loaded { filename, lines }; - Clamp
scroll_offsetto newmax_scroll()after re-render - ratatui handles buffer resize automatically for
Viewport::Fullscreen
- If
- This ensures horizontal rules, code block borders, and table widths adapt to the new terminal width
max_scroll()recomputes on every draw based onlast_content_height, so vertical scroll is handled naturally
Preserve ALL Phase 1 behavior:
- Double Ctrl+C quit mechanism
- Login shell mode suppressing 'q' key
show_goodbye()function unchangedShutdownReasonenum unchangedDOUBLE_PRESS_WINDOWunchangedcargo checkpasses. Mentally trace: App starts with DocumentState::Loaded, draw() shows content with status bar, j/k adjust scroll_offset, q still quits, Ctrl+C double-press still works. app.rs has DocumentState enum, scroll_offset field, status bar with filename+hints in reverse video, BBS error screen for missing files, j/k/arrow/PgUp/PgDn scroll keys, resize handling. All Phase 1 quit behavior preserved.
mod app;
mod config;
mod highlighter;
mod renderer;
mod signals;
mod terminal;
mod vault;
Update the startup sequence in fn main():
After step 3 (config loading) and before step 4 (panic hook), add:
// 3a. Initialize syntax highlighting (one-time, ~23ms)
highlighter::init_highlighter();
// 3b. Load initial document (index.md from vault)
let initial_doc = match vault::load_document(&app_config.vault_path, "index.md") {
vault::VaultDocument::Loaded { path, content } => {
// Get terminal width for rendering — use a reasonable default before terminal init
// We'll re-render if needed, but 80 is safe for initial parse
let width = ratatui::crossterm::terminal::size()
.map(|(w, _)| w)
.unwrap_or(80);
let filename = path.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "index.md".to_string());
let lines = renderer::render_markdown(&content, width);
app::DocumentState::Loaded { filename, lines }
}
vault::VaultDocument::Missing { path } => {
app::DocumentState::Missing { path }
}
vault::VaultDocument::ReadError { path, reason } => {
app::DocumentState::Error { path, reason }
}
};
Update App::new() call in step 7:
let mut app_state = app::App::new(is_login_shell, app_config, initial_doc);
Pass raw content to App for resize re-rendering:
When loading a Loaded document, also pass the raw markdown string to App so it can re-render on resize. Update App::new() signature to accept an optional raw content string, or store it alongside the DocumentState.
Remove the #[allow(dead_code)] on config field in app.rs if it's now used (vault_path is accessed). Actually, config is passed at construction but vault loading happens in main.rs, so config may still appear unused inside App. Keep the allow if needed, or add a method to access vault_path for future use.
cargo buildsucceeds- Create a test vault:
mkdir -p /tmp/bbs-test-vault && echo '# Welcome\n\nHello **world**!\n\n- Item one\n- Item two\n\n> A blockquote\n\n---\n\n```rust\nlet x = 42;\n```' > /tmp/bbs-test-vault/index.md - Run:
cargo run -- --config /dev/nullwith a bbs.toml pointing to the test vault (or modify the default path for testing) - Verify: content displays with colored headings, styled text, and a status bar at the bottom
- Verify: j/k scroll content, q exits cleanly
- Verify: without index.md, the BBS error screen appears
main.rs declares all 7 modules (app, config, highlighter, renderer, signals, terminal, vault). Startup sequence initializes highlighter, loads index.md via vault, renders via renderer, passes DocumentState to App. Running
cargo rundisplays styled markdown content with scrolling and status bar. Missing index.md shows error screen.
<success_criteria>
- Running
cargo rundisplays index.md from the configured vault with full markdown styling - Missing index.md shows BBS error screen instead of crashing
- Content scrolls smoothly with j/k/arrows/PgUp/PgDn
- Status bar visible at bottom with filename and keyboard hints
- All Phase 1 quit/safety behavior preserved
- Terminal resize handled gracefully </success_criteria>