Tools
A tool is a small utility that opens in place inside the notch: the tool tray along the bottom of the panel holds the ones the user chose, next to Blipbar’s own (JSON, Color, HTTP…). Where a blip comes to you, a tool is something you reach for: look up a package, decode a token, convert a file. An extension can offer blips, tools, or both.
| Build a… | When the thing is… | Runs |
|---|---|---|
| Blip | Something you keep checking: a value, a status, a session | On the host’s schedule, while it’s placed |
| Tool | Something you do on demand, with inputs: a lookup, a conversion | Only when the user presses its button |
A tool declares its inputs (fields) and buttons (actions) and returns typed
output blocks; Blipbar draws all of it with the same kit its built-in tools use.
You never draw a pixel, which is why an extension’s tool looks like one that shipped with
the app.
A complete tool
Section titled “A complete tool”import { code, defineExtension, status, table, tool } from "@blipbar/api";
const WHO = ["Owner", "Group", "Everyone"] as const;
/** 7 → "rwx", 5 → "r-x". */function letters(digit: number): string { return `${digit & 4 ? "r" : "-"}${digit & 2 ? "w" : "-"}${digit & 1 ? "x" : "-"}`;}
/** 7 → "Read, write, run", 0 → "Nothing". */function words(digit: number): string { const can = [digit & 4 && "read", digit & 2 && "write", digit & 1 && "run"].filter(Boolean).join(", "); return can ? can[0]!.toUpperCase() + can.slice(1) : "Nothing";}
export default defineExtension({ tools: { chmod: tool({ title: "chmod", icon: "lock.shield", summary: "Read Unix file permissions", category: "developer", fields: [ { id: "mode", type: "text", placeholder: "755" }, { id: "show", type: "choice", options: [ { id: "letters", title: "rwx" }, { id: "words", title: "Words" }, ], default: "letters", }, ], actions: [{ id: "explain", title: "Explain", role: "primary" }], live: true, // local and instant, so it runs as you type async run(_action, values) { // Inferred from `fields`: values.mode is a string, values.show is "letters" | "words". const mode = values.mode.trim(); if (!/^[0-7]{3}$/.test(mode)) return [status("Type three digits from 0 to 7, like 755.", "warning")];
const digits = [...mode].map(Number); const everyoneCanWrite = ((digits[2] ?? 0) & 2) !== 0; const symbolic = digits.map(letters).join(""); return [ everyoneCanWrite ? status(`${symbolic} · anyone can change it`, "warning") : status(symbolic), table(digits.map((digit, i) => ({ name: WHO[i] ?? "", value: values.show === "letters" ? letters(digit) : words(digit) }))), code(`chmod ${mode} file`, "shell"), ]; }, }), },});That’s the whole extension: package.json needs no interval or update(), and no
permissions, since nothing leaves the Mac. values is typed from fields (a typo
like values.moed doesn’t compile), and action from actions.
What happens around it:
blipkit buildwritesdist/tools.jsonbeside the bundle: each tool’s spec, validated. The app reads it when it finds your extension, so the tool shows up in the tray’s list without running any of your code. An invalid tool fails the build and names the problem (tools.chmod.fields[1].default: must be one of the options' ids).- The tool’s id is
<extension-id>/<name>, where the name is its key intools(dev.yourname.chmod/chmod). Keep names stable: it’s what the user’s tray stores. - Running it loads your extension on demand, with its blip side off (no
interval, noupdate()), so a tool in the tray costs nothing until it’s used.
Fields
Section titled “Fields”type |
Draws as | Extra keys | values[id] is |
|---|---|---|---|
text |
One line | string |
|
code |
Several lines, monospaced | language? ("json", "http") |
string |
file |
A drop well with Choose… | types? (uniform type identifiers, default any file), multiple? |
string[], absolute paths |
choice |
Segments (a menu past 5) | options: [{ id, title }] |
the chosen option’s id |
toggle |
A switch, label beside it | boolean |
|
number |
A value, with a slider when min and max are set |
min?, max?, step?, unit? |
number |
pairs |
Name/value rows with add and remove | { name, value }[] |
Every field takes id, label? (omit it for a tool’s single main input),
placeholder? and default?. values is always complete: a field the user left
alone arrives as its default, or empty ("", min or 0, false, the first
option, []).
Spec-level options: summary (one line: what it does for you), category
(image, video, pdf, developer, design, everyday), live (run the first
action as the inputs change; only for instant, local work, never the network),
remembersInputs (default true), and clipboardKinds (offer this tool when the
clipboard holds json, jwt, url, base64, timestamp, color, uuid or curl).
Blocks
Section titled “Blocks”| Builder | Draws as |
|---|---|
status(text, tone?) |
The headline. tone is neutral (default), positive ✓, warning !, or critical ×: the shape carries the meaning, never color alone. |
text(value) |
Plain, selectable text. The same text() you use for typed values. |
code(text, language?) |
A monospaced well with Copy, scrolling past a few lines. |
table(rows) |
Label/value rows with Copy. Pass [{ name, value, url? }], or an object (table({ License: pkg.license })) whose undefined values are skipped. A row with a url (http/https) becomes a link. |
files(paths) |
Files you produced: revealed in Finder or dragged out. |
image(path) |
An image file: a QR code, a preview. |
color(hex) |
A swatch with its hex. |
Table values are typed, exactly like a blip’s: number(157687813),
date(publishedAt), currency(12.5, "USD"), number(75429, { unit: "bytes" }).
The app formats them for the viewer’s locale (and dates read relative, “3 days ago”),
so never format a number or a date into a string yourself.
Errors, files, the network
Section titled “Errors, files, the network”run() gets a ToolContext: the usual ctx (preferences, storage, log,
exec…) plus two things tools need.
- Throw to fail, with a message for the user: what happened and what to do. The
app shows exactly your message (never a stack trace) as a failed status.
Something that isn’t a failure, like “no package with that name”, is better as a
warningstatus you return. ctx.signalis aborted when a run takes over 30 seconds. Pass it tofetch, and declare every host inpermissions.network, exactly as for blips.ctx.dataDiris a folder only your extension writes to. A file you return infiles,imageor a drop’soutputsmust be inside it, or be one the user gave the tool in this run; anything else is dropped from the output. Never write next to, or over, the user’s own files.
async run(_action, values, ctx) { let response: Response; try { response = await fetch(`https://api.example.com/items/${encodeURIComponent(values.id)}`, { signal: ctx.signal }); } catch { throw new Error("Can't reach Example. Check your connection and try again."); } if (response.status === 404) return [status(`No item “${values.id}”`, "warning")]; // …}Blipbar’s own npm tool is built this way: three requests in parallel, typed facts, links, and deprecated or missing packages as warnings. Test yours against responses you captured once, not against the live service.
Drop actions make a tool a drop target: drag files onto the notch and it offers
them, no questions asked. Declare dropActions: [{ id, title, icon, accepts }] and a
drop(action, files, ctx) that returns { message, outputs?, copiedText? }, writing
its outputs into ctx.dataDir.
Testing a tool
Section titled “Testing a tool”npx blipkit validate --tool chmod --values '{"mode": "644"}'runs the tool once (the first action unless you pass--action) and validates what it returns. Add--jsonfor just the blocks.- Keep the logic in plain functions (data in, blocks out) and unit-test those; call
extension.tools.chmod.run("explain", values, ctx)directly for the rest.defineExtensionkeeps your object’s type, so that call is fully typed.
The tool sits in a black notch beside the user’s work. The kit keeps it native; these keep it calm.
- One job, one screen. A title of one or two words (it sits under an icon), one main input, and at most one primary action with a verb: “Look Up”, “Convert”.
- Fit without scrolling. The panel tops out around 440 points. Show the four to six facts that answer the question; link to the rest.
- Headline first. Start the output with a
statusthat answers it (“express 5.2.1”, “rwxr-xr-x”), then detail. - Say what to do. “Can’t reach npm. Check your connection and try again.”, not “Error: fetch failed”. Sentence case, no exclamation marks.
- Tones mean state.
positivefor success,warningfor “look at this”,criticalfor failure;neutralfor everything else, which is most things. - Typed values, not strings. Numbers, sizes, money and dates go through the value helpers so they read right in every locale.
- No network in
livetools, and nothing leaves the Mac that the user didn’t ask to send.