ZEUGHAUS-FILES(5) Zeughaus Manual

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:

  1. --state-dir path on zeughaus-runner, for serving and for trigger and hold. The editor has no such option.
  2. $ZEUGHAUS_STATE_DIR, when set and not empty.
  3. $XDG_STATE_HOME/zeughaus, when XDG_STATE_HOME is set and not empty.
  4. $HOME/.local/state/zeughaus, when HOME is set and not empty.
  5. .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's runner column 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.pem when missing and always trusted by it; presented by every editor on this machine and by zeughaus-runner trigger and hold. 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 .pem is 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 SIGUSR1 restart, as pretty-printed JSON through workspace.json.tmp and 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.

version
2.
items
The top level in display order, each either {"Tab": ...} or {"Group": ...}. A tab has id, title (a user-supplied name or null), accent (RGBA or null) and root, its split tree: Split nodes with id, axis (Horizontal or Vertical), ratio and the children first and second, and Leaf nodes with pane_id and surface ({"Graph": <node id>}, {"Terminal": <id>} or "Empty"). A group has id, name, color_rgba, locked and its tabs.
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), cwd and artifacts, 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, rows and log (a job's log file, appended to, or null).
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 exit record with code=0) beyond the newest --keep-runs (default 50) are deleted. Failed runs and runs without an exit record are never deleted.

log

Everything the program wrote to its terminal. Created before the program starts; the process holding the PTY appends each chunk before the terminal parses it.

exit

The 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>

code is none when there is no exit code: a signal, a kill, a program that could not start. killed is true when the terminal was closed or the child killed on request.

code

The program's exit code on one line. Written only for a keep_on_failure run on unix whose program exited non-zero: the /bin/sh wrapper 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 artifacts globs 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 (or zeughaus ctl socket restart) before it execs itself; the PID survives the exec, so two editors restarting at once cannot take each other's file. The new process finds it through ZEUGHAUS_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 is 2; 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 GraphDocument of zeughaus-core/src/document.rs: the rows of the store's node and edge tables. Ctrl+S in the editor writes every node and edge the editor holds, pretty-printed, to a file chosen in a file dialog; Ctrl+O loads one (.zgh or .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, 0 for the root (default 0); params, setting values as typed, as [key, value] string pairs (default empty); runner, on a top-level graph the sha256: fingerprint of the runner that executes it, empty everywhere else (default empty).
edges[]
id; from_node and from_pin, the source; to_node and to_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.screen node, so the consent dialog is not asked again. Written best effort through a temporary file and a rename. It follows XDG_STATE_HOME (else $HOME/.local/state, else the temporary directory), not --state-dir or ZEUGHAUS_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

install(8)source