logs.gokuls.in

1 pull request merged across 1 repo

bahdotsh/feedr

Adds three newsboat-style extensibility primitives so users can self-serve the long tail of "save to X / notify on Y / pipe to Z" workflows without feedr chasing one-off integrations:

  • pipe-to — pipes the focused article (body / title / url / metadata, configurable per macro step) to a shell command's stdin. The TUI is suspended for the duration so pagers and editors take over the terminal cleanly.
  • exec_on_new — fires a per-item command after each refresh. The first successful fetch of a feed seeds the seen set silently, so enabling this on a 200-item feed doesn't notify-spam.
  • Macros — bind a chord (default prefix , then key) to an ordered chain of actions / pipe-to / exec steps. Newsboat-compatible string syntax (open-in-browser ; pipe-to "yt-dlp %u"); existing newsboat macros mostly paste in directly.

Design notes

Safety stance. Templates are tokenized once via shlex::split and %X placeholders are substituted into individual argv slots. Commands are not run through sh -c. That means pipe-to "tee out.txt | wc -l" won't pipeline — wrap it in a script. The alternative is letting a malicious feed title execute arbitrary commands, which is not a trade I was willing to make. Documented in the auto-generated config comments.

Sequential macro semantics. All three step kinds (Action, PipeTo, Exec) are queued onto app.pending_macro_steps (FIFO) and drained in order by the TUI loop. Earlier draft ran Action steps inline and queued only the externals — that broke pipe-to "X" ; toggle-read ordering. Refactored before commit.

Terminal suspend. tui::suspend_for_command does the verified ratatui dance: LeaveAlternateScreen → DisableMouseCapture → disable_raw_mode → spawn → wait → enable_raw_mode → EnterAlternateScreen → EnableMouseCapture → terminal.clear(), with an RAII guard so a panicking child can't strand the user in raw mode. Pending input drained on return to absorb terminal-probe responses (less/vim background-color queries).

exec_on_new first-fetch suppression. A new persisted feeds_seeded: HashSet<String> records URLs that have been fetched at least once. The very first successful fetch silently seeds seen_items without firing hooks. Persists across restarts via feedr_data.json (additive serde(default) field, fully back-compat with v0.7.0 data files).

Config schema (all serde(default))

[hooks]
exec_on_new = 'notify-send "New: %t" "%f"'

[macros]
y = 'open-in-browser ; pipe-to "yt-dlp %u"'
w = 'pipe-to "wallabag-cli add %u" -- "Save to Wallabag"'
n = 'pipe-to "tee out.txt" stdin=metadata'

[macro_options]
prefix = ","
pipe_default_stdin = "body"  # body | title | url | metadata | none

Template variables: %t title, %u url, %a author, %d date, %f feed-title, %F feed-url, %% literal %.

Files touched

  • src/keybindings.rsMacroStep, MacroBinding, StdinKind, MacroOptions, parse_macro_string, build_macros, binding_display helper
  • src/config.rs[hooks], [macros], [macro_options] sections
  • src/app.rsseen_items/feeds_seeded persistence, ArticleContext, current_article_context, mark_feed_seen, expand_argv_template, make_pipe_payload
  • src/events.rs — macro-prefix detection in handle_key_event, run_macro queueing, dispatch_action
  • src/tui.rssuspend_for_command, spawn_detached, drain_macro_steps, fire_exec_on_new with first-fetch suppression
  • src/ui/modals.rs — Macros section in the help overlay
  • Cargo.tomlshlex = "1.3" (for argv tokenization)

Test plan

  • cargo test --lib — 103 tests passing (12 new for macro parser, 8 new for app helpers, 2 new for config back-compat / round-trip)
  • cargo test --tests — 4 integration tests passing
  • cargo clippy --all-targets --all-features -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • cargo build --release — clean
  • Manual smoke test: with [macros] x = 'pipe-to "cat"', press ,x on an article — body appears in stdout, terminal restores cleanly on Ctrl+D.
  • Manual smoke test: with [hooks] exec_on_new = 'tee -a /tmp/feedr.log', refresh — log file is silent on first fetch of a feed, then gets one line per genuinely new item on subsequent refreshes.
  • Manual smoke test: [macros] y = 'open-in-browser ; pipe-to "yt-dlp %u"' on a YouTube link — opens browser, then pipes URL to yt-dlp.

Notes for reviewers

  • This is item #3 of three planned newsboat-parity features; sync (#1) and Readability extraction (#2) are tracked separately.
  • The diverge-from-newsboat call on sh -c is the most consequential design decision here. Open to revisiting if there's pushback, but a per-step opt-in shell = true would be the right shape rather than a global flag.