Hotdry.

Article

Feedr:Rust TUI 终端 RSS 阅读器的声明式渲染与异步抓取流水线

深入解析 Feedr 如何用 Ratatui 与 Tokio 构建异步事件循环,实现声明式终端渲染、RSS/Atom 解析与 Mozilla Readability 全文提取的工程细节。

2026-05-15systems

Feedr 是一个用 Rust 编写的终端 RSS/Atom 阅读器,核心依赖 Ratatui 做 UI 渲染、Crossterm 处理终端输入、Tokio 驱动异步事件循环。这个组合在 Rust TUI 生态中已成标准范式:声明式 UI + 非阻塞 I/O + 受控帧率重绘。理解 Feedr 的架构,对任何想构建终端应用的 Rust 开发者都有直接参考价值。

声明式渲染与即时模式

Feedr 采用即时模式(Immediate Mode)渲染范式。终端 UI 每帧都会完全重绘,而非增量更新.diff 差量。相比传统保留模式(Retained Mode,即 GUI 框架常见的 "修改状态后再刷新"),即时模式的优势在于状态同步简单、无需维护脏标记,代价是持续占用 CPU 重绘。

Ratatui 正是这一范式的载体。在 Feedr 中,视图函数签名为:

fn view(&mut self) -> Result<()> {
    let (term_width, term_height) = self.terminal.size()?;
    self.terminal.draw(|f| {
        let screen_area = f.area();
        // 声明式组件链:Block → Paragraph → List → Table
        let content = Paragraph::new(content.clone())
            .block(Block::bordered().title("Article"))
            .scroll((scroll_offset, 0));
        f.render_widget(content, screen_area);
    })?;
    Ok(())
}

Ratatui 的 widget 系统通过链式调用构建 UI 树,每个 widget 都是可组合的积木(Paragraph、List、Table、Canvas)。f.render_widget() 将 widget 渲染到指定区域,坐标系统由 Crossterm 提供(原点左上),而 Ratatui Canvas API 使用笛卡尔坐标(原点左下),两者间的坐标转换是 Feedr 处理鼠标交互时的隐藏细节。

Feedr 的 tick_rate 配置为 100ms,即每秒 10 次状态更新;渲染帧率默认 30fps,通过 tokio::time::interval 双 interval 并行驱动。这种 "状态 tick + 渲染帧分离" 的模式确保动画流畅,同时避免状态更新过于频繁。

异步事件循环的三路复用

Feedr 的核心是一个 tokio::select! 宏驱动的异步循环,同时监听三路事件源:

pub async fn run(&mut self) -> Result<()> {
    let mut tick_interval = time::interval(Duration::from_millis(100));
    let mut frame_interval = time::interval(Duration::from_secs_f64(1.0 / 30.0));
    loop {
        tokio::select! {
            _tick = tick_interval.tick() => {
                self.event_tx.send(Message::Tick)?;
                self.update_models(); // 更新动画/计时器状态
            }
            _frame = frame_interval.tick() => {
                self.view()?; // 触发 UI 重绘
            }
            Some(message) = self.event_rx.recv() => {
                self.handle_message(message).await?;
            }
            Ok(ready) = tokio::task::spawn_blocking(|| crossterm::event::poll(Duration::from_millis(100))) => {
                if ready { self.handle_input(crossterm::event::read()?)?; }
            }
        }
    }
}

Tick 事件驱动业务逻辑(如订阅刷新计时器、未读计数递增);Frame 事件触发渲染;Input 事件通过 spawn_blocking 在阻塞线程池中轮询终端输入,避免异步 runtime 被同步 I/O 卡住。这是 TUI 应用的经典陷阱:终端输入 API 本质是同步的,必须扔到 blocking 线程。

Message 枚举封装所有状态变更信号(Tick、Render、MouseClick、KeyPress 等),update() 函数是纯函数式的状态迁移函数,输入 Message + 当前 Model,输出新 Model + UpdateCommand。这种设计灵感来自 The Elm Architecture(TEA),在 Rust TUI 社区被广泛借鉴。

RSS/Atom 解析与后台抓取

Feedr 的网络层依赖 reqwest(带 gzip/deflate/brotli 解压支持)与 feed-rs(RSS/Atom 解析)。后台刷新逻辑如下:

[general]
refresh_enabled = true
auto_refresh_interval = 300  # 5 分钟
refresh_rate_limit_delay = 2000  # 同域名 2 秒间隔

Rate limiting 按域名分组请求,防止触发 Reddit 等平台的 rate limit。refresh_rate_limit_delay = 2000 意味着如果用户订阅了 5 个 Reddit 子站 RSS,每次刷新会依次请求,每个间隔 2 秒。

抓取任务通过 tokio::spawn 在后台运行,结果通过 channel 回传给主循环。这种模式避免阻塞 UI,即使某个 feed 服务器响应慢,界面仍保持流畅。

Mozilla Readability 全文提取

大多数 RSS feed 只含摘要而非正文。Feedr 通过 dom_smoothie(Mozilla Readability 的 Rust 移植)实现全文提取:

  • 手动模式:在文章详情页按 Shift+F,Feedr 请求原文 URL,运行 Readability 算法提取正文,渲染到终端。
  • 自动模式:在 config.tomldefault_feeds 中设置 fulltext = true,新文章在刷新时自动提取。

提取内容仅存内存,重启后需重新请求。安全边界:per-feed 的 HTTP 认证头不会转发给文章 URL,防止凭证泄漏到第三方域名。短正文(疑似 JS 渲染或付费墙)会优雅降级回退到原始摘要。

新闻 boat 风格宏与安全边界

Feedr 支持 newsboat 式的外部命令钩子,用于两个场景:

  1. 宏(macros):按键触发文章操作链,如 'open-in-browser ; pipe-to "yt-dlp %u"',y 在浏览器打开并下载视频)。%t/%u/%a/%f/%F 是模板变量,展开后直接作为 argv 参数注入,不经过 shell。
  2. exec_on_new:刷新后发现新文章时触发通知命令,如 'notify-send "New: %t" "%f"'

安全设计的关键在于 "shell 不可达":命令从不经过 sh -c,模板变量无法通过引号逃逸嵌入 shell 命令。如果宏模板中出现不平衡引号或未知 action,Feedr 会在启动时警告而非静默失败。

可落地参数清单

若你计划构建类似应用或评估 Feedr,以下是关键配置项:

参数 默认值 说明
ui.tick_rate 100ms 状态更新频率,越低越跟手但 CPU 越高
general.refresh_rate_limit_delay 2000ms 同域名请求间隔,防封禁
network.http_timeout 15s 单次请求超时
general.max_dashboard_items 100 Dashboard 文章上限
ui.compact_mode "auto" 终端 ≤30 行时自动紧凑布局

Vim 风格导航:j/k 上下、g/G 首尾、Ctrl+U/D 翻页。键位可通过 [keybindings] 完全重映射。数据遵循 XDG 规范:配置在 ~/.config/feedr/config.toml,状态在 ~/.local/share/feedr/feedr_data.json


资料来源:Feedr GitHub 仓库(bahdotsh/feedr);Async Ratatui 事件循环参考实现(d-holguin/async-ratatui)。

systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com