Skip to content

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.

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 build writes dist/tools.json beside 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 in tools (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, no update()), so a tool in the tray costs nothing until it’s used.
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).

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.

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 warning status you return.
  • ctx.signal is aborted when a run takes over 30 seconds. Pass it to fetch, and declare every host in permissions.network, exactly as for blips.
  • ctx.dataDir is a folder only your extension writes to. A file you return in files, image or a drop’s outputs must 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.

  • 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 --json for 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. defineExtension keeps 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 status that 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. positive for success, warning for “look at this”, critical for failure; neutral for 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 live tools, and nothing leaves the Mac that the user didn’t ask to send.