pi-sysmon

extensionmaintained

bottom-style braille system monitor charts (CPU / memory / network / LLM token throughput) for the pi coding agent

by — · v0.7.0 · published 4d ago

$ pi install npm:pi-sysmon
downloads/mo
421
stars
0
last push
1d ago
open issues
0

Signals

license: MITtestspi manifest: missinginstall size: —deps: 0peer deps: 0

Download trend

861 downloads · last 12 weeks (weekly)

README

pi-sysmon

在 pi 里显示 bottom 风格的 braille 折线图

CPU · 内存 · 网络 · Tokens —— 用盲文点阵字符画的实时历史曲线

test license

四图并排:CPU / Memory / Network / Tokens

150 列宽下的默认四图。曲线由盲文字符绘制,y 刻度叠印在绘图区内侧、 时间标签嵌在下边框里。

English | 简体中文 | X @zzjcoo

特性

  • 真正的折线图,不是进度条或 sparkline —— 每个字符编码 2×4 个盲文点子像素,等效把终端分辨率放大 8 倍
  • 零依赖 —— 只用 Node 内置模块 + pi 的公开扩展 API
  • 直接读 /proc —— 不需要 systeminformation / pidusage 之类的包
  • 默认四图 —— CPU / 内存 / 网络 / Tokens(LLM 吞吐速率); Tokens 图实时显示本 pi 进程与模型 API 之间的输出 token 速率, 并与 pi 自身在状态栏显示的 ⚡ 23.0 t/s (avg) 相互印证
  • CPU 温度曲线 —— CPU 图以第二条(红色)曲线绘制温度,自带 0–100°C 刻度与 CPU% 共享绘图区 (右上角 100° 标注副轴刻度,标题栏同步显示当前 °C 读数,参照 Tokens 图 TPS/缓存率双轴的实现)。 Linux 读取 /sys/class/hwmon + /sys/class/thermal;macOS 无免 root 系统 API, 探测用户自装的 osx-cpu-temp / istats 辅助命令 —— 无可用源时图表安静降级为单曲线原样
  • 带坐标轴 —— y 轴刻度、x 轴线、时间窗标签,复刻 bottom 的排版
  • y 轴顶端 = 整窗真实最高值 —— 60s 窗口内任何一处的高度都能直接用顶端刻度读出来, 刻度绝不撒谎(配合 PI_SYSMON_SCALE_WINDOW=<1 还能开启尖峰后自动回落)
  • 自动剔除本机环回/容器流量 —— lo / veth* / docker* / br-* 不计入网速 (实测:本机推 30 MB/s 环回流量时,网速图只报物理网卡的 178 KB/s)
  • 响应式并排 —— 终端宽时四张图并排一行,变窄自动降级成 2×2 / 1 列竖排,不会把曲线挤成噪声
  • Tokens 图分上下行 —— 读数与 pi 自己的状态栏同口径(↑ 上行 input / ↓ 下行 output / R 缓存读),两边数字可以直接对照;上行是精确值,速率是估算值(带 ~)
  • 图表利用率高 —— 刻度叠在绘图区内侧、时间标签嵌进边框。同样的块高下 绘图面积比「刻度独占列 + 轴线独占行」的旧排版大 ~71% (50 列宽、8 行高:168 格 → 288 格)
  • 读数写在边框标题栏里(默认)—— 复用那行本来就有的 ─ 填充,零成本、不遮曲线;也可用 PI_SYSMON_LABEL=box 换回 bottom 的右上角浮框
  • 状态持久化 —— 显示偏好(模式 / 位置)与全局开关默认值记在配置文件里、重启保留;会话级的开关记在会话自身里
  • 多种显示模式 —— 图表(默认)/ 一行文字 / 整块底部;fullscreen TUI 下右下角有可点击标签, 一次点击即可在 chart ⇄ line 间切换
  • 纯 Linux 友好,其他平台不崩 —— 非 Linux 上采集自动退化为零值

