Files
bbs-md/.planning/phases/03-navigation-and-links/03-02-SUMMARY.md
T
ruohki 2c9ad682da docs(03-02): complete navigation, history, and breadcrumb status bar plan
- Create 03-02-SUMMARY.md documenting HistoryEntry, navigate_to/back/forward, Tab-cycling, breadcrumb
- Update STATE.md: Phase 3 complete, plan 2/2 done, decisions added, session updated
- Update ROADMAP.md: Phase 3 marked complete, 2/2 plans checked
- Mark requirements NAV-03, NAV-04, NAV-11 complete in REQUIREMENTS.md
2026-02-28 23:12:39 +01:00

8.8 KiB
Raw Blame History

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
03-navigation-and-links 02 app
navigation
history
link-cycling
breadcrumb
tab-navigation
requires provides affects
03-01 (LinkRecord from render_markdown, resolve_wiki_link, resolve_standard_link)
02-vault-core-and-rendering (vault.rs load_document, DocumentState)
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
End-user experience (core interactive vault browsing)
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
modified
path role
src/app.rs 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 role
src/main.rs Updated initial render to capture link_records, pass link_records and current_path to App::new()
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
duration_seconds completed_date tasks_completed tasks_total files_modified
198 2026-02-28 2 2 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

Implemented the complete navigation system in src/app.rs and updated src/main.rs.

HistoryEntry struct (new, in src/app.rs):

struct HistoryEntry {
    path: String,           // vault-relative path
    scroll_offset: u16,     // scroll position at time of navigation away
    selected_link: Option<usize>, // selected link index at time of navigation
}

Phase 3 fields added to App:

  • history: Vec<HistoryEntry> — browser-style navigation stack
  • history_index: usize — current position in history
  • link_records: Vec<crate::renderer::LinkRecord> — links from current render
  • selected_link: Option<usize> — currently highlighted link
  • current_path: String — vault-relative path for breadcrumb and link resolution

App::new() signature updated: Now accepts link_records: Vec<LinkRecord> 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()

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