--- phase: 04-bbs-polish-and-live-content plan: 01 type: execute wave: 1 depends_on: [] files_modified: - Cargo.toml - src/splash.rs - src/app.rs - src/main.rs autonomous: true requirements: - BBS-01 - BBS-02 must_haves: truths: - "When splash.txt exists in vault root, index.md page displays ANSI art header above the markdown content" - "When splash.txt is missing, index.md renders normally with no error or fallback banner" - "Every loaded page shows 'Last modified: Mon DD, YYYY | X.X KB' in the right side of the status bar" - "Status bar left side still shows breadcrumb, right side now shows metadata + keyboard hints" artifacts: - path: "src/splash.rs" provides: "load_splash() function parsing ANSI art from splash.txt into Vec>" min_lines: 10 - path: "src/app.rs" provides: "PageMeta struct, read_page_meta(), status bar metadata display, splash prepend logic" contains: "PageMeta" - path: "Cargo.toml" provides: "ansi-to-tui, notify, walkdir dependencies" contains: "ansi-to-tui" key_links: - from: "src/splash.rs" to: "ansi-to-tui crate" via: "IntoText trait on Vec" pattern: "into_text" - from: "src/app.rs" to: "src/splash.rs" via: "splash::load_splash() called during navigate_to for index.md" pattern: "load_splash" - from: "src/app.rs" to: "std::fs::metadata" via: "read_page_meta() reading mtime and size" pattern: "read_page_meta" --- Add ANSI art splash screen support and file metadata display to complete the BBS aesthetic. Purpose: The splash screen makes the landing page feel like a real BBS with colorful ANSI art. The file metadata (last-modified timestamp and file size) in the status bar gives users context about content freshness. Output: `src/splash.rs` module, updated `src/app.rs` with PageMeta and splash prepend, updated `Cargo.toml` with all Phase 4 dependencies (ansi-to-tui, notify, walkdir — added now so later plans don't need to touch Cargo.toml). @/Users/ruohki/.claude/get-shit-done/workflows/execute-plan.md @/Users/ruohki/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/04-bbs-polish-and-live-content/04-RESEARCH.md @src/app.rs @src/main.rs @Cargo.toml Task 1: Add Phase 4 dependencies and create splash + metadata modules Cargo.toml, src/splash.rs, src/main.rs 1. Add all three Phase 4 dependencies to Cargo.toml: ```toml notify = "6.1" ansi-to-tui = "8.0" walkdir = "2.5" ``` 2. Run `cargo tree | grep ratatui-core` to verify ansi-to-tui resolves to a single ratatui-core version. If two versions appear, add `ratatui-core = "0.1"` to force unification. 3. Create `src/splash.rs` with: ```rust use std::path::Path; use ansi_to_tui::IntoText; use ratatui::text::Line; /// Load ANSI art from `splash.txt` in the vault root. /// Returns None if file doesn't exist or can't be parsed (graceful degradation). pub fn load_splash(vault_path: &Path) -> Option>> { let bytes: Vec = std::fs::read(vault_path.join("splash.txt")).ok()?; let text = bytes.into_text().ok()?; Some(text.lines) } ``` 4. Add `mod splash;` to `src/main.rs` module declarations (after `mod signals;`). 5. Run `cargo build` to verify compilation with new dependencies. `cargo build` succeeds. `cargo tree | grep ratatui-core` shows exactly one version. `src/splash.rs` exists. All three Phase 4 crate dependencies compile. splash.rs provides load_splash(). Module is declared in main.rs. Task 2: Wire splash prepend on index and add file metadata to status bar src/app.rs, src/main.rs 1. Add file metadata types and helpers to `src/app.rs` (near the top, after use statements): ```rust use std::time::UNIX_EPOCH; /// File metadata for status bar display. struct PageMeta { modified: String, // "Feb 25, 2026" size: String, // "2.4 KB" } fn read_page_meta(full_path: &Path) -> Option { let meta = std::fs::metadata(full_path).ok()?; let secs = meta.modified().ok()?.duration_since(UNIX_EPOCH).ok()?.as_secs(); let (y, m, d) = unix_secs_to_ymd(secs); let months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; Some(PageMeta { modified: format!("{} {}, {}", months[(m-1) as usize], d, y), size: format_file_size(meta.len()), }) } fn unix_secs_to_ymd(secs: u64) -> (u32, u32, u32) { let mut days = (secs / 86400) as u32; let mut year = 1970u32; loop { let days_in_year = if is_leap(year) { 366 } else { 365 }; if days < days_in_year { break; } days -= days_in_year; year += 1; } let leap = is_leap(year); let month_days: [u32; 12] = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; let mut month = 0u32; for (i, &d) in month_days.iter().enumerate() { if days < d { month = i as u32 + 1; break; } days -= d; } (year, month, days + 1) } fn is_leap(year: u32) -> bool { (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) } fn format_file_size(bytes: u64) -> String { match bytes { 0..=1023 => format!("{} B", bytes), 1024..=1_048_575 => format!("{:.1} KB", bytes as f64 / 1024.0), _ => format!("{:.1} MB", bytes as f64 / 1_048_576.0), } } ``` 2. Add `page_meta: Option` field to the `App` struct (Phase 4 additions section). Initialize to `None` in `App::new()`. 3. Update `navigate_to()` — after loading a document successfully (inside the `VaultDocument::Loaded` match arm): - Compute full path: `let full_path = vault_path.join(vault_relative);` - Read metadata: `self.page_meta = read_page_meta(&full_path);` - Check if this is the index page and prepend splash if so: ```rust if vault_relative == "index.md" { if let Some(mut splash_lines) = crate::splash::load_splash(&vault_path) { splash_lines.push(Line::default()); // blank separator splash_lines.extend(lines); lines = splash_lines; // Adjust link records: offset all line_index values by the splash line count // splash_count = splash_lines_original_len + 1 (for blank separator) } } ``` - IMPORTANT: When splash lines are prepended, all `link_records` line_index values must be offset by the number of splash lines added. Store the splash line count and add it to each record's `line_index`. - For Missing/ReadError arms: set `self.page_meta = None;` 4. Apply the same splash prepend logic in `navigate_back()` and `navigate_forward()` — when the target path is "index.md", prepend splash lines and adjust link_record line indices. Also set page_meta via `read_page_meta()`. 5. Apply splash + metadata in `main.rs` initial load too — after rendering index.md, prepend splash lines if available and adjust link_records, then compute initial page_meta. 6. Update `handle_resize()`: when re-rendering, if `current_path == "index.md"`, prepend splash lines again and adjust link_records. Also note: splash lines are NOT re-rendered through render_markdown — they come from `load_splash()` directly and are prepended after the markdown render. 7. Update `draw_status_bar()` to include metadata on the right side: - In the normal (non-quit-prompt) branch, before the keyboard hints section: - If `self.page_meta` is `Some(meta)`: - Insert `format!("Last modified: {} | {}", meta.modified, meta.size)` into `right_parts` (before the hints entry). - Implement graceful truncation: if `left.len() + right.len() >= width`, progressively drop metadata fields: - First drop file size (show only "Last modified: ..."). - Then drop the entire metadata line. - Then abbreviate keyboard hints if still too wide. - Use `width.saturating_sub()` to prevent underflow. 8. Run `cargo build` to verify. `cargo build` succeeds. Status bar shows metadata when a document is loaded (visible on next run with a vault). Splash prepend logic handles both presence and absence of splash.txt. index.md shows splash art header when splash.txt exists. All loaded pages show "Last modified: Mon DD, YYYY | X.X KB" in status bar. Status bar gracefully truncates on narrow terminals. 1. `cargo build` — compiles without errors or warnings 2. Run with a vault containing `splash.txt` (ANSI art) — index.md shows art header above content 3. Run with a vault without `splash.txt` — index.md renders normally, no error 4. Check status bar — right side shows "Last modified: ... | ... KB" followed by keyboard hints 5. Resize terminal to <60 cols — status bar does not panic or wrap, metadata fields drop gracefully 6. Navigate to a different page and back to index — splash reappears, metadata updates - splash.txt ANSI art renders above index.md content with correct colors via ansi-to-tui - Missing splash.txt causes no error — graceful degradation to normal index.md - Every loaded document page shows BBS-format timestamp and file size in status bar - Status bar layout: left=breadcrumb, right=metadata+hints, with graceful truncation - Link Tab-cycling still works correctly on index.md when splash lines are prepended (line_index offsets are correct) After completion, create `.planning/phases/04-bbs-polish-and-live-content/04-01-SUMMARY.md`