截图

四张图各自的读数都写在边框标题栏里(复用那行本来就有的 ─ 填充, 不遮任何曲线)。y 轴刻度叠印在绘图区内侧、时间标签嵌在下边框里。

这两处省下的空间是可量化的。按同样的块高(8 行)、块宽 50 列比:

排版每块 chrome绘图区
旧(刻度独占 5 列 + 轴线独占 1 行)4 行42 × 4 = 168
现在(叠印 + 边框兼任轴)2 行48 × 6 = 288

增益拆开是两个因数相乘:宽度 42 → 48(+14%,刻度不再占列)× 行数 4 → 6(+50%,省下的两行还给数据)≈ +71%。

窄终端自动降级(95 列)

2×2 降级布局

终端变窄时排成 2×2,再窄就变 1 列竖排。断点在 96 列: ≥ 96 是四图并排一行(上方那张),< 96 就降到 2×2(这张是 95 列)。 bottom 在同样宽度下会把四张图硬挤成一行(每张 16 列左右),曲线就不可读了。

Tokens 图的上下行

┌ Tokens ─ ~58t/s ·92% ⌀87% ◔42% ─┐

读数刻意不与 pi 自己的状态栏重复:widget 模式(chart / line)下图表就挂在其旁边, 而 pi 状态栏本身常驻显示 ↑ 上行 input / ↓ 下行 output / R 缓存读 —— 这三个计数在这里就省去了。只有 /sysmon footer 模式(它替换了 pi 的状态栏, 连同其 token 读数一起消失)才会重新显示它们。几个细节:

  • 主曲线只画下行速率。两个方向的时间形状完全不同(上行是一次性整块上传、 下行是逐字流式,实测比例约 516:1),画同一根轴上会将下行压成 0.2% 高度。
  • ~ 只加在速率上 —— 它是从流式增量估算的;累计值来自 provider 的 精确 usage,所以不带 ~。哪个数字可信,一眼能看出来。
  • 块变窄时按重要度逐段丢弃:速率 → 瞬时命中率 → 累计命中率 → 上下文用量 → (footer 模式:上行 → 下行 → 缓存读)。

上下文用量:◔N%

◔N% 读数(半满圆 —— “上下文窗口有多满”)与 pi 自己状态栏显示的是同一个数字 (来自 ctx.getContextUsage(),pi 对活跃会话的估算,含 system prompt、工具结果、 compaction 边界),所以两边可以直接对照。它不从图表自己的 token 累计值重新推导: pi 的估算考虑了那些累计值看不到的东西。

颜色沿用 pi footer 的阈值:平常 muted,超过 70% 转黄(warning),超过 90% 转红(error)。 未知读数(还没有模型,或 /compact 刚结束、下一轮 LLM 回复前 pi 还报不出百分比) 降级为不显示,绝不会显示成假的 ◔0%。

缓存命中率:第二根曲线

缓存命中的 prompt token 计费远低于全新输入,所以命中率是唯一能回答“缓存到底有没有起作用” 的数字。Tokens 块从两个视角展示它:

  • ⌀N% —— 会话累计命中率(黄色曲线)。⌀ 读作“平均”;每次消息落地时从会话累计值 重新计算,因此曲线是阶梯状 —— 这是诚实的:底层数字是 provider 精确上报的总量, 两个消息之间没有更细的信息可画。
  • ·N% —— 瞬时命中率(仅标题栏)。最近一轮自己的 cacheRead / (cacheRead + input) 比例;这才是可据以行动的数字 —— 累计平均值会把一轮未命中缓存的请求藏很久。

黄色曲线有自己固定的 0–100% 量程,预先映射到 TPS 轴上且不参与 y 轴量程计算: 90% 的命中率永远渲染为绘图区高度的 90%,哪怕 3000 t/s 的流式尖峰把 TPS 轴顶抬高。 刻度列仍然只显示 TPS —— 靠颜色绑定(黄色曲线 ⇄ ⌀ 读数)区分两根线。

