Zrób własną tarczę
Twórz własne tarcze wskaźników w HTML, CSS i JavaScript. Każda tarcza działa z każdym źródłem, od baterii po liczby, które wysyłasz sam.
Szybki start
- Utwórz tarczę: W Ustawieniach kliknij Utwórz tarczę… (Option-Command-N). Multimeter utworzy folder z plikami startowymi, które już korzystają z danych w wersji 2.
- Edytuj pliki: Multimeter pokaże folder w Finderze. Edytuj pliki w dowolnym edytorze kodu.
- Oglądaj zmiany na żywo: Multimeter przeładowuje tarczę przy każdym zapisie. Wypróbuj ją z różnymi źródłami w oknie Dodaj wskaźnik… lub Dostosuj ten wskaźnik….
- Udostępnij: Skompresuj folder w Finderze, zmień rozszerzenie .zip na
.multimeteri zgłoś go do galerii. Każdy może go dodać przez Importuj… (Command-O) albo dwukrotnie klikając plik.
Jak działa wskaźnik
Wskaźnik na pasku menu to tarcza plus źródło plus ustawienia użytkownika. Tarcza to Twój folder: mała strona internetowa, którą Multimeter wczytuje do przezroczystego widoku o wysokości 22 punktów i szerokości width z manifestu. Źródło to to, co mierzy wskaźnik, wybierane przez użytkownika przy dodawaniu wskaźnika.
Gdy cokolwiek się zmieni, Multimeter ustawia window.meterData i wysyła zdarzenie meterUpdate. Tarcza nie musi wiedzieć, skąd pochodzą liczby: narysuj level (od 0 do 1) i pokaż formatted, a zadziała tak samo dla CPU, sieci, minutnika czy baterii.
Struktura plików
Każda tarcza to folder zawierający:
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
Manifest opisuje tarczę i mówi Multimeter, jaka jest szeroka i do czego służy:
{
"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
}| Klucz | Typ | Znaczenie |
|---|---|---|
name | string | Wyświetlana na liście tarcz i w galerii. |
description | string | Jedno zdanie o tym, co pokazuje tarcza. |
version | string | Numer Twojej wersji, np. 1.0.0. |
author | string | Twoje imię lub nazwa, widoczne w galerii. |
apiVersion | 2 | Ustaw 2, aby dostać dane opisane na tej stronie. |
sources | string[] | ["any"] dla tarczy, która potrafi narysować każde źródło, albo lista identyfikatorów źródeł, np. ["battery"] lub ["battery", "countdown", "timer"]. Multimeter proponuje Twoją tarczę tylko dla tych źródeł. |
defaultSource | string | Źródło wybrane, gdy ktoś po raz pierwszy dodaje Twoją tarczę, np. "cpu". |
width | number | Szerokość w punktach. Wysokość to zawsze 22. |
minWidth / maxWidth | number | Najmniejsza i największa szerokość, jaką użytkownik może ustawić w oknie Dostosuj. |
supportsLightMode / supportsDarkMode | boolean | Czy tarcza jest zaprojektowana na jasny i ciemny pasek menu. |
Manifest bez apiVersion i sources jest traktowany jak tarcza przede wszystkim do baterii, która nadal działa z każdym źródłem, więc wskaźniki zrobione dla wcześniejszych wersji dalej działają.
Dane wskaźnika
Multimeter umieszcza bieżące dane w window.meterData i wysyła je ponownie przy każdej zmianie. Oto wszystko, co zawierają (API w wersji 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
}| Pole | Znaczenie |
|---|---|
source | Identyfikator źródła (patrz tabela niżej). sourceName to jego przetłumaczona nazwa. |
label | Etykieta wskaźnika, np. CPU lub DOWN. Użytkownik może ją zmienić albo wyczyścić, więc może to być pusty ciąg. |
value | Surowy odczyt w unit albo null. Razem z min i max daje pełny obraz. |
level | Wartość umieszczona w zakresie od 0 do 1, przycięta do niego. Większości tarcz wystarczy tylko to. |
formatted | Odczyt jako tekst, z jednostką, sformatowany dla języka użytkownika: 23%, 12.4 W, 1.2 MB/s, 4:12. |
unit | %, W, B/s, rpm, s (minutniki i odliczania), pusty ciąg albo własna jednostka użytkownika. |
state | normal, warning lub critical, według progów alertów ustawionych przez użytkownika. Koloruj tarczę według niego. |
tint | Kolor wybrany przez użytkownika, np. #30D158, albo null, aby użyć własnych kolorów tarczy. |
history | Do 60 ostatnich poziomów (od 0 do 1), od najstarszego, do wykresów. |
warningLevel | warningLevel i criticalLevel umieszczają progi alertów na tej samej skali od 0 do 1 co level, a alertDirection mówi, czy alerty działają above czy below progu. Wszystkie trzy mają wartość null, gdy wskaźnik nie ma alertów. |
available | false, gdy źródła nie da się teraz odczytać: brak baterii, akcesorium jest wyłączone, adres internetowy nie odpowiada. Pokaż kreskę. |
colorScheme | Wygląd paska menu, light lub dark. Zależy od tapety, a nie tylko od ustawienia systemowego. |
reduceMotion | true, gdy włączone jest Ogranicz ruch. Pomiń animacje. |
battery | Bateria Maca, zawsze dostępna, niezależnie od tego, co mierzy wskaźnik: naładowanie, ładowanie, podłączenie do zasilania, pozostały czas, czas do pełna, cykle, kondycja i temperatura. |
Nasłuchiwanie aktualizacji
Narysuj raz to, co już jest w window.meterData, a potem ponownie przy każdym meterUpdate. Ustaw window.meterReady = true, gdy jesteś gotowy na dane.
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;Źródła
To źródła, które może mieć wskaźnik. Tarcza widzi identyfikator w source i dostaje gotowy formatted, więc rzadko trzeba traktować je inaczej.
| Źródło | source | unit | formatted wygląda tak |
|---|---|---|---|
| Bateria | battery | "%" | 82% |
| Akcesorium Bluetooth | bluetooth | "%" | 64% |
| Pobór mocy | power | "W" | 12.4 W |
| CPU | cpu | "%" | 23% |
| Pamięć | memory | "%" | 71% |
| Miejsce na dysku | disk | "%" | 58% |
| Obciążenie termiczne | thermal | "" | Normalne, Podwyższone, Wysokie, Krytyczne |
| Prędkość wentylatora* | fan | "rpm" | 2,400 rpm |
| Sieć | network | "B/s" | 1.2 MB/s, 340 KB/s |
| Minutnik | timer | "s" | 24:51 |
| Odliczanie | countdown | "s" | 3d 4h, 4:12, 12:05 |
| Postęp dnia | progress | "%" | 63% |
| Wysłana wartość | push | "" | 42 |
| Adres internetowy | url | "" | 1,284 |
| Plik | file | "" | 12 |
| Skrypt* | script | "" | 412 |
* Prędkość wentylatora i skrypty wymagają wersji Multimeter do pobrania bezpośrednio. Wersja z Mac App Store nie może odczytywać czujników wentylatorów ani uruchamiać skryptów; użyj w niej wysłanej wartości albo pliku.
Procenty zawsze mieszczą się w zakresie od 0 do 100. Moc, sieć i prędkość wentylatora skalują się do ostatniego szczytu, chyba że użytkownik ustawi zakres. Obciążenie termiczne ma cztery stopnie, więc level wynosi 0, 0.33, 0.67 lub 1. Dla minutników i odliczań level to część pozostałego czasu, a value to pozostałe sekundy.
Alerty i kolory
Każdy wskaźnik może mieć próg ostrzeżenia i próg krytyczny, ustawione przez użytkownika (bateria domyślnie poniżej 20% i 10%, CPU powyżej 70% i 90%, dysk powyżej 85% i 95%). Multimeter sam wylicza state. Tarcza powinna wyglądać spokojnie przy normal, robić się głośniejsza przy warning i critical i używać tint, gdy użytkownik wybrał kolor:
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; }Chcesz narysować strefy alertów na skali, na przykład tarczę zegarową z czerwonym końcem? Użyj warningLevel, criticalLevel i alertDirection.
Historia
history zawiera do 60 ostatnich poziomów, od najstarszego, już na skali od 0 do 1. Dodaj na końcu bieżący level i masz wykres. Przykład Wykres historii poniżej go rysuje.
Szczegóły baterii
Bateria wciąż jest wyjątkowa. data.battery to zawsze bateria Maca, niezależnie od tego, co mierzy wskaźnik, więc każda tarcza może pokazać błyskawicę ładowania. Tarcza przeznaczona tylko dla baterii powinna mieć w manifeście "sources": ["battery"]. timeRemaining i timeToFullCharge są w minutach i mają wartość null, dopóki macOS jeszcze szacuje.
// 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 ?? '');Tarcze pisane dla wersji 1 odczytują percentage, isCharging i formattedTime na najwyższym poziomie. Nadal są wysyłane: dla źródła baterii to wartości samej baterii, a dla innych źródeł isCharging ma wartość false, a formattedTime to czas pozostały na minutniku lub w odliczaniu (h:mm), w przeciwnym razie null.
Twoje własne dane
Cztery źródła pokazują liczby, które dostarczasz sam: wysłana wartość, adres internetowy, plik albo skrypt. Tarcza traktuje je jak każde inne źródło.
Wyślij wartość linkiem
Otwórz link multimeter://set skądkolwiek: z Terminala, skryptu powłoki, Skrótów (Otwórz adresy URL), Raycasta, zadania cron, skryptu budowania. Dodaj wskaźnik ze źródłem Wysłana wartość i tym samym kluczem, a zaktualizuje się w chwili otwarcia linku. open -g zostawia Multimeter w tle.
# 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"| Parametr | Znaczenie |
|---|---|
key | Którego wskaźnika dotyczy. Wielkość liter w kluczach nie ma znaczenia. |
value | Liczba. Wymagana. |
min / max | Zakres, żeby level wyszedł poprawnie. Domyślnie od 0 do 100. |
label | Tekst etykiety wskaźnika. |
unit | Jednostka dodawana po liczbie, np. cups. Użyj % dla procentów. |
formatted | Twój własny tekst dla formatted, używany zamiast liczby. |
Wysłane wartości działają też w wersji z Mac App Store. Multimeter pamięta ostatnią wartość dla każdego klucza.
Adresy internetowe, pliki i skrypty
Wszystkie trzy odczytują ten sam format: zwykłą liczbę albo JSON z value i dowolnymi dodatkami. Wszystko poza value jest opcjonalne.
42
42.5
42%
{"value": 42, "min": 0, "max": 100, "label": "Deploys", "unit": "", "formatted": "42 left"}Wskazujesz na większy JSON, na przykład odpowiedź API? Podaj wskaźnikowi ścieżkę klucza, a sam wyłuska liczbę:
{"data": {"count": 17}} → key path data.count
{"items": [{"value": 9.5}]} → key path items.0.value- Adres internetowy: pobierany w wybranym przez Ciebie odstępie, najczęściej co 30 sekund (domyślnie co 5 minut).
- Plik: odczytywany ponownie przy każdej zmianie. Przydatny dla wszystkiego, co potrafi zapisać plik.
- Skrypt: polecenie powłoki uruchamiane przez
zshw wybranym przez Ciebie odstępie (domyślnie co 30 sekund). Multimeter pokazuje liczbę, którą wypisze. Foldery Homebrew są wPATH, a polecenie, które trwa dłużej niż 10 sekund, zostaje przerwane.
# 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}'Wersja z Mac App Store obsługuje adresy internetowe i pliki, ale nie skrypty. Tam niech Twój skrypt wyśle link albo zapisze plik.
Jasny i ciemny pasek menu
Pasek menu jest jasny albo ciemny w zależności od tapety. Multimeter dodaje light-mode lub dark-mode do body, ustawia colorScheme w danych, a prefers-color-scheme również odpowiada paskowi menu. Użyj tego, co wolisz:
/* 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; }
}Ograniczony ruch
Gdy włączone jest Ogranicz ruch, reduceMotion ma wartość true, a prefers-reduced-motion jest spełnione. Zadbaj o czytelność wskaźnika i zrezygnuj z podskoków, pulsowania i przesunięć:
body.reduce-motion * { transition: none !important; animation: none !important; }
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; animation: none !important; }
}Kompletne przykłady
Dwie kompletne tarcze, działające na żywo na tej stronie na tych samych danych, które wysyła Multimeter. Skopiuj pliki do folderu i zaimportuj go.
Tarcza startowa z Utwórz tarczę
Dokładnie to, co zapisuje Utwórz tarczę…: etykieta, pasek i sformatowana wartość, a w komentarzach opis API. Koloruje się według state, respektuje tint i dopasowuje się do wyglądu paska menu.
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();
}Wykres historii
Wykres liniowy z ostatniej minuty na podstawie history, rysowany na płótnie canvas w rzeczywistej rozdzielczości ekranu.
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;Wskazówki do testowania
- Zmieść wszystko w 22 punktach wysokości i sprawdź tarczę przy szerokości
minWidthimaxWidth. - Używaj czcionki systemowej (
-apple-system) ifont-variant-numeric: tabular-nums, żeby cyfry nie skakały. - Wypróbuj tarczę z kilkoma źródłami w oknach Dodaj wskaźnik… i Dostosuj ten wskaźnik…: z procentem, prędkością sieci, minutnikiem i niedostępnym źródłem.
- Sprawdź jasny i ciemny pasek menu, wybrany kolor oraz stany ostrzeżenia i krytyczny.
- Błędy z Twojego JavaScriptu pojawiają się w Console.app. Możesz też otworzyć
index.htmlw Safari i wysłać dane z konsoli: - Niech pliki będą małe, a połączenia sieciowe zbędne, żeby tarcza rysowała się od razu.
// 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 }));Gotowy do tworzenia?
$19.99 jednorazowo. Bez subskrypcji. Zwrot w ciągu 30 dni.
Kup za $19.99