231 lines
9.8 KiB
Markdown
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>
|