Files
designcraft/docs/control-protocol.md
T
aiongg 9c977f41a6 Merge print-color (#1374 with main: egui 0.37, craft-libs e3b9e239) into print-color-separations
print_pdf, shared by Export PDF, File › Print and Separations Preview, takes main's two
changes to the inline export pipeline: rasterize_layered runs only for PDF/A-2b (other
standards keep hidden placed-PDF layers as optional content), and the report gets
GRAPHIC_LAYER_RENDERING_WARNING when the source document has graphic layer visibility
overrides and the writer did not report it. The Separations Preview command matches
craft_prepress::Conversion with a fallback arm, since the enum is non-exhaustive at
craft-libs e3b9e239.
2026-10-25 19:54:40 +00:00

16 KiB
Raw Blame History

Control protocol

Start the app with --control <port> (or DESIGNCRAFT_CONTROL_PORT). The server listens on 127.0.0.1 only and speaks JSON lines: one request object per line, one reply per line.

For an isolated native test profile, set DESIGNCRAFT_CONFIG_DIR to a dedicated directory before launch. UI/engine preferences and the Recovery directory then live under that root. An unset override preserves the usual platform paths (or DesignCraftData next to the executable when a portable.txt sits there); an explicitly empty override disables those paths. DESIGNCRAFT_NO_PREFS still disables preference reads/writes independently of recovery.

Control pointer and key input is queued across native frames. After a drag, wait for both the expected document state and document.inspect.canUndo before testing undo; an object can appear in the preview before its undo entry is committed. On macOS, default shortcuts owned by the native menu (including Command-Z) are skipped by the egui shortcut handler. ui.key does not send a Cocoa menu event; use ui.menu.invoke for a menu-command test and test physical accelerators separately.

Only requests are read. Every line must be a JSON object with a string method (id and params are optional; blank lines are skipped). Anything else gets one error reply ({"ok": false, "error": "… closing the connection"}) and the server closes the connection, so nothing sent after it on that connection runs. That covers text that isn't JSON, a JSON array or number, an object without method, invalid UTF-8, and a line longer than 4 MiB. An HTTP request (for example a web page's cross-origin fetch to 127.0.0.1:<port>) therefore can't smuggle a command in its body: its request line is rejected first. At most 16 connections are served at once; further ones get an error line and are closed. Clients that get an error reply should reconnect. The port has no authentication, so only enable it while you use it. Transport: apps/designcraft/src/control_server.rs.

