Most of Cherry Pi's harness is written in Lua. It manages project layers, jobs, policy checks, receipts and the tools Picache uses. The terminal desktop is written in Go: it renders the pane tree and handles the keyboard and pointer. Both are packed into Cherry Pi's executable. A Lua script can work with that running desktop through picache.
Lua appears at several levels here. It runs the harness itself. It lets you script your own terminal workspace with picache do. Picache can also run a Lua script in code mode with a limited set of tools. The language is shared; what each script may do depends on who started it and where it runs.
From your own pane, a script can open a test pane beside the code, wait for its process to exit and bring the failure into view. It can return a small, machine-readable account of what happened. It works on the live desktop, with the panes and programs already there.
The shape of picache do takes a cue from Rex's Lua scripting. Cherry Pi applies its own layer context and caller identity to each workspace call. The Lua runtime that governs those calls is also part of Cherry Pi itself; this is a way to work with the machinery underneath the TUI, not a separate macro recorder.
A test beside the code
Save the following as tests.lua in a project layer. It opens a terminal pane on the right, runs the project's Python tests and waits for the process to finish:
local P = picache
local tests, err = P.split{
side = "right",
label = "TESTS",
focus = false,
command = "python3 -m unittest -v",
}
if not tests then error(err, 0) end
local done, why = P.wait(
"terminal.child_exited", { pane_id = tests.pane_id }, 600)
if not done then error(why, 0) end
if done.exit_code ~= 0 then
assert(P.zoom(tests.pane_id, true))
end
return { pane_id = tests.pane_id, exit_code = done.exit_code }
Run it from a Cherry Pi terminal with picache do tests.lua. The recording uses --yes in its disposable project so the tape can run unattended; when you run the script yourself, leave that flag out and answer any confirmation it needs.
focus = false leaves the keyboard in the original pane. P.wait watches the new pane's terminal.child_exited event for up to 600 seconds. The event carries the process exit code. On a failure, P.zoom brings the test output forward; the terminal keeps that output after the command exits. The returned table prints as JSON, including the pane ID you can use in a later call.
Run a script, pass it a value
picache do accepts a Lua file, an inline expression or standard input. Arguments after the script arrive in picache.args. A plain key=value is converted to a number or boolean when it reads as one; --args accepts a JSON object when you need nested values or an exact string.
picache do tests.lua
picache do -e 'return picache.session.view()' --json
picache do - < layout.lua
picache do layout.lua name=dev count=2
picache do layout.lua --args '{"name":"true"}'
A returned value prints as JSON. A script that returns nothing prints nothing. When a call fails, it gives Lua nil and a reason, which is why the test script checks err and why before continuing. A Lua error prints the message and ends the script with an error status.
Commands and panes have different jobs
Some commands only need an answer. picache.exec runs a local program without opening a pane. Its argument list goes directly to the program, so a file name with spaces needs no shell quoting. It returns the output and exit status:
local result, err = picache.exec(
{ "git", "status", "--short" }, { timeout = 10 })
if not result then error(err, 0) end
return { code = result.code, files = result.stdout }
Use a pane when the program's screen matters. A test run, server or interactive tool can stay visible after the script ends. The test example uses a pane so you can see the failure in context, keep its output and return to it. The command and the Lua script have separate lifetimes: picache do finishes, while the pane remains in the layer.
React to the workspace
P.wait is for one event. A longer-running script can register picache.on to handle each matching event, or use picache.after and picache.every for timed work. For example, this watch logs terminal exits until the time limit ends:
picache.on("terminal.child_exited", function(ctx, event)
picache.log("pane " .. event.pane_id ..
" exited " .. event.exit_code)
end)
picache do watch.lua --for 30s
The event stream starts with the script, so a split followed immediately by a wait can still catch a fast process exit. Handlers and timers run one at a time in the script. A script can also call picache.session.view() to read the pane tree and live run IDs. The longer TUI article shows what those arrangements look like from the operator's side.
Whose script runs?
picache do runs locally as the caller. In your shell, it has Lua's standard library, including file and process operations. In an agent's confined pane, it stays within that pane's confinement. Calls through picache.* carry the pane's token: the harness checks the caller, the requested effect and the available grants, then leaves a receipt. A plain Lua file operation or shell command runs outside those workspace verbs, with the access the calling process already has.
Cherry Pi also has picache script for Lua that you did not write or fully trust. It runs in a capsule with a limited set of workspace verbs and a call budget. Those two entry points have different authority, so choose the one that matches who wrote the script. The manual has the detailed comparison.