Crie seu próprio mostrador

Crie seus próprios mostradores de medidor com HTML, CSS e JavaScript. Cada mostrador funciona com qualquer fonte, da bateria aos números que você mesmo envia.

Início rápido

  1. Crie um mostrador: Nos Ajustes, clique em Criar mostrador… (Opção + Comando + N). O Multimeter cria uma pasta com arquivos iniciais que já usam os dados da versão 2.
  2. Edite os arquivos: O Multimeter mostra a pasta no Finder. Edite os arquivos em qualquer editor de código.
  3. Veja as mudanças ao vivo: O Multimeter recarrega seu mostrador sempre que você salva. Teste com fontes diferentes em Adicionar medidor… ou Personalizar este medidor….
  4. Compartilhe: Comprima a pasta no Finder, troque a extensão .zip por .multimeter e envie para a galeria. Qualquer pessoa pode adicioná-lo com Importar… (Comando + O) ou clicando duas vezes no arquivo.

Como um medidor funciona

Um medidor na barra de menus é um mostrador mais uma fonte mais os ajustes da pessoa. O mostrador é a sua pasta: uma pequena página web que o Multimeter carrega numa visualização transparente com 22 pontos de altura e a largura definida em width no seu manifest. A fonte é o que o medidor mede, escolhida pela pessoa ao adicionar o medidor.

Sempre que algo muda, o Multimeter atualiza window.meterData e envia um evento meterUpdate. Seu mostrador não precisa saber de onde vêm os números: desenhe level (de 0 a 1) e exiba formatted, e ele funciona igual para CPU, rede, um timer ou a bateria.

Estrutura de arquivos

Cada mostrador é uma pasta com:

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

O manifest descreve seu mostrador e diz ao Multimeter qual a largura dele e para que ele foi feito:

{
  "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
}
ChaveTipoO que significa
namestringAparece na lista de mostradores e na galeria.
descriptionstringUma linha sobre o que o mostrador exibe.
versionstringSeu número de versão, como 1.0.0.
authorstringSeu nome, exibido na galeria.
apiVersion2Defina como 2 para os dados descritos nesta página.
sourcesstring[]["any"] para um mostrador que desenha qualquer fonte, ou uma lista de ids de fonte como ["battery"] ou ["battery", "countdown", "timer"]. O Multimeter só oferece seu mostrador para essas fontes.
defaultSourcestringA fonte escolhida quando alguém adiciona seu mostrador pela primeira vez, como "cpu".
widthnumberLargura em pontos. A altura é sempre 22.
minWidth / maxWidthnumberA menor e a maior largura que a pessoa pode definir em Personalizar.
supportsLightMode / supportsDarkModebooleanSe o mostrador foi feito para barras de menus claras e escuras.

Um manifest sem apiVersion e sources é tratado como um mostrador pensado para a bateria que ainda funciona com qualquer fonte, então os medidores feitos para versões anteriores continuam funcionando.

Dados do medidor

O Multimeter coloca os dados atuais em window.meterData e envia de novo a cada mudança. Aqui está tudo o que vem neles (versão 2 da 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
}
CampoO que significa
sourceO id da fonte (veja a tabela abaixo). sourceName é o nome traduzido.
labelO rótulo do medidor, como CPU ou DOWN. A pessoa pode renomear ou apagar, então pode ser uma string vazia.
valueA leitura bruta em unit, ou null. Com min e max, você tem o quadro completo.
levelO valor posicionado na faixa de 0 a 1, limitado a ela. A maioria dos mostradores só precisa disso.
formattedA leitura como texto, com unidade, formatada para o idioma da pessoa: 23%, 12.4 W, 1.2 MB/s, 4:12.
unit%, W, B/s, rpm, s (timers e contagens regressivas), uma string vazia ou a unidade definida pela pessoa.
statenormal, warning ou critical, conforme os níveis de alerta que a pessoa definiu. Use para escolher suas cores.
tintUma cor como #30D158 escolhida pela pessoa, ou null para usar as cores do seu mostrador.
historyAté 60 níveis recentes (de 0 a 1), do mais antigo ao mais novo, para gráficos.
warningLevelwarningLevel e criticalLevel posicionam os níveis de alerta na mesma escala de 0 a 1 que level, e alertDirection diz se os alertas disparam acima (above) ou abaixo (below) deles. Os três são null quando o medidor não tem alertas.
availablefalse quando a fonte não pode ser lida no momento: sem bateria, acessório desligado, endereço web com falha. Mostre um traço.
colorSchemeA aparência da barra de menus, light ou dark. Ela acompanha o papel de parede, não só o ajuste do sistema.
reduceMotiontrue quando Reduzir Movimento está ativado. Pule as animações.
batteryA bateria do Mac, sempre presente, não importa o que o medidor mede: carga, carregando, na tomada, tempo restante, tempo até a carga completa, ciclos, saúde e temperatura.

