# CADCraft control protocol Start the desktop app with `--control PORT` (or `CADCRAFT_CONTROL_PORT=PORT`). It listens on `127.0.0.1:PORT` for JSON lines: one request per line, one reply per line. A port given with `--control` wins over the variable; a bare `--control` keeps the variable's port. ```json {"id": 1, "method": "cmdline.input", "params": {"text": "circle 0,0 5"}, "token": ""} {"id": 1, "ok": true, "result": {"prompt": "Command:", "output": ["Command: CIRCLE"], ...}} ``` Every request must carry the session `token`. Because loopback TCP cannot tell local operating system users apart, the app writes an unguessable per-session token to an owner-only file when the control server starts, and refuses (`unauthorized`) any request without a matching token. The file is `cadcraft-control-.token` in the per-user runtime directory (`$XDG_RUNTIME_DIR`, else `~/.cache`; `%LOCALAPPDATA%` on Windows); its path is logged at startup. A client reads the token from that file — which requires the desktop user's own filesystem access — and includes it on each line. The MCP bridge (`cadcraft-cli mcp --connect`) does this automatically. Errors come back as `{"id": …, "ok": false, "error": "message"}`. An unknown method's error points to `control.methods`, which lists the methods the server answers. A line that is not a JSON object with a string `method` (an HTTP request, text that isn't JSON, bytes that aren't UTF-8) gets one error reply and the server closes the connection; nothing sent after it runs. Blank lines are skipped. The server bounds what a client can hold: 16 connections at once (more get an error line and are closed), 256 MiB per request line (longer closes the connection), 64 queued requests (more are answered `{"ok": false, "error": "busy: …", "state": "not-run"}`), and 10 minutes idle or 30 seconds per blocked reply write before the socket is closed. The server waits up to 60 s for the app to answer a request. After that it replies with a timeout, and `state` tells whether sending the request again is safe: - `{"id": …, "ok": false, "error": "timeout", "state": "not-run"}`: the app had not started the request. It is cancelled and will never run, so sending it again is safe. - `{"id": …, "ok": false, "error": "timeout", "state": "may-have-run"}`: the app had already started it. It may still finish and change the drawing, and its late result is discarded. Inspect the drawing (`drawing.inspect`) before sending it again. The table below describes top-level control-protocol methods: names for the request's `method` field. `engine.commands` returns a separate command catalog, not a list of additional methods. | Method | Params | What it does | |---|---|---| | `cmdline.input` | `{text}` | Type a line at the command line exactly like a user. Starts commands by name/alias and answers prompts (points `x,y`, `@dx,dy`, `@d QuickCalc** and **Ctrl/Cmd+8** invoke `ui.toggle.quickcalc` instead, closing the panel when present and reopening it otherwise. The shortcut also works with text-field focus. Toggling preserves the running drawing command and pending command-line input. The expression, history and calculator variables survive closing/reopening within the same session; they are not saved across application restarts. Invoke these UI commands through `engine.execute`, for example `{"command":"ui.dock","params":{"operation":"float","panel":"layers","rect":[100,100,320,440]}}`. Advanced `ui.dock` operations use the same validated transaction as pointer gestures: `moveFloating` updates `rect`, `resizeSplit` accepts a boolean child `path` and `size` (`{"Ratio":0.5}`, `{"FixedFirst":280}`, or `{"FixedSecond":280}`), `setStackOpen` takes `open`, and `resizeStack` takes a point `height` or null. A center `move` can include `before` (panel ID or null to append) for tab order. The equivalent `{"action": ...}` form accepts a serialized shared docking action; this is also the form emitted by the renderer. Protected-canvas and app-specific panel rules apply equally to both forms. ## Command-line replies The app and the headless session reply with the same fields. `cmdline.state`, `cmdline.key` and `cmdline.input` return the command-line state: | Field | Value | |---|---| | `prompt` | The prompt text (`"Command:"` when idle). | | `running` | The id of the running command, or `null`. | | `keywords` | The current prompt's options. | | `accept` | The kinds of input the current prompt accepts, or `null` when idle. | | `buffer` | Text typed in the command line but not yet submitted (always `""` headless). | | `history` | The last 20 history lines. | | `historyExpanded` | Whether the history is expanded (always `false` headless). | `cmdline.input` adds: - `output`: the history lines the input added. This includes the error message when the line was refused. - `error`: the error message, only when the line was refused (unknown command, invalid input…). A refused line still replies `"ok": true`, because the text was typed. Check `error`. `app.open` without a string `path` fails with `"missing path"` before any command runs. ## Interface language Select a language with `engine.execute` and the UI command `ui.language`: ```json {"id":1,"method":"engine.execute","params":{"command":"ui.language","params":{"lang":"uk"}}} ``` Supported values are `auto`, `en`, `uk`, `pt-BR`, `de`, `es`, `cs`, `ru`, `fr`, `zh`, `it`, `ko`, `sv`, `ar` and `hu`. Chinese needs an installed CJK font, Arabic an installed Arabic font; Korean needs a font covering the Korean interface from the optional craft-fonts input or the operating system. Without a suitable font, the saved choice is retained and the result's `shown` is `en`. `ui.set` also accepts `{"interfaceLanguage":"uk"}`; `ui.inspect` reports the saved choice in its UI state. Invalid choices leave the current language unchanged. Menus, command labels and prompt text returned by the control channel retain their English source text. See [localization.md](localization.md).