Workshop

workshop (app id dev.mixos.workshop) is a native window onto the Bus of the node it runs on. It asks noded which services are registered, asks each service to describe its verbs, and shows them as a tree. Select a verb to read its arguments and description, type a JSON body, and call it. The reply is shown with its return code. Workshop also lists the mesh nodes this node can see.

It is for people and agents who want to see what a node offers and try a verb by hand. It ships in the mixos-desktop package set. Nothing else in the workspace depends on it.

Workshop is also the reference app for driving a window over the Bus. Everything in its window is a Bus verb, and Driving apps over the Bus uses it as the worked example.

The source is apps/workshop.

How it is built

Workshop follows the engine-first rule in AGENTS.md §4.1. It is two crates:

  • inspector (crates/inspector): the headless engine. No UI and no I/O. It holds the protocol model, the discovery and call rules, and the handling of every workshop.* verb.
  • workshop: the egui shell. It draws the engine's state, runs the engine's effects on a small tokio runtime, and owns the command registry that the menus, the shortcuts and the Bus all go through.

The engine never does work itself. The shell feeds it events (Bus deliveries, finished discoveries and calls, edits made in the window), and the engine returns Effects for the shell to carry out: Discover, Call, Show, Reply, Commands, Execute, Describe, Drive, Copy and Exit. Every effect that leaves the process carries a ticket. A completion whose ticket is not the one in flight is ignored, so a late reply can never land on a newer operation.

inspector

ModuleWhat it holds
modelVerb, Snapshot (services, each with its verbs or an error; peers), Selection, the parsers for noded.list, noded.peers, HELP and app.describe, the limits, and describe(), the app's own verb list
engineEngine, Effect, Dialog, Row and RowKind (the services tree), UiState (what a person arranges), Session (the session theme)
busthe connection, through the shared citizen library: start, probe, forward, show, and discover

Discovery reads noded.info, noded.list and noded.peers. The local node appears first, followed by sorted peers. Each peer is queried through noded.<node>.bus, then its services through <service>.<node>.bus. Local membership and service identities are published as soon as they arrive, without waiting for remote nodes or verb descriptions. Discovery queries up to eight remote nodes concurrently, with a separate local slot. Each read has a three-second deadline (DISCOVERY_CALL_BUDGET); info and list run together. Across all nodes and refreshes, at most twelve discovery calls run at once: six reserved for local reads and six shared by remote reads. A cancelled description retains its permit until the native request finishes. Discovery asks for HELP, falling back to app.describe, eight services at a time per node, with a three-second budget for the description phase. Every node update wakes the view. The status counts completed nodes and unreachable nodes; unreachable rows include the reason and refresh retries them. Failed discovery and descriptions appear beneath the affected node or service. Each service retains its ServiceInfo; labels show version and short SHA when advertised, and Details shows the full identity including binary and dirty state.

The limits are fixed in model:

  • a JSON body is at most 65,536 bytes;
  • a reply is shown up to 1,000,000 bytes, then marked as truncated.

workshop

ModuleWhat it holds
src/main.rsoptions, single-instance launch, the eframe window
src/lib.rsthe string catalogue: strings(), label(), label_with()
src/shell.rsShell: applies events, settles effects, runs Bus work, follows the desktop profile, wires the toolkit drive layer
src/view.rsview(): draws the engine's state and returns what the person did as UiEvents; it never changes state itself
src/commands.rsregistry(): every command, with its menu, shortcut, icon and when it is enabled
i18n/en/workshop.ftlthe English strings

The shell applies one event and settles its effects before it reads the next, so a later delivery can never change what an earlier command acts on. Within one frame, edits are applied before commands, so a call uses the body the person sees. Bus work wakes the window only when something finished; nothing polls.