{"id": 1, "method": "engine.execute", "params": {"command": "frame.create", "params": {"rect": [36, 36, 300, 200], "content": "text"}}}
{"id": 1, "ok": true, "result": {"id": 10, "story": 11}}
Method Params What it does
engine.execute / ui.menu.invoke {command, params} Run any engine or UI command (see engine.commands). ui.menu.invoke without params acts like choosing the menu item: a command labelled "…" that takes parameters opens its dialog. A key the command's params description doesn't name is an error (unknown parameter …), not ignored
engine.commands — Every command: id, label, menu path, shortcut, params doc, enabled
document.inspect — Pages, spreads, items, stories (overset), styles, swatches, selection
ui.inspect — Tool, UI state, view (zoom/origin), canvas rect, panel layout (docking), perf, Separations Preview (separations: plate, ink limit, each separated spread's plates)
ui.docking — The panel layout in use, the focused panel and the docking hit rects (see Shared panel docking)
ui.menu.list / ui.tool.list — Menu tree / Tools panel groups
ui.tool.select {tool} Select a tool (selection, type, rectangleFrame, …)
ui.pointer {events:[{kind: down|drag|up|move|doubleclick, x, y, space?: "screen"|"canvas", clicks?}], mods?} Drive the active tool through the same code path as the mouse; clicks on a down is the press count (3 = triple click); returns the selection, the tool and its cursor at the last event (e.g. LoadedText, ThreadLink, Unthread)
ui.key / ui.text {key, shift?, alt?, cmd?} / {text} Synthetic keyboard input (typing into a text frame)
ui.ime {preedit?, active?: [start, end], commit?} Input method events as the system IME sends them: marked text (shown in place, active is the clause being converted, in characters) and the committed text (typed as one text.insert)
ui.paste {text} What Ctrl/Cmd+V delivers from the system clipboard: text into the text being edited, or SVG markup (vector art copied in Illustrator, VectorCraft or Figma) as editable paths in the middle of the view
ui.move / ui.click / ui.drag screen points, button?: left|right|middle, count?: 1–255 (click/drag, default 1), steps?: positive integer (drag, default 8) Real egui pointer input — reaches every widget, menu and panel
ui.set {brightness?, panel?, rulers?, guides?, frameEdges?, baselineGrid?, documentGrid?, textThreads?, screenMode?, zoom?, page?, fit?} UI state
ui.dialog.open {id, fields?} Open a dialog by id (e.g. paragraphStyleOptions or characterStyleOptions with {name, section}, or {new: true} for a new style)
ui.dialog.set / ui.dialog.confirm / ui.dialog.cancel {field, value} Fill and confirm the open dialog
ui.resize, ui.focus Window control
ui.screenshot {path?} PNG of the whole window
ui.render {path?, page?, scale?, bleed?} Render a page headlessly (PNG; base64 if no path)
app.open / app.save / app.export Files
app.quit Quit without asking about unsaved changes (the app.quit command, like ⌘Q, asks)

Data merge runs through engine.execute like any command: data.source.select, data.fields, data.placeholder.add / .remove, data.options, data.preview / data.preview.stop, data.merge. data.merge creates and activates a new merged document; the template stays as it was (agents.md).

Headless window screenshots (locked screen, hidden window): cargo run -p designcraft-ui-egui --example ui_shot -- script.jsonl, where each line is one of the requests above, {"shot": "/abs/out.png"} or {"steps": n} (renders the whole UI offscreen with wgpu).

The MCP server (designcraft-cli mcp) wraps the same methods for Claude and other agents.

Click and drag limits

ui.click and ui.drag validate the whole gesture before queuing any input. They permit at most 10,003 pending synthetic events, including control input already waiting for native frames. A click uses 1 + 2 * count events; a drag uses 1 + steps + 2 * count. The count includes the initial click or drag; further clicks occur at its endpoint. An empty queue therefore accepts a single drag with up to 10,000 steps. count is a JSON integer from 1 through 255 and steps a positive JSON integer when supplied; invalid or over-budget requests return ok: false and leave pending input unchanged. Oversized gestures are rejected rather than truncated. Wait for queued input to finish before retrying a request that would exceed the pending-event limit.

Shared panel docking

Panel arrangements support tab grouping and reordering, horizontal and vertical splits, and movable, resizable floating windows. A floating window holds tab groups split side by side or one above another, like the dock area. Drag a panel tab, or use its context menu. On the desktop each floating window is a window of its own; on the web floating windows stay within the application viewport.

The window is one panel layout: the protected canvas panel (the document area) beside a dock at the logical end edge, the right in left-to-right languages and the left in right-to-left ones. Each built-in workspace has a default layout. Essentials and most others have an outer column with the Properties · Pages · Layers group and an iconic column with the other panels; Essentials Classic and Typography have their named iconic groups only. A panel a workspace opens (Printing and Proofing: Swatches) is a group below Properties · Pages · Layers. The canvas panel never moves, floats, closes or takes tabs; a panel or tab group dropped over it floats there. Workspace saves retain its groups, active tabs, split sizes, floating geometry, and the positions of hidden panels. Resetting a built-in workspace restores its default dock. Preferences saved before the canvas joined the layout load with nothing lost: the classic dock becomes the workspace's default layout, and a customized dock-only layout becomes the dock's expanded columns (iconic if the dock was collapsed), with the remaining panels (or the workspace's named groups) in the iconic column and its floating windows and hidden-panel positions kept. A ui.json the app cannot read in full is copied to ui.unreadable.json before it is saved again.

The window.panel.*, window.workspace.* and ui.dock commands are craft-ui's shared docking commands (DockCommand), with the same ids and params in every craft app. panel, anchor, target and before take a panel ID or its English name in any case; onto is read as anchor.

  • window.panel.move {panel, anchor, zone?, before?, group?, placement?} moves a panel into the target group. zone is tab (default), center, left, right, top, bottom, above or below; before reorders tabs by panel ID (null appends). Beside a panel in a dock column, top and bottom add a group above or below its group and left and right add a column on that side; in a floating window, top and bottom add a group and left and right are errors (craft-ui's Placement::for_zone). group: true moves the panel's whole tab group; placement is a serialized craft-ui Placement and takes precedence over zone and before.
  • window.panel.float {panel, x?, y?, width?, height?, group?, column?} (and window.floatPanel) detaches a panel, its tab group (group) or its dock column (column). Without a position it floats 24 points inside the dock area's top-left corner, clear of the menu bar; a panel floats at its own size and a tab group or column at the size a drag would float it at. Given values replace these. A panel already alone in a floating window moves or resizes, and a tab group that is already a whole floating window keeps its rect unless the params change it.
  • window.panel.dock {panel, anchor?, zone?, before?, group?, column?, side?} (and window.dockPanel) docks beside anchor, or in a new column of the dock at side (Start or End; a new dock when there is none). A panel that already leads that dock's last column stays where it is. zone places the panel beside anchor, so a zone without anchor or side is an error. Without anchor and side, a panel that floated from the dock goes back to that place, and another floating panel joins the first dock group. For the float-only Tabs panel (tabs), which has its own window outside the layout, float opens that window and dock closes it.
  • window.panel.activate {panel} (and window.panel {panel}) reveals and selects a panel, restoring its saved position when possible; a panel in an iconic column opens as a flyout beside its icon. window.panel.close {panel} hides it.
  • window.panel.closeGroup {panel} hides the panel and the panels tabbed with it, and window.panel.closeOthers {panel} hides the panels tabbed with it; the result lists them in closed.
  • window.panel.minimize {panel, minimized?} minimizes the panel's group to its tab strip and window.panel.iconic {panel, iconic?} collapses its dock column to icons; without the flag they toggle.
  • window.workspace.new {name}, window.workspace.delete {name} and window.workspace.reset (also ui.dock.reset) are window.newWorkspace, window.deleteWorkspace and window.resetWorkspace. window.workspace {name} switches workspaces as before.
  • ui.dock {action} or ui.dock {operation, …} is the shape LightCraft, PDFCraft, CADCraft and SoundCraft use: open, close, activate, float {rect}, moveFloating {rect}, move {target, zone?, before?}, setStackOpen {open}, resizeStack {height} and resizeSplit {path, size}, with rect as [x, y, width, height].
  • window.panel.window (a panel into or out of its own window) and window.workspace.lock are not available in DesignCraft and return an error.
  • window.collapseToIcons collapses every dock column to icons.

Panel IDs are stable across themes. Invalid IDs, non-finite or invalid geometry, and invalid destinations return an error without changing the arrangement.

window.panel.layout {action} also accepts the shared, externally tagged action schema. For example, {"action":{"MoveFloating":{"panel":"properties","rect":[44,66,360,450]}}} updates a floating window's position and size; {"action":{"ResizeSplit":{"path":[],"size":{"Ratio":0.35}}}} resizes the root divider (false/true path entries select first/second children), and {"action":{"ResizeWindowSplit":{"panel":"swatches","path":[],"size":{"Ratio":0.6}}}} resizes a divider in the floating window that holds panel, by its path from the window's root. Floating windows saved as a stack of groups ({"groups": […], "rect": […]}) load as groups one above another, sized by their saved heights. Move with placement: {"Tab":{"before":"swatches"}} reorders tabs, and SetStackOpen {panel,open} / ResizeStack {panel,height} control accordion entries. The UI dispatches these same commands. Unknown panel IDs, invalid geometry, and stale split paths return an error without changing the workspace.

app.tablePanel (including Shift+F9) toggles the Table panel. ui.set with closePanel closes the last explicitly opened panel; dockExpanded:false collapses every dock column to icons and dockExpanded:true expands the outer column. Activating a floating panel also raises its group. These operations change only UI layout.

Clicking an icon in an iconic column opens its group as a flyout beside the column, on the canvas side; clicking the icon again closes it. With Auto-Collapse Iconic Panels on (the default; a column bar's menu toggles it), Escape or a click outside the flyout closes it too.

ui.docking returns the panel layout in use; ui.inspect includes the same object as docking:

{"layout": {"root": {"Split": {"axis": "Horizontal", "size": {"FixedSecond": 297.0},
   "first": {"Tabs": {"panels": ["canvas"], "active": 0}},
   "second": {"Dock": {"side": "End", "columns": [
     {"groups": [{"panels": ["properties", "pages", "layers"], "active": 0}], "width": 260.0,
      "iconic": false, "iconic_width": 37.0},
     {"groups": [{"panels": ["stroke"], "active": 0}, …], "width": 260.0, "iconic": true,
      "iconic_width": 37.0}]}}}},
  "floating": []},
 "customized": false, "focused": "canvas", "flyout": null, "autoCollapse": true,
 "rightToLeft": false, "hidden": [],
 "hitRects": [{"hit": {"Tab": "properties"}, "rect": [983.0, 0.0, 74.0, 27.0]},
              {"hit": {"DragBar": "properties"}, "rect": [1190.0, 0.0, 56.0, 27.0]},
              {"hit": {"Icon": "stroke"}, "rect": [1243.0, 14.0, 37.0, 30.0]}, …]}

layout is craft-ui's serialized Layout; customized is false while the workspace's default layout is in use; focused is the panel (or canvas) last clicked or selected, or null; flyout is the panel whose iconic-column flyout is open (as drawn in the last frame, or as a command that opens one sets it); autoCollapse is the Auto-Collapse Iconic Panels setting; hidden lists closed panels that remember their place. hitRects lists where each docking gesture starts in the last frame, from craft-ui's DockArea::hit_rects: hit is a serialized craft-ui Hit (Tab, DragBar, Border, Overflow, ColumnBar, Collapse, StackToggle, Title, Close, Icon, IconGrip, Resize with its edge, or Divider), naming groups, columns and windows by their first panel; rect is [x, y, width, height] in points. Drag from a rect's centre with ui.drag, or click it with ui.click, to script a gesture.