NAME
zeughaus-files - the zeughaus state directory and the files in it
SYNOPSIS
<state-dir>/
├── runner.pem runner identity (certificate and key)
├── client.pem local client identity (certificate and key)
├── clients/
│ └── <name>.pem client certificates provisioned by hand
├── editor.toml editor theme and tab bar placement
├── themes/
│ └── <name>.toml WezTerm colour schemes
├── workspace.json the runner's tabs, groups, splits, terminals
├── terminals/
│ └── <id>/ one per live terminal
│ ├── spec.json
│ ├── sock
│ └── shim.log
├── runs/
│ └── <id>/ one per job run
│ ├── log
│ ├── exit
│ ├── code
│ └── artifacts/
└── restore-<pid>.json an editor restart in progress
file.zgh, anywhere: a graph saved by the editor.
DESCRIPTION
The state directory holds what one machine keeps for zeughaus: the credentials of the weida link, the editor's preferences, and the runner's workspace, terminals and job runs. It is state, neither configuration nor cache: it holds private keys, and the runner's shells and run logs. The graphs themselves are in the store, and a .zgh file is wherever its user saved it.
Both binaries use the same directory. The runner writes the credentials, workspace.json, terminals/ and runs/; the editor writes editor.toml and its restore files; clients/ and themes/ are filled by hand. One runner per state directory: a runner reattaches every terminal under terminals/, and two runners on one directory fight over them.
Resolution
The first of these that applies:
--state-dirpath onzeughaus-runner, for serving and fortriggerandhold. The editor has no such option.$ZEUGHAUS_STATE_DIR, when set and not empty.$XDG_STATE_HOME/zeughaus, whenXDG_STATE_HOMEis set and not empty.$HOME/.local/state/zeughaus, whenHOMEis set and not empty..zeughaus-state, relative to the working directory.
On Windows %LOCALAPPDATA%\zeughaus\state takes the place of 3 and 4. An editor on the machine of a runner started with --state-dir path finds that runner's client.pem only with ZEUGHAUS_STATE_DIR set to path.
CREDENTIALS
The runner's listener requires a client certificate on every path; these files are what it proves itself with and whom it lets in. The runner creates the state directory owner-only (mode 0700) and restricts it to that again on every start when group or others have any access.
runner.pem- The runner's identity, certificate and private key. Its
sha256:fingerprint is what the runner's endpoint URL pins and what a graph'srunnercolumn names, so it has to outlive restarts. Created by the runner when missing. Replacing it changes the fingerprint: graphs that name the old one are no longer this runner's, and editors must learn the new endpoint from the store. client.pem- The client identity of this machine, certificate and private key. Created by the runner next to
runner.pemwhen missing and always trusted by it; presented by every editor on this machine and byzeughaus-runner triggerandhold. The editor and the client commands never create it: a key the runner has never seen authenticates nothing. A remote editor needs this file copied into its own state directory (see zeughaus-install(8)). clients/name.pem- Certificates of further clients, provisioned by hand; a certificate is enough, no key. Every file ending in
.pemis trusted, in name order; other files are ignored. There is no enrollment protocol.
The runner reads the trusted set once, when its listener starts, and prints a [runner] trusted client sha256:... line per key; a new file in clients/ takes effect at the next start or SIGUSR1 restart.
A key file is written atomically: to <name>.pem.<pid>.tmp, created exclusively with mode 0600, synced, then renamed into place. A PEM file that exists but does not parse is never overwritten; the runner reports ... exists but is not a usable PEM identity ... -- move it aside by hand and serves no endpoint, and a clients/ file that does not parse stops the listener the same way. For the editor and the client commands an unparsable client.pem reads as absent.
PREFERENCES AND THEMES
Native editor only; the browser editor has no state directory.
editor.toml- One flat TOML table, written by the editor whenever the theme or the tab bar's placement changes and read at start. A missing key keeps its default. A file that cannot be read or parsed earns one line on standard error and the defaults. There is one file per state directory and one choice per window: the window that changed it last wins.
themes/name.toml- A terminal colour scheme in WezTerm's format, offered as the theme name (the file stem) after the bundled pack, sorted by name, as "Theme / name" in the palette. Read at editor start. A file that does not parse is skipped with its reason on standard error.
Keys of editor.toml
theme- The theme's name, matched first against the bundled pack, then against the theme files. Default
"Dark". A name nothing answers to leaves the default in place. The bundled names are those of iced's built-in themes:Light,Dark,Dracula,Nord,Solarized Light,Solarized Dark,Gruvbox Light,Gruvbox Dark,Catppuccin Latte,Catppuccin Frappé,Catppuccin Macchiato,Catppuccin Mocha,Tokyo Night,Tokyo Night Storm,Tokyo Night Light,Kanagawa Wave,Kanagawa Dragon,Kanagawa Lotus,Moonfly,Nightfly,Oxocarbon,Ferra. A theme file named like one of them is listed but never chosen by this key. tabs- Where the tab bar sits:
"top"(default) or"left".
theme = "Nord"
tabs = "left"
Theme files
Only the [colors] table is read; [metadata], [colors.indexed] and every other key are ignored, so a scheme from iTerm2-Color-Schemes' wezterm/ directory is dropped in unchanged. Colours are #rrggbb; the # may be left out.
ansi- Eight colours, ANSI 0 to 7. Required.
brights- Eight colours, ANSI 8 to 15. Required.
foreground,background- Required.
cursor_bg- The cursor. Default: the foreground.
selection_bg- The selection. Default: the foreground mixed 30% over the background.
The scheme is the whole theme: the editor derives its widget palette from it (background, text from the foreground, primary from blue, success from green, warning from yellow, danger from red), and pin colours are ANSI slots. A minimal file, themes/Depot.toml, offered as Depot:
[colors]
foreground = "#d8dee9"
background = "#2e3440"
ansi = ["#3b4252", "#bf616a", "#a3be8c", "#ebcb8b", "#81a1c1", "#b48ead", "#88c0d0", "#e5e9f0"]
brights = ["#596377", "#bf616a", "#a3be8c", "#ebcb8b", "#81a1c1", "#b48ead", "#8fbcbb", "#eceff4"]
WORKSPACE
workspace.json- The runner's workspace, for the runner that comes next. Written by the runner's terminal multiplexer after every structural change and before a
SIGUSR1restart, as pretty-printed JSON throughworkspace.json.tmpand a rename; read once at start. Not written when terminals live in the runner process (non-unix builds).
The current version is 2. A version 1 file is migrated: its flat list of tabs becomes top-level tabs, its graph panes (which named no graph) are dropped along with any tab left empty, and its free-text group label is ignored. A file of any other version, or one that does not parse, is ignored with a line on standard error and the runner starts with a fresh workspace.
version2.items- The top level in display order, each either
{"Tab": ...}or{"Group": ...}. A tab hasid,title(a user-supplied name ornull),accent(RGBA ornull) androot, its split tree:Splitnodes withid,axis(HorizontalorVertical),ratioand the childrenfirstandsecond, andLeafnodes withpane_idandsurface({"Graph": <node id>},{"Terminal": <id>}or"Empty"). A group hasid,name,color_rgba,lockedand itstabs. next_tab,next_group,next_split,next_pane,next_terminal- The id counters, so a restarted runner continues them and an editor keeps its active tab and focus.
owned,detached- Terminals the runner owns (job runs), and those of them no pane shows.
terminals- Per live terminal:
id,label,scrollback_rows,size(cols,rows: the grid its output is replayed at) and, for a job's terminal,run:run_dir,started(Unix seconds),cwdandartifacts, what a runner that adopts the run needs to finish its record.
At start the runner reattaches every listed terminal, rebuilds the workspace around the ones that came back (dead panes removed), kills a reattached terminal nothing refers to, closes every shim under terminals/ that did not reattach, gives each graph it owns that no pane shows a tab, and continues the terminal ids past the highest saved counter, directory name and reattached id.
TERMINALS
On unix every terminal lives in a shim process, the runner binary started as zeughaus-runner shim <state-dir>/terminals/<id>.
terminals/id/- The shim's directory, named by the numeric terminal id, created by the runner with mode
0700: whoever can connect to its socket can type into the shell. The shim removes it when a session tells it to close (a closed shell pane, a terminal closed from the palette). A runner at start closes every shim here that nothing refers to, and removes its directory when its socket does not answer. Names that are not numbers are ignored. spec.json- What the shim starts, written by the runner before it starts the shim:
label,program(null: the login shell),args,cwd,env([key, value]pairs),scrollback_rows,cols,rowsandlog(a job's log file, appended to, ornull). sock- The Unix socket the shim serves with length-prefixed postcard frames (protocol version
1), one session at a time: a new session replaces the one before it. A session is first sent up to the last 4 MiB of output, then live output. shim.log- The shim's own standard error, truncated each time the runner starts a shim in this directory.
RUNS
Each run of a job.run node has a run directory. Its program sees the directory as ZEUGHAUS_RUN_DIR and may write artifacts there itself; the node's dir output carries it.
runs/id/Named by a decimal run id. A starting runner continues after the highest id on disk, so a restart never reuses one; names that are not numbers are ignored. When a run starts, successful runs (an
exitrecord withcode=0) beyond the newest--keep-runs(default 50) are deleted. Failed runs and runs without anexitrecord are never deleted.logEverything the program wrote to its terminal. Created before the program starts; the process holding the PTY appends each chunk before the terminal parses it.
exitThe exit record, written when the run ends, also by a runner that adopted the run after a restart:
code=0 killed=false started=<unix seconds> finished=<unix seconds>codeisnonewhen there is no exit code: a signal, a kill, a program that could not start.killedistruewhen the terminal was closed or the child killed on request.codeThe program's exit code on one line. Written only for a
keep_on_failurerun on unix whose program exited non-zero: the/bin/shwrapper writes it before replacing itself with$SHELL, and the runner ends the run from it while that shell lives on in the terminal.artifacts/The files the node's
artifactsglobs match, relative to the run's working directory, copied when the run ends with their relative paths kept. Directories a glob matches are skipped, not walked.
RESTORE FILES
restore-pid.json- Written by an editor on
SIGUSR1(orzeughaus ctlsocketrestart) before itexecs itself; the PID survives theexec, so two editors restarting at once cannot take each other's file. The new process finds it throughZEUGHAUS_RESTORE, reads it once and deletes it. A file of another version, or one that does not parse, is ignored with a line on standard error. The current version is2; fields added later default when missing. version,width,height,maximized- The format version and the window's logical size and maximized state. Not its position: a Wayland client can neither read nor set it.
active,focused_pane- The tab in front and the focused pane, each as runner key and id.
collapsed- Collapsed sections and groups, as runner key and group id (or
null). cameras,node_sizes,selected- Per graph its camera (x, y, zoom); the sizes of nodes the user resized; the selected node ids.
palette,rename- An open palette's input and highlighted entry; a node rename in progress and what was typed so far.
scrolls- Per scrolled-back terminal the rows above the live screen, counted from the bottom, because a restarted runner numbers its rows anew.
views- Nested containers open as views in a section without a runner.
document- Without a store, the scratch graph as a
GraphDocument(see GRAPH FILES); with a store,null.
GRAPH FILES
- file
.zgh - A graph as one JSON document, the
GraphDocumentofzeughaus-core/src/document.rs: the rows of the store'snodeandedgetables.Ctrl+Sin the editor writes every node and edge the editor holds, pretty-printed, to a file chosen in a file dialog;Ctrl+Oloads one (.zghor.json). Loading replaces the editor's document and writes nothing to the store; a file that does not parse is ignored. Pins and settings definitions are not stored: both processes regenerate them from the plugins.
The top level has two arrays:
{
"nodes": [
{ "id": 1, "type_id": "graph.sub", "display_name": "Graph",
"x": 0.0, "y": 0.0, "parent": 0, "params": [], "runner": "" },
{ "id": 2, "type_id": "transform.const_f64", "display_name": "Const (f64)",
"x": 100.0, "y": 200.0, "parent": 1, "params": [["value", "0.5"]], "runner": "" },
{ "id": 3, "type_id": "transform.sin", "display_name": "Sin",
"x": 320.0, "y": 200.0, "parent": 1, "params": [], "runner": "" }
],
"edges": [
{ "id": 4, "from_node": 2, "from_pin": "value", "to_node": 3, "to_pin": "input" }
]
}
Ids here are short for reading; the editor gives each process a unique id range, so real ids are large.
nodes[]id;type_id, the node type (see zeughaus-nodes(7));display_name;x,y, the position in its graph;parent, the container node it lives in,0for the root (default0);params, setting values as typed, as[key, value]string pairs (default empty);runner, on a top-level graph thesha256:fingerprint of the runner that executes it, empty everywhere else (default empty).edges[]id;from_nodeandfrom_pin, the source;to_nodeandto_pin, the target. Pins are named as the node types declare them.
On load, every root-level node that is not a graph.sub is moved into a new graph.sub named Graph, as a file from before graphs were top-level containers needs, and the editor's id counters are advanced past every id in the file.
OUTSIDE THE STATE DIRECTORY
~/.spacetimedb_client_credentials/zeughaus-host-database- The token the store issued, one file per store and database; host is the store address with every character other than a letter or digit replaced by
-(zeughaus-127-0-0-1-3000-zeughaus). Saved by the SpacetimeDB SDK on every connect, by the editor and the runner alike, so a process comes back as the same identity. Two runners on one machine share it. $XDG_STATE_HOME/zeughaus/screencast_token- The restore token xdg-desktop-portal granted the runner's
capture.screennode, so the consent dialog is not asked again. Written best effort through a temporary file and a rename. It followsXDG_STATE_HOME(else$HOME/.local/state, else the temporary directory), not--state-dirorZEUGHAUS_STATE_DIR.
FUTURE DIRECTIONS
Not built: reading run files in the editor (the runner serves any file of a run directory on /runs; nothing calls it), and workspace nodes (checkout, btrfs snapshot) producing the working directory a job runs in, with caches per runner under its state directory.
SEE ALSO
zeughaus(1), zeughaus-runner(1), zeughaus-install(8), zeughaus-architecture(7), zeughaus-keys(7), zeughaus-nodes(7), zeughaus-glossary(7)
zeughaus-link/src/credentials.rs, zeughaus/src/prefs.rs, zeughaus-theme/src/wezterm.rs, zeughaus-runner/src/mux/persist.rs, zeughaus-terminal/src/shim/, zeughaus-runner/src/jobs.rs, zeughaus-job/src/node.rs, zeughaus/src/app/restore.rs, zeughaus-core/src/document.rs