May 15, 2026
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/Nto cycle matches with wrap-around,ESCto clear. - Works against both the feed-provided summary and the Readability-extracted full text — the query survives
Shift+Ftoggling, 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>soParagraph::wrapkeeps doing soft-wrap for free; match ranges are emitted as styled spans within each source line. - Current match (the one
ncursor 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
Appat render time son/Nevent 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-features— 199 lib tests + 4 integration tests pass (22 new tests added: pure helpers, scroll math, state machine, and event-dispatch mode transitions) - Manual:
/foohighlights,n/Ncycle and viewport scrolls,ESCclears,Shift+Fkeeps 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 overarticle_scraperbecause 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::spawnworker that posts the result back over a long-livedmpsc::channel. Same pattern feedr already uses forspawn_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
Authorizationheaders 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_seensemantics asexec_on_new— first observation of a feed seeds silently to avoid a firehose). - Failure handling: pages with
<200chars of extracted body, non-HTML content-type, or>5 MBresponse are rejected with a clear reason. The detail view falls back to the summary and lets the user retry withShift+F.
What changed
Cargo.toml: adddom_smoothie = "0.17"src/config.rs: optionalfulltextfield onDefaultFeed(additive, backward-compatible)src/feed.rs: newExtractedArticle+extract_article(HTTP) +extract_from_html(pure test seam)src/app.rs:ExtractionStateenum,App::extracted/extracted_order/fulltext_feeds/show_extracted/pending_extraction_requests, LRU cache helpers, prune-on-feed-removesrc/keybindings.rs:KeyAction::FetchFullText+ default bindShift+Fsrc/events.rs: dispatch inView::FeedItemDetail+ macro whitelistsrc/tui.rs: long-livedmpscchannel,spawn_pending_extractionsworker pool, hoistedmark_feed_seensoexec_on_newand fulltext share one mark per feed arrivalsrc/ui/detail.rs: render extracted vs summary, four-state indicator (Summary/Extracting…/Full-text/Full-text failed), inline failure reasonsrc/ui/modals.rs: help overlay entryREADME.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 (Summary→Extracting…→Full-text/Full-text failed) and toggle behavior - Manual smoke: set
fulltext = trueon 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 showsShift+F Toggle/fetch full-text (Readability)in the Item Detail section