Vee documentation
Debugging and testing plugins
Vee's authoring tools run without the app, without installing anything into your plugins folder, and against Vee's real parser — so what you see in the terminal is what the menu bar would build. This page is the workflow: preview a plugin, watch it re-render as you save, catch authoring mistakes, and work out why a plugin is misbehaving.
The four commands, shortest feedback loop last:
| Command | Use it when |
|---|---|
vee render <plugin> | You want to see the parsed menu tree once. |
vee show <plugin> | You want a live terminal view on the plugin's own cadence. |
vee dev <path> | You are actively editing — it re-runs on every save. |
vee lint <plugin> | You want the authoring mistakes called out, in CI or in your editor. |
For the full subcommand list and flags, see CLI and URL actions.
vee render
Renders exactly what Vee would show, so you can see a plugin's output — text or JSON protocol — without installing it:
$ vee render ./cpu.5s.sh
CPU 12% [sfimage=cpu]
---
Top processes [href=https://example.com/procs]
───
Refresh [refresh]Parse diagnostics (unknown params, malformed lines) and a non-zero exit, a timeout, or anything on stderr are surfaced too — it's the fastest way to answer "why doesn't my plugin look right?".
vee show
Where vee render prints one static tree, vee show is a live view of what the
plugin's menu-bar dropdown would look like — rendered natively in your terminal.
It re-runs the plugin on the cadence encoded in its filename and repaints, so you
can edit a script and watch the result without ever installing it into the menu
bar:
$ vee show ./cpu.10s.sh # or an installed plugin by name: vee show cpuThe dropdown is rendered the way a terminal can: color=/ANSI as real color,
progress= as a Unicode block gauge (████████░░░░ 72%), sparkline= as a
block sparkline (▁▂▃▅▇█), and toggle=/slider= as inline state. The things a
terminal can't draw — SF Symbols and base64 images — are shown by name ([cpu],
[img]) rather than dropped. A status line reports the plugin's cadence and last
exit code; parse diagnostics and stderr surface below, exactly like vee render.
Press r to refresh now and q (or Ctrl-C) to quit. A plugin with no
interval token in its filename (.manual) simply renders once and waits for r.
Flags: --once prints a single frame instead of watching (also what happens when
stdout is piped); --no-color disables ANSI color (as does a NO_COLOR
environment variable or a non-TTY stdout); --dir DIR sets the folder a plugin
name is resolved against.
vee show is a view, not a controller — it displays a row's action (with a small
trailing glyph: ↗ link, $ shell, ⟳ refresh, ⌘ Shortcut) but does not fire
it. Activating items, the interactive control popovers, and the embedded WebView
remain the menu bar's job.
vee dev
Where vee show re-runs on the plugin's own cadence — so an edit to a 5-minute
plugin appears five minutes later — vee dev re-runs on save. Put it in a
split terminal beside your editor and the loop is: edit, save, see it.
$ vee dev ./cpu.10s.shEach save re-runs the file and repaints the status line, the parsed menu tree,
and any lint findings. A save that breaks the script repaints with the exit code
and stderr and keeps watching — the loop does not exit over a typo. r
re-runs now, q quits.
--text: preview a menu without running anything
vee dev --text <path> treats the file as plugin output rather than a program.
Nothing is executed, so the file needs no execute bit and no shebang, and a save
carries no risk of running code:
$ cat menu.txt
CPU 42% | color=red
---
Open dashboard | href=https://example.com
$ vee dev --text menu.txtThis is how to design a menu's shape before writing the script that produces it — and, as the next section explains, it is the only mode in which an editor can put a diagnostic on the right line.
--push: see the real thing in the menu bar
A terminal shows structure; only Vee can show Vee's render. vee dev --push also
sends each save to the running app as a real status item, with no file written
to your plugins folder:
$ vee dev --push ./cpu.10s.shThe item updates in place on each save and disappears when you quit. It is
opt-in — plain vee dev never touches your menu bar. If Vee is not running it is
started in the background (without stealing focus), and the loop says so.
One limitation, stated plainly: a pushed preview travels over the
setephemeralplugin URL action, and because any web page can open a vee://
URL, Vee strips shell=/bash= actions from ephemeral content. Those rows still
appear in the preview so you can confirm you wrote them correctly, but clicking
one does nothing. The loop prints a note when your menu contains one. To test an
executable action, install the plugin normally.
vee lint
Catches the common authoring mistakes before you ship — especially the quoting bugs the SDKs prevent by construction:
$ vee lint ./broken.5s.sh
Lint findings:
warning [line 3]: value for 'tooltip' contains a space but isn't quoted; wrap it in quotes (e.g. tooltip="a b")
warning [line 4]: unknown parameter 'frobnicate'vee lint exits 1 when it finds anything, so you can wire it into a
pre-commit hook or CI.
Diagnostics in your editor (--format compact)
--format compact emits path:line:col: severity: message — the shape VS Code,
vim, and emacs already parse — so findings become inline diagnostics with no
Vee-specific editor extension:
$ vee lint --text --format compact menu.txt
menu.txt:1:1: warning: unknown parameter 'colour'
menu.txt:4:1: warning: value for 'color' contains a space but isn't quoted; wrap it in quotes (e.g. color="a b")VS Code — add to .vscode/tasks.json:
{
"version": "2.0.0",
"tasks": [
{
"label": "vee lint",
"type": "shell",
"command": "vee lint --text --format compact ${file}",
"problemMatcher": {
"owner": "vee",
"fileLocation": ["autoDetect", "${workspaceFolder}"],
"pattern": {
"regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
"file": 1, "line": 2, "column": 3, "severity": 4, "message": 5
}
}
}
]
}vim — :set errorformat=%f:%l:%c:\ %t%*[^:]:\ %m then :cexpr system('vee lint --text --format compact ' . expand('%')).
Which file a finding names, and why
Lint runs over a plugin's output, not its source. That distinction decides what a line number can honestly refer to:
| Mode | What is linted | Path in compact output | In your editor |
|---|---|---|---|
--text | the file itself | the real path | diagnostics land on the correct line |
| default (executed) | the script's stdout | <stdout> | listed, but never placed on a line |
A script emitting fifty rows from one echo inside a loop makes the
output-line → source-line mapping many-to-one and unrecoverable. Naming the
script would put squiggles on lines that have nothing wrong with them, so
executed-script findings are attributed to <stdout>, which resolves to no file.
You still see every finding; your editor just does not mark an innocent line.
If you want squiggles that land exactly, lint the protocol text with --text.
Execution timeouts
A plugin run is killed after 30 seconds by default, and the menu shows the plugin as errored. Long-running work needs either a longer timeout or a different shape:
- Raise the limit for one plugin with
<vee.timeout>, which accepts a plain number of seconds or a duration token (ms/s/m/h/d) and is clamped to 1s–1h:# <vee.timeout>2m</vee.timeout>
- If the work is genuinely unbounded, make the plugin streaming instead — see Streaming. A streaming plugin is not subject to the one-shot timeout, because it is expected to keep running and keep printing.
A plugin that regularly approaches its timeout is usually better off caching: do
the slow work on a long interval, write the result to
SWIFTBAR_PLUGIN_CACHE_PATH, and have the fast plugin read it.
Exit codes and standard error
- Exit code 0 — normal. Whatever the plugin printed to standard output is parsed and rendered.
- Non-zero exit — Vee marks the plugin as errored and surfaces it in the menu and the Debug console. Any output the plugin did print is still parsed best-effort, so a partial menu is better than a blank one.
- Standard error — never parsed as menu output. It is captured and shown in the Debug console, which makes it the right place to write your own debug tracing: it will not corrupt the menu. The exception is a streaming plugin, whose standard error is discarded rather than captured — if you are debugging one, print your tracing somewhere you can read it instead.
vee dev reports all three on every save and keeps watching, so a script that
breaks does not end the loop.
Output is capped at 8 MB
Standard output and standard error are each captured up to 8 MB; beyond that
the rest is discarded. When it happens you get an Output truncated at 8 MB
diagnostic in the Debug console rather than silence. The cap is orders of
magnitude beyond any real menu, so hitting it almost always means a plugin is
printing something it did not mean to — a log, a file dump, an unbounded loop.
The Debug console
The app keeps a per-plugin Debug console — the last run's standard output, standard error, exit status, and every parse diagnostic Vee produced while reading the output. Open it from the Plugin Manager for the plugin you are investigating.
Use it when the terminal tools disagree with the menu bar: the console shows the run Vee actually performed, including the environment it injected, which is the usual explanation for "it works in my shell but not in Vee" (see Troubleshooting).
See also
- CLI and URL actions — the full subcommand and flag reference, plus
vee newandvee search. - Troubleshooting — symptoms and fixes when a plugin does not appear, errors, or times out.
- Plugin authoring reference — the output format the tools on this page parse.
- Plugin SDKs — typed builders that prevent the quoting mistakes
vee lintreports.