Plugin API
Runx plugins are small Lua 5.5 files that return a table. They can contribute search results, implement command-routed actions, and perform system tasks through the runx.* runtime API.
You do not need a manifest, a build step, or a plugin framework. A plugin is just a Lua script plus optional [plugin.<id>] configuration in your config.toml.
Where Plugins Live
By default, Runx scans:
~/Library/Application Support/runx/plugins/
You can add more plugin directories through [plugins].directories in config.toml.
- One directory is one plugin: Each plugin is a subdirectory containing
init.luaas its entry point (e.g.plugins/calc/init.lua). requiresupport: Plugins can load sibling files viarequire("utils")which resolves toutils.luaorutils/init.luain the same directory.- Fresh state: Runx evaluates the plugin in a fresh Lua state on every search or action. Do not rely on global variables surviving between calls. Garbage collection is disabled since the entire state is discarded after each call.
Editor Support
Runx writes Lua language server metadata next to your user config:
~/Library/Application Support/runx/.luarc.json~/Library/Application Support/runx/types/runx.lua
Open ~/Library/Application Support/runx as your editor workspace to get runx.* completions and diagnostics while editing plugins under plugins/.
Minimal Plugin
A plugin must return a table containing its identity and at least one search or action handler.
return {
id = "hello", -- Unique identifier
name = "Hello", -- Display name for the UI
badge = "HI", -- Default badge for full-style items
-- Generic search handler
search = function(query)
if query ~= "hello" then return {} end
return {
{
title = "Say hello",
subtitle = "A simple plugin example",
payload = { kind = "greet" },
},
}
end,
-- Execution handler
run = function(payload)
if payload.kind == "greet" then
return "Hello from Runx!"
end
end,
}Plugin Table Reference
| Key | Required | Type | Default | Meaning |
|---|---|---|---|---|
id | no | string | Directory name | Stable identifier used to match [plugin.<id>] config and command routes. |
name | no | string | Directory name | Display name shown in the UI. |
badge | no | string | "PLG" | Default badge shown on full-style items. |
search | no | function | - | Generic search entrypoint for normal queries. |
run | no | function | - | Called when a plugin item is activated. |
\<handler\> | no | function | - | Named routed handlers referenced by commands. |
Search Modes
1. Generic Search
If a plugin exports search(query), Runx calls it during normal search. Use this for plugins that should always contribute results (like a calculator that activates on numbers).
search = function(query) -> { items... }2. Routed Commands (Recommended)
Routed commands allow you to trigger a plugin with a specific prefix (e.g., calc 1+1). This is faster and prevents your plugin from interfering with general search results.
Plugins can declare their own default commands by exporting a commands table. These are used automatically unless the user overrides them in config.toml:
return {
id = "echo",
commands = { echo = "handle_echo" },
handle_echo = function(raw, argv)
return {
{ title = raw, payload = { text = raw } },
}
end,
run = function(payload)
runx.copy_text(payload.text)
return "Copied!"
end,
}Users can override a plugin's default commands in config.toml:
[plugin.echo.commands]
"e" = "handle_echo"When [plugin.<id>.commands] is present in the user's config, it completely replaces the plugin's built-in commands table.
Lua handler signature:
-- raw: everything after "echo " (e.g., "hello world")
-- argv: shell-parsed arguments (e.g., {"hello", "world"})
handle_echo = function(raw, argv)
-- return items...
endImportant: Once a plugin has command routes (either self-declared or from config), its generic search(query) function is ignored.
Search Result Items
Handlers must return an array of item tables.
| Key | Required | Type | Default | Meaning |
|---|---|---|---|---|
title | yes | string | - | The main visible label. |
payload | yes | table | - | Opaque data passed to run(payload) on activation. |
subtitle | no | string | "" compact / plugin name full | Secondary text (shown in full style). |
score | no | integer | 0 | Ranking priority (higher is better). |
badge | no | string | "" compact / plugin badge full | Small text tag on the right. |
icon | no | string | - | URL or data: URI (replaces badge). |
style | no | string | compact | compact (single line) or full (two lines). |
id | no | string | plugin:\<plugin_id\>:\<title\> | Stable ID for selection memory. |
Visual Styles
compact(Default): A slim, single-line row. Subtitles and badges are hidden unless the UI theme explicitly enables them for compact rows.full: A taller, two-line row. Shows thesubtitlebelow thetitleand thebadge(oricon) on the right.
Payload
The payload table is any JSON-serializable Lua table. It is stored when your search handler returns it and passed back to run(payload) when the user activates the item. Runx treats it as opaque data — structure it however you like.
payload = {
kind = "copy_password",
account = "github.com",
}run(payload) and Feedback
The run function executes the requested action. You can return a value to control the feedback message logged by Runx (visible in debug.log or terminal output):
| Return Value | Logged Message |
|---|---|
nil | "Ran <Plugin Name>" |
string | The returned string. |
"" | Nothing logged. |
Runtime Helpers (runx.*)
Runx provides a built-in runx table with utility functions. Parameters marked with ? are optional.
System & Environment
runx.api_version: integer
Current API version (currently 1).
runx.plugin_path: string
Absolute path to the current .lua file.
runx.plugin_dir: string
Directory containing the current plugin.
runx.plugin_config: table
Your plugin's [plugin.<id>] config (excludes commands).
runx.home_dir() -> string
Returns the user's home directory path.
runx.getenv(name: string) -> string?
Returns an environment variable or nil.
Files & Execution
Subprocess helpers search the system PATH plus any extra directories listed in [plugins].search_paths in your config.toml.
runx.read_text(path: string) -> string
Reads a UTF-8 file and returns its contents.
runx.walk_files(root: string) -> string[]
Lists all files recursively under root. Returns paths relative to root.
runx.parse_args(raw: string) -> string[]
Parses a string into an argv array using shell quoting rules.
runx.exec_capture(cmd: string, args: string[], first_line?: boolean, trim?: boolean) -> string
Runs a command and returns stdout.
first_line(defaultfalse) — return only the first line of output.trim(defaulttrue) — strip leading/trailing whitespace.- Errors if the command exits with a non-zero status.
runx.exec_status(cmd: string, args: string[], silence_stderr?: boolean) -> true
Runs a command; returns true on success, errors on failure.
silence_stderr(defaultfalse) — discard stderr output.
runx.exec_json(cmd: string, args: string[]) -> table
Runs a command, parses stdout as JSON, and returns it as a Lua table. Errors if the command fails or output is not valid JSON.
runx.json_decode(text: string) -> table
Parses a JSON string and returns it as a Lua table. Errors if the input is not valid JSON.
Clipboard & Interaction
runx.copy_text(text: string)
Copies text to the system clipboard.
runx.clipboard_text() -> string
Returns the current clipboard text.
runx.type_text(text: string)
Types text into the previously active app (requires Accessibility).
Utilities
runx.fuzzy_score(target: string, query: string) -> integer
Returns the same fuzzy match score Runx uses internally.
Example: Fuzzy-Filtered Bookmarks
A complete plugin that uses runx.fuzzy_score to filter and rank results:
return {
id = "bookmarks",
name = "Bookmarks",
badge = "BM",
search = function(query)
if query == "" then return {} end
local bookmarks = {
{ title = "GitHub", url = "https://github.com" },
{ title = "Rust Documentation", url = "https://doc.rust-lang.org" },
{ title = "Lua Reference", url = "https://www.lua.org/manual/5.5/" },
}
local results = {}
for _, bm in ipairs(bookmarks) do
local score = runx.fuzzy_score(bm.title, query)
if score > 0 then
results[#results + 1] = {
title = bm.title,
subtitle = bm.url,
score = score,
style = "full",
payload = { kind = "open", url = bm.url },
}
end
end
return results
end,
run = function(payload)
runx.exec_status("open", { payload.url })
return ""
end,
}Debugging & Errors
- Validation Errors: If your search handler returns an invalid item (e.g., missing
titleor invalidkind), Runx will show a descriptive error in the UI. - Lua Errors: If your code throws an error (via
error()or a syntax mistake), Runx captures the message and displays it as a notification. - Logging: Use
print()to send output to the Runx processstdout. If you run Runx from a terminal, you'll see these logs.