两个读数只在 provider 上报过缓存读(R)之后才出现;从未碰过缓存的会话会原样退回 之前的单曲线块。line 模式同样显示 ·N%/⌀N%(门控条件完全一致),并且 token 组 在重要度顺序中排到了 NET 之前 —— line 放不下时整组从尾部丢弃,而 token 读数比网络 速率更难从屏幕其他地方找回。

安装

用 pi 安装(推荐)

# 从 npm 装最新版
pi install npm:pi-sysmon

# 或锁定具体版本
pi install npm:pi-sysmon@0.4.0

# 或直接装 git 源
pi install git:github.com/zzjcool/pi-sysmon

重启 pi 即可看到曲线,默认就是开启的。升级用 pi update npm:pi-sysmon, 卸载用 pi remove npm:pi-sysmon。

从 git clone 装:单文件(手动安装里最简单)

git clone https://github.com/zzjcool/pi-sysmon && cd pi-sysmon && npm install
npm run build:single    # 生成 dist/pi-sysmon.ts
cp dist/pi-sysmon.ts ~/.pi/agent/extensions/pi-sysmon.ts

从 git clone 装:目录形式(多文件)

git clone https://github.com/zzjcool/pi-sysmon
cp -r pi-sysmon ~/.pi/agent/extensions/pi-sysmon

⚠️ 目录形式必须放在子目录里。pi 的自动发现会把 extensions/ 下的每个 .ts 都当成扩展加载,平铺多个文件会导致 braille.ts 被当作扩展而整个加载失败。

不安装先试用

pi -e npm:pi-sysmon

使用

/sysmon                    开 / 关(**只影响当前会话**)
/sysmon on | off           显式开 / 关(只影响当前会话)
/sysmon global on | off    设置**新会话**的默认开关(同时立即作用于当前会话)
/sysmon chart              图表模式(默认)
/sysmon below|above        图表 / line 放编辑器下方(默认)/ 上方(持久化)
/sysmon line               一行文字模式(显示 `CPU … TOK …` 的那一行;别名 `status`)
/sysmon footer             用图表替换整个底部(可以比 widget 模式更高)

chart / line 是互斥的显示模式(点某个模式名 = 切过去并打开,不会把监控关掉); on / off / above / below 与模式正交。

点击切换模式(仅 fullscreen TUI)

在 pi 的 fullscreen TUI 模式下(--tui-mode fullscreen,或在 /settings 里切 TUI mode), 面板右下角会出现一个可点击的 [line] / [chart] 小标签 —— 点一下就在 chart 和 line 之间切换,不用敲命令:

┌ CPU ─ 12% ───────────────────────┐
│ ⡿⢸⣿⡇ ...                       │
└ 60s ──────────────────────── 0s ┘
                              [line]
  • 点击走的是和 /sysmon chart / /sysmon line 同一条路径 —— 模式切换作为全局显示偏好持久化, 会话级开关状态完全不受影响。
  • line 模式下标签和文字同行(行高仍严格为 1 行 —— 标签的列从指标文字里借,指标照旧 从尾部整段丢弃)。
  • fullscreen 下图表用自己的行预算支付 chip 那一行,面板总行数不会超过 WIDGET_MAX_ROWS。
  • 标签以外的点击不会被吞:在面板上拖拽选中文字的行为和以前完全一样。
  • regular 模式(默认)完全不接管鼠标 —— 那时终端拥有回滚区 —— 所以不渲染标签, 面板行为和以前一模一样。请用 /sysmon chart | line 切换。

为什么标签没有悬停高亮:tmux/zellij/screen 下 pi 只开启 button-motion 鼠标上报(没有 move 事件),所以标签必须在没有任何悬停反馈的情况下也看起来能点。

会话级 vs 全局级