Rules

  • One operation at a time. A refresh or a call in flight makes the window busy. Bus requests that need it idle are refused with BUSY.
  • A call is sent once. Workshop never retries. A transport failure is reported as return code 10 with the outcome unknown.
  • No call on bad input. An unadvertised target, a body over the limit or invalid JSON is refused before anything is sent. An empty body is allowed.
  • Closing answers everything. Quit waits until accepted work is finished, and every Bus request already accepted gets an answer before the connection stops.
  • One window per session. A second launch finds the running Workshop on the Bus, asks it to show its window through compd, and exits.

Bus

Workshop registers as workshop on the local broker. Its verbs are registered in verbs.conf.mix with owner workshop.

  • Reading: workshop.ping, workshop.info, workshop.tree, workshop.reply, workshop.commands, plus HELP and app.describe.
  • Changing state: workshop.filter, workshop.expand, workshop.select_row, workshop.select, workshop.body, workshop.split, workshop.dialog, workshop.theme, workshop.call, workshop.execute, workshop.refresh, workshop.show, workshop.quit.
  • The window's drive verbs, from the toolkit: workshop.ui.tree, workshop.ui.click, workshop.ui.pointer, workshop.ui.scroll, workshop.ui.key, workshop.ui.type, workshop.ui.menu, workshop.ui.menu.list, workshop.ui.menu.open, workshop.ui.menu.close, workshop.ui.capture, workshop.window and workshop.window.state.

Arguments, answers and refusal codes (BUSY, DISCONNECTED, ARGUMENT, UNKNOWN_VERB, UNKNOWN_COMMAND, DISABLED) are tabled in Driving apps over the Bus. Tree rows use node:<node>, service:<node>:<service> and verb:<node>:<service>:<verb>, for example node:alpha, service:alpha:noded and verb:alpha:noded:noded.list. A verb selection keeps its routed service address so calls reach the selected node.

Calling a verb from Mix through Workshop:

$body = json_encode({service: "noded", verb: "noded.list"})
send workshop workshop.call body=$body timeout=30
print $result

The answer is {service, verb, request_body, rc, body}.

Commands and keys

Every menu item, shortcut and workshop.execute id is one command in src/commands.rs:

CommandMenuKey
file.refreshFile › Refresh servicesCtrl+R
file.quitFile › QuitCtrl+Q
edit.formatEdit › Format JSON bodyCtrl+Shift+F
edit.clearEdit › Clear body
edit.copyEdit › Copy reply
bus.callBus › Call verbCtrl+Enter
help.shortcutsHelp › Keyboard shortcutsCtrl+/
help.aboutHelp › About Workshop
view.searchtitle bar search buttonCtrl+F
view.modetitle bar light/dark button

View › Theme adds the view.theme.*, view.mode.*, view.style.*, view.decorations.* and view.captions.* choices. They set the theme of this window only; the session's theme file is never written. The Help menu opens with a field that searches every command. The desktop profile can rebind the shortcuts (keymaps.workshop.<id>), and workshop.commands reports the ones in force.

Running it

workshop [--headless] [--noded-url URL] [--service NAME] [--comp NAME]
  • --noded-url: the broker to use. The default is the session's noded.
  • --headless: run the same engine and Bus port without a native window or renderer. Repaint events wake the logic thread. Use a distinct --service name for a test instance; duplicate registrations are refused.
  • --service: the Bus name to register as, workshop by default.
  • --comp: the compositor's service name, used to raise the window. The default is $MIXOS_COMP_SERVICE, or comp.

Service names may contain only ASCII letters, digits, -, _ and ..

The window follows the desktop profile's look and keymaps from settingsd, through the settings follower, and updates when they change. With no theme chosen it starts in Studio Dark.

Testing

On an authorised build worker at a pushed commit:

cargo test --locked -p inspector -p workshop
  • The engine's unit tests (crates/inspector/src/engine.rs, model.rs) cover discovery, selection, the body, calls, the Bus verbs, refusals, tickets and quitting, with no UI.
  • tests/shell.rs runs the real Shell on a logging Bus handle (the inspector testing feature) and checks every reply, call and the final quit in order.
  • tests/drive.rs drives the window over its Bus verbs against the real view: clicks, typing, the title bar controls, menus.
  • tests/view.rs checks the dialogs, the body buttons and their tooltips, and the search field through kittest.
  • tests/snapshot.rs takes offscreen snapshots in several themes into tests/snapshots/. Regenerate with UPDATE_SNAPSHOTS=1 and look at the images before committing them.
  • tests/registry.rs checks that the verb registry lists exactly the verbs Workshop describes, and that read-only verbs are class query (verb classes).
  • crates/inspector/tests/native.rs runs discovery, calls and single-instance registration against an isolated real broker. It is ignored by default and needs WORKSHOP_TEST_URL.
  • crates/inspector/tests/discovery.rs uses a fake broker with a node that never replies. Local and healthy rows arrive before its timeout, discovery settles within a bounded time, and the next refresh retries the failed node.

Macro Bus verbs

The shared macro Bus surface uses this process's catalogue and runner. Subscribe to <service>.macros.finished for completion, where service is the instance’s registered Bus name (for example term-1234 for a second Term instance). Verbs keep the app’s canonical prefix, such as workshop.macros.run.

VerbArgumentsResult
workshop.macros.list{}Catalogue and diagnostics
workshop.macros.reload{}Catalogue and diagnostics
workshop.macros.run{id}Acceptance and run number
workshop.macros.status{}Current or last run
workshop.macros.output{}Retained output and dropped flag
workshop.macros.stop{}Whether termination was requested
workshop.macros.new{title}Create a global user macro; {id,title,path,scanned}, without opening an editor; scan failures add warning and preserve the file
workshop.macros.get{id, app?}Read {id,tier,path,text}, at most 256 KiB
workshop.macros.put{id,text,app?}Save a user macro atomically and refresh its owning Session
workshop.macros.remove{id,app?}Delete a user macro; built-ins refuse with DENIED
workshop.macros.move{id,tier,app?,destination?}Move between user tiers; refuse a destination clash
workshop.macros.copy{id,app?}Copy a built-in into its corresponding user tier
workshop.macros.open{app,id?}Select a macro in Details, or expand the app's branch
workshop.traffic{limit?,filter?}Read up to 1,024 retained rows and the dropped count
workshop.observe{on,filter?,body?}Start or stop observation; body defaults to redacted

The global branch has the stable key macros:global. Global user files live directly in the user macro root, for example /home/user/.config/mixos/macros/mesh-ping.mix. Workshop owns their Session. App branches prefer the running app's catalogue and use the Library only when the app is stopped, with a “not running” label. Running an app macro while its app is stopped refuses with UNAVAILABLE. app selects the source app; omit it or use global for globals. Moving a global into an app requires tier: "user-app" and destination: "ced", for example.

Details uses the shared editor document. Save (Ctrl+S), Run (Ctrl+Enter), Stop, Copy to user, Move, Delete with confirmation, Open in Ced and Show in Dopus dispatch through Workshop's command registry. A -- target: header offers only its allowed apps; any offers running apps. The chosen app is passed as MIXOS_MACRO_TARGET. Without a header, Run remains target-free. Each app serves the same management verbs through its own macro Session. Macro requests are capped at 8 KiB, except put: its envelope is capped at 1544 KiB and its text at 256 KiB.

The collapsible Traffic dock combines broker observations and native <service>.command.executed / <service>.macros.finished topics. It retains at most 1,024 rows and 8 MiB, reports dropped rows and redacts sensitive topic fields before retaining them. Broker observation defaults to redacted bodies; none is also supported. A refused observer leaves the reason in the dock and keeps topic deliveries available. The host must explicitly admit workshop* in observe.allowed_services. Run mix --serve tools/workshop_observe.mix --name workshop-probe to check the advertised observe extension and open and close a probe stream.

Open Workshop in an app's macro menu launches or focuses Workshop; a launch with --app ced --id tidy-imports selects that macro. The global folder in Dopus is the fallback when Workshop is not installed. Headless instances use the same management and traffic verbs.

This page as Markdown · This page describes the current main branch of MixOS.