himalaya 信封列表相对日期渲染:`envelope.list.datetime-relative` 配置指南
himalaya 信封列表相对日期渲染:envelope.list.datetime-relative 配置指南
本文基于 himalaya 邮件 CLI 项目中的 envelope-relative-date 变更(见 cairn/changes/envelope-relative-date/proposal.md)整理而成,介绍如何让 envelope list 与 envelope search 的 DATE 列按消息年龄渲染为“今天的时间 / yesterday / 星期几”等相对日期,从而在保持信息量的同时大幅压缩表格宽度。读完本文,你将掌握该配置项的全局与按账号设置方法、四档年龄分桶规则、时区与 JSON 输出的行为边界,以及其背后的源码实现与测试证据。
背景:为什么需要相对日期
日常邮件列表里,绝大多数消息都来自今天或昨天。若每一行都渲染完整的日期与 UTC 偏移(默认格式 %F %R%:z),表格宽度会白白浪费在读者已经了然于心的信息上(对应上游 issue #510)。参考 Alpine 的做法:今天显示时间、昨天显示 yesterday、更早显示星期几,himalaya 在 envelope-relative-date 变更中把这一思路落地为可配置选项。
功能总览
变更引入了一个新的配置项 envelope.list.datetime-relative,默认关闭。开启后,envelope list 与 envelope search 的 DATE 列会按消息年龄选择不同的日期格式:
| 消息年龄 | DATE 列渲染内容 | 对应 chrono 格式 |
|---|---|---|
| 今天 | 一天中的时间(如 11:35) |
%R |
| 昨天 | 字面文本 yesterday |
— |
| 2~6 天前(一周内) | 星期几(如 Saturday) |
%A |
| 更早或日期在未来 | 回退到 datetime-fmt(默认 %F %R%:z) |
自定义 |
该选项只影响表格输出;--json 输出保持不变,仍然给出精确日期。这一约束在变更规范(cairn/spec/config.md)中被正式表述为一条需求:Envelope dates MAY render relative to today。
配置方法
全局配置
在配置文件顶层设置,位于 <a href="https://link.gitcode.com/i/3d3a307493ef71f9a8981f261e017d1e" target="_blank">envelope.list] 相关的 kebab-case 命名空间中(示例见 [config.sample.toml):
# chrono strftime format of the DATE column, defaulting to `"%F %R%:z"`.
envelope.list.datetime-fmt = "%F %R%:z"
# Convert a `Date` header to the local timezone before formatting. Defaults to
# false, which keeps the wire offset.
envelope.list.datetime-local-tz = false
# Render recent dates relative to today, in the local timezone: the time of day
# for today, `yesterday`, then the weekday for the past week. Older dates fall
# back to `datetime-fmt`. Defaults to false.
envelope.list.datetime-relative = true
按账号配置
在账号块内同样可以覆盖(见 config.sample.toml),与全局配置结构一致:
[accounts.alice]
email = "alice@example.org"
# Per-account overrides for the global options above.
envelope.list.datetime-fmt = "%F %R%:z"
envelope.list.datetime-local-tz = false
envelope.list.datetime-relative = true
从源码结构看,账号级配置通过合并逻辑覆盖全局值:在 src/account/context.rs 中,账号配置的 datetime_relative 优先于全局配置(other.datetime_relative.or(self.datetime_relative)),最终读取时以 unwrap_or(false) 兜底(src/account/context.rs),保证未设置时行为与旧版本一致。
渲染规则细节
相对日期的分桶逻辑集中在 src/shared/envelope/list.rs 的 Envelopes::format_date 方法中:
fn format_date(&self, date: Option<DateTime<FixedOffset>>, today: NaiveDate) -> String {
let Some(date) = date else {
return String::new();
};
if self.datetime_relative {
let date = date.with_timezone(&Local);
return match (today - date.date_naive()).num_days() {
0 => date.format("%R").to_string(),
1 => String::from("yesterday"),
2..7 => date.format("%A").to_string(),
_ => date.format(&self.datetime_fmt).to_string(),
};
}
if self.datetime_local_tz {
date.with_timezone(&Local)
.format(&self.datetime_fmt)
.to_string()
} else {
date.format(&self.datetime_fmt).to_string()
}
}
要点解读:
- 四档分桶:按
(today - 日期).num_days()计算年龄差,0 天渲染%R时间,1 天渲染yesterday,2~6 天渲染%A星期几,其余(更老或未来日期)回退到datetime-fmt。 - 始终使用本地时区:相对日期只有在本地时区下才有意义,因此相对分支会先把日期转换到
Local。这意味着datetime-local-tz对相对日期行不产生额外效果(相对分支已经强制本地时区)。 - 星期几标签固定为英文:
chrono的%A不依赖 locale,输出为Saturday这类英文星期名。 - 缺失日期:信封没有日期时渲染为空字符串。
- 默认格式:
EnvelopeListConfig中datetime_fmt缺省为"%F %R%:z"(见 src/config.rs 的字段注释),测试中则简化为%F以便断言。
源码实现与调用链
配置结构定义
EnvelopeListConfig 位于 src/config.rs,使用 #[serde(rename_all = "kebab-case", deny_unknown_fields)],因此配置键写作 datetime-relative、datetime-fmt、datetime-local-tz:
/// `envelope list` rendering options under `envelope.list.*`.
pub struct EnvelopeListConfig {
/// chrono `strftime` format of the DATE column, defaulting to `"%F %R%:z"`.
pub datetime_fmt: Option<String>,
/// Whether the `Date:` offset is converted to the local timezone
/// before formatting, the default `false` keeping the wire offset.
pub datetime_local_tz: Option<bool>,
/// Whether a recent date renders relative to today in the local
/// timezone (time of day, `yesterday`, weekday), an older one falling
/// back to `datetime-fmt`, the default `false` never doing so.
pub datetime_relative: Option<bool>,
// ... page_size, table colors ...
}
命令如何读取配置
envelope list 命令在 src/shared/envelope/list.rs 的 execute 中,把账号合并后的三个日期选项组装进 Envelopes 输出结构(datetime_fmt、datetime_local_tz、datetime_relative 均标记为 #<a href="https://link.gitcode.com/i/6d41bd668fe6b9c351bba8d3147110b5" target="_blank">serde(skip)],不进入 JSON);envelope search 命令在 [src/shared/envelope/search.rs 中同样读取 account.datetime_relative() 并构造 Envelopes。也就是说,两条命令共享同一套渲染代码与同一组配置。
表格渲染时(fmt::Display for Envelopes,src/shared/envelope/list.rs),每个信封行都会调用 self.format_date(env.date, today),其中 today 在进入渲染前只计算一次(let today = Local::now().date_naive();),保证同一张表内“今天”的口径一致。这正是变更任务清单中“Envelopes::format_date picks the format per row, today read once per table”的实现体现。
测试验证
变更附带了覆盖各年龄分桶的单元测试(src/shared/envelope/list.rs):
#[test]
fn relative_date_follows_age() {
let envelopes = envelopes(true);
let render = |date| envelopes.format_date(date, today());
assert_eq!(render(local(2026, 9, 30, 11, 35)), "11:35");
assert_eq!(render(local(2026, 9, 29, 11, 35)), "yesterday");
assert_eq!(render(local(2026, 9, 26, 11, 35)), "Saturday");
assert_eq!(render(local(2026, 9, 23, 11, 35)), "2026-09-23");
assert_eq!(render(None), "");
}
#[test]
fn absolute_date_ignores_age() {
let envelopes = envelopes(false);
let date = local(2026, 9, 30, 11, 35);
let expected = date.unwrap().format("%F").to_string();
assert_eq!(envelopes.format_date(date, today()), expected);
}
测试以固定日期 2026-09-30 为“今天”,验证了五个断言:
- 当天消息渲染为时间
11:35(%R); - 前一天渲染为
yesterday; - 四天前(2~6 天窗口内)渲染为星期几
Saturday; - 七天前(超过一周)回退到绝对格式
2026-09-23(此处datetime-fmt为测试简化的%F); - 缺失日期渲染为空字符串。
第二个测试 absolute_date_ignores_age 则保证选项关闭时,即使日期就在“今天”,仍走原绝对格式路径,旧行为完全不受影响。
行为边界与注意事项
- 仅影响表格,不影响 JSON:
Envelopes中的日期选项均为#[serde(skip)],--json输出始终携带精确日期,适合脚本与自动化消费。 - 相对日期依赖本地时区:同一封信在不同时区的终端上会呈现不同的相对标签;若你的使用场景强调绝对时间口径,请保持该选项关闭。
- 星期标签为英文:
%A由 chrono 直接输出英文星期名,不随系统 locale 变化。 - 未来日期:年龄差为负(日期在未来)时落入
_分支,同样回退到datetime-fmt,不会出现奇怪的相对标签。 - 与
datetime-local-tz的关系:相对分支内部已经强制with_timezone(&Local),因此同时开启两个选项时,相对行的显示结果不受datetime-local-tz额外影响。
变更落地与文档归档
该功能于 2026-09-30 落地,对应的变更记录链条包括:
- 变更提案:cairn/changes/envelope-relative-date/proposal.md
- 需求增量(新增能力描述):cairn/changes/envelope-relative-date/delta.md
- 落地日志:cairn/log/2026-09-30-envelope-relative-date.md
- 合并进配置规范:cairn/spec/config.md
- 更新日志:CHANGELOG.md(条目:Added
envelope.list.datetime-relative, rendering recent dates relative to today: the time for today,yesterday, then the weekday for the past week, older dates falling back todatetime-fmt)
如果你经常在窄终端窗口里刷邮件列表,且大部分信件集中在最近一周,开启 envelope.list.datetime-relative = true 能显著压缩 DATE 列宽度,让 SUBJECT 与 FROM/TO 列获得更多空间——而 --json 的精确数据始终在背后兜底。