/sysmon on|off 是会话级的:写在当前会话自己的 entry 里,所以 /resume 回到这个会话时 开关和离开时一模一样,而其它会话、以后新开的会话都不受影响 —— 在某个项目里关掉, 只在这个会话里关掉。

/sysmon global on|off 把新会话的默认值写进 <configDir>/pi-sysmon.json, 并立即作用于当前会话(否则「以后默认关」却眼睁睁看着图表还在跑,会让人以为命令坏了)。

启动时的优先级:会话选择 → --sysmon → 全局默认 → 内置默认(开)。 模式(chart/line/footer)与位置(above/below)仍是全局显示偏好, 继续写在配置文件里 —— 否则每开一个会话都要重新选一遍图表。

line 模式显示什么

一行、按重要度降序、放不下时从尾部整段丢弃(不会切出 ↑1.0 这种半截数字):

CPU 12%  MEM 60% 37G  NET ↑592K/s ↓34K/s  TOK ~0t/s ↑5.7k ↓89 R2.7k
└─ 系统指标 ─────────────────────────┘ └─ LLM token ──────────────────┘
  • CPU / MEM / NET 与图表模式的对应块同源、同配色;
  • TOK 是 LLM token 吞吐:~<速率>(由流式 delta 估算,所以带 ~) 加会话累计的 ↑input ↓output RcacheRead(来自 message_end 的精确 usage, 逐字对齐 pi footer 的 ↑↓R 口径,可以直接和底部那行对照);
  • 无快照(非 Linux / /proc 不可读)时仍会输出 TOK 段 —— 它是唯一不依赖 /proc 的指标。

line 用 widget 实现而不是 setStatus,所以它和图表一样遵循 above/below: /sysmon below 后图表与 line 会出现在同一个位置。 (setStatus 的内容永远被 pi 内建 footer 渲染,位置钉死在底部,改不了。)

图表按终端宽度响应式排版(以默认四图为例):

终端宽度布局
≥ 96 列4 张图并排一行(共 8 行)
48 – 95 列2 列 × 2 行
< 48 列1 列竖排

行数按 widget 模式默认预算计算(PI_SYSMON_CHART_HEIGHT=6、WIDGET_MAX_ROWS=18); footer 模式预算更大(40 行),行数会更多。

每张图最少需要 24 列(边框 2 + 绘图区 22)。刻度是叠印在绘图区上的、 不占独立列,所以这个下限比旧排版(刻度独占 5 列)低不少。 比这更窄时 bottom 会把 N 张图硬挤成一团(50 列时每张只有 16 列,曲线已不可读), 本项目改为降级排列。

列数还会随块数自适应(块数由 PI_SYSMON_TOKENS / PI_SYSMON_DISKS 决定): 三图时 ≥ 72 列即并排(旧行为不变);块数 ≥4 时跳过 3 列档,避免排成 「3+1」那种第二组只有一个块加一整行空格子的形态。

配置

环境变量默认说明
PI_SYSMON_INTERVAL1000采样间隔(毫秒,下限 500)
PI_SYSMON_POINTS60历史点数(覆盖 PI_SYSMON_WINDOW 换算出的点数)
PI_SYSMON_CHART_HEIGHT6每张图的绘图行数(不含上/下边框那 2 行)
PI_SYSMON_LABELtitle读数位置:title(边框标题栏)/ box(右上角浮框)/ both / none
PI_SYSMON_WINDOW60横轴时间窗长度(秒)
PI_SYSMON_SCALE_WINDOW1(= 量程窗 == 显示窗)速率图量程取样比例:1 = y 轴顶端为整窗真实最高值;设 <1(如 1/6)开启「尖峰过去约 10s 后自动回落」(此时超出量程的尖峰会显示为 + 标记)
PI_SYSMON_MODEchart初始模式(chart / line / footer)
PI_SYSMON_PLACEMENTbelowEditorchart / line 挂在编辑器下方(默认)还是上方:belowEditor / aboveEditor(也可用 /sysmon below / /sysmon above 随时切换,会持久化;footer 模式不受影响)
PI_SYSMON_TOKENS开LLM token 吞吐图。设 0 回到旧的三图形态
PI_SYSMON_DISKS—设 1 再加一块磁盘 I/O 图(第 5 块)

