Створіть власний циферблат
Створюйте власні циферблати індикаторів на HTML, CSS і JavaScript. Кожен циферблат працює з будь-яким джерелом, від батареї до чисел, які ви надсилаєте самі.
Швидкий старт
- Створіть циферблат: У Параметрах клацніть «Створити циферблат…» (Option-Command-N). Multimeter створить папку з початковими файлами, які вже використовують дані версії 2.
- Редагуйте файли: Multimeter покаже папку у Finder. Редагуйте файли в будь-якому редакторі коду.
- Бачте зміни наживо: Multimeter перезавантажує циферблат щоразу, коли ви зберігаєте файл. Спробуйте його з різними джерелами у вікні «Додати індикатор…» або «Налаштувати цей індикатор…».
- Поділіться: Стисніть папку у Finder, перейменуйте .zip на
.multimeterі надішліть до галереї. Будь-хто зможе додати його через «Імпортувати…» (Command-O) або подвійним клацанням файлу.
Як працює індикатор
Індикатор у рядку меню це циферблат плюс джерело плюс налаштування користувача. Циферблат це ваша папка: маленька вебсторінка, яку Multimeter завантажує в прозоре подання заввишки 22 пункти й завширшки width з вашого маніфесту. Джерело це те, що вимірює індикатор; його вибирає користувач, коли додає індикатор.
Щойно щось змінюється, Multimeter задає window.meterData і надсилає подію meterUpdate. Циферблату не потрібно знати, звідки беруться числа: малюйте level (від 0 до 1) і показуйте formatted, і це однаково працюватиме для ЦП, мережі, таймера чи батареї.
Структура файлів
Кожен циферблат це папка, у якій є:
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"] для циферблата, що може малювати будь-яке джерело, або список ідентифікаторів джерел, як-от ["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 | Ідентифікатор джерела (див. таблицю нижче). 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 розміщують пороги сповіщень на тій самій шкалі від 0 до 1, що й level, а 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 і отримує вже готовий formatted, тож обробляти їх по-різному доводиться рідко.
| Джерело | source | unit | Вигляд formatted |
|---|---|---|---|
| Батарея | battery | "%" | 82% |
| Bluetooth-аксесуар | bluetooth | "%" | 64% |
| Споживання енергії | power | "W" | 12.4 W |
| ЦП | 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%, для ЦП вище 70% і 90%, для диска вище 85% і 95%). Multimeter сам обчислює state. Циферблат має виглядати спокійно в стані 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 подаються в хвилинах і дорівнюють null, поки macOS ще оцінює.
// 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 звідусіль: з Термінала, скрипту оболонки, Швидких команд («Відкрити 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 запам’ятовує останнє значення для кожного ключа.
Вебадреси, файли й скрипти
Усі три читають однаковий вивід: просте число або JSON із value і будь-якими додатковими полями. Усе, крім 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запускає з вибраним вами інтервалом (раз на 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 додає light-mode або dark-mode до body, задає 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. Також можна відкрити
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