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.toml的default_feeds中设置fulltext = true,新文章在刷新时自动提取。
提取内容仅存内存,重启后需重新请求。安全边界:per-feed 的 HTTP 认证头不会转发给文章 URL,防止凭证泄漏到第三方域名。短正文(疑似 JS 渲染或付费墙)会优雅降级回退到原始摘要。
新闻 boat 风格宏与安全边界
Feedr 支持 newsboat 式的外部命令钩子,用于两个场景:
- 宏(macros):按键触发文章操作链,如
'open-in-browser ; pipe-to "yt-dlp %u"'(,y在浏览器打开并下载视频)。%t/%u/%a/%f/%F是模板变量,展开后直接作为argv参数注入,不经过 shell。 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)。
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。