开关有两个作用域,所以存两处:

  • <configDir>/pi-sysmon.json(configDir 默认 ~/.pi/agent)存 mode、placement 以及全局默认值 enabled —— 由 /sysmon global on|off 写入;
  • 当前会话的开关是会话 entry,由 /sysmon on|off 写入, 因此 /resume 这个会话时能恢复,而又不会影响其它任何会话。

--sysmon 命令行开关是「本次启动强制打开」—— 它盖过全局默认,但不盖过会话里 手动输入的 /sysmon off(那是更具体的表达)。

配置兼容: 早期只带 enabled: false 的 pi-sysmon.json(会话级开关之前的形态) 会被读作「全局默认关」,即从未动过开关的会话默认关 —— 正好等价于旧版该字段表达的行为。 想要回到「默认开」,执行 /sysmon global on。

发布内容包括什么

pi-sysmon 是一个 pi package:扩展入口写在 package.json 的 pi 字段里, 运行时除 pi 本身外不需要任何第三方 dependencies(两个 pi 包是可选 peerDependencies, 由 pi 自己提供)。npm tarball 里包含 src/、docs/、两份 README、changelog 与 license —— 也就是 pi install npm:pi-sysmon 拉取的内容。

实现说明

图表没有用任何 TUI 绘图库,而是自己合成字符。核心是 braille 点阵:

每个 braille 字符(U+2800–U+28FF)对应 8 个可独立点亮的点,排成 2 列 × 4 行:

(0,0) (1,0)      bit0  bit3
(0,1) (1,1)  →   bit1  bit4
(0,2) (1,2)      bit2  bit5
(0,3) (1,3)      bit6  bit7

所以一个 width × height 的字符区域,实际分辨率是 2*width × 4*height。 把数据点线性映射到子像素坐标,再用 Bresenham 连成线,就能得到平滑的折线。

这与 bottom 的做法一致(ratatui 的 Marker::Braille)。我们对齐了 bottom 的这些参数:

项bottom本项目
绘制字符Marker::Braille(2×4 子像素)同
连线算法Bresenham同
百分比图 y 轴固定 0 .. 100.5同
动态图 y 轴窗口内最大值 × 1.5(留白)同
网格线无,只有轴线 + y 刻度 + 两端时间标签同
默认采样间隔1000 ms同
每张图Block 边框,标题嵌在上边框里同
y 刻度位置独立列(占 5 列宽)叠印在绘图区内侧(不占列)
y 刻度数量百分比 2 个、速率 4 个只标顶端 1 个(带单位)
x 时间标签单独占一行嵌在下边框里(省 1 行)
x 轴线单独占一行由 0 基线兼任(省 1 行)
右上角读数框覆盖画在绘图区右上角,空间不够就整块消失同(阈值算法不同,见下)
宽度不够时把 N 张图硬挤到一行(50 列时每张 16 列)降级成 2 列 / 1 列
横轴默认窗口60 s同
自动量程回落滞后计数后降档(net_auto)默认不回落(顶端 = 整窗最高值);可选 PI_SYSMON_SCALE_WINDOW=1/6 开启

架构

src/
├── metrics.ts      # 采集层:读 /proc/{stat,meminfo,net/dev,diskstats} + os.loadavg
├── state.ts        # 决策层:会话级 vs 全局开关的优先级 + `/sysmon` 参数解析(纯函数)
├── braille.ts      # 渲染层:数据 → braille 点阵 → 字符行(纯函数,无副作用)
├── tokens.ts       # 估算层:LLM 流式增量 → token 数(纯函数 + 闭包 meter)
├── blocks.ts       # 组装层:历史 + 快照 → MetricBlock[](纯数据 → 纯数据)
├── chart-panel.ts  # 布局层:响应式列数、边框、刻度、浮动读数框、并排拼接
└── index.ts        # 扩展层:pi 生命周期、命令、配置持久化、定时刷新

