自分だけのフェイスを作る
HTML、CSS、JavaScript で、自分だけのメーターのフェイスを作りましょう。どのフェイスも、バッテリーから自分で送る数字まで、すべてのソースで使えます。
クイックスタート
- フェイスを作成: 設定で「フェイスを作成…」(Option-Command-N)をクリックします。Multimeter が、バージョン 2 のデータをすでに使うひな形ファイルのフォルダを作ります。
- ファイルを編集する: Multimeter がフォルダを Finder で表示します。好きなコードエディタでファイルを編集してください。
- 変更をすぐに確認: 保存するたびに Multimeter がフェイスを再読み込みします。「メーターを追加…」や「このメーターをカスタマイズ…」で、いろいろなソースで試してみてください。
- 共有する: Finder でフォルダを圧縮し、.zip の名前を
.multimeterに変えて、ギャラリーに投稿します。誰でも「読み込む…」(Command-O)か、ファイルをダブルクリックして追加できます。
メーターのしくみ
メニューバーのメーターは、フェイスとソース、そしてユーザーの設定でできています。フェイスはあなたのフォルダです。Multimeter はこの小さな Web ページを、高さ 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 | フェイスが何を表示するかを 1 行で。 |
version | string | バージョン番号。たとえば 1.0.0。 |
author | string | あなたの名前。ギャラリーに表示されます。 |
apiVersion | 2 | このページで説明しているデータを使うには 2 を指定します。 |
sources | string[] | どのソースでも描けるフェイスなら ["any"]、そうでなければ ["battery"] や ["battery", "countdown", "timer"] のようなソース ID のリスト。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)にアラートが出るかを示します。メーターにアラートがない場合、3 つとも null です。 |
available | ソースをいま読み取れないときは false: バッテリーがない、アクセサリの電源が切れている、Web アドレスにアクセスできない、など。ダッシュを表示しましょう。 |
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 アクセサリ | 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 |
| Web アドレス | url | "" | 1,284 |
| ファイル | file | "" | 12 |
| スクリプト* | script | "" | 412 |
* ファン速度とスクリプトには、Multimeter の直接ダウンロード版が必要です。Mac App Store 版ではファンのセンサーを読んだりスクリプトを実行したりできないので、代わりに送信した値やファイルを使ってください。
パーセントは常に 0 から 100 です。電力、ネットワーク、ファン速度は、ユーザーが範囲を設定しない限り、最近のピークに合わせて目盛りが変わります。熱圧力は 4 段階なので、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 です。
自分のデータ
4 つのソースは、あなたが用意した数字を表示します: 送信した値、Web アドレス、ファイル、スクリプト。フェイスはほかのソースと同じように扱います。
リンクで値を送る
multimeter://set リンクを、Terminal、シェルスクリプト、ショートカット(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 はキーごとに最後の値を覚えています。
Web アドレス、ファイル、スクリプト
3 つとも同じ出力を読みます: プレーンな数字か、value と任意の追加項目を含む JSON です。value 以外はすべて省略できます。
42
42.5
42%
{"value": 42, "min": 0, "max": 100, "label": "Deploys", "unit": "", "formatted": "42 left"}API のレスポンスのような大きな JSON を指定するときは、メーターにキーパスを指定すれば、数字を取り出します:
{"data": {"count": 17}} → key path data.count
{"items": [{"value": 9.5}]} → key path items.0.value- Web アドレス: 指定した間隔で取得します。最短で 30 秒ごと(変更しなければ 5 分ごと)。
- ファイル: ファイルが変わるたびに読み直します。ファイルを書き出せるものなら何でも使えて便利です。
- スクリプト: 指定した間隔で
zshで実行するシェルコマンド(変更しなければ 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 版では Web アドレスとファイルは使えますが、スクリプトは使えません。その場合は、スクリプトからリンクを送るか、ファイルに書き出してください。
ライトとダークのメニューバー
メニューバーは壁紙によってライトにもダークにもなります。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; }
}完全なサンプル
完成したフェイスが 2 つ、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 から直近 1 分間の折れ線グラフを、画面の実際の解像度で 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 に表示されます。
index.htmlを Safari で開き、コンソールからデータを送ることもできます: - ファイルは小さく保ち、ネットワークへのアクセスは避けて、フェイスがすぐに描かれるようにしましょう。
// 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で購入