Ouvir atualizações

Desenhe uma vez com o que já estiver em window.meterData e de novo a cada meterUpdate. Defina window.meterReady = true quando estiver pronto para receber dados.

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;

Fontes

Estas são as fontes que um medidor pode ter. Seu mostrador vê o id em source e recebe formatted já pronto, então raramente é preciso tratá-las de forma diferente.

FontesourceunitExemplo de formatted
Bateriabattery"%"82%
Acessório Bluetoothbluetooth"%"64%
Consumo de energiapower"W"12.4 W
CPUcpu"%"23%
Memóriamemory"%"71%
Espaço em discodisk"%"58%
Pressão térmicathermal""Normal, Moderada, Alta, Crítica
Velocidade da ventoinha*fan"rpm"2,400 rpm
Redenetwork"B/s"1.2 MB/s, 340 KB/s
Timertimer"s"24:51
Contagem regressivacountdown"s"3d 4h, 4:12, 12:05
Progresso do diaprogress"%"63%
Valor enviadopush""42
Endereço weburl""1,284
Arquivofile""12
Script*script""412

* Velocidade da ventoinha e scripts precisam da versão de download direto do Multimeter. A versão da Mac App Store não consegue ler os sensores das ventoinhas nem rodar seus scripts; nela, use um valor enviado ou um arquivo.

Porcentagens sempre vão de 0 a 100. Consumo de energia, rede e velocidade da ventoinha se ajustam ao pico recente, a menos que a pessoa defina uma faixa. A pressão térmica tem quatro níveis, então level é 0, 0.33, 0.67 ou 1. Em timers e contagens regressivas, level é a parte do tempo que resta e value são os segundos restantes.

Alertas e cores

Cada medidor pode ter um nível de aviso e um crítico, definidos pela pessoa (por padrão, bateria abaixo de 20% e 10%, CPU acima de 70% e 90%, disco acima de 85% e 95%). O Multimeter calcula state para você. Um mostrador deve parecer tranquilo em normal e chamar mais atenção ao passar para warning e critical, e usar tint quando a pessoa escolheu uma cor:

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; }

Quer desenhar as zonas de alerta numa escala, como um ponteiro com a ponta vermelha? Use warningLevel, criticalLevel e alertDirection.

Histórico

history guarda até 60 níveis recentes, do mais antigo ao mais novo, já na escala de 0 a 1. Adicione o level atual no final e você tem um gráfico. O exemplo Gráfico de histórico abaixo desenha um.

Detalhes da bateria

A bateria continua especial. data.battery é sempre a bateria do Mac, não importa o que o medidor mede, então qualquer mostrador pode exibir um raio de carregamento. Um mostrador feito só para a bateria deve declarar "sources": ["battery"] no manifest. timeRemaining e timeToFullCharge estão em minutos e são null enquanto o macOS ainda está 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 ?? '');

Mostradores escritos para a versão 1 leem percentage, isCharging e formattedTime no nível principal. Eles continuam sendo enviados: na fonte bateria são os valores da própria bateria, e nas outras fontes isCharging é false e formattedTime é o tempo restante de um timer ou contagem regressiva (h:mm), senão null.

Seus próprios dados

Quatro fontes exibem números que você fornece: um valor enviado, um endereço web, um arquivo ou um script. Seu mostrador as trata como qualquer outra fonte.

