feat(04-02): wire virtual directory listing page with history and Tab-cycling
- navigate_to_directory() builds tree view via list_vault_files() + build_directory_lines() - follow_selected_link() routes __directory__ sentinel to navigate_to_directory() - navigate_back/forward regenerate directory listing fresh when target_path == __directory__ - handle_resize() skips re-render for directory pages (fixed-width text, no markdown) - build_directory_lines() emits yellow-bold dirs and cyan bracketed file links with LinkRecord
This commit is contained in:
+148
-2
@@ -313,6 +313,11 @@ impl App {
|
||||
/// We re-render so horizontal rules, code block borders, and table widths adapt.
|
||||
/// On index.md, splash lines are re-prepended after the re-render.
|
||||
fn handle_resize(&mut self, new_width: u16) {
|
||||
// Directory listing is fixed-width text — no width-based re-render needed
|
||||
if self.current_path == "__directory__" {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(ref content) = self.raw_content.clone() {
|
||||
let vault_path = self.config.vault_path.clone();
|
||||
let (mut lines, mut link_records) = crate::renderer::render_markdown(
|
||||
@@ -524,6 +529,46 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Navigate to the virtual directory listing page.
|
||||
///
|
||||
/// Generates a tree view of all vault files as styled lines with link records.
|
||||
/// Saves current scroll/link state to history and pushes a new history entry.
|
||||
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;
|
||||
}
|
||||
|
||||
/// Navigate back one step in the history stack, restoring scroll and link selection.
|
||||
fn navigate_back(&mut self) {
|
||||
if self.history_index == 0 {
|
||||
@@ -543,7 +588,23 @@ impl App {
|
||||
|
||||
// Re-load and re-render the document (per research: don't cache rendered output)
|
||||
let vault_path = self.config.vault_path.clone();
|
||||
if let crate::vault::VaultDocument::Loaded { path, content } =
|
||||
|
||||
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 if let crate::vault::VaultDocument::Loaded { path, content } =
|
||||
crate::vault::load_document(&vault_path, &target_path)
|
||||
{
|
||||
let filename = path
|
||||
@@ -605,7 +666,23 @@ impl App {
|
||||
let target_link = self.history[self.history_index].selected_link;
|
||||
|
||||
let vault_path = self.config.vault_path.clone();
|
||||
if let crate::vault::VaultDocument::Loaded { path, content } =
|
||||
|
||||
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 if let crate::vault::VaultDocument::Loaded { path, content } =
|
||||
crate::vault::load_document(&vault_path, &target_path)
|
||||
{
|
||||
let filename = path
|
||||
@@ -664,9 +741,13 @@ impl App {
|
||||
// Resolve wiki-link to vault-relative path
|
||||
match crate::vault::resolve_wiki_link(&vault_path, &dest) {
|
||||
Some(resolved) => {
|
||||
if resolved == Path::new("__directory__") {
|
||||
self.navigate_to_directory();
|
||||
} else {
|
||||
let rel = resolved.to_string_lossy().to_string();
|
||||
self.navigate_to(&rel);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Broken wiki-link — already shown as red/strikethrough in render.
|
||||
// Do nothing on Enter for broken links.
|
||||
@@ -674,6 +755,9 @@ impl App {
|
||||
}
|
||||
} else {
|
||||
// Standard markdown link — resolve relative to current document's directory
|
||||
if dest == "__directory__" {
|
||||
self.navigate_to_directory();
|
||||
} else {
|
||||
match crate::vault::resolve_standard_link(&vault_path, &self.current_path, &dest) {
|
||||
Some(resolved) => {
|
||||
let rel = resolved.to_string_lossy().to_string();
|
||||
@@ -689,6 +773,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Link cycling helpers ───────────────────────────────────────────────────
|
||||
|
||||
@@ -1053,6 +1138,67 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Directory listing builder ─────────────────────────────────────────────────
|
||||
|
||||
/// 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(); // This line's index in the lines vec
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// ── Breadcrumb helper ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Build a breadcrumb trail from a vault-relative path.
|
||||
|
||||
Reference in New Issue
Block a user