logs.gokuls.in

2 pull requests merged across 1 repo

bahdotsh/feedr

  • Adds /-style find-in-page to the article detail view: live highlights as you type, n/N to cycle matches with wrap-around, ESC to clear.
  • Works against both the feed-provided summary and the Readability-extracted full text — the query survives Shift+F toggling, dies on article focus change.
  • Ripgrep-style smart-case matching: case-sensitive iff the query has any uppercase character, ASCII-fold case-insensitive otherwise.
  • Repurposes / in the detail view from cross-feed search (which made little sense from inside an article) to article search.
  • New KeyActions: OpenArticleSearch (/), NextMatch (n), PrevMatch (N). All remappable via the existing [keybindings] config.

Design notes

  • Body is rendered as a styled Vec<Line> so Paragraph::wrap keeps doing soft-wrap for free; match ranges are emitted as styled spans within each source line.
  • Current match (the one n cursor sits on) gets the bold filled block; non-current matches stay subtle (foreground tint, no fill) so a heavily-matched page doesn't look like a Christmas tree.
  • Jump-to-match uses a sibling helper to count_wrapped_lines (wrapped_row_of_line) that returns the cumulative wrapped-row index for a given source line, so scrolling lands exactly where the wrapped output puts the match.
  • Body text + content width cached on App at render time so n/N event handling can resolve match → scroll without re-deriving the body source. State is in-memory only — nothing persisted.
  • Footer prompt is a 1-row split off the bottom of the content area, only carved when the search is actually in use; quiet reading is unchanged.

Test plan

  • cargo build --release — clean
  • cargo fmt --all -- --check — clean
  • cargo clippy --all-targets --all-features -- -D warnings — clean
  • cargo test --all-features199 lib tests + 4 integration tests pass (22 new tests added: pure helpers, scroll math, state machine, and event-dispatch mode transitions)
  • Manual: /foo highlights, n/N cycle and viewport scrolls, ESC clears, Shift+F keeps the query alive on the same article, navigating to a different article drops the query
  • Manual: query with uppercase (e.g. Foo) only matches case-sensitively; all-lowercase matches case-insensitively

Adds Mozilla-Readability full-text article extraction to feedr. Press Shift+F in the article detail view to fetch the linked URL and render the actual article inline, replacing the typically-truncated feed summary. Press again to toggle back. Per-feed fulltext = true auto-extracts newly-seen items on refresh.

Closes the daily paper-cut of summary-only feeds without leaving the terminal.

Design choices

  • Crate: dom_smoothie — pure sync, MSRV matches feedr's exactly (1.75), actively maintained (v0.17.0, March 2026), MIT, configurable parse budget. Picked over article_scraper because that one is async-only and would drag tokio + a full async reqwest stack into a codebase that has been deliberately single-threaded synchronous from day one.
  • Threading: each extraction runs on a std::thread::spawn worker that posts the result back over a long-lived mpsc::channel. Same pattern feedr already uses for spawn_feed_refresh. The UI never blocks on extraction.
  • State: App::extracted: HashMap<item_id, ExtractionState> with LRU cap of 500. In-memory only — no disk persistence in v1. A restart re-fetches on demand.
  • Security: per-feed Authorization headers are deliberately NOT forwarded to article URLs. Article URLs are third-party hosts; propagating bearer tokens would be a credential leak.
  • Auto-fetch scope: newly-seen items only (same mark_feed_seen semantics as exec_on_new — first observation of a feed seeds silently to avoid a firehose).
  • Failure handling: pages with <200 chars of extracted body, non-HTML content-type, or >5 MB response are rejected with a clear reason. The detail view falls back to the summary and lets the user retry with Shift+F.

What changed

  • Cargo.toml: add dom_smoothie = "0.17"
  • src/config.rs: optional fulltext field on DefaultFeed (additive, backward-compatible)
  • src/feed.rs: new ExtractedArticle + extract_article (HTTP) + extract_from_html (pure test seam)
  • src/app.rs: ExtractionState enum, App::extracted / extracted_order / fulltext_feeds / show_extracted / pending_extraction_requests, LRU cache helpers, prune-on-feed-remove
  • src/keybindings.rs: KeyAction::FetchFullText + default bind Shift+F
  • src/events.rs: dispatch in View::FeedItemDetail + macro whitelist
  • src/tui.rs: long-lived mpsc channel, spawn_pending_extractions worker pool, hoisted mark_feed_seen so exec_on_new and fulltext share one mark per feed arrival
  • src/ui/detail.rs: render extracted vs summary, four-state indicator (Summary / Extracting… / Full-text / Full-text failed), inline failure reason
  • src/ui/modals.rs: help overlay entry
  • README.md + CLAUDE.md: docs

Test plan

  • cargo test --all-features — 130 lib + 4 integration tests pass (9 new)
  • cargo clippy --all-targets --all-features -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • Manual smoke: add a summary-only feed (e.g. Hacker News), open an item, press Shift+F, verify the four states (SummaryExtracting…Full-text / Full-text failed) and toggle behavior
  • Manual smoke: set fulltext = true on a feed, refresh, confirm newly-seen items auto-extract in the background without blocking the UI
  • Manual smoke: try a JS-rendered / paywalled page (Bloomberg / NYT) and confirm graceful fallback to the summary with a clear failure reason
  • Manual smoke: confirm ? help overlay shows Shift+F Toggle/fetch full-text (Readability) in the Item Detail section