依赖方向单向:index → {chart-panel, blocks, metrics},blocks → chart-panel, chart-panel → braille、blocks → tokens。braille.ts 与 tokens.ts 不依赖任何其它本模块。

分层原则:braille.ts 是纯函数(输入数值数组,输出字符串数组),因此可以脱离 pi 单独测试与复用;metrics.ts 只负责读数,不关心怎么显示。

与 bottom 的排版是怎么对齐的

不是靠肉眼调参数,而是读 bottom/ratatui 的源码把算法抄准,再用受控宽度的真实 btm 抓帧逐字符核对(┌ CPU ─ 1.91 1.80 2.17 ───┐ 那种)。 例如:

  • x 轴线不延伸到 y 轴那一列 —— ratatui 的 Chart::layout 在放下 y 轴后执行了 x += 1;
  • 左下时间标签的末位落在 y 轴列上 —— labels_alignment = Left 时首个 x 标签的区域是 [chart_left, graph_left)(左含右不含)再右对齐;
  • y 刻度位置 用 dy = i * (plotH - 1) / (n - 1)(索引 0 在底部)。

三处有意偏离:

  1. 读数默认写在边框标题栏里(PI_SYSMON_LABEL=title),而不是 bottom 的右上角浮框。 标题栏那一行本来就要写块名,剩下的 ─ 填充是纯装饰 —— 拿来放读数零成本, 且不遮任何曲线。bottom 的浮框是覆盖画在绘图区右上角的,会真的吃掉一块绘图区。 想换回 bottom 那种浮框就设 PI_SYSMON_LABEL=box。

  2. 浮框显隐阈值(仅在 box/both 模式生效)。bottom 用 hidden_legend_constraints 这套按比例的阈值(Network 是 9/10 × 3/4),但它是按 40+ 列宽的图校准的, 套到本项目 20~30 列的块上会导致读数框永远不显示。 本项目改成「放得下(legendW <= plotW)且浮框下面还留得出一行曲线(legendH < rows)」—— 后者是为了避免浮框下边框与绘图区底部的 0% 基线叠成一条双横线。

  3. 读数是分级片段,块窄时从尾部逐段丢弃。比如 Network 的顺序是 瞬时速率 → 累计流量,窄块下先丢累计流量,保住更重要的瞬时速率。

测试

npm test          # 167 项单元测试(braille 13 + layout 74 + tokens 31 + state 26 + extension 23)
npm run typecheck # tsc strict
npm run check     # 两者都跑

test/braille.test.ts 覆盖 braille 位映射(对照 Unicode 标准逐点验证)、坐标映射、 输出尺寸、边界输入(空数据 / 全零 / 单点 / NaN / Infinity / 极小宽度)。

test/layout.test.ts 覆盖响应式布局(列数断点、列宽之和恒等于总宽、行数恒定) 以及两条会让 pi 崩掉/错位的硬约束:对 8..220 列全宽度 × 多个高度与块数组合, 断言每行可见宽度不越界、行数与布局声明一致 —— 这两条是扫全宽度而不是抽查几个宽度。

line 模式也在这里扫全宽度:plainLineSegs + renderStyledLine 在 8..220 列下 渲染宽度必须恰好等于声明宽度(多一列就 pi 退出),且窄到 1 列也不抛异常、 CPU 段永远保留。

test/tokens.test.ts 覆盖 token 估算(英文 chars/4、CJK 逐字、emoji 算一个、 非字符串防御)、每秒桶的排空语义(关闭期积压不得变成假尖峰), 以及 fmtTps / tokenAxis 的宽度上界(标题栏读数越界会让 pi 退出)。

