Vee

Vee documentation

JSON output format

Alongside the xbar/SwiftBar text protocol, Vee understands an optional structured-JSON output format. A plugin prints a single JSON object describing its title and menu, and Vee decodes it directly — no line parsing, no |-separated parameters, no quoting rules.

When to use JSON vs the text protocol

The text protocol is compact and familiar, and every plugin can use it. Reach for JSON when the escaping and nesting of the text format start to get in the way:

  • No quoting or escaping games. Menu text, URLs, and shell arguments that contain spaces, |, or quotes are just JSON strings. There is no | title="two words" dance and no \" escaping.
  • Typed items. Booleans are real booleans ("separator": true, "terminal": false), sizes are numbers, and there is no ambiguity between a value and a parameter name.
  • Clean nesting. Submenus are arrays nested inside an item ("submenu": [ … ]) rather than depth-prefixed with -- dashes, so deep menus stay readable and are trivial to build from a data structure.

If you are emitting a menu from structured data (an API response, a config object), JSON is usually the shorter path. For quick one-liners the text protocol is still the easy default.

Opting in

JSON is opt-in per run. Vee uses the JSON parser only when your plugin's output is a top-level JSON object that declares the format version:

  • The first non-whitespace character of stdout must be {.
  • The object must contain the key "vee": 1 (the current format version).

If both hold and the object decodes, Vee renders it as JSON. Otherwise it falls back to the text parser (OutputParser.parseAuto tries JSON first, then the text format). This means:

  • Malformed JSON, or JSON missing the "vee" key, silently falls through to the text parser rather than erroring.
  • Text-protocol plugins are unaffected — text rarely begins with {, and even if it does, the "vee" requirement keeps the two formats from colliding.

The minimal opt-in is a title-only object:

json
{"vee":1,"title":[{"text":"Hello"}]}

Schema

Top level

json
{ "vee": 1, "title": [ … ], "items": [ … ] }
KeyTypeRequiredMeaning
veenumberyesFormat version. Must be 1. Its presence is what opts the run into JSON.
titlearray of JSONTitlenoThe menu-bar title line(s). Multiple entries render as multiple title lines.
itemsarray of JSONItemnoThe dropdown body, top to bottom.

JSONTitle

An entry in title.

KeyTypeRequiredMeaning
textstringyesThe title text shown in the menu bar.
colorstringnoText color (a named color or hex, e.g. "green", "#34c759").
sfimagestringnoAn SF Symbol name to render alongside the text.
sizenumbernoFont size in points.

JSONItem

An entry in items (or in a submenu). Every field is optional; the shape of the item depends on which fields you set.

KeyTypeMeaning
textstringThe item's label. Omitted for a separator.
separatorbooleanWhen true, this entry is a divider; all other fields are ignored.
headerbooleanWhen true, render as a real, non-interactive section header rather than a normal row (the JSON spelling of header=true).
colorstringText color (named or hex).
hrefstringA URL to open when the item is clicked.
shellstringA command to run on click (a launch path or command name).
paramsarray of stringArguments passed to shell, in order.
terminalbooleanWhen true, run the shell command in Terminal; otherwise run it in the background.
refreshbooleanWhen true, clicking re-runs the plugin.
sfimagestringAn SF Symbol name to render alongside the text.
sizenumberFont size in points.
disabledbooleanWhen true, the item is shown greyed-out and not clickable.
checkedbooleanWhen true, the item shows a checkmark.
tooltipstringHover tooltip text.
submenuarray of JSONItemChild items, forming a nested submenu.
alternateJSONItemAn alternate item shown when Option is held (mirrors the text protocol's alternate=true).

Examples

Title-only menu

The smallest useful JSON plugin — just a menu-bar title, no dropdown.

json
{
  "vee": 1,
  "title": [{ "text": "CPU 12%", "color": "green", "sfimage": "cpu" }]
}

A dropdown with a clickable link, a divider, a nested submenu, and an Option-key alternate on the first item.

json
{
  "vee": 1,
  "title": [{ "text": "Build ✓", "color": "green" }],
  "items": [
    {
      "text": "Open dashboard",
      "href": "https://ci.example.com/builds",
      "alternate": { "text": "Open dashboard (raw logs)", "href": "https://ci.example.com/builds/raw" }
    },
    { "separator": true },
    {
      "text": "Recent",
      "submenu": [
        { "text": "#4210 passed", "color": "green", "href": "https://ci.example.com/4210" },
        { "text": "#4209 failed", "color": "red", "href": "https://ci.example.com/4209" }
      ]
    }
  ]
}

A shell-action item with params

An item that runs a command when clicked, passing arguments via params.

json
{
  "vee": 1,
  "title": [{ "text": "Deploy" }],
  "items": [
    {
      "text": "Restart web server",
      "shell": "/usr/bin/sudo",
      "params": ["systemctl", "restart", "nginx"],
      "terminal": true,
      "tooltip": "Runs in Terminal so you can watch it"
    },
    { "text": "Refresh", "refresh": true }
  ]
}

A runnable example

The repository ships a runnable JSON plugin at plugins/examples/json-demo.ts. It builds a {"vee":1,…} object with a colored title, a link, a separator, and a submenu, then prints it — a good starting point to copy.

Rich params

The Vee-native inline controls are available in JSON too, as typed item fields:

FieldTypeNotes
sparklinenumber[]Inline chart popover data. Non-finite values are dropped.
togglebooleanOn/off switch.
slider{ "min": number, "max": number, "value": number }Requires min < max; value is clamped into range.
progressnumberA completion fraction, clamped to 0…1. The fill uses the item's color.
trackColorstringProgress track color (named or hex).
progressWidthnumberProgress bar width in points.
progressHeightnumberProgress bar height in points.
chart{ "kind": "pie" | "donut" | "stackedbar", "values": number[], "labels"?: string[], "colors"?: string[], "w"?: number, "h"?: number }A categorical share chart. values must be finite and >= 0 with a positive total; at most 8 segments (a longer series folds its tail into "Other"). labels/colors are positional — a color that is null, blank, malformed, or unrecognised keeps that segment's palette slot. w/h are the inline size in points (chartw=/charth=), clamped to 8–200; "w": "full" stretches the chart to the row's width.
json
{
  "vee": 1,
  "title": [{ "text": "System" }],
  "items": [
    { "text": "Load history", "sparkline": [1, 2, 3, 5, 8, 13] },
    { "text": "Notifications", "toggle": true },
    { "text": "Volume", "slider": { "min": 0, "max": 100, "value": 40 } },
    { "text": "Disk usage", "color": "green", "progress": 0.72, "trackColor": "#333333", "progressWidth": 80, "progressHeight": 6 },
    { "text": "By category", "chart": { "kind": "donut", "values": [45, 30, 25], "labels": ["Documents", "Photos", "Apps"] } }
  ]
}

These map to exactly the same controls as the text protocol's sparkline= / toggle= / slider= / progress= / pie=,donut=,stackedbar= (see the plugin authoring reference) — the JSON form collapses the three chart shapes into one chart object with a kind, since they describe the same data — so colors, SF Symbols, links, shell actions, submenus, alternates, checkmarks, tooltips, and the rich controls are all available in both formats.

Editor validation (JSON Schema)

The JSON output format has a published schema too:

json
{
  "$schema": "https://vee.navbytes.io/schemas/json-output.schema.json",
  "vee": 1,
  "title": [{ "text": "System" }],
  "items": [{ "text": "Hello" }]
}

As with the widget card, $schema is ignored by Vee, and CI validates the schema against the shipped fixtures.

See also