feat(quick-1): add arrow key navigation and directory descriptions

- Add bare Left arrow key as alias for navigate_back (same as Backspace)
- Add bare Right arrow key as alias for navigate_forward (same as Alt+Right)
- Update handle_key doc comment to list new Left/Right bindings
- Add description field to DirEntry struct in vault.rs
- Add extract_frontmatter_description() to parse YAML frontmatter
- Populate description from frontmatter in list_vault_files()
- Show descriptions in DarkGray beside file entries in build_directory_lines()
- Truncate descriptions to keep total line width within 78 chars
- Link span_len covers only [name] portion, not the description
This commit is contained in:
2026-03-01 11:38:50 +01:00
parent 67509c3863
commit f0ec2edc53
2 changed files with 102 additions and 7 deletions
+42 -6
View File
@@ -450,6 +450,8 @@ impl App {
/// - `Backspace` — navigate back in history /// - `Backspace` — navigate back in history
/// - `Alt+Left` — navigate back in history /// - `Alt+Left` — navigate back in history
/// - `Alt+Right` — navigate forward in history /// - `Alt+Right` — navigate forward in history
/// - `Left` — navigate back in history (same as Backspace)
/// - `Right` — navigate forward in history (same as Alt+Right)
/// - `j` / `Down` — scroll down one line /// - `j` / `Down` — scroll down one line
/// - `k` / `Up` — scroll up one line /// - `k` / `Up` — scroll up one line
/// - `PgDn` — scroll down one page /// - `PgDn` — scroll down one page
@@ -498,6 +500,13 @@ impl App {
KeyCode::Right if key.modifiers.contains(KeyModifiers::ALT) => { KeyCode::Right if key.modifiers.contains(KeyModifiers::ALT) => {
self.navigate_forward(); self.navigate_forward();
} }
// Bare Left/Right arrows also navigate back/forward
KeyCode::Left => {
self.navigate_back();
}
KeyCode::Right => {
self.navigate_forward();
}
// ── Scrolling keys — do NOT dismiss the quit prompt ─────────────── // ── Scrolling keys — do NOT dismiss the quit prompt ───────────────
KeyCode::Char('j') | KeyCode::Down => { KeyCode::Char('j') | KeyCode::Down => {
self.scroll_down(1); self.scroll_down(1);
@@ -1362,13 +1371,40 @@ fn build_directory_lines(
let span_len = display.chars().count(); let span_len = display.chars().count();
let line_index = lines.len(); // This line's index in the lines vec let line_index = lines.len(); // This line's index in the lines vec
lines.push(Line::from(vec![ // Build spans: indent + [name] + optional description in DarkGray
let mut spans = vec![
Span::raw(indent), Span::raw(indent),
Span::styled( Span::styled(display.clone(), Style::default().fg(Color::LightCyan)),
display, ];
Style::default().fg(Color::LightCyan),
), if let Some(ref desc) = entry.description {
])); // Truncate description so total line width stays within ~78 chars
// Total used: indent_chars + span_len + 2 (separator) + desc
let budget = 78usize
.saturating_sub(indent_chars)
.saturating_sub(span_len)
.saturating_sub(2); // 2 spaces separator
let truncated = if desc.chars().count() > budget {
if budget > 3 {
let trimmed: String = desc.chars().take(budget - 3).collect();
format!("{}...", trimmed)
} else {
String::new()
}
} else {
desc.clone()
};
if !truncated.is_empty() {
spans.push(Span::raw(" "));
spans.push(Span::styled(
truncated,
Style::default().fg(Color::DarkGray),
));
}
}
lines.push(Line::from(spans));
link_records.push(crate::renderer::LinkRecord { link_records.push(crate::renderer::LinkRecord {
line_index, line_index,
+60 -1
View File
@@ -1,4 +1,4 @@
use std::io; use std::io::{self, BufRead};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use walkdir::WalkDir; use walkdir::WalkDir;
@@ -201,6 +201,62 @@ pub struct DirEntry {
pub is_dir: bool, pub is_dir: bool,
/// Vault-relative path (only for .md files; None for directories). /// Vault-relative path (only for .md files; None for directories).
pub vault_path: Option<String>, pub vault_path: Option<String>,
/// Short description from the file's YAML frontmatter `description:` field.
/// None if the file has no frontmatter or no description key.
pub description: Option<String>,
}
// ── Frontmatter parsing ───────────────────────────────────────────────────────
/// Extract the `description` value from a YAML frontmatter block at the top of a file.
///
/// Reads only the first 20 lines to avoid loading large files entirely.
/// The frontmatter must start with `---` on the first line and be closed by
/// another `---` line. The `description:` key is matched case-sensitively.
///
/// Handles both bare values (`description: some text`) and quoted values
/// (`description: "some text"`).
///
/// Returns `Some(description)` if found, `None` otherwise.
fn extract_frontmatter_description(path: &Path) -> Option<String> {
let file = std::fs::File::open(path).ok()?;
let reader = io::BufReader::new(file);
let mut lines = reader.lines().take(20);
// First line must be exactly "---"
let first = lines.next()?.ok()?;
if first.trim() != "---" {
return None;
}
// Scan remaining lines for description key, stop at closing "---"
for line in lines {
let line = line.ok()?;
let trimmed = line.trim();
if trimmed == "---" {
// End of frontmatter — description not found
break;
}
if let Some(rest) = trimmed.strip_prefix("description:") {
let value = rest.trim();
// Strip surrounding quotes if present
let value = if (value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\''))
{
&value[1..value.len() - 1]
} else {
value
};
if !value.is_empty() {
return Some(value.to_string());
}
}
}
None
} }
/// List all markdown files and directories in the vault, sorted alphabetically. /// List all markdown files and directories in the vault, sorted alphabetically.
@@ -231,6 +287,7 @@ pub fn list_vault_files(vault_path: &Path) -> Vec<DirEntry> {
name, name,
is_dir: true, is_dir: true,
vault_path: None, vault_path: None,
description: None,
}); });
} else if name.ends_with(".md") { } else if name.ends_with(".md") {
let display_name = name.strip_suffix(".md").unwrap_or(&name).to_string(); let display_name = name.strip_suffix(".md").unwrap_or(&name).to_string();
@@ -240,11 +297,13 @@ pub fn list_vault_files(vault_path: &Path) -> Vec<DirEntry> {
.unwrap_or(entry.path()) .unwrap_or(entry.path())
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string();
let description = extract_frontmatter_description(entry.path());
entries.push(DirEntry { entries.push(DirEntry {
depth, depth,
name: display_name, name: display_name,
is_dir: false, is_dir: false,
vault_path: Some(rel), vault_path: Some(rel),
description,
}); });
} }
// Skip non-.md files silently // Skip non-.md files silently