制作你自己的表盘
用 HTML、CSS 和 JavaScript 制作你自己的仪表表盘。每个表盘都能搭配任何数据源,从电池到你自己发送的数字。
快速开始
- 创建表盘: 在设置中点按“创建表盘…”(Option-Command-N)。Multimeter 会生成一个入门文件夹,里面的文件已经使用第 2 版数据。
- 编辑文件: Multimeter 会在访达中显示这个文件夹。用任何代码编辑器修改里面的文件即可。
- 实时查看改动: 每次存储时,Multimeter 都会重新载入你的表盘。可以在“添加仪表…”或“自定义此仪表…”中换不同的数据源试试。
- 分享: 在访达中压缩文件夹,把 .zip 重命名为
.multimeter,然后提交到图库。任何人都能通过“导入…”(Command-O)或连按文件来添加它。
仪表的工作原理
菜单栏中的仪表由表盘、数据源和用户的设置组成。表盘就是你的文件夹:一个小网页,Multimeter 会把它载入一个 22 点高的透明视图,宽度为清单文件中的 width。数据源是仪表测量的东西,由用户在添加仪表时选择。
只要有任何变化,Multimeter 就会设置 window.meterData 并发送 meterUpdate 事件。你的表盘不需要知道数字从哪里来:绘制 level(0 到 1)并显示 formatted,CPU、网络、计时器或电池都一样适用。
文件结构
每个表盘都是一个文件夹,包含:
my-meter/
├── manifest.json # name, size, which sources it's made for
├── index.html # the page Multimeter loads
├── styles.css
└── meter.js # listens for meterUpdate and drawsmanifest.json
清单文件描述你的表盘,并告诉 Multimeter 它有多宽、为什么而做:
{
"name": "My Meter",
"description": "What it shows, in one line.",
"version": "1.0.0",
"author": "Your Name",
"apiVersion": 2,
"sources": ["any"],
"defaultSource": "cpu",
"width": 60,
"minWidth": 40,
"maxWidth": 100,
"supportsLightMode": true,
"supportsDarkMode": true
}| 键 | 类型 | 含义 |
|---|---|---|
name | string | 显示在表盘列表和仪表库中。 |
description | string | 用一行话说明表盘显示什么。 |
version | string | 你的版本号,比如 1.0.0。 |
author | string | 你的名字,显示在仪表库中。 |
apiVersion | 2 | 设为 2,即可使用本页介绍的数据。 |
sources | string[] | 能绘制任何数据源的表盘用 ["any"],否则填写数据源 ID 列表,比如 ["battery"] 或 ["battery", "countdown", "timer"]。Multimeter 只会在这些数据源下提供你的表盘。 |
defaultSource | string | 别人第一次添加你的表盘时选中的数据源,比如 "cpu"。 |
width | number | 以点为单位的宽度。高度始终为 22。 |
minWidth / maxWidth | number | 用户在“自定义”中能设置的最窄和最宽宽度。 |
supportsLightMode / supportsDarkMode | boolean | 表盘是否为浅色和深色菜单栏设计。 |
没有 apiVersion 和 sources 的清单文件,会被视为以电池为主、但仍能搭配任何数据源的表盘,所以为早期版本制作的仪表仍然能用。
仪表数据
Multimeter 会把当前数据放在 window.meterData 中,每次变化时再发送一次。里面的全部内容如下(API 第 2 版):
window.meterData = {
apiVersion: 2,
source: "cpu", // what this meter measures
sourceName: "CPU", // that source's name, translated
label: "CPU", // the meter's label ("" if the person cleared it)
value: 23.4, // raw value in `unit` (null if unavailable)
min: 0, max: 100, // the range `level` is measured against
level: 0.234, // value placed in the range, 0 to 1
formatted: "23%", // ready to show, unit included, in the person's language
unit: "%", // "%", "W", "B/s", "rpm", "s", or your own
state: "normal", // "normal", "warning" or "critical"
tint: null, // "#30D158" if the person picked a color
history: [0.21, 0.25], // up to 60 recent levels, oldest first
warningLevel: 0.7, // alert levels on the same 0 to 1 scale (or null)
criticalLevel: 0.9,
alertDirection: "above", // "above" or "below" (or null)
available: true, // false when the source can't be read right now
colorScheme: "dark", // the menu bar: "light" or "dark"
reduceMotion: false, // System Settings > Accessibility > Reduce motion
strings: { timeLeft: "left" },
battery: { // always the Mac's battery, whatever the source
level: 0.82, percentage: 82, isCharging: false, isPluggedIn: false,
hasBattery: true, timeRemaining: 312, timeToFullCharge: null,
formattedTime: "5:12", cycleCount: 212, health: 0.94, temperature: 30.5
},
// Version 1 fields, so battery meters keep working with any source:
percentage: 23, isCharging: false, isPluggedIn: false,
timeRemaining: null, timeToFullCharge: null, formattedTime: null,
cycleCount: 212, health: 0.94, temperature: 30.5, hasBattery: true
}| 字段 | 含义 |
|---|---|
source | 数据源 ID(见下表)。sourceName 是它的本地化名称。 |
label | 仪表的标签,比如 CPU 或 DOWN。用户可以重命名或清空它,所以可能是空字符串。 |
value | 以 unit 为单位的原始读数,或 null。配合 min 和 max 就能看到全貌。 |
level | 数值在 0 到 1 范围内的位置,超出部分会被截断。大多数表盘只需要这个。 |
formatted | 带单位的读数文本,按用户的语言格式化:23%、12.4 W、1.2 MB/s、4:12。 |
unit | %、W、B/s、rpm、s(计时器和倒计时)、空字符串,或用户自己的单位。 |
state | 根据用户设置的提醒阈值,取值为 normal、warning 或 critical。可据此给表盘着色。 |
tint | 用户选择的颜色,比如 #30D158;为 null 时使用表盘自己的颜色。 |
history | 最多 60 个最近的比例值(0 到 1),从旧到新,用于绘制图表。 |
warningLevel | warningLevel 和 criticalLevel 把提醒阈值放在与 level 相同的 0 到 1 刻度上,alertDirection 说明提醒是在高于(above)还是低于(below)阈值时触发。仪表没有设置提醒时,三者都为 null。 |
available | 暂时无法读取数据源时为 false:没有电池、配件已关闭、网址无法访问等。此时显示一个短横线即可。 |
colorScheme | 菜单栏的外观,light 或 dark。它跟随墙纸,而不只是系统设置。 |
reduceMotion | 开启“减弱动态效果”时为 true。请跳过动画。 |
battery | Mac 的电池,无论仪表测量什么都始终提供:电量、是否充电、是否接通电源、剩余时间、充满所需时间、循环计数、健康和温度。 |
监听更新
先用 window.meterData 中已有的数据绘制一次,之后每次收到 meterUpdate 时再重绘。准备好接收数据时,设置 window.meterReady = true。
function render(data) {
// draw the meter from data.level, data.formatted, data.state ...
}
window.addEventListener('meterUpdate', (e) => render(e.detail));
render(window.meterData || { level: 0.5, formatted: '50%', state: 'normal' });
window.meterReady = true;数据源
以下是仪表可用的数据源。你的表盘会在 source 中看到 ID,formatted 也已写好,所以很少需要区别对待。
| 数据源 | source | unit | formatted 示例 |
|---|---|---|---|
| 电池 | battery | "%" | 82% |
| 蓝牙配件 | bluetooth | "%" | 64% |
| 功耗 | power | "W" | 12.4 W |
| CPU | cpu | "%" | 23% |
| 内存 | memory | "%" | 71% |
| 磁盘空间 | disk | "%" | 58% |
| 热压力 | thermal | "" | 正常, 偏高, 严重, 危急 |
| 风扇转速* | fan | "rpm" | 2,400 rpm |
| 网络 | network | "B/s" | 1.2 MB/s, 340 KB/s |
| 计时器 | timer | "s" | 24:51 |
| 倒计时 | countdown | "s" | 3d 4h, 4:12, 12:05 |
| 时间进度 | progress | "%" | 63% |
| 发送的数值 | push | "" | 42 |
| 网址 | url | "" | 1,284 |
| 文件 | file | "" | 12 |
| 脚本* | script | "" | 412 |
* 风扇转速和脚本需要 Multimeter 的直接下载版。Mac App Store 版无法读取风扇传感器,也无法运行你的脚本;在那里请改用发送的数值或文件。
百分比始终是 0 到 100。功耗、网络和风扇转速会按最近的峰值自动缩放,除非用户设置了范围。热压力有四档,所以 level 为 0、0.33、0.67 或 1。对于计时器和倒计时,level 是剩余时间的占比,value 是剩余秒数。
提醒与颜色
每个仪表都可以由用户设置警告和危急阈值(默认:电池低于 20% 和 10%,CPU 高于 70% 和 90%,磁盘高于 85% 和 95%)。state 由 Multimeter 为你算好。表盘在 normal 时应显得平静,进入 warning 和 critical 后越来越醒目;用户选了颜色时请使用 tint:
document.body.classList.remove('warning', 'critical');
if (data.state !== 'normal') document.body.classList.add(data.state);
// The person's color wins over the face's own
if (data.tint) document.body.style.setProperty('--accent', data.tint);
else document.body.style.removeProperty('--accent');body.warning { --accent: #ff9f0a; }
body.critical { --accent: #ff453a; }想在刻度上画出提醒区间,比如一端是红色的表盘?使用 warningLevel、criticalLevel 和 alertDirection。
历史记录
history 保存最多 60 个最近的比例值,从旧到新,已经是 0 到 1 的刻度。在末尾加上当前的 level,就是一张图表。下面的“历史图表”示例就画了一张。
电池细节
电池依然特别。无论仪表测量什么,data.battery 始终是 Mac 的电池,所以任何表盘都能显示充电闪电图标。只为电池制作的表盘,应在清单文件中写明 "sources": ["battery"]。timeRemaining 和 timeToFullCharge 以分钟为单位,macOS 仍在估算时为 null。
// A battery-only face: "sources": ["battery"] in manifest.json
const b = data.battery;
bolt.hidden = !b.isCharging;
time.textContent = b.isCharging
? (b.timeToFullCharge ? `${b.timeToFullCharge} min to full` : '')
: (b.formattedTime ?? '');为第 1 版编写的表盘会读取顶层的 percentage、isCharging 和 formattedTime。这些仍会发送:对电池数据源来说它们是电池自身的值;对其他数据源,isCharging 为 false,formattedTime 是计时器或倒计时的剩余时间(h:mm),否则为 null。
你自己的数据
有四种数据源显示你提供的数字:发送的数值、网址、文件或脚本。你的表盘像对待其他数据源一样对待它们。
用链接发送数值
在任何地方打开 multimeter://set 链接:Terminal、shell 脚本、快捷指令(打开 URL)、Raycast、cron 任务、构建脚本。用“发送的数值”数据源和相同的键添加一个仪表,链接一打开它就会更新。open -g 可让 Multimeter 留在后台。
# Set a value (the meter shows the "deploys" key)
open -g "multimeter://set?key=deploys&value=42"
# Range, label, unit and your own text ride along
open -g "multimeter://set?key=coffee&value=3&min=0&max=5&label=Coffee&unit=cups"
open -g "multimeter://set?key=build&value=0.8&max=1&formatted=80%25%20done"| 参数 | 含义 |
|---|---|
key | 发给哪个仪表。键不区分大小写。 |
value | 数字。必填。 |
min / max | 范围,让 level 计算正确。默认为 0 到 100。 |
label | 仪表标签的文本。 |
unit | 加在数字后面的单位,比如 cups。百分比请用 %。 |
formatted | 你自己的 formatted 文本,用来代替数字显示。 |
发送的数值在 Mac App Store 版中也能用。Multimeter 会记住每个键的最后一个数值。
网址、文件和脚本
三者读取相同的输出:一个纯数字,或包含 value 及任意附加项的 JSON。除 value 外都是可选的。
42
42.5
42%
{"value": 42, "min": 0, "max": 100, "label": "Deploys", "unit": "", "formatted": "42 left"}指向更大的 JSON,比如 API 响应?给仪表设置一个键路径,它就会把数字挑出来:
{"data": {"count": 17}} → key path data.count
{"items": [{"value": 9.5}]} → key path items.0.value- 网址:按你选择的间隔获取,最快每 30 秒一次(不更改则每 5 分钟一次)。
- 文件:文件一有变化就重新读取。任何能写文件的东西都能方便地使用。
- 脚本:按你选择的间隔用
zsh运行的 shell 命令(不更改则每 30 秒一次)。Multimeter 会显示它输出的数字。Homebrew 的文件夹已在PATH中,运行超过 10 秒的命令会被停止。
# Open pull requests waiting on you (direct download version)
gh pr list --search "review-requested:@me" --json number --jq length
# Free memory pressure as a percentage
memory_pressure | awk '/percentage/ {print 100 - $5}'Mac App Store 版可以使用网址和文件,但不能使用脚本。在那里,请让你的脚本发送链接或写入文件。
浅色和深色菜单栏
菜单栏会随墙纸变浅或变深。Multimeter 会给 body 加上 light-mode 或 dark-mode,在数据中设置 colorScheme,prefers-color-scheme 也会与菜单栏一致。用哪种都行:
/* Dark menu bar (the default) */
body { color: white; }
/* Light menu bar */
body.light-mode { color: black; }
/* Same thing with the media query, which follows the menu bar too */
@media (prefers-color-scheme: light) {
body { color: black; }
}减弱动态效果
开启“减弱动态效果”时,reduceMotion 为 true,prefers-reduced-motion 也会匹配。保持仪表清晰易读,去掉弹跳、脉动和扫动:
body.reduce-motion * { transition: none !important; animation: none !important; }
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; animation: none !important; }
}完整示例
两个完整的表盘,用与 Multimeter 发送的相同数据在本页面上实时运行。把文件复制到一个文件夹中并导入即可。
“创建表盘”的入门表盘
正是“创建表盘…”生成的内容:一个标签、一个进度条和格式化后的数值,注释里附有 API 说明。它按 state 着色,遵循 tint,并跟随菜单栏的外观。
starter/manifest.json
{
"name": "My Face",
"description": "A label, a bar and the value. Works with any source.",
"version": "1.0.0",
"author": "Your Name",
"apiVersion": 2,
"sources": ["any"],
"defaultSource": "cpu",
"width": 70,
"minWidth": 50,
"maxWidth": 120,
"supportsLightMode": true,
"supportsDarkMode": true
}starter/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="meter">
<span id="label"></span>
<span id="track"><span id="fill"></span></span>
<span id="value">--</span>
</div>
<script src="meter.js"></script>
</body>
</html>starter/styles.css
* { margin: 0; padding: 0; box-sizing: border-box; }
/* The meter is 22 points tall and as wide as manifest.json says. */
body {
font: 600 11px/1 -apple-system, BlinkMacSystemFont, sans-serif;
font-variant-numeric: tabular-nums;
color: white; /* dark menu bar */
background: transparent;
height: 22px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
--accent: #30d158; /* replaced by the meter's color, if one is set */
}
/* Light menu bar: the system setting, or the preview's light mode. */
#track { background: rgba(255, 255, 255, 0.25); }
@media (prefers-color-scheme: light) { body { color: black; } #track { background: rgba(0, 0, 0, 0.18); } }
body.light-mode { color: black; }
body.light-mode #track { background: rgba(0, 0, 0, 0.18); }
body.dark-mode { color: white; }
body.dark-mode #track { background: rgba(255, 255, 255, 0.25); }
#meter { display: flex; align-items: center; gap: 4px; }
#label { font-size: 8px; letter-spacing: 0.04em; opacity: 0.7; }
#track { width: 18px; height: 6px; border-radius: 3px; position: relative; overflow: hidden; }
#fill { position: absolute; inset: 0 auto 0 0; width: 0; background: var(--accent); border-radius: 3px; transition: width 0.4s ease; }
/* state is "normal", "warning" or "critical" (from the meter's alert levels). */
body.warning { --accent: #ffd60a; }
body.critical { --accent: #ff453a; }
@media (prefers-reduced-motion: reduce) { #fill { transition: none; } }starter/meter.js
// Multimeter meter API, version 2. The app sets window.meterData and fires
// a 'meterUpdate' event whenever anything changes. The fields you'll use most:
//
// level 0.0 to 1.0, the value within the meter's range
// formatted ready-to-show text with its unit: "23%", "1.2 MB/s", "4:12"
// label short label ("CPU"), may be "" if hidden
// value, unit, min, max the raw number and its range
// state "normal", "warning" or "critical"
// tint the color picked in Customize ("#30D158"), or null
// history up to 60 recent levels (0 to 1), oldest first, for graphs
// source what it measures: "battery", "cpu", "memory", "network", "timer"...
// available false when there's nothing to read (no battery, URL failing)
// battery the Mac's battery, always: level, isCharging, isPluggedIn,
// hasBattery, timeRemaining, timeToFullCharge, health...
// isCharging true only when this meter shows the battery and it's charging
// colorScheme "light" or "dark"; reduceMotion true if animations should stop
//
// Full reference: https://multimeterapp.com/creating-meters
class Meter {
constructor() {
this.label = document.getElementById('label');
this.fill = document.getElementById('fill');
this.value = document.getElementById('value');
window.addEventListener('meterUpdate', (e) => this.render(e.detail));
if (window.meterData) this.render(window.meterData);
window.meterReady = true;
}
render(data) {
const level = Math.max(0, Math.min(1, data.level || 0));
this.label.textContent = data.label || '';
this.label.style.display = data.label ? '' : 'none';
this.value.textContent = data.available === false ? '\u2013' : (data.formatted || Math.round(level * 100) + '%');
this.fill.style.width = (level * 100) + '%';
document.body.classList.toggle('warning', data.state === 'warning');
document.body.classList.toggle('critical', data.state === 'critical');
if (data.tint) document.body.style.setProperty('--accent', data.tint);
else document.body.style.removeProperty('--accent');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => new Meter());
} else {
new Meter();
}历史图表
用 history 绘制最近一分钟的折线图,以屏幕的真实分辨率画在 canvas 上。
history-graph/manifest.json
{
"name": "History Graph",
"description": "The last minute as a little line graph.",
"version": "1.0.0",
"author": "Your Name",
"apiVersion": 2,
"sources": ["any"],
"defaultSource": "network",
"width": 44,
"minWidth": 30,
"maxWidth": 80,
"supportsLightMode": true,
"supportsDarkMode": true
}history-graph/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
html, body { margin: 0; height: 22px; background: transparent; overflow: hidden; }
canvas { display: block; width: 100%; height: 22px; }
</style>
</head>
<body>
<canvas id="graph"></canvas>
<script src="meter.js"></script>
</body>
</html>history-graph/meter.js
const canvas = document.getElementById('graph');
const ctx = canvas.getContext('2d');
function render(data) {
// Draw at the screen's real resolution so the line stays sharp
const scale = window.devicePixelRatio || 1;
const w = canvas.clientWidth, h = 22;
canvas.width = w * scale;
canvas.height = h * scale;
ctx.setTransform(scale, 0, 0, scale, 0, 0);
ctx.clearRect(0, 0, w, h);
const points = (data.history || []).concat(data.level ?? 0);
if (points.length < 2) return;
const dark = data.colorScheme !== 'light';
const color = data.tint
|| (data.state === 'critical' ? '#ff453a' : data.state === 'warning' ? '#ff9f0a' : dark ? '#fff' : '#1d1d1f');
const x = (i) => (i / (points.length - 1)) * (w - 2) + 1;
const y = (v) => h - 3 - v * (h - 6);
// Soft fill under the line, then the line
ctx.beginPath();
points.forEach((v, i) => (i ? ctx.lineTo(x(i), y(v)) : ctx.moveTo(x(i), y(v))));
ctx.lineTo(x(points.length - 1), h);
ctx.lineTo(x(0), h);
ctx.closePath();
ctx.globalAlpha = 0.22;
ctx.fillStyle = color;
ctx.fill();
ctx.globalAlpha = 1;
ctx.beginPath();
points.forEach((v, i) => (i ? ctx.lineTo(x(i), y(v)) : ctx.moveTo(x(i), y(v))));
ctx.strokeStyle = color;
ctx.lineWidth = 1.5;
ctx.lineJoin = 'round';
ctx.stroke();
}
window.addEventListener('meterUpdate', (e) => render(e.detail));
render(window.meterData || { history: [0.2, 0.4, 0.3, 0.6, 0.5], level: 0.7 });
window.meterReady = true;测试技巧
- 让所有内容保持在 22 点高度以内,并在
minWidth和maxWidth下检查你的表盘。 - 使用系统字体(
-apple-system)和font-variant-numeric: tabular-nums,数字就不会跳动。 - 在“添加仪表…”和“自定义此仪表…”中用几种数据源试试你的表盘:百分比、网速、计时器、不可用的数据源。
- 检查浅色和深色菜单栏、着色,以及警告和危急状态。
- JavaScript 的错误会显示在 Console.app 中。你也可以在 Safari 中打开
index.html,从控制台向它发送数据: - 保持文件小巧,避免网络请求,让表盘立刻绘制出来。
// Paste in Safari's console with your index.html open to try a value
window.meterData = { level: 0.92, formatted: '92%', label: 'CPU', state: 'critical',
available: true, colorScheme: 'dark', history: [] };
window.dispatchEvent(new CustomEvent('meterUpdate', { detail: window.meterData }));准备好动手了吗?
$19.99 一次性购买。无需订阅。30 天内可退款。
以 $19.99 购买