Pinchos are the little snacks pinned to a Basque bar top with a skewer.
This app pins your own snacks to the macOS menu bar: one declarative TOML file, native NSStatusItems, no runtime bloat.
pinchos.toml is to your menu bar what starship.toml is to your terminal prompt.
SwiftBar and xbar host a folder of one-script-per-plugin. Pinchos is one config file with a module system: declare items, pick an interval, point at a shell command.
The other reason pinchos exists: a popular provider-quota menu bar app was measured burning 1GB of RSS to show a single percentage that updates once a minute. Pinchos exists to make that class of widget cost two orders of magnitude less. No Electron, no webview, no Dock icon. Idle RSS with 3 items at 60s intervals stays under 15MB, and idle CPU is effectively zero between ticks.
Requires macOS 14+ and Xcode 15+ (or a standalone Swift 5.10+ toolchain).
git clone https://github.com/douglasjarquin/pinchos.git
cd pinchos
swift build -c releaseThe binary lands at .build/release/pinchos. Run it directly:
.build/release/pinchosIt has no Dock icon and no main window (NSApp.setActivationPolicy(.accessory)) — it lives entirely in the menu bar.
Quit it from any item's right-click menu, or killall pinchos.
Pinchos doesn't manage login items for you (that's out of scope for v1). Two options, pick whichever fits:
# One-off, from Terminal or a script:
open /path/to/pinchos/.build/release/pinchos
# Or add it under System Settings -> General -> Login Items,
# pointing at the same release binary.Pinchos reads $XDG_CONFIG_HOME/pinchos/pinchos.toml if XDG_CONFIG_HOME is set, otherwise ~/.config/pinchos/pinchos.toml.
The file is edited live: pinchos watches it with a DispatchSource file-system-object source and applies configuration diffs without relaunching.
Unchanged items keep their existing status item, timer, runner, and displayed output.
Added items are appended when native status-item placement can preserve declaration order, removed items are torn down, and changed items update in place.
If a declaration-order change cannot be represented by native insertion, pinchos rebuilds the configured status items to restore the requested order.
Reload notifications are coalesced so in-place writes are applied after the file settles, and a malformed file leaves the last good configuration running.
The release binary also provides setup, validation, diagnostics, and one-shot execution commands.
.build/release/pinchos --help
.build/release/pinchos init
.build/release/pinchos validate
.build/release/pinchos doctor
.build/release/pinchos config-path
.build/release/pinchos open-config
.build/release/pinchos run codexUse pinchos <command> --help for command-specific help.
init creates the config directory and writes the documented example only when the config does not already exist.
validate rejects missing, empty, malformed, and semantically invalid configurations with item, key, and source-line context when available.
doctor reports config accessibility, shell and command availability, working directories, icons, environment prerequisites, and launch-at-login state when the app bundle exposes it.
config-path prints the resolved path without creating files, while open-config opens that path in its default application and creates an empty file only when necessary.
run <item> uses the same configured shell vector, working directory, merged environment, timeout, and output bounds as the menu-bar app.
CLI exit codes are suitable for scripts.
0 means success, 2 means invalid command usage, 3 means config or open failure, and 4 means doctor found a problem.
run preserves a configured command's exit code, uses 124 for timeouts, and uses 127 for launch failures.
Every item is a [item.<name>] table. Items render left-to-right in the order their tables appear in the file.
[item.<name>]
type = "command" # required, the only v1 module type
run = "<shell command>" # required, executed with `shell` on its interval
shell = ["/bin/zsh", "-lc"] # optional, default ["/bin/sh", "-c"]
working_directory = "~/src/project" # optional, tilde-expanded; relative paths are relative to this config file
interval = "60s" # optional, default "60s". Formats: "30s", "5m", "1h", or "manual"
timeout = "15s" # optional, default "15s", minimum "1s". Terminates the command process group when it expires
max_output = "64KiB" # optional, default "64KiB" per stdout/stderr stream. Formats: "B", "KiB", "MiB"
format = "{output}%" # optional. {output} is the trimmed last stdout line of `run`. Absent = raw output.
click = "<shell command>" # optional, run (fire-and-forget) on left-click
refresh_on_click = true # optional, refresh `run` on left-click when `click` is absent
error_text = "–" # optional, default "–". Shown when `run` fails, instead of the item disappearing.
on_error = "keep_last" # optional, default "replace". Keep the last successful value when `run` fails.
stale_after = "15m" # optional. Mark the last successful value stale at or after this age.
tooltip = "Updated {updated_at} ({status})" # optional. Native tooltip template.
icon = "/path/to/icon.svg" # optional, a local image file (SVG/PNG/PDF) rendered as a template icon left of the text
[item.<name>.env]
PATH = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
AWS_PROFILE = "production"
[[item.<name>.action]]
title = "Open usage"
run = "open https://example.com/usage"
[[item.<name>.action]]
title = "Refresh now"
refresh = truetooltipis rendered by the native status-item tooltip. Supported placeholders are{output}(the full retained stdout, including newlines),{updated_at}(the last successful completion time),{attempted_at}(the last command start time),{duration}(the last run duration with three decimal places and anssuffix),{exit_status}(the last exit code or terminal result),{error}(the latest bounded stderr line or terminal error),{stale}(yesorno), and{status}(running,fresh,stale,error, orunavailable).- Timestamps use UTC ISO-8601 format.
- Before the first successful run,
{output}and{updated_at}are empty, while{attempted_at}and diagnostic placeholders become available after an attempt. {{and}}escape literal braces.- An unknown placeholder or unmatched brace is a configuration error, so raw placeholder braces never leak into a native tooltip.
- The full output is the command runner's retained output subject to
max_output; a truncation flag and byte counts remain visible in the right-click diagnostics menu. shellis an executable path followed by the arguments used to invoke it;runis appended as the final argument. The default is[/bin/sh, -c], preserving the original behavior.shellandworking_directoryare resolved when the config loads. A leading~expands to the launching user's home directory, and relative filesystem paths are resolved relative to the config file, includingiconpaths.working_directoryis optional. When omitted, the command inherits Pinchos's process working directory.[item.<name>.env]values merge with the inherited Pinchos environment. Configured values override inherited variables with the same name immediately beforerunstarts, while variables not listed remain available to the command. This keeps configured values stable even when the selected shell runs login startup files.- Environment variable names must use letters, digits, and underscores, and must start with a letter or underscore.
timeoutaccepts whole seconds, minutes, or hours and terminates the command's process group after the configured duration.interval = "manual"runs the item once when it is first activated, then disables its periodic timer. Use Refresh Now from the item's right-click menu, or setrefresh_on_click = true, for later runs.refresh_on_clickonly applies whenclickis absent. A configuredclickcommand keeps ownership of the normal left-click action.- An item may declare zero or more
actiontables. - Actions appear in declaration order at the top of the item's right-click menu.
- A
runaction uses the item's shell, working directory, environment, timeout, and output bound. - A
refresh = trueaction invokes the native item refresh without starting a second shell command. - Repeated command-action invocations are skipped while that action is running, and the skipped count is retained in the item's diagnostics.
- Timeout and cancellation terminate the process group with
SIGTERMfollowed immediately bySIGKILL, so managed descendants cannot outlive an item or the app. - A command that exits while leaving same-group background work running remains owned by its item until that work exits or the item is removed.
max_outputis an independent retained-tail limit for stdout and stderr, so64KiBcan retain up to 64KiB from each stream while both streams continue draining.- Retaining the tail keeps the final output line and the most recent stderr diagnostic available even when a command emits more than the configured limit.
iconis a plain filesystem path, not a built-in icon library — pinchos ships with no bundled icon catalog. Point it at any image file you like; it's drawn as a template image (tinted automatically for light/dark menu bars) at 16x16, to the left of the item's text. A missing or unreadable file just falls back to text-only — it never crashes the app.- A failing command never crashes pinchos. With the default
on_error = "replace", it renderserror_text; withon_error = "keep_last", it retains the last successful title and full output while marking the item with a compact warning indicator. stale_afteruses the last successful completion as its clock origin and becomes stale when the age is greater than or equal to the configured threshold. A first-run failure iserror/unavailablerather than a fabricated stale value.- Command runs for a given item never overlap: if the previous run for that item hasn't finished when the next tick fires, the tick is skipped.
- Manual refreshes use the same per-item execution gate as scheduled ticks, so repeated Refresh Now actions are skipped while a run is active.
- While a refresh is running, the last good value stays visible and the item's native tooltip reports the running state. The right-click diagnostics menu reports the full retained value, state, last attempt, last success, stale flag, duration, exit/error details, and the hardened runner's per-stream diagnostics.
- Skipped ticks are counted in the item's right-click diagnostics menu without replacing the last completed result.
- The same timeout and output bounds apply to an optional click command, which is cancelled when its item is removed or Pinchos quits.
- The diagnostics menu reports the last exit code or signal, duration, skipped ticks, per-stream truncation, and the latest bounded stderr line.
- An unresolvable shell or working directory is reported in the config warning; a launch failure during execution is retained in the item's diagnostics menu with the resolved path.
- A malformed config keeps the last good config running untouched, and pinchos additionally shows a single
pinchos ⚠︎item; click it to see the parse error (with line number when available), reload, or quit. Fix the file and it clears automatically on the next successful reload. - Right-click any item for its configured actions, Refresh Now when no built-in refresh action is configured, item diagnostics, then the global Open Config, Reload Config, and Quit Pinchos actions.
- Right-click the warning item for its recovery actions.
- The app is fully usable without ever touching the config file.
[item.claude]
type = "command"
run = "quota-axi --provider claude --json | jq -r '.providers[0].windows[] | select(.label==\"week\") | .percentRemaining'"
interval = "5m"
timeout = "15s"
max_output = "64KiB"
format = "{output}%"
icon = "/path/to/pinchos/example/icons/claude.svg"
click = "open https://claude.ai/settings/usage"
[item.codex]
type = "command"
run = "quota-axi --provider codex --json | jq -r '.providers[0].windows[] | select(.label==\"week\") | .percentRemaining'"
interval = "5m"
format = "{output}%"
icon = "/path/to/pinchos/example/icons/codex.svg"
click = "open https://chatgpt.com/codex/settings/usage"This composes quota-axi (a CLI that reports local provider quota windows) with jq to pull the weekly window's remaining percentage out of its JSON (each provider labels its 7-day window "week", though the id differs by provider), one item per provider, each with its own brand icon.
quota-axi is one option here, not a dependency — run is any shell command, so this same pattern works for a stock price, a CI status, a battery reading (pmset -g batt), or a clock (date '+%H:%M').
The two icon files under example/icons/ are MIT-licensed brand marks vendored from steipete/CodexBar - see example/icons/NOTICE.md for attribution. Swap in whatever icon you like for your own items; pinchos has no opinion on where it comes from.
See example/pinchos.toml for a full working config with four items (claude, codex, clock, battery).
Sources/PinchosCore— UI-free library: TOML parsing (via TOMLKit), duration and byte-size parsing,{output}templating, the config-diff engine, and bounded process-group command execution with concurrent stdout/stderr draining.Sources/pinchos— the AppKit executable: oneNSStatusItemand one per-item scheduledDispatchSourceTimerwhen configured, plus manual refresh actions, declarative per-item menu actions, menu and lifecycle projection ofPinchosCorerunner snapshots, and aConfigWatcher(DispatchSourceFileSystemObject) for live reload.
TOMLKit is a maintained Swift wrapper around toml++, a mature C++17 TOML parser — full spec compliance (escaping, nested tables, arrays, dotted keys) without hand-rolling a parser, which is exactly the kind of correctness-critical, already-solved problem worth depending on rather than reimplementing.
One wrinkle: TOMLKit's underlying store is a std::map, so iterating a TOMLTable returns keys in alphabetical, not declaration, order — a known, currently-unresolved limitation upstream (marzer/tomlplusplus#62). Since v1's left-to-right item ordering is a hard requirement, ConfigParser does a small line-scan over the raw text to record the order [item.*] headers appear in, then uses TOMLKit purely for spec-compliant parsing and per-item value access. This isn't a second TOML parser — it's a thin, separately-tested pass that only looks for top-level [item.X] headers.
Everything else in the app has no third-party dependency.
See docs/manual-qa/ for the evidence captured for v1: the example config running in the bar, a live-reload edit applied without relaunch, and a deliberately broken config recovering via the pinchos ⚠︎ item.
.github/workflows/ci.yml runs swift build and swift test on a macOS runner for every PR and every push to main.
No additional module types beyond command, no nested/JSON-path format placeholders, no preferences UI, no login-item management, no code signing/notarization/distribution pipeline, no Homebrew formula, no multi-bar layout engine. See the project brief for the full list.