Створіть власний циферблат

Створюйте власні циферблати індикаторів на HTML, CSS і JavaScript. Кожен циферблат працює з будь-яким джерелом, від батареї до чисел, які ви надсилаєте самі.

Швидкий старт

  1. Створіть циферблат: У Параметрах клацніть «Створити циферблат…» (Option-Command-N). Multimeter створить папку з початковими файлами, які вже використовують дані версії 2.
  2. Редагуйте файли: Multimeter покаже папку у Finder. Редагуйте файли в будь-якому редакторі коду.
  3. Бачте зміни наживо: Multimeter перезавантажує циферблат щоразу, коли ви зберігаєте файл. Спробуйте його з різними джерелами у вікні «Додати індикатор…» або «Налаштувати цей індикатор…».
  4. Поділіться: Стисніть папку у 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 draws

manifest.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
}
КлючТипЩо означає
namestringПоказується у списку циферблатів і в галереї.
descriptionstringОдин рядок про те, що показує циферблат.
versionstringНомер вашої версії, наприклад 1.0.0.
authorstringВаше ім’я, яке показується в галереї.
apiVersion2Укажіть 2, щоб отримувати дані, описані на цій сторінці.
sourcesstring[]["any"] для циферблата, що може малювати будь-яке джерело, або список ідентифікаторів джерел, як-от ["battery"] чи ["battery", "countdown", "timer"]. Multimeter пропонує ваш циферблат лише для цих джерел.
defaultSourcestringДжерело, вибране, коли хтось уперше додає ваш циферблат, наприклад "cpu".
widthnumberШирина в пунктах. Висота завжди 22.
minWidth / maxWidthnumberНайменша й найбільша ширина, яку користувач може задати у вікні «Налаштувати».
supportsLightMode / supportsDarkModebooleanЧи розрахований циферблат на світлий і темний рядок меню.

Маніфест без 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 (таймери й зворотні відліки), порожній рядок або власна одиниця користувача.
statenormal, warning або critical, залежно від порогів сповіщень, які задав користувач. Розфарбовуйте циферблат відповідно.
tintКолір, вибраний користувачем, наприклад #30D158, або null, щоб використовувати власні кольори циферблата.
historyДо 60 останніх рівнів (від 0 до 1), від найстарішого, для графіків.
warningLevelwarningLevel і criticalLevel розміщують пороги сповіщень на тій самій шкалі від 0 до 1, що й level, а alertDirection вказує, чи спрацьовують сповіщення above чи below них. Усі три дорівнюють null, якщо в індикатора немає сповіщень.
availablefalse, коли джерело зараз не вдається прочитати: немає батареї, аксесуар вимкнено, вебадреса не відповідає. Покажіть риску.
colorSchemeВигляд рядка меню, light або dark. Він залежить від шпалер, а не лише від системного налаштування.
reduceMotiontrue, коли ввімкнено «Зменшення руху». Пропускайте анімації.
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, тож обробляти їх по-різному доводиться рідко.

ДжерелоsourceunitВигляд 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