Envie um valor com um link

Abra um link multimeter://set de qualquer lugar: Terminal, um script de shell, Atalhos (Abrir URLs), Raycast, uma tarefa cron, um script de build. Adicione um medidor com a fonte Valor enviado e a mesma chave, e ele atualiza no instante em que o link abre. open -g mantém o Multimeter em 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âmetroO que significa
keyPara qual medidor é. As chaves não diferenciam maiúsculas de minúsculas.
valueO número. Obrigatório.
min / maxA faixa, para level sair certo. O padrão é de 0 a 100.
labelTexto para o rótulo do medidor.
unitUma unidade adicionada depois do número, como cups. Use % para porcentagem.
formattedSeu próprio texto para formatted, usado no lugar do número.

Valores enviados também funcionam na versão da Mac App Store. O Multimeter lembra o último valor de cada chave.

Endereços web, arquivos e scripts

Os três leem a mesma saída: um número simples, ou JSON com um value e qualquer um dos extras. Tudo além de value é opcional.

42
42.5
42%

{"value": 42, "min": 0, "max": 100, "label": "Deploys", "unit": "", "formatted": "42 left"}

Apontando para um JSON maior, como a resposta de uma API? Dê ao medidor um caminho de chave e ele pega o número:

{"data": {"count": 17}}                → key path  data.count
{"items": [{"value": 9.5}]}             → key path  items.0.value
  • Endereço web: consultado no intervalo que você escolher, no máximo a cada 30 segundos (a cada 5 minutos, se você não mudar).
  • Arquivo: lido de novo sempre que o arquivo muda. Prático para qualquer coisa que consiga gravar um arquivo.
  • Script: um comando de shell executado com zsh no intervalo que você escolher (a cada 30 segundos, se você não mudar). O Multimeter mostra o número que ele imprime. As pastas do Homebrew estão no PATH, e um comando que leva mais de 10 segundos é interrompido.
# 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}'

A versão da Mac App Store pode usar endereços web e arquivos, mas não scripts. Nela, faça seu script enviar um link ou gravar um arquivo.

Barras de menus claras e escuras

A barra de menus fica clara ou escura dependendo do papel de parede. O Multimeter adiciona light-mode ou dark-mode ao body, define colorScheme nos dados, e prefers-color-scheme também acompanha a barra de menus. Use o que preferir:

/* 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; }
}

Movimento reduzido

Quando Reduzir Movimento está ativado, reduceMotion é true e prefers-reduced-motion corresponde. Mantenha o medidor legível e tire os quiques, as pulsações e as varreduras:

body.reduce-motion * { transition: none !important; animation: none !important; }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

Exemplos completos

Dois mostradores completos, rodando ao vivo nesta página com os mesmos dados que o Multimeter envia. Copie os arquivos para uma pasta e importe.

O mostrador inicial do Criar mostrador

Exatamente o que o Criar mostrador… escreve: um rótulo, uma barra e o valor formatado, com a API nos comentários. Ele muda de cor conforme state, respeita tint e acompanha a aparência da barra de menus.

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áfico de histórico

Um gráfico de linha do último minuto a partir de history, desenhado num canvas na resolução real da tela.

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;

Dicas de teste

  • Mantenha tudo dentro de 22 pontos de altura e confira seu mostrador no minWidth e no maxWidth.
  • Use a fonte do sistema (-apple-system) e font-variant-numeric: tabular-nums para os números não ficarem pulando.
  • Teste seu mostrador com várias fontes em Adicionar medidor… e Personalizar este medidor…: uma porcentagem, velocidade de rede, um timer, uma fonte indisponível.
  • Confira barras de menus claras e escuras, uma tonalidade e os estados de aviso e crítico.
  • Os erros do seu JavaScript aparecem no Console.app. Você também pode abrir seu index.html no Safari e enviar dados pelo console:
  • Mantenha os arquivos pequenos e evite requisições de rede, para o mostrador aparecer na hora.
// 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 }));

Pronto para criar?

$19.99 em pagamento único. Sem assinatura. Reembolso em 30 dias.

Comprar por $19.99