Crea tu propia esfera
Crea tus propias esferas de medidor con HTML, CSS y JavaScript. Cada esfera funciona con cualquier fuente, desde la batería hasta los números que tú mismo le envíes.
Inicio rápido
- Crea una esfera: En Ajustes, haz clic en Crear esfera… (Opción + Comando + N). Multimeter crea una carpeta con archivos de inicio que ya usan los datos de la versión 2.
- Edita los archivos: Multimeter muestra la carpeta en el Finder. Edita los archivos con cualquier editor de código.
- Mira los cambios en vivo: Multimeter recarga tu esfera cada vez que guardas. Pruébala con distintas fuentes en Añadir medidor… o Personalizar este medidor….
- Compártelo: Comprime la carpeta en el Finder, cambia la extensión .zip por
.multimetery envíala a la galería. Cualquiera puede añadirla con Importar… (Comando + O) o haciendo doble clic en el archivo.
Cómo funciona un medidor
Un medidor en la barra de menús es una esfera más una fuente más los ajustes de la persona. La esfera es tu carpeta: una pequeña página web que Multimeter carga en una vista transparente de 22 puntos de alto y tan ancha como el width de tu manifest. La fuente es lo que mide el medidor, y la elige la persona al añadirlo.
Cada vez que algo cambia, Multimeter actualiza window.meterData y envía un evento meterUpdate. Tu esfera no necesita saber de dónde vienen los números: dibuja level (de 0 a 1) y muestra formatted, y funcionará igual para la CPU, la red, un temporizador o la batería.
Estructura de archivos
Cada esfera es una carpeta que contiene:
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
El manifest describe tu esfera y le dice a Multimeter qué ancho tiene y para qué está hecha:
{
"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
}| Clave | Tipo | Qué significa |
|---|---|---|
name | string | Se muestra en la lista de esferas y en la galería. |
description | string | Una línea sobre lo que muestra la esfera. |
version | string | Tu número de versión, como 1.0.0. |
author | string | Tu nombre, que aparece en la galería. |
apiVersion | 2 | Ponlo en 2 para los datos descritos en esta página. |
sources | string[] | ["any"] para una esfera que puede dibujar cualquier fuente, o una lista de ids de fuente como ["battery"] o ["battery", "countdown", "timer"]. Multimeter solo ofrece tu esfera para esas fuentes. |
defaultSource | string | La fuente elegida cuando alguien añade tu esfera por primera vez, como "cpu". |
width | number | Ancho en puntos. La altura siempre es 22. |
minWidth / maxWidth | number | El ancho mínimo y máximo que la persona puede darle en Personalizar. |
supportsLightMode / supportsDarkMode | boolean | Si la esfera está diseñada para barras de menús claras y oscuras. |
Un manifest sin apiVersion ni sources se trata como una esfera pensada para la batería que sigue funcionando con cualquier fuente, así que los medidores hechos para versiones anteriores siguen funcionando.
Datos del medidor
Multimeter pone los datos actuales en window.meterData y los vuelve a enviar con cada cambio. Esto es todo lo que contiene (versión 2 de la API):
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
}| Campo | Qué significa |
|---|---|
source | El id de la fuente (ver la tabla de abajo). sourceName es su nombre traducido. |
label | La etiqueta del medidor, como CPU o DOWN. La persona puede cambiarla o borrarla, así que puede ser una cadena vacía. |
value | La lectura sin procesar en unit, o null. Junto con min y max te da el panorama completo. |
level | El valor situado en el rango de 0 a 1, limitado a ese rango. La mayoría de las esferas solo necesitan esto. |
formatted | La lectura como texto, con la unidad incluida y formateada para el idioma de la persona: 23%, 12.4 W, 1.2 MB/s, 4:12. |
unit | %, W, B/s, rpm, s (temporizadores y cuentas atrás), una cadena vacía o la unidad que defina la persona. |
state | normal, warning o critical, según los niveles de alerta que definió la persona. Úsalo para elegir tus colores. |
tint | Un color como #30D158 que eligió la persona, o null para usar los colores propios de tu esfera. |
history | Hasta 60 niveles recientes (de 0 a 1), del más antiguo al más nuevo, para gráficas. |
warningLevel | warningLevel y criticalLevel sitúan los niveles de alerta en la misma escala de 0 a 1 que level, y alertDirection indica si las alertas saltan por encima (above) o por debajo (below). Los tres son null cuando el medidor no tiene alertas. |
available | false cuando la fuente no se puede leer en este momento: no hay batería, el accesorio está apagado, una dirección web está fallando. Muestra un guion. |
colorScheme | El aspecto de la barra de menús, light o dark. Sigue al fondo de pantalla, no solo al ajuste del sistema. |
reduceMotion | true cuando Reducir movimiento está activado. Omite las animaciones. |
battery | La batería del Mac, siempre presente mida lo que mida el medidor: carga, si está cargando, si está conectado, tiempo restante, tiempo hasta la carga completa, ciclos, salud y temperatura. |
Escuchar actualizaciones
Dibuja una vez con lo que ya haya en window.meterData y otra vez con cada meterUpdate. Pon window.meterReady = true cuando estés listo para recibir datos.
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;Fuentes
Estas son las fuentes que puede tener un medidor. Tu esfera ve el id en source y recibe formatted ya escrito, así que rara vez necesitas tratarlas de forma distinta.
| Fuente | source | unit | formatted se ve así |
|---|---|---|---|
| Batería | battery | "%" | 82% |
| Accesorio Bluetooth | bluetooth | "%" | 64% |
| Consumo | power | "W" | 12.4 W |
| CPU | cpu | "%" | 23% |
| Memoria | memory | "%" | 71% |
| Espacio en disco | disk | "%" | 58% |
| Presión térmica | thermal | "" | Normal, Moderada, Alta, Crítica |
| Velocidad del ventilador* | fan | "rpm" | 2,400 rpm |
| Red | network | "B/s" | 1.2 MB/s, 340 KB/s |
| Temporizador | timer | "s" | 24:51 |
| Cuenta atrás | countdown | "s" | 3d 4h, 4:12, 12:05 |
| Progreso del día | progress | "%" | 63% |
| Valor enviado | push | "" | 42 |
| Dirección web | url | "" | 1,284 |
| Archivo | file | "" | 12 |
| Script* | script | "" | 412 |
* La velocidad del ventilador y los scripts necesitan la versión de descarga directa de Multimeter. La versión de la Mac App Store no puede leer los sensores del ventilador ni ejecutar tus scripts; allí usa un valor enviado o un archivo.
Los porcentajes siempre van de 0 a 100. El consumo, la red y la velocidad del ventilador se escalan a su pico reciente, salvo que la persona defina un rango. La presión térmica tiene cuatro pasos, así que level es 0, 0.33, 0.67 o 1. En temporizadores y cuentas atrás, level es la parte del tiempo que queda y value son los segundos restantes.
Alertas y colores
Cada medidor puede tener un nivel de aviso y uno crítico, definidos por la persona (por defecto, la batería por debajo del 20% y el 10%, la CPU por encima del 70% y el 90%, el disco por encima del 85% y el 95%). Multimeter calcula state por ti. Una esfera debería verse tranquila en normal y llamar más la atención al pasar a warning y critical, y usar tint cuando la persona eligió un color:
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; }¿Quieres dibujar las zonas de alerta en una escala, como un dial con el extremo rojo? Usa warningLevel, criticalLevel y alertDirection.
Historial
history guarda hasta 60 niveles recientes, del más antiguo al más nuevo, ya en la escala de 0 a 1. Añade el level actual al final y tienes una gráfica. El ejemplo Gráfica de historial de abajo dibuja una.
Detalles de la batería
La batería sigue siendo especial. data.battery siempre es la batería del Mac, mida lo que mida el medidor, así que cualquier esfera puede mostrar un rayo de carga. Una esfera hecha solo para la batería debería indicar "sources": ["battery"] en su manifest. timeRemaining y timeToFullCharge están en minutos y son null mientras macOS sigue calculando.
// 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 ?? '');Las esferas escritas para la versión 1 leen percentage, isCharging y formattedTime en el nivel superior. Se siguen enviando: para la fuente de batería son los valores de la propia batería, y para otras fuentes isCharging es false y formattedTime es el tiempo que queda en un temporizador o cuenta atrás (h:mm), y si no, null.
Tus propios datos
Cuatro fuentes muestran números que tú das: un valor enviado, una dirección web, un archivo o un script. Tu esfera las trata como cualquier otra fuente.
Envía un valor con un enlace
Abre un enlace multimeter://set desde cualquier sitio: Terminal, un script de shell, Atajos (Abrir URL), Raycast, una tarea cron, un script de compilación. Añade un medidor con la fuente Valor enviado y la misma clave, y se actualizará en cuanto se abra el enlace. open -g mantiene Multimeter en segundo plano.
# 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"| Parámetro | Qué significa |
|---|---|
key | Para qué medidor es. Las claves no distinguen mayúsculas de minúsculas. |
value | El número. Obligatorio. |
min / max | El rango, para que level salga bien. Por defecto, de 0 a 100. |
label | Texto para la etiqueta del medidor. |
unit | Una unidad que se añade tras el número, como cups. Usa % para un porcentaje. |
formatted | Tu propio texto para formatted, que se usa en lugar del número. |
Los valores enviados también funcionan en la versión de la Mac App Store. Multimeter recuerda el último valor de cada clave.
Direcciones web, archivos y scripts
Los tres leen la misma salida: un número simple, o JSON con un value y cualquiera de los extras. Todo salvo value es opcional.
42
42.5
42%
{"value": 42, "min": 0, "max": 100, "label": "Deploys", "unit": "", "formatted": "42 left"}¿Apuntas a un JSON más grande, como la respuesta de una API? Dale al medidor una ruta de clave y sacará el número:
{"data": {"count": 17}} → key path data.count
{"items": [{"value": 9.5}]} → key path items.0.value- Dirección web: se consulta cada cierto tiempo que tú eliges, como mucho cada 30 segundos (cada 5 minutos si no lo cambias).
- Archivo: se vuelve a leer cada vez que el archivo cambia. Útil para cualquier cosa que pueda escribir un archivo.
- Script: un comando de shell que se ejecuta con
zshcada cierto tiempo que tú eliges (cada 30 segundos si no lo cambias). Multimeter muestra el número que imprime. Las carpetas de Homebrew están en elPATH, y un comando que tarda más de 10 segundos se detiene.
# 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}'La versión de la Mac App Store puede usar direcciones web y archivos, pero no scripts. Para scripts allí, haz que tu script envíe un enlace o escriba un archivo.
Barras de menús claras y oscuras
La barra de menús es clara u oscura según el fondo de pantalla. Multimeter añade light-mode o dark-mode a body, pone colorScheme en los datos, y prefers-color-scheme también coincide con la barra de menús. Usa lo que prefieras:
/* 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; }
}Movimiento reducido
Cuando Reducir movimiento está activado, reduceMotion es true y prefers-reduced-motion coincide. Mantén el medidor legible y quita los rebotes, los pulsos y los barridos:
body.reduce-motion * { transition: none !important; animation: none !important; }
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; animation: none !important; }
}Ejemplos completos
Dos esferas completas, funcionando en vivo en esta página con los mismos datos que envía Multimeter. Copia los archivos en una carpeta e impórtala.
La esfera inicial de Crear esfera
Exactamente lo que escribe Crear esfera…: una etiqueta, una barra y el valor formateado, con la API en sus comentarios. Se colorea según state, respeta tint y sigue el aspecto de la barra de menús.
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();
}Gráfica de historial
Una gráfica de líneas del último minuto a partir de history, dibujada en un canvas a la resolución real de la pantalla.
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;Consejos para probar
- Mantén todo dentro de 22 puntos de alto y revisa tu esfera en su
minWidthy sumaxWidth. - Usa la fuente del sistema (
-apple-system) yfont-variant-numeric: tabular-numspara que los números no bailen. - Prueba tu esfera con varias fuentes en Añadir medidor… y Personalizar este medidor…: un porcentaje, la velocidad de red, un temporizador, una fuente no disponible.
- Revisa barras de menús claras y oscuras, un tinte y los estados de aviso y crítico.
- Los errores de tu JavaScript aparecen en Console.app. También puedes abrir tu
index.htmlen Safari y enviarle datos desde la consola: - Mantén los archivos pequeños y evita las solicitudes de red, para que la esfera se dibuje al instante.
// 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 }));¿Listo para crear?
$19.99, pago único. Sin suscripción. Reembolso en 30 días.
Comprar por $19.99