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

  1. 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.
  2. Edita los archivos: Multimeter muestra la carpeta en el Finder. Edita los archivos con cualquier editor de código.
  3. 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….
  4. Compártelo: Comprime la carpeta en el Finder, cambia la extensión .zip por .multimeter y 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 draws

manifest.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
}
ClaveTipoQué significa
namestringSe muestra en la lista de esferas y en la galería.
descriptionstringUna línea sobre lo que muestra la esfera.
versionstringTu número de versión, como 1.0.0.
authorstringTu nombre, que aparece en la galería.
apiVersion2Ponlo en 2 para los datos descritos en esta página.
sourcesstring[]["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.
defaultSourcestringLa fuente elegida cuando alguien añade tu esfera por primera vez, como "cpu".
widthnumberAncho en puntos. La altura siempre es 22.
minWidth / maxWidthnumberEl ancho mínimo y máximo que la persona puede darle en Personalizar.
supportsLightMode / supportsDarkModebooleanSi 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
}
CampoQué significa
sourceEl id de la fuente (ver la tabla de abajo). sourceName es su nombre traducido.
labelLa etiqueta del medidor, como CPU o DOWN. La persona puede cambiarla o borrarla, así que puede ser una cadena vacía.
valueLa lectura sin procesar en unit, o null. Junto con min y max te da el panorama completo.
levelEl valor situado en el rango de 0 a 1, limitado a ese rango. La mayoría de las esferas solo necesitan esto.
formattedLa 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.
statenormal, warning o critical, según los niveles de alerta que definió la persona. Úsalo para elegir tus colores.
tintUn color como #30D158 que eligió la persona, o null para usar los colores propios de tu esfera.
historyHasta 60 niveles recientes (de 0 a 1), del más antiguo al más nuevo, para gráficas.
warningLevelwarningLevel 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.
availablefalse 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.
colorSchemeEl aspecto de la barra de menús, light o dark. Sigue al fondo de pantalla, no solo al ajuste del sistema.
reduceMotiontrue cuando Reducir movimiento está activado. Omite las animaciones.
batteryLa 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.

Fuentesourceunitformatted se ve así
Bateríabattery"%"82%
Accesorio Bluetoothbluetooth"%"64%
Consumopower"W"12.4 W
CPUcpu"%"23%
Memoriamemory"%"71%
Espacio en discodisk"%"58%
Presión térmicathermal""Normal, Moderada, Alta, Crítica
Velocidad del ventilador*fan"rpm"2,400 rpm
Rednetwork"B/s"1.2 MB/s, 340 KB/s
Temporizadortimer"s"24:51
Cuenta atráscountdown"s"3d 4h, 4:12, 12:05
Progreso del díaprogress"%"63%
Valor enviadopush""42
Dirección weburl""1,284
Archivofile""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ámetroQué significa
keyPara qué medidor es. Las claves no distinguen mayúsculas de minúsculas.
valueEl número. Obligatorio.
min / maxEl rango, para que level salga bien. Por defecto, de 0 a 100.
labelTexto para la etiqueta del medidor.
unitUna unidad que se añade tras el número, como cups. Usa % para un porcentaje.
formattedTu 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 zsh cada 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 el PATH, 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 minWidth y su maxWidth.
  • Usa la fuente del sistema (-apple-system) y font-variant-numeric: tabular-nums para 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.html en 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