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.
|
/// 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.
|
/// On index.md, splash lines are re-prepended after the re-render.
|
||||||
fn handle_resize(&mut self, new_width: u16) {
|
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() {
|
if let Some(ref content) = self.raw_content.clone() {
|
||||||
let vault_path = self.config.vault_path.clone();
|
let vault_path = self.config.vault_path.clone();
|
||||||
let (mut lines, mut link_records) = crate::renderer::render_markdown(
|
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.
|
/// Navigate back one step in the history stack, restoring scroll and link selection.
|
||||||
fn navigate_back(&mut self) {
|
fn navigate_back(&mut self) {
|
||||||
if self.history_index == 0 {
|
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)
|
// Re-load and re-render the document (per research: don't cache rendered output)
|
||||||
let vault_path = self.config.vault_path.clone();
|
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)
|
crate::vault::load_document(&vault_path, &target_path)
|
||||||
{
|
{
|
||||||
let filename = path
|
let filename = path
|
||||||
@@ -605,7 +666,23 @@ impl App {
|
|||||||
let target_link = self.history[self.history_index].selected_link;
|
let target_link = self.history[self.history_index].selected_link;
|
||||||
|
|
||||||
let vault_path = self.config.vault_path.clone();
|
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)
|
crate::vault::load_document(&vault_path, &target_path)
|
||||||
{
|
{
|
||||||
let filename = path
|
let filename = path
|
||||||
@@ -664,9 +741,13 @@ impl App {
|
|||||||
// Resolve wiki-link to vault-relative path
|
// Resolve wiki-link to vault-relative path
|
||||||
match crate::vault::resolve_wiki_link(&vault_path, &dest) {
|
match crate::vault::resolve_wiki_link(&vault_path, &dest) {
|
||||||
Some(resolved) => {
|
Some(resolved) => {
|
||||||
|
if resolved == Path::new("__directory__") {
|
||||||
|
self.navigate_to_directory();
|
||||||
|
} else {
|
||||||
let rel = resolved.to_string_lossy().to_string();
|
let rel = resolved.to_string_lossy().to_string();
|
||||||
self.navigate_to(&rel);
|
self.navigate_to(&rel);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
// Broken wiki-link — already shown as red/strikethrough in render.
|
// Broken wiki-link — already shown as red/strikethrough in render.
|
||||||
// Do nothing on Enter for broken links.
|
// Do nothing on Enter for broken links.
|
||||||
@@ -674,6 +755,9 @@ impl App {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Standard markdown link — resolve relative to current document's directory
|
// 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) {
|
match crate::vault::resolve_standard_link(&vault_path, &self.current_path, &dest) {
|
||||||
Some(resolved) => {
|
Some(resolved) => {
|
||||||
let rel = resolved.to_string_lossy().to_string();
|
let rel = resolved.to_string_lossy().to_string();
|
||||||
@@ -689,6 +773,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Link cycling helpers ───────────────────────────────────────────────────
|
// ── 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 ─────────────────────────────────────────────────────────
|
// ── Breadcrumb helper ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Build a breadcrumb trail from a vault-relative path.
|
/// Build a breadcrumb trail from a vault-relative path.
|
||||||
|
|||||||
Reference in New Issue
Block a user