Files
2026-02-28 23:41:11 +01:00

231 lines
9.8 KiB
Markdown

---
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<Line<'static>>"
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<u8>"
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"
---
<objective>
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).
</objective>
<execution_context>
@/Users/ruohki/.claude/get-shit-done/workflows/execute-plan.md
@/Users/ruohki/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
</context>
<tasks>
<task type="auto">
<name>Task 1: Add Phase 4 dependencies and create splash + metadata modules</name>
<files>Cargo.toml, src/splash.rs, src/main.rs</files>
<action>
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<Vec<Line<'static>>> {
let bytes: Vec<u8> = 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.
</action>
<verify>`cargo build` succeeds. `cargo tree | grep ratatui-core` shows exactly one version. `src/splash.rs` exists.</verify>
<done>All three Phase 4 crate dependencies compile. splash.rs provides load_splash(). Module is declared in main.rs.</done>
</task>
<task type="auto">
<name>Task 2: Wire splash prepend on index and add file metadata to status bar</name>
<files>src/app.rs, src/main.rs</files>
<action>
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<PageMeta> {
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<PageMeta>` 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.
</action>
<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.</verify>
<done>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.</done>
</task>
</tasks>
<verification>
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
</verification>
<success_criteria>
- 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)
</success_criteria>
<output>
After completion, create `.planning/phases/04-bbs-polish-and-live-content/04-01-SUMMARY.md`
</output>