--- phase: 04-bbs-polish-and-live-content plan: 02 type: execute wave: 2 depends_on: - 04-01 files_modified: - src/vault.rs - src/app.rs - src/main.rs autonomous: true requirements: - LIVE-02 must_haves: truths: - "User can follow a [[Directory]] wiki-link to see a tree view of all vault documents" - "Directory listing shows folder hierarchy with indentation, directories in yellow bold, files as cyan navigable links" - "User can Tab-cycle between directory entries and press Enter to open a document" - "Directory page participates in navigation history (back/forward works)" - "Navigating to [[Directory]] and back preserves scroll position and link selection" artifacts: - path: "src/vault.rs" provides: "[[Directory]] magic interception in resolve_wiki_link + list_vault_files() function" contains: "__directory__" - path: "src/app.rs" provides: "navigate_to_directory() method, DocumentState handling for virtual directory page" contains: "navigate_to_directory" key_links: - from: "src/vault.rs" to: "walkdir crate" via: "list_vault_files() using WalkDir recursive traversal" pattern: "WalkDir::new" - from: "src/app.rs" to: "src/vault.rs" via: "follow_selected_link detects __directory__ sentinel and calls navigate_to_directory" pattern: "__directory__" - from: "src/app.rs" to: "src/vault.rs" via: "navigate_to_directory calls list_vault_files to build the listing" pattern: "list_vault_files" --- Add a virtual directory listing page accessible via `[[Directory]]` wiki-link, displaying all vault documents in a navigable tree view. Purpose: Users can discover all available documents in the vault without knowing their paths. The directory listing acts as a table of contents for the entire vault. Output: Updated `src/vault.rs` with directory interception and file listing, updated `src/app.rs` with directory page navigation and rendering. @/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 @.planning/phases/04-bbs-polish-and-live-content/04-01-SUMMARY.md @src/vault.rs @src/app.rs @src/renderer.rs Task 1: Add [[Directory]] interception and vault file listing src/vault.rs 1. In `resolve_wiki_link()`, add a magic sentinel check BEFORE the filesystem scan: ```rust // Magic sentinel — [[Directory]] navigates to the virtual directory page if raw_target.eq_ignore_ascii_case("directory") { return Some(PathBuf::from("__directory__")); } ``` Place this at the very top of `resolve_wiki_link()`, before the `rfind('/')` split. 2. Add `list_vault_files()` function to `vault.rs`: ```rust use walkdir::WalkDir; /// Entry in the vault directory listing. pub struct DirEntry { /// Indentation depth (1-based, depth 1 = vault root children) pub depth: usize, /// Display name (without .md extension for files) pub name: String, /// True if this entry is a directory pub is_dir: bool, /// Vault-relative path (only for .md files; None for directories) pub vault_path: Option, } /// List all markdown files and directories in the vault, sorted alphabetically. /// Skips hidden files/directories (starting with '.') and non-.md files. /// Returns entries with depth for tree indentation. pub fn list_vault_files(vault_path: &Path) -> Vec { let mut entries = Vec::new(); for entry in WalkDir::new(vault_path) .sort_by_file_name() .into_iter() .filter_map(|e| e.ok()) .skip(1) // skip vault root itself { let depth = entry.depth(); let name = entry.file_name().to_string_lossy().to_string(); // Skip hidden files/dirs if name.starts_with('.') { continue; } let is_dir = entry.file_type().is_dir(); if is_dir { entries.push(DirEntry { depth, name, is_dir: true, vault_path: None, }); } else if name.ends_with(".md") { let display_name = name.strip_suffix(".md").unwrap_or(&name).to_string(); let rel = entry.path() .strip_prefix(vault_path) .unwrap_or(entry.path()) .to_string_lossy() .to_string(); entries.push(DirEntry { depth, name: display_name, is_dir: false, vault_path: Some(rel), }); } // Skip non-.md files silently } entries } ``` 3. Run `cargo build` to verify vault.rs compiles. `cargo build` succeeds. `resolve_wiki_link()` returns `Some(PathBuf::from("__directory__"))` when called with "Directory" (case-insensitive). `list_vault_files()` is defined and exported. [[Directory]] wiki-link resolves to the `__directory__` sentinel. list_vault_files() returns structured directory entries with depth, name, is_dir, and vault_path. Task 2: Wire directory as virtual page with Tab-cycling navigation src/app.rs, src/main.rs 1. Add a `navigate_to_directory()` method to `App`: ```rust /// Navigate to the virtual directory listing page. /// Generates a tree view of all vault files as styled lines with link records. fn navigate_to_directory(&mut self) { let vault_path = self.config.vault_path.clone(); // Save current state to history if let Some(entry) = self.history.get_mut(self.history_index) { entry.scroll_offset = self.scroll_offset; entry.selected_link = self.selected_link; } // Truncate forward history self.history.truncate(self.history_index + 1); // Build directory listing let dir_entries = crate::vault::list_vault_files(&vault_path); let (lines, link_records) = build_directory_lines(&dir_entries); self.document = DocumentState::Loaded { filename: "[Directory]".to_string(), lines, }; self.raw_content = None; // No raw markdown for virtual pages self.link_records = link_records; self.selected_link = None; self.scroll_offset = 0; self.current_path = "__directory__".to_string(); self.filename = "[Directory]".to_string(); self.page_meta = None; // No file metadata for virtual pages // Push history entry self.history.push(HistoryEntry { path: "__directory__".to_string(), scroll_offset: 0, selected_link: None, }); self.history_index = self.history.len() - 1; } ``` 2. Create the `build_directory_lines()` helper function (module-level in app.rs): ```rust /// Build styled lines and link records from vault directory entries. fn build_directory_lines( entries: &[crate::vault::DirEntry], ) -> (Vec>, Vec) { let mut lines: Vec> = Vec::new(); let mut link_records: Vec = Vec::new(); // Title header lines.push(Line::from(Span::styled( " VAULT DIRECTORY".to_string(), Style::default().fg(Color::LightCyan).add_modifier(Modifier::BOLD), ))); lines.push(Line::from(Span::styled( "=".repeat(40), Style::default().fg(Color::LightCyan), ))); lines.push(Line::default()); // blank separator for entry in entries { let indent = " ".repeat(entry.depth.saturating_sub(1)); let indent_chars = indent.chars().count(); if entry.is_dir { // Directory: yellow bold, not a link lines.push(Line::from(vec![ Span::raw(indent), Span::styled( format!("{}/", entry.name), Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD), ), ])); } else if let Some(ref vault_rel) = entry.vault_path { // File: cyan link — navigable via Tab-cycling let display = format!("[{}]", entry.name); let col_offset = indent_chars; let span_len = display.chars().count(); let line_index = lines.len(); // Will be this line's index lines.push(Line::from(vec![ Span::raw(indent), Span::styled( display, Style::default().fg(Color::LightCyan), ), ])); link_records.push(crate::renderer::LinkRecord { line_index, col_offset, span_len, dest: vault_rel.clone(), is_wiki: false, // Direct vault-relative path, not a wiki-link }); } } (lines, link_records) } ``` 3. Update `follow_selected_link()` in app.rs: - After resolving a wiki-link with `resolve_wiki_link()`, check if the resolved path is the directory sentinel: ```rust // Inside the is_wiki branch, after resolve_wiki_link returns Some: if resolved == Path::new("__directory__") { self.navigate_to_directory(); } else { let rel = resolved.to_string_lossy().to_string(); self.navigate_to(&rel); } ``` - For the non-wiki branch (standard links), if `dest == "__directory__"`, call `navigate_to_directory()` instead. 4. Update `navigate_back()` and `navigate_forward()`: - When the `target_path` is `"__directory__"`, call `navigate_to_directory()`-style logic instead of `load_document()`. Specifically: ```rust if target_path == "__directory__" { // Regenerate directory listing (always fresh, never cached) let dir_entries = crate::vault::list_vault_files(&vault_path); let (lines, link_records) = build_directory_lines(&dir_entries); self.document = DocumentState::Loaded { filename: "[Directory]".to_string(), lines, }; self.raw_content = None; self.link_records = link_records; self.selected_link = target_link; self.scroll_offset = target_scroll; self.current_path = "__directory__".to_string(); self.filename = "[Directory]".to_string(); self.page_meta = None; } else { // existing load_document() logic } ``` - IMPORTANT: Do NOT save/push new history entries in navigate_back/forward — they restore existing history positions. Only update current state fields. The existing history entry management (saving scroll/link to current index, then moving index) remains unchanged. 5. Update `handle_resize()`: - If `current_path == "__directory__"`, skip the `render_markdown` re-render. The directory listing is not markdown and does not need width-based re-render. The tree view spans are fixed-width text. Do nothing for directory pages on resize. 6. Run `cargo build` to verify everything compiles. `cargo build` succeeds. The follow_selected_link() method correctly routes `__directory__` to navigate_to_directory(). navigate_back/forward handle the `__directory__` case. handle_resize skips re-render for directory pages. [[Directory]] wiki-link navigates to a tree view page. Tab-cycling selects directory entries. Enter opens the selected document. Back/Forward navigates through directory page in history. Directory listing is always regenerated fresh (never stale). 1. `cargo build` — compiles without errors 2. Add `[[Directory]]` link to index.md in a test vault — following it shows tree view 3. Tree view shows folders (yellow bold with `/`) and files (cyan `[name]` bracketed) 4. Tab-cycle between file entries — REVERSED highlight moves correctly 5. Press Enter on a file entry — navigates to that document 6. Press Backspace — returns to directory listing with scroll/link selection preserved 7. Navigate forward — returns to the document 8. Case-insensitive: `[[directory]]` and `[[DIRECTORY]]` both work - [[Directory]] wiki-link navigates to virtual tree view page - Tree shows hierarchy with indentation, dirs yellow bold, files cyan bracketed links - Tab-cycling works on directory entries using existing link model - Directory participates in back/forward history - Directory listing is regenerated each visit (not stale) - handle_resize does not crash on directory page After completion, create `.planning/phases/04-bbs-polish-and-live-content/04-02-SUMMARY.md`