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 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04-bbs-polish-and-live-content | 02 | execute | 2 |
|
|
true |
|
|
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.
<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/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.-
Add
list_vault_files()function tovault.rs: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<String>, } /// 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<DirEntry> { 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 } -
Run
cargo buildto verify vault.rs compiles.cargo buildsucceeds.resolve_wiki_link()returnsSome(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.
// 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<Line<'static>>, Vec<crate::renderer::LinkRecord>) {
let mut lines: Vec<Line<'static>> = Vec::new();
let mut link_records: Vec<crate::renderer::LinkRecord> = 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)
}
-
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:// 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__", callnavigate_to_directory()instead.
- After resolving a wiki-link with
-
Update
navigate_back()andnavigate_forward():- When the
target_pathis"__directory__", callnavigate_to_directory()-style logic instead ofload_document(). Specifically: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.
- When the
-
Update
handle_resize():- If
current_path == "__directory__", skip therender_markdownre-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.
- If
-
Run
cargo buildto verify everything compiles.cargo buildsucceeds. 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).
<success_criteria>
- 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 </success_criteria>