test/state.test.ts 覆盖开关优先级链(会话选择 → --sysmon → 全局默认 → 内置默认开)、 会话 entry 的倒序扫描,以及 /sysmon 参数解析(globally 不能被当成 global 子命令; 未知输入必须被拒绝,而不是默默 toggle)。

test/extension.test.ts 用桩化的 pi API + 临时配置目录驱动真实的 index.ts, 把这次设计的核心作用域规则钉死:/sysmon off 不得碰配置文件; /sysmon global off 既写默认值、又立即作用于当前会话;新会话继承默认值、 而 /resume 恢复会话自己的选择;headless 下 /sysmon global off 仍然两者都落盘。

验证方法:真机抓帧才是唯一可信的

单元测试能捉住几何与坏值,但捉不住打包/加载类问题 —— 例如扩展在真实 pi 里根本没被加载、或 bundle 残留相对 import。 本项目用 pty 启动真实 pi、用 pyte 回放屏幕来验证, 这一步捉到过多次「单测全绿但真机不显示」的事故。

开发时踩过的坑

这些是实际踩到并修复的,记下来避免重犯:

  1. 宽度算错会让 pi 直接崩溃退出。 pi 的渲染器发现某行超过终端宽度会抛 uncaughtException 并退出。自定义组件必须用 truncateToWidth() 截断, 且不能用 String.slice()(它按字节算,会把 ANSI 转义也算进去)。
  2. 面板高度变化会挪动编辑器,破坏鼠标选区。 若组件在有/无数据时行数不同, 编辑器会上下位移,导致「选中文字后复制不了」。所以高度必须恒定, 无数据时也要占满同样的行数。
  3. 多文件平铺安装会让 pi 启动失败。 见上文安装说明。
  4. yMax <= 0 或数据含 NaN 会算出 NaN 坐标,非空断言 ! 会掩盖这个 问题并在运行时崩溃。所有坐标都要做有限性检查。
  5. 不同扩展的 setStatus 共享同一行,窄终端上会互相挤压截断。

贡献

欢迎 issue 和 PR。跑 npm run check 确保测试与类型检查通过。

也欢迎在 X 上找我:@zzjcoo

致谢

本项目受 bottom(btm)启发。

pi-sysmon 最初的想法就是「把 btm 那种终端里的 braille 折线图搬进 pi」。 不只是视觉上的致敬 —— 本项目把 bottom 当作行为基准,去读它的源码、 用受控宽度抓它的帧,逐字符核对排版:

  • 盲文点阵(Marker::Braille,2×4 子像素)与 Bresenham 连线
  • 百分比图固定 0 .. 100.5、动态图取窗口最大值 × 1.5 的留白
  • y 刻度位置 dy = i * (plotH - 1) / (n - 1)(索引 0 在底部)
  • x 轴线不延伸到 y 轴那一列(ratatui Chart::layout 的 x += 1)
  • 左下时间标签的末位落在 y 轴列上(labels_alignment = Left 的半开区间再右对齐)

也正因为它是个成熟工具,我们才看得出哪里不该照抄 —— 比如本项目把读数放进边框标题栏(bottom 是画在右上角浮框,会吃掉一块绘图区), 以及宽度不够时选择降级排列而不是把 N 张图硬挤成 16 列。 这些偏离都在上文「与 bottom 的排版是怎么对齐的」里逐条记了原因。

感谢 Clement Tsang 和 bottom 的贡献者们。

本项目的另一个前提是 pi 提供的扩展 API —— setWidget 的 placement、message_update 的流式事件、Theme 取色, 没有这些就没有这个扩展。

许可证

MIT —— 随便用。

本项目的灵感与排版算法参照来自 bottom(MIT 许可),但没有复制它的代码: 所有实现都是按它的可观察行为重写的。