pi-sysmon
v0.7.0
Published
bottom-style braille system monitor charts (CPU / memory / network / LLM token throughput) for the pi coding agent
Maintainers
Readme
pi-sysmon
在 pi 里显示 bottom 风格的 braille 折线图
CPU · 内存 · 网络 · Tokens —— 用盲文点阵字符画的实时历史曲线
150 列宽下的默认四图。曲线由盲文字符绘制,y 刻度叠印在绘图区内侧、 时间标签嵌在下边框里。
特性
- 真正的折线图,不是进度条或 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,再窄就变 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:[email protected]
# 或直接装 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_INTERVAL | 1000 | 采样间隔(毫秒,下限 500) |
| PI_SYSMON_POINTS | 60 | 历史点数(覆盖 PI_SYSMON_WINDOW 换算出的点数) |
| PI_SYSMON_CHART_HEIGHT | 6 | 每张图的绘图行数(不含上/下边框那 2 行) |
| PI_SYSMON_LABEL | title | 读数位置:title(边框标题栏)/ box(右上角浮框)/ both / none |
| PI_SYSMON_WINDOW | 60 | 横轴时间窗长度(秒) |
| PI_SYSMON_SCALE_WINDOW | 1(= 量程窗 == 显示窗) | 速率图量程取样比例:1 = y 轴顶端为整窗真实最高值;设 <1(如 1/6)开启「尖峰过去约 10s 后自动回落」(此时超出量程的尖峰会显示为 + 标记) |
| PI_SYSMON_MODE | chart | 初始模式(chart / line / footer) |
| PI_SYSMON_PLACEMENT | belowEditor | chart / 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 在底部)。
三处有意偏离:
读数默认写在边框标题栏里(
PI_SYSMON_LABEL=title),而不是 bottom 的右上角浮框。 标题栏那一行本来就要写块名,剩下的─填充是纯装饰 —— 拿来放读数零成本, 且不遮任何曲线。bottom 的浮框是覆盖画在绘图区右上角的,会真的吃掉一块绘图区。 想换回 bottom 那种浮框就设PI_SYSMON_LABEL=box。浮框显隐阈值(仅在
box/both模式生效)。bottom 用hidden_legend_constraints这套按比例的阈值(Network 是 9/10 × 3/4),但它是按 40+ 列宽的图校准的, 套到本项目 20~30 列的块上会导致读数框永远不显示。 本项目改成「放得下(legendW <= plotW)且浮框下面还留得出一行曲线(legendH < rows)」—— 后者是为了避免浮框下边框与绘图区底部的 0% 基线叠成一条双横线。读数是分级片段,块窄时从尾部逐段丢弃。比如 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 回放屏幕来验证,
这一步捉到过多次「单测全绿但真机不显示」的事故。
开发时踩过的坑
这些是实际踩到并修复的,记下来避免重犯:
- 宽度算错会让 pi 直接崩溃退出。 pi 的渲染器发现某行超过终端宽度会抛
uncaughtException并退出。自定义组件必须用truncateToWidth()截断, 且不能用String.slice()(它按字节算,会把 ANSI 转义也算进去)。 - 面板高度变化会挪动编辑器,破坏鼠标选区。 若组件在有/无数据时行数不同, 编辑器会上下位移,导致「选中文字后复制不了」。所以高度必须恒定, 无数据时也要占满同样的行数。
- 多文件平铺安装会让 pi 启动失败。 见上文安装说明。
yMax <= 0或数据含NaN会算出NaN坐标,非空断言!会掩盖这个 问题并在运行时崩溃。所有坐标都要做有限性检查。- 不同扩展的
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 许可),但没有复制它的代码: 所有实现都是按它的可观察行为重写的。
