# O manifesto de plugins

Um plugin do Peek é um mini app descrito num arquivo JSON, o manifesto. Ele diz três coisas: de onde ler, o que desenhar e quais botões oferecer. Qualquer pessoa pode escrever, versionar, compartilhar e importar um.

Esta página descreve cada chave do manifesto, como o app lê cada uma hoje. Para montar um plugin sem escrever JSON, veja o capítulo [Plugins do Manual](/manual#plugins): o editor do Peek grava este mesmo arquivo.

<!-- ia -->

## Princípios

**Módulo e plugin são coisas diferentes.** O módulo vem no app e lê o que o app alcança: dados do Mac, tarefas, links, feeds. O plugin lê um endereço, um programa ou um script, e é descrito num manifesto.

**Estrutura declarativa, lógica por script.** Fontes, ícone, cartão e botões são declarados no JSON. A lógica que não cabe ali vem de um programa que devolve JSON ou de um script em AppleScript ou JavaScript for Automation (JXA).

**Chave desconhecida é ignorada.** Um valor desconhecido também: ele volta ao padrão. A exceção é o `type` de um componente, que precisa existir na lista de componentes. Por isso existe o `schema`: ele impede que um app antigo instale um plugin pela metade.

**Segredo fora do arquivo.** Tokens e senhas ficam nas Chaves do Mac ou no arquivo de segredos do app, nunca no manifesto. Um plugin pode circular sem levar o token de ninguém.

**Segurança visível.** Antes de instalar, o app mostra tudo o que o plugin lê e roda, derivado das chaves usadas. Não existe bloco de permissões no arquivo.

## Um plugin mínimo

```json
{
  "schema": 2,
  "id": "plugin.exemplo.dolar",
  "name": "Dólar",
  "interval": 600,
  "icon": { "kind": "symbol", "symbolName": "dollarsign.circle" },
  "source": {
    "kind": "http",
    "url": "https://economia.awesomeapi.com.br/json/last/USD-BRL"
  },
  "slot": { "label": "{{USDBRL.bid | shape #,##0.00}}" },
  "card": {
    "title": "Dólar",
    "components": [
      { "type": "hero", "value": "{{USDBRL.bid}}", "caption": "Compra", "format": "money", "currency": "BRL" }
    ]
  }
}
```

Só `id` é obrigatório. Sem fonte pronta, o plugin entra e pede para terminar a configuração em Configurações › Plugins.

## A raiz

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `schema` | número | `1` | Versão do formato. Veja [A versão](#a-versão-schema). |
| `id` | texto | obrigatório | Identificador único. O editor gera `plugin.` seguido de um UUID. |
| `name` | texto | `""` | O nome do plugin. Vazio vira «Plugin». |
| `icon` | objeto | símbolo `puzzlepiece.extension` | O que aparece dentro do ícone. |
| `interval` | número, em segundos | `300` | De quanto em quanto tempo ler. |
| `source` | objeto | `{ "kind": "http" }` | De onde ler. |
| `slot` | objeto | vazio | O rótulo e o arco em volta do ícone. |
| `card` | objeto | cartão vazio | O cartão que abre quando o cursor para no ícone. |
| `actions` | lista | `[]` | Os botões. |
| `attention` | objeto | `{ "mode": "never" }` | Quando o ícone pisca e salta. |
| `role` | `reading` \| `button` | `reading` | Com `button`, o ícone é o botão: não lê sozinho, não tem cartão, e o clique roda a fonte. |
| `button` | objeto | padrões | Como o ícone-botão se comporta. Só vale com `role: "button"`. |
| `settings` | lista | `[]` | Campos que quem instala preenche. |
| `history` | objeto | ausente | Guarda um campo de cada leitura, para gráficos e comparação. |
| `readsOnItsOwn` | booleano | `true` | Com `false`, o plugin não tem relógio: lê só quando o cartão pede ou o ícone é clicado. |
| `answer` | texto | `""` | Uma resposta escrita à mão, lida com o `format` da fonte. Com `source.kind: "panel"` ela é o dado do plugin. Com uma fonte de verdade, é o exemplo que o editor desenha antes da primeira leitura. Até 256 KB. |
| `parameters` | lista | `[]` | Forma antiga de valores `{{param.nome}}`. Prefira `settings`. |

## A versão: schema

O app grava sempre o menor `schema` que cobre o que o plugin usa, e recusa na importação um `schema` maior que o que ele conhece, com a mensagem «Este plugin precisa de uma versão mais nova do Peek». A versão atual do app lê até o `schema` 12.

Quem escreve à mão declara o número da linha mais alta que o plugin alcança:

| `schema` | Quando |
| --- | --- |
| `12` | Um componente `list` ou `tabs`. |
| `11` | Um componente `breakdown`. |
| `10` | Um componente `heatmap`, ou uma cor `#RRGGBB` em qualquer mapa de cores. |
| `9` | Um componente `controls`, ou um botão com `place: "control"`. |
| `8` | Um componente `properties`, um componente com `width: "half"`, uma grade com `look: "tiles"`, ou um botão com `place: "property"`. |
| `7` | Uma `answer` na raiz. |
| `6` | `needs` num botão, ou os recados `addToList`, `dropFromList` e `readLater`. |
| `5` | `card.picture`, o recado `remember`, ou `headline` junto com componentes. |
| `4` | Uma coluna de tabela que não é texto, ou um botão com `place: "column"`. |
| `3` | `source.kind: "panel"`, um componente `buttons`, um botão com `place: "panel"` ou com `rules`, ou `query` em qualquer chamada. |
| `2` | `card.components`, `settings`, `pagination`, `history`, `slot.style` diferente de `arc`, um `run` em lista, ou os recados `openApp`, `notify`, `playSound` e `runShortcut`. |
| `1` | Nenhuma das anteriores. |

> [!TIP]
> Na dúvida, declare o número mais alto da tabela que se aplica. O app regrava o valor certo quando o plugin é salvo no editor.

## O ícone: icon

O que aparece dentro do círculo na borda da tela.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `kind` | `symbol` \| `word` \| `picture` | `symbol` | Um símbolo, uma palavra curta ou uma imagem. |
| `symbolName` | texto | `puzzlepiece.extension` | Nome de um SF Symbol. |
| `word` | texto com campos | `""` | Uma palavra no lugar do símbolo, como «CPU». Aceita campos: `{{[0].code}}`. Vazia, volta ao símbolo. |
| `pictureSource` | texto com campos | `""` | Endereço `https` ou caminho de arquivo de uma imagem. O app recorta em círculo e guarda uma cópia. |

- Com campos em `pictureSource`, a imagem é buscada de novo quando o endereço muda entre leituras.
- `pictureFilename` é gerado pelo app. Não escreva à mão.
- O nome antigo de `word` era `text`, que continua sendo lido.

```json
"icon": { "kind": "word", "word": "{{USDBRL.code}}" }
```

## A fonte: source

A fonte diz de onde o plugin lê. Há quatro portas.

| `kind` | O que é |
| --- | --- |
| `http` | Uma chamada com método, parâmetros, cabeçalhos e corpo. |
| `command` | Um ou mais programas do Mac, de caminho absoluto, sem shell. |
| `script` | AppleScript ou JXA. |
| `panel` | Não lê nada. O plugin é um painel de botões, ou usa a `answer` escrita à mão. |

Um `kind` desconhecido é lido como `command`, então o plugin aparece pedindo configuração em vez de sumir.

### A requisição: http

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `method` | texto | `GET` | `GET`, `POST`, `PUT`, `PATCH` ou `DELETE`. |
| `url` | texto com campos | `""` | O endereço. |
| `query` | lista de `{ name, value }` | `[]` | Parâmetros acrescentados ao endereço. O app escapa cada valor, então espaço e acento não quebram nada. |
| `headers` | lista de `{ name, value }` | `[]` | Cabeçalhos. O valor aceita campos. |
| `body` | texto com campos | `""` | O corpo. Vai em qualquer método quando não está vazio. |

```json
"source": {
  "kind": "http",
  "url": "https://api.github.com/repos/{{$settings.repo}}/issues",
  "query": [{ "name": "per_page", "value": "20" }],
  "headers": [{ "name": "Authorization", "value": "Bearer {{$settings.token}}" }]
}
```

- Só endereços `https`, ou `http` para `localhost`, `127.0.0.1`, `::1` e nomes terminados em `.local`.
- O app acrescenta `Accept: application/json` quando ele falta, e `Content-Type: application/json` quando há corpo.
- Um redirecionamento para outro servidor perde os cabeçalhos do plugin, para o token não vazar.
- Um valor de `settings`, `$global` ou digitado pelo usuário é escapado para endereço no `url` e para JSON no `body`.
- Sucesso é HTTP 2xx.

### Programas: command

```json
"source": {
  "kind": "command",
  "steps": [
    { "program": "/usr/bin/pmset", "arguments": ["-g", "batt"] },
    { "program": "/usr/bin/grep", "arguments": ["-o", "[0-9]*%"] }
  ],
  "format": { "kind": "text" }
}
```

| Chave de cada passo | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `program` | texto com campos | `""` | Caminho absoluto de um executável. |
| `arguments` | lista de textos com campos | `[]` | Um item por argumento. Sem shell, sem aspas, sem curingas. |
| `isEnabled` | booleano | `true` | Passo desligado fica no arquivo e não roda. |

- Os passos se encadeiam: a saída de um entra no próximo. No máximo 8.
- Sucesso é código de saída 0 em todos os passos.
- A forma antiga, `command` com `arguments` direto na fonte, continua sendo lida.

### Scripts: script

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `language` | `applescript` \| `javascript` | `applescript` | `javascript` é JXA. |
| `script` | texto com campos | `""` | O script, entregue ao `osascript`. |

```json
"source": {
  "kind": "script",
  "language": "javascript",
  "script": "JSON.stringify({ naoLidos: Application('Mail').inbox.unreadCount() })"
}
```

Na primeira vez que um script fala com outro app, o macOS pede a permissão de Automação.

### O formato da resposta: format

A resposta não precisa ser JSON. `source.format` diz como ler os bytes, e o resultado vira a árvore que os campos leem.

| `kind` | O que vira |
| --- | --- |
| `json` | O JSON como veio. É o padrão. |
| `text` | `{ "text": "…" }` com a saída inteira. |
| `lines` | `{ "lines": [{ "index": 0, "line": "…" }] }`, uma por linha não vazia. |
| `columns` | `{ "rows": [{ "c1": "…", "c2": "…" }] }`. Com `hasHeader`, a primeira linha dá os nomes. |
| `keyValue` | `{ "nome": "valor" }`, separando cada linha no primeiro `:` ou `=`. |
| `regex` | Os grupos nomeados `(?<nome>…)` viram chaves. Com `all`, `{ "matches": [ … ] }`. |
| `plist` | Uma property list, como JSON. |
| `files` | `{ "files": [{ "path", "name", "ext", "exists", "isFolder", "size", "modified" }] }`, uma linha por caminho. |

Outras chaves de `format`: `separator` (`whitespace`, `tab`, `comma`, `semicolon`) para `columns`, `pattern` para `regex`, e `skip`, o número de linhas a pular no topo.

### Limites

| Porta | Tempo máximo | Tamanho máximo |
| --- | --- | --- |
| `http` | 15 s | 2 MB |
| `command` | 15 s | 2 MB |
| `script` | 15 s | 2 MB |

Cada servidor recebe no máximo duas chamadas ao mesmo tempo. Uma resposta 429 deixa aquele servidor em paz pelo tempo do `Retry-After`, ou por 60 s dobrando a cada 429 seguido, até 15 min.

## O intervalo

`interval` é em segundos, com mínimo de 5 para `http`, `command` e `script`. O editor oferece 5, 10, 15 e 30 s, 1, 2, 5, 10, 15 e 30 min e 1 h.

- Fora da tomada, o app multiplica a espera. Com a tela apagada, nada é lido.
- Não há relógio com `role: "button"`, com `readsOnItsOwn: false` ou com `source.kind: "panel"`.

## A borda: slot

O que aparece em volta do ícone e abaixo dele.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `label` | texto com campos | `""` | O rótulo curto abaixo do ícone. |
| `style` | `arc` \| `number` \| `status` \| `sparkline` | `arc` | O que o anel desenha. |
| `value` | número | `""` | `arc`: o valor do arco. |
| `total` | número | `""` | `arc`: o total. Sem total, um `value` acima de 1 é lido como percentual. |
| `figure` | texto com campos | `""` | `number`: o número no lugar do ícone. Vazio usa `value`. |
| `status` | texto com campos | `""` | `status`: o valor que decide a cor. |
| `statusColors` | mapa valor → cor | `{}` | `status`: a cor de cada valor. |
| `sparkCount` | número | `24` | `sparkline`: quantas leituras guardadas desenhar. Pede `history`. |
| `alertsOnThreshold` | booleano | `false` | Manda uma notificação quando o arco passa de 80% e de 100%, uma vez por cruzamento. |

Um campo de número aceita `{{caminho}}`, um caminho solto (`data.total`) ou um número escrito (`100`).

As cores são `positive`, `attention`, `negative`, `neutral` ou `#RRGGBB`. Sem entrada no mapa, a própria palavra decide: `ok`, `up`, `success` e `green` são positivas, `warn`, `warning` e `yellow` pedem atenção, `error`, `down`, `bad` e `red` são negativas.

```json
"slot": {
  "style": "status",
  "label": "{{status.description}}",
  "status": "{{status.indicator}}",
  "statusColors": { "none": "positive", "minor": "attention", "major": "negative" }
}
```

> [!NOTE]
> Os campos do `slot` enxergam a resposta e `$settings`. `$global` e `param` ficam vazios ali.

## O cartão: card

O cartão abre quando o cursor para no ícone. Ele tem um topo e uma pilha de componentes.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `isEnabled` | booleano | `true` | Com `false`, o ícone não abre cartão. |
| `title` | texto com campos | `""` | O título. Vazio usa o `name` do plugin. |
| `subtitle` | texto com campos | `""` | A linha menor abaixo do título. |
| `headline` | texto com campos | `""` | Um número à direita do título. |
| `picture` | imagem | nenhuma | A marca no topo do cartão. Sem ela, o ícone do plugin. |
| `emptyMessage` | texto com campos | `""` | A frase quando a lista está vazia. Vazia, «Nenhum item na lista.». |
| `components` | lista | `[]` | Os componentes, desenhados de cima para baixo. |
| `rowLimit` | número | `5` | O tamanho de página padrão, quando o componente `list` mostra todos os itens. |

`metrics`, `tabs` e `shape` são do formato anterior aos componentes. O app converte um cartão assim em componentes ao abrir o plugin e grava o plugin no formato novo.

### Uma imagem: picture

`card.picture`, a imagem de uma aba, a imagem de um item da lista e a marca de um botão usam o mesmo objeto:

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `kind` | `none` \| `symbol` \| `address` \| `file` | `none` | Nada, um SF Symbol, um endereço ou um arquivo. |
| `value` | texto com campos | `""` | O nome do símbolo, o endereço ou o caminho. |
| `shape` | `rounded` \| `round` | `rounded` | Cantos arredondados ou círculo. |
| `scale` | número | `1` | De 0,6 a 2,5. |

### Chaves de todo componente

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `id` | UUID | gerado | O identificador do componente. Uma aba aponta os componentes dela por ele. |
| `type` | texto | `hero` | O tipo do componente. Um `type` desconhecido impede o plugin de abrir. |
| `title` | texto com campos | `""` | Um título acima do componente. Não vale para `tabs`. |
| `width` | `full` \| `half` | `full` | Meia largura. Não vale para `list`, `heatmap` e `tabs`. |

Dois componentes `half` seguidos dividem a mesma linha. Um `half` sozinho ocupa meia linha e deixa a outra metade vazia.

Quando falta algo a um componente, só ele mostra o problema, no próprio lugar. Os outros continuam funcionando.

### Três jeitos de apontar um dado

Os componentes leem a resposta de três jeitos, e cada chave aceita um deles:

- **Caminho de lista**: um caminho puro, sem `{{ }}`, como `items`, `data.rows` ou `$`. É o que as chaves `path` pedem. Com `| entries` no fim, um objeto vira linhas `{ key, value }`: `"path": "languages | entries"`.
- **Texto com campos**: palavras e `{{campos}}` misturados, como `Fechamento de {{date | ago}}`. Dentro de uma lista, o campo é relativo à linha.
- **Número**: `{{campo}}`, um caminho solto ou um número escrito.

### Formatos de número

`hero`, `chart`, `stages` e as colunas de tabela aceitam `format`:

| `format` | Exemplo |
| --- | --- |
| `number` | `1.240,5`. É o padrão, com até duas casas. |
| `integer` | `1.241` |
| `money` | `R$ 1.240,50`, com `currency` (`BRL`, `USD`…). |
| `compact` | `1,2 mil` |
| `percent` | `42%`. Um valor até 1 é lido como fração. |
| `bytes` | `1,2 GB` |
| `duration` | `5 h 12 min`, com `unit`: `seconds` (padrão), `minutes` ou `hours`. |

Números escritos como texto também são lidos: `"R$ 1.240,50"` vira 1240,5.

## Os componentes

Há catorze. O editor mostra todos em **Adicionar componente**, com uma etiqueta **Combina** nos que servem para a resposta que está na tela.

| `type` | No editor | Para quê |
| --- | --- | --- |
| `hero` | Destaque | Um número importante, a variação e a distância até a meta. |
| `chart` | Tendência | Como um valor se moveu ao longo do tempo. |
| `heatmap` | Mapa de calor | Intensidade por dia e hora. |
| `stages` | Etapas | Uma contagem por etapa, na ordem. |
| `breakdown` | Distribuição | Como os itens de uma lista se dividem entre grupos. |
| `grid` | Grade de status | Muitos itens de uma vez, cada um bem ou não. |
| `timeline` | Linha do tempo | O que aconteceu e o que vem, por data. |
| `table` | Tabela | Colunas de valores curtos. |
| `text` | Texto | Um parágrafo para ler, com um botão de copiar. |
| `list` | Lista | Itens de uma lista, cada um montado com textos, imagens, ícones, botões e indicadores. |
| `buttons` | Botões | Um painel de botões, cada um rodando alguma coisa. |
| `properties` | Propriedades | Pares de nome e valor, com botões ao lado de cada um. |
| `controls` | Controles | Liga, desliga e ajusta direto no cartão. |
| `tabs` | Abas | Abas no topo; cada aba mostra um ou mais componentes do cartão. |

### Destaque: hero

Um número grande, a variação desde a leitura anterior e, se houver meta, uma barra até ela.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `value` | número | obrigatório | O número. |
| `previous` | número | `""` | Com o que comparar. Vazio usa a penúltima leitura guardada em `history`. |
| `total` | número | `""` | A meta. Acima de zero, desenha a barra. |
| `caption` | texto com campos | `""` | A linha abaixo do número. |
| `goodDirection` | `up` \| `down` | `up` | Qual direção pinta a variação de verde. |
| `format`, `currency`, `unit` | | `number` | Como escrever o número e a meta. |

```json
{ "type": "hero", "value": "{{vendas.hoje}}", "total": "{{vendas.meta}}", "caption": "Vendas de hoje", "format": "money", "currency": "BRL", "width": "half" }
```

### Tendência: chart

Uma linha, uma área ou barras ao longo do tempo.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `style` | `line` \| `area` \| `bar` | `line` | O desenho. |
| `path` | caminho de lista | `""` | Os pontos. Vazio desenha as leituras guardadas em `history`. |
| `x` | texto com campos | `""` | O rótulo ou a data de cada ponto. |
| `y` | número | `""` | O valor de cada ponto. |
| `dateFormat` | texto | `""` | Como ler a data, no padrão `dd/MM/yyyy`. Vazio reconhece ISO 8601 e Unix. |
| `height` | `small` \| `medium` | `small` | Baixo ou alto. |
| `format`, `currency`, `unit` | | `number` | Como escrever os valores. |

- Precisa de pelo menos dois pontos. Sem `path`, mostra «Coletando dados» até juntar três leituras.
- Quando todo `x` é uma data, os pontos são ordenados do mais antigo ao mais novo.
- Se a lista for de números puros, cada item é o valor, e `x` pode apontar outra lista com os rótulos.

```json
{ "type": "chart", "style": "area", "path": "$", "x": "{{timestamp}}", "y": "{{bid}}", "height": "medium", "format": "money", "currency": "BRL" }
```

### Mapa de calor: heatmap

Células coloridas pela soma dos valores. Sempre na largura toda.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `heatStyle` | `grid` \| `compact` \| `calendar` | `grid` | Semana × hora, uma faixa só, ou um mês. |
| `path` | caminho de lista | obrigatório | Os itens. |
| `value` | número | `""` | O valor de cada item. Itens sem número são pulados. |
| `heatRow`, `heatColumn` | texto com campos | `""` | Em que linha e coluna cada item cai. |
| `heatRows`, `heatColumns` | lista de textos | `[]` | Os nomes das linhas e das colunas, na ordem. O valor de cada item precisa ser igual a um deles. |
| `date` | texto com campos | `""` | `calendar`: o dia de cada item. O mês desenhado é o da data mais recente. |
| `dateFormat` | texto | `""` | Como ler a data. |
| `heatColor` | cor | `blue` | A cor das células. |
| `scale` | `automatic` \| `fixed` | `automatic` | Escala pelo menor e pelo maior valor, ou pelos limites abaixo. |
| `scaleMin`, `scaleMax` | número | `0`, `100` | Os limites da escala fixa. |
| `legend` | booleano | `true` | Mostra a legenda. |
| `heatUnit` | texto | `""` | A unidade na dica de cada célula, como «visitas». |

As cores de componente são `red`, `orange`, `yellow`, `green`, `teal`, `blue`, `indigo`, `purple`, `pink`, `gray` ou `#RRGGBB`.

```json
{ "type": "heatmap", "heatStyle": "calendar", "path": "downloads", "date": "{{day}}", "value": "{{downloads}}", "heatColor": "green", "heatUnit": "downloads" }
```

### Etapas: stages

Uma contagem por etapa, na ordem da resposta, com uma barra proporcional à maior. Serve também para um ranking: basta a resposta já vir ordenada.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `path` | caminho de lista | obrigatório | As etapas, já na ordem. |
| `label` | texto com campos | `""` | O nome da etapa. |
| `value` | número | obrigatório | A contagem. |
| `limit` | número | ausente | Quantas etapas mostrar. Sem ele, todas. |
| `format`, `currency`, `unit` | | `number` | Como escrever os valores. |
| `filters` | `{ list, field }` | ausente | Clicar numa etapa filtra o componente `list` de posição `list` (contando só as listas) aos itens cujo `field` é igual ao nome da etapa. |

```json
{ "type": "stages", "path": "etapas", "label": "{{nome}}", "value": "{{total}}", "filters": { "list": 0, "field": "{{etapa}}" } }
```

### Distribuição: breakdown

Separa os itens de uma lista em grupos que você define e mostra quanto cada grupo tem.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `breakdownStyle` | `bar` \| `funnel` \| `list` | `bar` | Uma barra dividida, um funil com a passagem entre grupos, ou uma lista. |
| `path` | caminho de lista | obrigatório | Os itens. |
| `groupField` | texto com campos | obrigatório | O campo que decide o grupo, sempre como `{{campo}}`. |
| `groupKind` | `number` \| `text` \| `boolean` | descoberto no primeiro item | Como comparar. |
| `measure` | `count` \| `sum` | `count` | Contar itens ou somar um campo. |
| `sumField` | número | `""` | O campo somado, com `measure: "sum"`. |
| `shows` | `amount` \| `percent` \| `both` | `amount` | O que cada grupo escreve. |
| `prefix`, `suffix` | texto | `""` | Em volta do valor, como `R$`. |
| `groups` | lista | obrigatório | Os grupos. |
| `others` | `{ shows, name, color }` | escondido, «Outros», `gray` | O grupo do que não coube em nenhum. |

Cada grupo tem `name`, `color` e a regra do seu `groupKind`:

- `number`: `from`. O item entra no grupo de maior `from` que não passa do valor dele.
- `text`: `match`, uma lista separada por vírgula. Maiúsculas não importam.
- `boolean`: `truth`, `true` ou `false`.

```json
{
  "type": "breakdown",
  "path": "pedidos",
  "groupField": "{{status}}",
  "groupKind": "text",
  "shows": "both",
  "groups": [
    { "name": "Pagos", "color": "green", "match": "paid, captured" },
    { "name": "Pendentes", "color": "yellow", "match": "pending" }
  ],
  "others": { "shows": true, "name": "Outros", "color": "gray" }
}
```

### Grade de status: grid

Muitos itens de uma vez, cada um com um ponto de cor. Os problemas aparecem primeiro.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `path` | caminho de lista | obrigatório | Os itens. |
| `label` | texto com campos | obrigatório | O nome de cada item. |
| `status` | texto com campos | `""` | O status de cada item. |
| `statusColors` | mapa valor → cor | `{}` | A cor de cada status. |
| `look` | `chips` \| `tiles` | `chips` | Nome com ponto, ou só quadrados. |
| `tipTitle`, `tipText` | texto com campos | `""` | A dica ao passar o cursor, em `tiles`. |
| `rowURL` | texto com campos | `""` | O que abre no clique. |
| `limit` | número | `5` | Quantos itens, de 6 a 60. |

```json
{ "type": "grid", "path": "pods", "label": "{{nome}}", "status": "{{fase}}", "limit": 36, "statusColors": { "Running": "positive", "Pending": "attention", "Failed": "negative" } }
```

### Linha do tempo: timeline

Eventos por data, com uma marca no agora.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `path` | caminho de lista | obrigatório | Os eventos. |
| `date` | texto com campos | obrigatório | A data de cada evento. Os que não têm data legível são pulados. |
| `dateFormat` | texto | `""` | Como ler a data. |
| `rowTitle` | texto com campos | `""` | O título do evento. |
| `rowSubtitle` | texto com campos | `""` | A segunda linha. |
| `rowURL` | texto com campos | `""` | O que abre no clique. |
| `range` | `today` \| `week` \| `upcoming` \| `all` | `upcoming` | Quais eventos. `upcoming` mostra os próximos e o último que passou. |
| `limit` | número | `5` | Quantos eventos, de 1 a 12. |

```json
{ "type": "timeline", "path": "$", "date": "{{date}}", "rowTitle": "{{name}}", "range": "upcoming", "limit": 8 }
```

### Tabela: table

Colunas sobre uma lista. Todas as linhas entram, e o que passa de `limit` rola. Um clique no título da coluna ordena.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `path` | caminho de lista | obrigatório | As linhas. |
| `columns` | lista | obrigatório | As colunas, até 8. |
| `sortable` | booleano | `true` | Ordenar pelo título. |
| `limit` | número | `5` | Linhas visíveis, de 1 a 12. |
| `rowEvent` | `none` \| `open` \| `run` | `open` | O que o clique na linha faz. |
| `rowURL` | texto com campos | `""` | O que abre, com `open`. |
| `event` | botão | vazio | O botão que a linha roda, com `run`. Mesmo formato de um item de `actions`. |

Cada coluna:

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `title` | texto | `""` | O título. |
| `kind` | `text` \| `image` \| `status` \| `button` | `text` | O que a célula mostra. |
| `value` | texto com campos | `""` | O valor, o endereço da imagem, o status ou o rótulo do botão. |
| `align` | `leading` \| `trailing` | `leading` | Alinhamento. |
| `format`, `currency` | | ausente | Com `format`, o texto vira número formatado e a coluna ordena como número. |
| `isEnabled` | booleano | `true` | Coluna desligada fica no arquivo e não aparece. |
| `shape`, `scale` | | `rounded`, `1` | `image`: forma e tamanho. |
| `hover` | texto com campos | `""` | `image`: texto que aparece acima da imagem com o ponteiro parado nela, como `{{login}}`. |
| `statusColors`, `showsWord` | | `{}`, `false` | `status`: as cores, e se a palavra aparece ao lado do ponto. |
| `action` | texto | `""` | `button`: o `name` de um botão com `place: "column"`. |
| `symbolName`, `tint`, `isProminent` | | | `button`: símbolo, cor e preenchimento. O símbolo aceita campos, como `{{symbol}}`, e muda de linha para linha. |

```json
{
  "type": "table",
  "path": "$",
  "limit": 8,
  "rowEvent": "none",
  "columns": [
    { "title": "Dia", "value": "{{timestamp | shape dd/MM}}" },
    { "title": "Fechamento", "value": "{{bid}}", "align": "trailing", "format": "money", "currency": "BRL" },
    { "title": "", "kind": "button", "action": "Copiar", "symbolName": "doc.on.doc", "align": "trailing" }
  ]
}
```

### Texto: text

Um parágrafo, com negrito, itálico e código em Markdown, e um botão de copiar.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `value` | texto com campos | obrigatório | O texto. |
| `maxHeight` | `small` \| `medium` | `medium` | A altura antes de rolar. |
| `copy` | sim ou não | `true` | Mostra o botão de copiar. |

```json
{ "type": "text", "value": "{{explanation}}", "maxHeight": "small" }
```

### Lista: list

Os itens de uma lista da resposta. Cada item é desenhado com a mesma estrutura: colunas lado a lado, cada coluna com linhas, cada linha com itens lado a lado. Sempre na largura toda.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `path` | caminho de lista | obrigatório | Os itens. |
| `limit` | `3` \| `5` \| `10` \| `0` | `5` | Quantos itens mostrar. `0` mostra todos. |
| `spacing` | `compact` \| `medium` \| `wide` | `medium` | O espaço vertical de cada item: 7, 10 ou 14 pt. |
| `columnGap` | `8` \| `12` \| `16` | `12` | O espaço entre as colunas. |
| `separator` | booleano | `true` | Uma linha fina entre os itens. |
| `highlight` | booleano | `false` | Um fundo no item ao passar o cursor. |
| `structure` | lista | obrigatório | As colunas. |
| `rowEvent` | `none` \| `open` \| `run` | `open` | O que o clique no item faz. |
| `rowURL` | texto com campos | `""` | O que abre, com `open`. Aceita endereço ou caminho de arquivo. |
| `event` | botão | vazio | O botão que o item roda, com `run`. |
| `rowDetail` | texto com campos | `""` | Um texto ao lado do item no hover. |
| `sections` | texto com campos | `""` | Um título acima de cada grupo de itens com o mesmo valor. |

Uma coluna de `structure`:

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `width` | `auto` \| `fill` \| `fixed` | `fill` | A largura. `fill` divide o que sobra. |
| `fixedWidth` | número | `48` | A largura em pt, com `fixed`. |
| `align` | `top` \| `center` \| `bottom` | `center` | O alinhamento vertical das linhas. |
| `lineGap` | `2` \| `4` \| `6` \| `8` | `4` | O espaço entre as linhas. |
| `padding` | `0` \| `4` \| `8` \| `12` | `0` | O espaçamento interno. |
| `background` | `none` \| `card` | `none` | `card` é um fundo um pouco mais claro com cantos de 8 pt. |
| `lines` | lista | `[]` | As linhas. |

Uma linha:

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `gap` | `4` \| `6` \| `8` \| `12` | `6` | O espaço entre os itens. |
| `distribute` | `start` \| `center` \| `end` \| `between` | `start` | Como os itens se distribuem. |
| `align` | `top` \| `center` \| `bottom` | `center` | O alinhamento vertical dos itens. |
| `items` | lista | `[]` | Os itens, desenhados lado a lado. |

Cada item tem `id` e `kind`, mais as chaves do tipo:

| `kind` | Chaves |
| --- | --- |
| `text` | `content` (texto com campos), `size` (11, 12, 13, 15, 17 ou 20), `weight` (`regular`, `medium`, `semibold`, `bold`), `ink`, `lines` (1, 2, 3 ou `0` para livre), `fills` (ocupa o que sobra e encolhe primeiro), `textAlign` (`leading`, `center`, `trailing`, só com `fills`). |
| `image` | `picture` (símbolo, endereço com campos ou arquivo), `size` (16, 20, 24, 32, 44, 56 ou 72), `form` (`square`, `rounded`, `circle`), `border`, `hover` (texto que aparece acima da imagem com o ponteiro parado nela, como `{{login}}`). Sem imagem, um gradiente gerado do item, com as iniciais no círculo a partir de 24 pt. |
| `icon` | `symbolName`, `size` (11, 12, 13, 15, 18 ou 22), `ink`, `content` (texto ao lado), `background` (`none`, `circle`, `square`), `tintFrom` com `rules` para a cor seguir um campo. |
| `button` | `action` (o `id` de um botão com `place: "row"`), `look` (`filled`, `outline`, `subtle`, `circle`), `buttonSize` (`small`, `medium`, `large`), `ink`. Nome, ícone e `shows` vêm do botão. |
| `indicator` | `content` (o valor), `indicator` (`dotAndText`, `dot`, `text`), `size` (11, 12 ou 13), `rules` (`{ value, tint, label }`, comparando o texto sem diferenciar maiúsculas), `otherTint`. |
| `space` | `spaceWidth` (`0` para flexível, ou 4, 8, 16, 24). |

`ink` é `primary`, `secondary`, `tertiary`, um matiz (`blue`, `red`, `orange`, `yellow`, `green`, `teal`, `indigo`, `purple`, `pink`) ou `#RRGGBB`.

```json
{
  "type": "list",
  "path": "items",
  "limit": 5,
  "structure": [
    { "width": "auto", "lines": [
      { "items": [{ "kind": "image", "picture": { "kind": "address", "value": "{{thumb}}" }, "size": 56, "form": "rounded" }] }
    ] },
    { "width": "fill", "lines": [
      { "items": [{ "kind": "text", "content": "{{title}}", "weight": "medium", "lines": 2, "fills": true }] },
      { "items": [
        { "kind": "space", "spaceWidth": 0 },
        { "kind": "text", "content": "{{source}} · {{age}}", "size": 12, "ink": "secondary" }
      ] }
    ] }
  ],
  "rowURL": "{{url}}"
}
```

Uma lista com o formato anterior, com `tabs`, é convertida ao abrir o plugin: cada aba vira um componente `list`, e um componente `tabs` aponta para elas.

### Abas: tabs

Uma barra de abas. Cada aba mostra um ou mais componentes do cartão, logo abaixo da barra e na ordem da aba. Um componente posto numa aba sai do fluxo do cartão. Sem título e sempre na largura toda.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `tabStyle` | `pill` \| `square` | `pill` | O formato das abas. |
| `tabAlign` | `start` \| `center` \| `end` | `start` | O alinhamento da barra. |
| `tabRing` | booleano | `false` | Um anel de 2 pt na aba selecionada. |
| `tabRingColor` | matiz ou `#RRGGBB` | `purple` | A cor do anel. |
| `tabLine` | booleano | `true` | Uma linha abaixo da barra. |
| `pages` | lista | `[]` | As abas, na ordem. |

Uma aba:

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `id` | UUID | gerado | O identificador da aba. |
| `kind` | `text` \| `symbol` \| `image` | `text` | O que a aba mostra. |
| `name` | texto com campos | `""` | O texto da aba, ou a dica no hover com símbolo e imagem. Aceita campos, como `Revisão {{prs \| count}}`. |
| `symbolName` | texto | `sun.max` | O símbolo, com `symbol`. |
| `picture` | imagem | arquivo | A imagem, com `image`: `file` ou `address`. |
| `imageSize` | número | `20` | O tamanho da imagem, de 12 a 40 pt. |
| `form` | `square` \| `rounded` \| `circle` | `circle` | A forma da imagem. |
| `border` | booleano | `false` | Uma borda fina em volta da imagem. |
| `symbolSize` | 11, 13, 15, 18, 22 ou 28 | `15` | O tamanho do símbolo. |
| `showsName` | booleano | `false` | Mostra `name` ao lado do símbolo ou da imagem, em vez de só no hover. |
| `textSize` | 11, 12, 13, 15, 17 ou 20 | `13` | O tamanho do texto. |
| `weight` | `regular` \| `medium` \| `semibold` \| `bold` | `medium` | O peso do texto. |
| `ink` | `primary`, `secondary`, `tertiary`, matiz ou `#RRGGBB` | `primary` | A cor do texto e do símbolo. As abas não selecionadas aparecem mais apagadas. |
| `bodies` | lista de UUID | `[]` | Os `id` dos componentes que a aba mostra, na ordem. Um `body` com um `id` só, do formato anterior, também é lido. |

- Um componente `tabs` não pode ficar dentro de uma aba.
- Se o mesmo componente estiver em mais de uma aba, vale a primeira na ordem do cartão.
- Um componente removido do cartão sai da aba. Tirado só da aba, ele volta ao fluxo do cartão.
- A primeira aba abre selecionada. Com o cartão em foco, ⌘1 a ⌘9 escolhem a aba.

### Botões: buttons

O painel de botões. O componente não guarda botões: ele desenha os itens de `actions` com `place: "panel"`.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `group` | texto | `""` | Vazio mostra todos os botões de painel. Preenchido, só os do mesmo `group`. Assim dois painéis convivem no cartão. |
| `resultSpot` | `above` \| `below` \| `leading` \| `trailing` | `below` | Onde aparece o texto ou a imagem que um botão trouxe. À esquerda e à direita, divide a largura com os botões. |

```json
{ "type": "buttons", "resultSpot": "below" }
```

A aparência de cada botão está em [Botões do painel](#botões-do-painel).

### Propriedades: properties

Pares de nome e valor, cada um com botões ao lado.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `layout` | `columns` \| `stacked` \| `grid` | `columns` | Duas colunas, empilhado ou em grade. |
| `properties` | lista de `{ id, name, value }` | `[]` | Os pares fixos. `value` aceita campos. |
| `path` | caminho | `""` | Um objeto da resposta. Cada campo dele vira um par, depois dos fixos. |

Um botão ao lado de um par é um item de `actions` com `place: "property"` e `property` igual ao `id` do par. Sem `property`, ele aparece em todos os pares que vieram de `path`. Dentro do botão, `{{name}}` e `{{value}}` são o par clicado.

```json
{
  "type": "properties",
  "layout": "columns",
  "properties": [
    { "id": "8F1C2A4E-0B5D-4C7A-9E21-3D4F5A6B7C80", "name": "Plano", "value": "{{plan.name}}" },
    { "id": "1A2B3C4D-5E6F-4A7B-8C9D-0E1F2A3B4C5D", "name": "Chave", "value": "{{api_key}}" }
  ]
}
```

### Controles: controls

Interruptores e níveis de 0 a 100, direto no cartão.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `controlStyle` | `tiles` \| `list` \| `compact` | `tiles` | Ladrilhos, lista ou compacto. |
| `perRow` | `2` \| `3` | `2` | Ladrilhos por linha. |
| `controls` | lista | `[]` | Os controles. |

Cada controle:

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `id` | UUID | obrigatório | O botão que ele roda aponta para este `id`. |
| `name` | texto com campos | `""` | O nome. |
| `kind` | `toggle` \| `level` | `toggle` | Interruptor ou nível. |
| `path` | caminho | `""` | Onde a resposta diz o estado atual. |
| `isOn`, `level` | | `false`, `50` | O estado fixo, quando não há `path`. |

Mudar um controle roda o item de `actions` com `place: "control"` e `control` igual ao `id` dele. O valor novo chega em `{{value}}`. Se a ação falhar, o controle volta ao que era.

```json
"card": {
  "components": [
    {
      "type": "controls",
      "controls": [
        { "id": "EF459F1A-12E6-454B-A63E-95009EDAA3C9", "name": "Sala", "kind": "toggle", "path": "[entity_id=light.sala].state" }
      ]
    }
  ]
},
"actions": [
  {
    "name": "Sala",
    "place": "control",
    "control": "EF459F1A-12E6-454B-A63E-95009EDAA3C9",
    "successReport": "nothing",
    "run": {
      "kind": "http",
      "method": "POST",
      "url": "http://homeassistant.local:8123/api/services/light/toggle",
      "headers": [{ "name": "Authorization", "value": "Bearer {{$settings.token}}" }],
      "body": "{\"entity_id\": \"light.sala\"}"
    }
  }
]
```

## Botões: actions

Todo botão do plugin é um item de `actions`. O lugar onde ele aparece é `place`.

| `place` | Onde aparece |
| --- | --- |
| `card` | No topo do cartão, ao lado do título. É o padrão. |
| `row` | Num item `button` de um componente `list`, em cada item da lista. |
| `panel` | No componente `buttons`. |
| `column` | Numa coluna `button` de uma tabela, ligado pelo `name`. |
| `property` | Ao lado de um par do componente `properties`. |
| `control` | Não aparece: roda quando um controle muda. |

### As chaves de um botão

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `name` | texto | `""` | O nome do botão. Vazio, «Rodar». |
| `mark` | imagem | símbolo `bolt.fill` | O símbolo ou a imagem do botão. |
| `place` | texto | `card` | Onde ele aparece. |
| `shows` | `both` \| `icon` \| `text` | `icon` | `row`: ícone e texto, só ícone ou só texto. |
| `property` | UUID | ausente | `property`: ao lado de qual par. |
| `control` | UUID | ausente | `control`: qual controle o dispara. |
| `hint` | texto com campos | `""` | A dica ao passar o cursor. |
| `isProminent` | booleano | `false` | Botão preenchido, no topo, na linha e nas propriedades. |
| `needs` | texto com campos | `""` | `card`: o botão só aparece enquanto este campo tem valor. |
| `asksForValue` | booleano | `false` | Pede um valor antes de rodar. O que for digitado é `{{$input}}`. |
| `question` | texto | `""` | O texto de exemplo do campo. Vazio, «Digite um valor». |
| `confirms` | booleano | `false` | Pede confirmação antes de rodar. |
| `confirmText` | texto com campos | `""` | A pergunta. Vazia, «Rodar nome?». |
| `runningText` | texto com campos | `""` | O que a linha diz enquanto roda. Vazio, «Rodando…». |
| `spinsRing` | booleano | `true` | O ícone mostra que está ocupado. |
| `successReport` | `message` \| `output` \| `nothing` | `message` | O que aparece quando dá certo: uma frase, a saída do programa, ou nada. |
| `successMessage` | texto com campos | `""` | A frase. `{{@campo}}` lê a resposta do botão. |
| `failureReport` | `message` \| `output` \| `nothing` | `message` | O mesmo, quando falha. Uma falha nunca fica muda: sem frase, aparece o erro. |
| `failureMessage` | texto com campos | `""` | A frase da falha. |
| `run` | objeto ou lista | `{ "kind": "http" }` | O que o botão roda. |
| `stopsOnFailure` | booleano | `true` | Numa lista de passos, para no primeiro que falha. |
| `rules` | lista | `[]` | O que fazer conforme a resposta. |

Quando o botão dá certo, o plugin lê a fonte de novo e redesenha o ícone e o cartão.

Arquivos antigos com `symbolName`, `picture`, `isWide`, `statusHues` e `thenRun` continuam sendo lidos.

### O que um botão roda: run

`run` tem o mesmo formato de `source`: `http`, `command` e `script` funcionam igual, com os mesmos limites. Um botão ainda tem a quinta porta, `onTheMac`, que faz algo no próprio Mac:

| `errand` | Chaves | O que faz |
| --- | --- | --- |
| `copy` | `text` | Copia o texto. |
| `openLink` | `text` | Abre o endereço no navegador. |
| `openApp` | `text` | Abre um app pelo bundle id (`com.apple.Music`), caminho ou nome. |
| `notify` | `title`, `text` | Manda uma notificação do sistema. |
| `playSound` | `sound` | Toca um dos sons de Configurações › Notificações. |
| `runShortcut` | `text`, `input` | Roda um Atalho pelo nome, com `input` como entrada. |
| `remember` | `variable`, `text` | Grava um valor numa variável global. |
| `addToList` | `variable`, `text` | Acrescenta uma linha a uma lista: um campo `textList` do plugin ou uma variável global. |
| `dropFromList` | `variable`, `text` | Tira uma linha dessa lista. |
| `readLater` | `text` | Guarda o endereço em Ler mais tarde. |

```json
"run": { "kind": "onTheMac", "errand": "copy", "text": "{{url}}" }
```

O nome antigo de `text` nos recados era `address`, que continua sendo lido.

### Passos em sequência

`run` aceita uma lista. Os passos rodam em ordem, e cada um lê a resposta do anterior com `@`. Por padrão, um passo que falha para os seguintes, e a mensagem diz qual falhou.

```json
"run": [
  { "kind": "http", "method": "POST", "url": "https://api.exemplo.com/deploy" },
  { "kind": "onTheMac", "errand": "openLink", "text": "{{@deploy.url}}" }
]
```

> [!NOTE]
> O editor visual edita só o primeiro passo. Uma sequência se escreve no JSON.

### Botões do painel

Com `place: "panel"`, o botão ganha forma própria, como um ícone da tela de início do iPhone.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `size` | `small` \| `medium` \| `large` | `medium` | Um quadrado, meia linha ou a linha inteira. A linha recebe botões até o próximo não caber. |
| `hue` | cor | `theme` | `theme`, `red`, `orange`, `yellow`, `green`, `teal`, `blue`, `indigo`, `purple` ou `pink`. |
| `label` | texto com campos | `""` | A palavra no botão. Vazia, o `name`. |
| `spot` | `top` \| `leading` \| `trailing` | `top` | O símbolo acima, antes ou depois da palavra. |
| `titleSpot` | `inside` \| `below` | `inside` | A palavra dentro do botão ou abaixo dele. |
| `shape` | `rounded` \| `round` | `rounded` | `small`: quadrado ou círculo. |
| `group` | texto | `""` | Em qual componente `buttons` ele entra. |
| `status` | texto com campos | `""` | Um campo da leitura que decide a aparência em repouso. |
| `states` | lista de `{ value, hue, symbolName, label }` | `[]` | A aparência para cada valor de `status`. |

`status` e `states` são o jeito de nove lâmpadas acenderem e apagarem com uma leitura só:

```json
{
  "name": "Sala",
  "place": "panel",
  "size": "small",
  "mark": { "kind": "symbol", "value": "lightbulb" },
  "status": "{{[entity_id=light.sala].state}}",
  "states": [
    { "value": "on", "hue": "yellow", "symbolName": "lightbulb.fill", "label": "Acesa" },
    { "value": "off", "hue": "theme", "symbolName": "lightbulb", "label": "Apagada" }
  ],
  "run": { "kind": "http", "method": "POST", "url": "http://homeassistant.local:8123/api/services/light/toggle" }
}
```

## Regras: rules

Um botão olha o que voltou e decide o que fazer. As regras rodam em ordem, e **toda regra que bate acontece**. Um se/senão são duas regras.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `test` | texto | `worked` | O que verificar. |
| `field` | texto com campos | `""` | O campo olhado, quase sempre com `@`, como `{{@state}}`. Vazio é a resposta inteira. |
| `value` | texto com campos | `""` | Com o que comparar. |
| `then` | lista | `[]` | O que acontece quando a regra bate. |

| `test` | No editor | Bate quando |
| --- | --- | --- |
| `worked` | deu certo | O botão deu certo. |
| `failed` | falhou | O botão falhou. |
| `isTrue` | é verdadeiro | O campo é verdadeiro. |
| `isFalse` | é falso | O campo é falso. |
| `equals` | é | O campo é igual a `value`. |
| `differs` | não é | O campo é diferente de `value`. |
| `has` | contém | O campo contém `value`, sem diferenciar maiúsculas. |
| `above` | é maior que | O campo é um número maior que `value`. |
| `below` | é menor que | O campo é um número menor que `value`. |
| `isEmpty` | está vazio | O campo está vazio. |
| `exists` | veio | O campo existe na resposta. |

O que pode acontecer em `then`, cada item com um `kind`:

| `kind` | Chaves | O que faz |
| --- | --- | --- |
| `mark` | `hue`, `symbolName`, `label` | Muda o próprio botão. |
| `say` | `text` | Escreve uma linha no cartão, no lugar da `successMessage` ou da `failureMessage`. Vazio, o que voltou. |
| `show` | `text` | Abre um painel de texto junto do componente `buttons`. Vazio, a resposta inteira. |
| `picture` | `picture` | Mostra uma imagem de um endereço `https`. |
| `run` | `run` | Roda outra chamada, com a resposta do botão em `@`. |
| `reread` | | Relê o plugin, mesmo se o botão falhou. |
| `attention` | | Faz o ícone piscar e saltar. |

```json
"rules": [
  { "test": "equals", "field": "{{@state}}", "value": "on", "then": [{ "kind": "mark", "hue": "yellow", "label": "Acesa" }] },
  { "test": "failed", "then": [{ "kind": "say", "text": "Não consegui falar com a lâmpada." }] }
]
```

- Um `run` de regra não dispara outras regras. São no máximo quatro por clique, e eles param no primeiro que falha.
- A marca de uma regra vale até o botão rodar de novo ou até a próxima leitura. Ela não é guardada quando o app fecha.
- O botão veste, nesta ordem: o que a última regra disse, depois o `states` da leitura, depois as chaves fixas.
- Toda execução entra no histórico do plugin, com horário, gatilho, resultado e o erro real. O app guarda as 60 últimas.

## O ícone como botão: role e button

Com `role: "button"`, o ícone vira o botão: o clique roda a `source` e a resposta passa a ser o dado do ícone. Não há leitura periódica nem cartão.

| Chave de `button` | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `confirms` | booleano | `false` | Pergunta antes de rodar. |
| `question` | texto com campos | `""` | A pergunta. Vazia, «Rodar nome?». |
| `okLabel` | texto | `""` | O botão de confirmar. Vazio, «Sim». |
| `runningText` | texto com campos | `""` | O que aparece enquanto roda. |
| `successReport`, `failureReport` | `message` \| `output` \| `nothing` | `message` | O que aparece depois. |
| `successMessage`, `failureMessage` | texto com campos | `""` | As frases. |

Uma pergunta sem resposta se cancela em 5 segundos.

## Chamar atenção: attention

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `mode` | `never` \| `newRows` \| `countAbove` | `never` | Nunca, quando aparece uma linha nova, ou quando um número passa de um limite. |
| `countPath` | número | `""` | `countAbove`: o número observado. |
| `comparison` | `above` \| `below` | `above` | Passar para cima ou para baixo. |
| `threshold` | número ou texto com campos | `0` | O limite. Aceita `{{$settings.limite}}`. |

- `newRows` compara as linhas de todas as listas com a leitura anterior. A primeira leitura só anota.
- `countAbove` avisa na passagem, não enquanto o número continua do outro lado.
- O aviso faz o ícone piscar e saltar, e segue o som e o peek de Configurações › Notificações.

```json
"attention": { "mode": "countAbove", "countPath": "{{items | count}}", "comparison": "above", "threshold": "{{$settings.limite}}" }
```

## Configurações do plugin: settings

Campos que quem instala preenche, na tela de instalação e depois em Configurações › Plugins. Assim um plugin compartilhado funciona sem ninguém editar o arquivo.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `key` | texto | obrigatório | O nome, lido como `{{$settings.chave}}`. Letras, números e `_`. |
| `label` | texto | `key` | O rótulo. |
| `help` | texto | `""` | Uma linha de ajuda. |
| `type` | `text` \| `number` \| `toggle` \| `choice` \| `textList` \| `secret` | `text` | O tipo. |
| `default` | conforme o tipo | vazio | O valor inicial. Não vale para `secret`. |
| `required` | booleano | `false` | A instalação só termina com o campo preenchido. |
| `options` | lista | `[]` | `choice`: textos ou `{ value, label }`. Sem `default`, a primeira opção vem marcada. |
| `placeholder` | texto | `""` | O exemplo dentro do campo vazio. |

```json
"settings": [
  { "key": "repo", "label": "Repositório", "type": "text", "placeholder": "dono/nome", "required": true },
  { "key": "limite", "label": "Avisar acima de", "type": "number", "default": 5 },
  { "key": "periodo", "label": "Período", "type": "choice", "default": "daily",
    "options": [{ "value": "daily", "label": "Hoje" }, { "value": "weekly", "label": "Semana" }] },
  { "key": "feeds", "label": "Endereços", "type": "textList" },
  { "key": "token", "label": "Token do GitHub", "type": "secret", "required": true }
]
```

- `{{$settings.chave}}` vale em todo lugar que aceita campos: fonte, botões, ícone e cartão.
- `number` chega como número, `toggle` como verdadeiro ou falso, `textList` como lista de linhas.
- Cada `secret` fica fora do manifesto, nas Chaves do Mac ou no arquivo de segredos do app.
- O arquivo exportado leva as definições e os `default`, nunca o que foi preenchido.

> [!TIP]
> Todo token vai num campo `secret` de `settings`. É o jeito de compartilhar o plugin sem entregar a sua chave.

## Variáveis globais

Um valor escrito uma vez em Configurações › Variáveis e lido em qualquer plugin, como `{{$global.cidade}}`. Os tipos são os mesmos de `settings`, e um segredo fica nas Chaves do Mac.

`settings` é do plugin e viaja com ele. `$global` é do app e vale para todos os plugins. O recado `remember` grava uma variável global a partir de um botão.

## Campos e filtros

Todo texto que aceita campos mistura palavras com `{{ }}`:

```text
{{caminho}}
{{caminho | filtro}}
{{caminho | filtro argumento}}
{{caminho ?? outro ?? "texto fixo"}}
```

- `??` tenta cada alternativa até achar uma com valor.
- Um filtro por campo. Filtros não se encadeiam.
- Um campo vazio leva junto o texto colado nele: `{{nome}} · {{nota}}` sem nota não deixa o ponto sobrando.

### Caminhos

| Caminho | O que lê |
| --- | --- |
| `campo.sub` | Um campo. Dentro de uma lista, um campo da linha. |
| `lista[0]`, `lista[-1]` | Um item pela posição. Negativo conta do fim. |
| `lista[*].nome` | Todos os itens. |
| `lista[status=open]` | Os itens com aquele valor. |
| `$` | A resposta inteira. |
| `/campo` | Um campo da resposta inteira, de dentro de uma linha. |
| `@campo` | A resposta do botão que acabou de rodar. |
| `$input` | O valor digitado no botão. |
| `$settings.chave` | Uma configuração do plugin. |
| `$global.nome` | Uma variável global. |
| `$page`, `$offset`, `$size`, `$cursor` | A página que está sendo buscada. Veja [Paginação](#paginação). |

### Campos do momento

| Campo | O que traz |
| --- | --- |
| `{{$clipboard}}` | O texto da área de transferência. |
| `{{$frontmostApp}}` | O nome do app na frente. |
| `{{$now}}` | Data e hora atual, em ISO 8601. |
| `{{$activeURL}}` | O endereço da aba aberta no navegador da frente. |
| `{{$activeTitle}}` | O título dessa aba. |

`$activeURL` e `$activeTitle` funcionam no Safari e nos navegadores Chromium (Chrome, Arc, Brave, Edge, Vivaldi, Opera), e só quando um botão roda. Na primeira vez, o macOS pede a permissão de Automação para cada navegador.

### Filtros

| Filtro | O que faz | Exemplo |
| --- | --- | --- |
| `count` | Quantos itens há numa lista. | `{{items \| count}}` → `12` |
| `round` | Arredonda. | `{{temp \| round}}` → `24` |
| `percent` | Uma fração em percentual. | `{{taxa \| percent}}` → `42%` |
| `bytes` | Tamanho de arquivo. | `{{size \| bytes}}` → `1.2 GB` |
| `ago` | Quanto tempo faz. | `{{created \| ago}}` → `5 min` |
| `date` | Data e hora curtas. | `{{start \| date}}` → `23/09 10:00` |
| `clock` | Segundos em relógio. | `{{left \| clock}}` → `19:40` |
| `duration` | Segundos, ou minutos com `minutes`, por extenso. | `{{mins \| duration minutes}}` → `5 h 12 min` |
| `upper`, `lower`, `trim` | Maiúsculas, minúsculas, sem espaços nas pontas. | `{{code \| upper}}` |
| `plural` | Quantos itens há numa lista, com a palavra no singular ou no plural. | `{{items \| plural item/itens}}` → `3 itens` |
| `shape` | Um padrão de número, data ou texto. | `{{bid \| shape #,##0.00}}`, `{{data \| shape dd/MM/yyyy}}` |

`shape` aceita padrões de número (`#,##0.00`, `R$ #,##0.00`, `+#,##0;-#,##0`), de data (`dd/MM`, `HH:mm`, `MM/yyyy`) e `upper`, `lower` e `title`.

> [!WARNING]
> `count` e `plural` contam itens de lista. Num campo que já é número, eles respondem 1. Para escrever um número com a palavra, use `{{total}} itens`.

## Paginação

Com `source.pagination`, o cartão ganha um botão **Ver mais** no fim da lista, que busca a próxima página e acrescenta as linhas.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `kind` | `page` \| `offset` \| `cursor` \| `nextURL` \| `linkHeader` | `page` | Como a API pagina. |
| `listPath` | caminho | a primeira aba | A lista que cresce. |
| `start` | número | `1` em `page`, `0` em `offset` | O primeiro valor. |
| `size` | número | `card.rowLimit` | Itens por página. |
| `cursorPath` | caminho | `""` | `cursor`: onde a resposta traz o próximo cursor. |
| `nextURLPath` | caminho | `""` | `nextURL`: onde a resposta traz o próximo endereço. |
| `hasMorePath` | caminho | `""` | Um campo que diz se há mais. |
| `maxPages` | número | `10` | Limite de páginas, até 50. |
| `buttonText` | texto | `Ver mais` | O texto do botão. |

- `page` troca `{{$page}}` no endereço ou no corpo: 1, 2, 3…
- `offset` troca `{{$offset}}` (0, `size`, 2 × `size`…) e `{{$size}}`.
- `cursor` troca `{{$cursor}}` pelo cursor lido na página anterior.
- `nextURL` busca o endereço lido em `nextURLPath`, com os mesmos cabeçalhos.
- `linkHeader` segue o `rel="next"` do cabeçalho `Link`, como no GitHub. `nextURL` e `linkHeader` são só de `http`.

Linhas repetidas entre páginas são descartadas, pelo `rowURL` ou pelo título e a segunda linha. A leitura periódica relê só a primeira página.

```json
"pagination": { "kind": "linkHeader", "listPath": "$" }
```

## Leituras guardadas: history

O app pode guardar um campo de cada leitura, para desenhar a evolução sem a API ter histórico.

| Chave | Tipo | Padrão | O que é |
| --- | --- | --- | --- |
| `path` | número | `""` | O campo guardado a cada leitura boa. |
| `keep` | número | `48` | Quantas leituras guardar, de 2 a 500. |

Um componente `chart` sem `path`, o `slot` com `style: "sparkline"` e o `previous` vazio de um `hero` usam essas leituras. Elas ficam neste Mac, fora do manifesto.

```json
"history": { "path": "{{USDBRL.bid}}", "keep": 96 }
```

## A instalação

Um plugin que vem de fora mostra o que faz antes de entrar. A lista sai das chaves usadas:

- o que ele lê, com que frequência, e se toca a rede;
- os programas e scripts que roda, com o conteúdo à vista;
- cada botão, e se ele pede confirmação;
- os campos de `settings`, preenchíveis ali mesmo;
- se pede token;
- os campos do momento que lê, e quando;
- os apps que abre e os Atalhos que roda;
- as permissões que o macOS pode pedir;
- os botões novos, quando é uma versão nova de um plugin já instalado.

Todo plugin importado abre primeiro no editor, com a prévia do cartão, e o botão **Instalar** mostra esta tela com os campos que faltam. A prévia usa a `answer` do manifesto. Sem `answer`, um plugin de endereço que já tem tudo o que precisa faz uma leitura de verdade, e os outros usam valores que o app monta a partir dos campos que o cartão lê.

> [!TIP]
> Um plugin que pede token ou endereço fica melhor com uma `answer` no formato da resposta real, com poucas linhas e todos os campos que o cartão usa.

Um plugin se instala de três jeitos: **Configurações › Plugins › Importar…**, arrastando o arquivo para a lista, ou por um link `peek://install?url=` seguido do endereço `https` do arquivo. O link baixa até 2 MB e abre a mesma tela.

> [!CAUTION]
> Programas e scripts rodam no seu Mac com as suas permissões. Leia o conteúdo na tela de instalação antes de confirmar.

## Um exemplo completo

Pull requests esperando a sua revisão, com o número no ícone, uma lista no cartão e um botão para copiar o endereço:

```json
{
  "schema": 12,
  "id": "plugin.exemplo.revisoes",
  "name": "Revisões",
  "interval": 300,
  "icon": { "kind": "symbol", "symbolName": "arrow.triangle.pull" },
  "source": {
    "kind": "http",
    "url": "https://api.github.com/search/issues",
    "query": [{ "name": "q", "value": "is:pr is:open review-requested:{{$settings.usuario}}" }],
    "headers": [{ "name": "Authorization", "value": "Bearer {{$settings.token}}" }]
  },
  "slot": { "label": "{{total_count}}" },
  "card": {
    "title": "Esperando revisão",
    "emptyMessage": "Nenhuma revisão pendente.",
    "components": [
      {
        "type": "list",
        "path": "items",
        "limit": 10,
        "rowURL": "{{html_url}}",
        "structure": [
          { "width": "auto", "lines": [
            { "items": [{ "kind": "image", "picture": { "kind": "address", "value": "{{user.avatar_url}}" }, "size": 32, "form": "circle" }] }
          ] },
          { "width": "fill", "lines": [
            { "items": [{ "kind": "text", "content": "{{title}}", "fills": true }] },
            { "items": [
              { "kind": "text", "content": "{{user.login}} · {{updated_at | ago}}", "size": 12, "ink": "secondary", "fills": true },
              { "kind": "button", "action": "6F1C2D3E-0000-4000-8000-000000000001", "look": "circle", "buttonSize": "small" }
            ] }
          ] }
        ]
      }
    ]
  },
  "actions": [
    {
      "id": "6F1C2D3E-0000-4000-8000-000000000001",
      "name": "Copiar link",
      "place": "row",
      "mark": { "kind": "symbol", "value": "doc.on.doc" },
      "run": { "kind": "onTheMac", "errand": "copy", "text": "{{html_url}}" },
      "successMessage": "Copiado"
    }
  ],
  "attention": { "mode": "newRows" },
  "settings": [
    { "key": "usuario", "label": "Seu usuário no GitHub", "type": "text", "required": true },
    { "key": "token", "label": "Token do GitHub", "type": "secret", "required": true }
  ]
}
```

O `schema` é 12 porque o cartão usa um componente `list`.
