--- phase: 03-navigation-and-links plan: "02" subsystem: app tags: - navigation - history - link-cycling - breadcrumb - tab-navigation dependency_graph: requires: - 03-01 (LinkRecord from render_markdown, resolve_wiki_link, resolve_standard_link) - 02-vault-core-and-rendering (vault.rs load_document, DocumentState) provides: - Browser-style navigation history with scroll and link state restoration - Tab/Shift-Tab link cycling with wrap-around and auto-scroll - Enter to follow links (wiki-link and standard link resolution) - Backspace and Alt+Left/Right back/forward navigation - Draw-time REVERSED modifier for selected link highlighting - Breadcrumb status bar with back/forward indicators and link counter affects: - End-user experience (core interactive vault browsing) tech_stack: added: [] patterns: - "HistoryEntry snapshot pattern: path + scroll_offset + selected_link saved per navigation" - "Browser-style forward history truncation on navigate_to after going back" - "Draw-time mutation: lines.clone() + REVERSED applied only for current frame, stored lines unchanged" - "Auto-scroll centering: link_line.saturating_sub(half) with max_scroll clamp" - "Breadcrumb via Path::components() map with .md suffix strip and > join" key_files: modified: - path: src/app.rs role: "Added HistoryEntry struct, Phase 3 navigation fields on App, navigate_to/back/forward, follow_selected_link, select_next/prev_link, scroll_to_selected_link, draw-time REVERSED modifier, build_breadcrumb, updated draw_status_bar with breadcrumb and nav indicators" - path: src/main.rs role: "Updated initial render to capture link_records, pass link_records and current_path to App::new()" decisions: - "Task 1 and Task 2 implemented together in single app.rs rewrite — draw-time REVERSED and breadcrumb are part of the same cohesive navigation system" - "Double-REVERSED fix: spans overlapping link range get modifier via both col range check and overlap check" - "navigate_to does NOT push history on Missing/ReadError — user stays on current page conceptually" - "navigate_back/forward re-loads from disk (no cache) per research recommendation" metrics: duration_seconds: 198 completed_date: "2026-02-28" tasks_completed: 2 tasks_total: 2 files_modified: 2 --- # Phase 03 Plan 02: Navigation, History, Tab-Cycling, and Breadcrumb Status Bar Summary **One-liner:** Browser-style navigation history with Tab/Shift-Tab link cycling, Enter-to-follow, Backspace/Alt+arrows back/forward, draw-time REVERSED link highlight, and breadcrumb status bar wired into the App event loop. ## What Was Built ### Task 1: Navigation state, history, link cycling, and navigate_to (commit d705313) Implemented the complete navigation system in `src/app.rs` and updated `src/main.rs`. **`HistoryEntry` struct** (new, in `src/app.rs`): ```rust struct HistoryEntry { path: String, // vault-relative path scroll_offset: u16, // scroll position at time of navigation away selected_link: Option, // selected link index at time of navigation } ``` **Phase 3 fields added to `App`:** - `history: Vec` — browser-style navigation stack - `history_index: usize` — current position in history - `link_records: Vec` — links from current render - `selected_link: Option` — currently highlighted link - `current_path: String` — vault-relative path for breadcrumb and link resolution **`App::new()` signature updated:** Now accepts `link_records: Vec` and `current_path: String`. Initializes `history` with one entry for the initial page at scroll 0, no selected link. **Navigation methods:** - `navigate_to(vault_relative)`: Saves current state to history, truncates forward history (browser fork), loads + renders new document, pushes new history entry. On Missing/ReadError, shows error screen without pushing to history. - `navigate_back()`: Decrements `history_index`, saves current scroll/link state, re-loads and re-renders target document with restored scroll and link selection. - `navigate_forward()`: Increments `history_index`, same re-load pattern as navigate_back. - `follow_selected_link()`: Reads `link_records[selected_link]`, dispatches to `resolve_wiki_link` (for wiki-links) or `resolve_standard_link` (for standard links), then calls `navigate_to`. - `select_next_link()` / `select_prev_link()`: Cycle with wrap-around (`% len`), then call `scroll_to_selected_link()`. - `scroll_to_selected_link()`: If selected link is outside viewport, centers it: `link_line.saturating_sub(half)`, clamped to `max_scroll()`. **Key bindings added** (in `handle_key`, after 'q', before scroll keys): ``` Tab → select_next_link() BackTab → select_prev_link() Enter → follow_selected_link() Backspace → navigate_back() Alt+Left → navigate_back() Alt+Right → navigate_forward() ``` **`handle_resize()` updated**: Now captures `link_records` from re-render, validates `selected_link` still in range, uses `Some(&self.config.vault_path)` for render-time broken wiki-link detection. **`main.rs` updated**: - `render_markdown` call now passes `Some(&app_config.vault_path)` for broken wiki-link detection at startup - Destructures `(lines, link_records)` from render result - `App::new()` call includes `initial_link_records` and `"index.md".to_string()` ### Task 2: Draw-time link selection and breadcrumb status bar (included in d705313) Both draw-time features were implemented together with Task 1 in the single coherent rewrite. **Draw-time REVERSED modifier** (in `draw()` → `DocumentState::Loaded` branch): - Calls `lines.clone()` to get a mutable copy for the current frame only (stored lines unchanged) - Walks `line.spans.iter_mut()`, tracking cumulative `col` (chars-count) - Applies `Modifier::REVERSED` to any span whose column range overlaps `[record.col_offset, record.col_offset + record.span_len)` - Uses both exact-range and overlap checks to handle multi-span link brackets **`build_breadcrumb(vault_relative)` helper** (private fn, bottom of app.rs): - Iterates `Path::components()`, strips `.md` suffix from each component - Joins with ` > ` separator - Examples: `"index.md"` → `"index"`, `"guides/getting-started.md"` → `"guides > getting-started"` **`draw_status_bar()` rewritten**: - Left side: ` {breadcrumb} ` from `build_breadcrumb(&self.current_path)` - Quit prompt mode: breadcrumb left + yellow bold REVERSED warning right (existing behavior preserved) - Normal mode right side (space-separated): - `< Back` if `history_index > 0` - `Link N/M` if `selected_link` is Some - `Forward >` if `history_index < history.len() - 1` - Keyboard hints: `Tab:Links Enter:Go Bksp:Back q:Quit` (or Ctrl+C×2 for login shell) - `#[allow(dead_code)]` removed from `config` field (now actively used) ## Verification Results 1. `cargo build` succeeds — zero errors, 2 pre-existing warnings (init_logging and is_within_vault — unrelated to this plan) 2. All required functions exist in src/app.rs: HistoryEntry, navigate_to, navigate_back, navigate_forward, follow_selected_link, select_next_link, select_prev_link, scroll_to_selected_link, build_breadcrumb 3. main.rs passes link_records and current_path to App::new() 4. Key bindings in correct match order: Ctrl+C > q > Tab > BackTab > Enter > Backspace > Alt+Left > Alt+Right > j/k/Down/Up > PgDn/PgUp > _ catch-all ## Deviations from Plan ### Implementation Notes **1. Tasks 1 and 2 implemented together:** - **Found during:** Implementation - **Note:** The draw-time REVERSED modifier and breadcrumb status bar are tightly coupled to the same `App` struct fields (link_records, selected_link, history, history_index, current_path) added in Task 1. Implementing them in a single coherent rewrite produced cleaner code than two separate partial passes. - **Impact:** Single commit covers both tasks. All required functionality is present. **2. Span overlap detection extended:** - **Found during:** Task 2 implementation - **Issue:** The plan's span-walking code only checked `col >= col_offset && col < col_offset + span_len`, which misses spans that start before `col_offset` but extend into the link range. - **Fix:** Added a second condition `col < col_offset + span_len && col + span_chars > col_offset` to catch overlapping spans. - **Files modified:** src/app.rs (draw method) ## Self-Check: PASSED Files verified: - `src/app.rs`: contains `struct HistoryEntry`, `navigate_to`, `navigate_back`, `navigate_forward`, `follow_selected_link`, `select_next_link`, `select_prev_link`, `scroll_to_selected_link`, `build_breadcrumb`, REVERSED modifier in draw(), breadcrumb in draw_status_bar() - `src/main.rs`: passes `Some(&app_config.vault_path)` to render_markdown, destructures `link_records`, calls `App::new()` with 6 arguments Commits verified: - `d705313`: feat(03-02): add navigation history, link cycling, and navigate_to to App