Files
wordcraft/docs/control-protocol.md
T
726652f088 Focus fills the screen; Full Screen View is a bindable command (#1678) (#1733)
* #1678: Focus fills the screen; Full Screen View is a bindable command

View › Focus goes full screen with the document on black and a slim bar
whose "⋯" drops the ribbon over the page. Full Screen View
(view.fullScreenView, no default key; bind one in Customize Keyboard)
shows the pages alone with the search box above them; the two turn each
other off. Full Screen (view.fullScreen, F11, #980) keeps its whole-window
behaviour. Escape leaves Focus or Full Screen View, then Full Screen, once
nothing nearer takes it; the system leaving full screen ends all three.
Ctrl+F1 collapses the ribbon (not on macOS). Immersive Reader no longer
turns Focus on. The page widget keeps a fixed id so it keeps focus when
the rulers hide.

Fixes #1678

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* #1678: one Focus path after the rebase; pin the dropped ribbon staying down on a ribbon click

main's Focus (#1017) consumed Escape in WordApp::ui before
leave_full_screen, so Escape would leave Focus even when it should first
collapse a selection or close a popup. That handler is removed: Focus,
Full Screen View and Full Screen are left through leave_full_screen
only. main's focus_hides_the_ribbon_and_escape_brings_it_back still
pins the panes coming back. Full Screen View and Show the ribbon added to
the hr, ko and sv catalogs, which main gained since.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz

* #1678: Show the ribbon is a named button; a bound Ctrl+F1 wins; Esc in Read Mode leaves a dialog's Escape alone

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz

* #1678: parity checklist regenerated after the rebase (Full Screen View counted)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz

* #1678: harness tests for the one Escape path and Ctrl+F1 through the app

In Focus with text selected on the page, the first Escape collapses the
selection and keeps Focus; the second leaves Focus (fails if the old
focus_mode Escape handler in WordApp::ui comes back). Ctrl+F1 sent as a
key event collapses the ribbon when unbound, and runs the user's command
when bound in the keymap, leaving the ribbon alone (not on macOS).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz

* #1678: review — a click on the dropped Focus ribbon gives the page the keyboard; only Ctrl+F1 yields to a binding

The ribbon dropped over the page in Focus lies over the canvas viewport and
in a Middle-order area, so refocus_after_chrome_click skipped clicks on it
and typing went nowhere until the page was clicked (which also put the
ribbon away). Clicks on the focus_ribbon layer now count as chrome. The
Focus test asserts the page has the keyboard after a ribbon click and that
typing reaches it with the ribbon still down.

dispatch checks the keymap ahead of an interface shortcut only for Ctrl+F1;
Alt+Q and Cmd+Option+R keep their place ahead of user bindings, as before
#1678 (test: a_bound_alt_q_still_opens_command_search).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr

* #1678: review — Ctrl+F1 (⌘⌥R on macOS) collapses or expands the ribbon; a shortcut of your own on Ctrl+F1 wins

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr

* #1678: review — ui-parity's Word column gives Word for Mac's ribbon shortcut (⌘⌥R)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr

* #1678: review — Focus's dropped ribbon is a command, ui.focusRibbon; "⋯" and a click on the page run it

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr

* #1678: review — the Focus UI test turns Focus on from the status bar's Focus button

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
2026-10-21 19:09:36 -04:00

12 KiB
Raw Blame History

Control protocol

Start the app with a control port: wordcraft --control 7981 (or WORDCRAFT_CONTROL_PORT=7981). A missing or invalid port after --control (not 1–65535) prints an error and exits with status 2. It listens on 127.0.0.1 only. Send one JSON object per line, with the window's key (see Keys); each gets one reply line:

{"id": 1, "key": "…", "method": "engine.execute", "params": {"command": "text.insert", "params": {"text": "Hello"}}}
{"id": 1, "ok": true, "result": {"anchor": {...}, "focus": {...}}}

Errors come back as {"id": …, "ok": false, "error": "message"}. A failed command leaves the document unchanged. A line that isn't valid JSON gets one error reply, then the server closes the connection (so an HTTP request sent to the port can't run a command); reconnect to continue.

Keys

Any local program (and possibly a web page) can reach a loopback port, so every request needs the window's key. Once the port is bound, the app writes a random key (256 bits, as hex) to <settings>/control-key.<instance> and removes the file when the window closes; a new start writes a new key. WORDCRAFT_PROFILE_DIR overrides the whole profile folder for isolated runs, including recovery copies. See local recovery. <settings> is the folder that holds the preferences: on Linux $XDG_CONFIG_HOME/wordcraft (by default ~/.config/wordcraft), on macOS ~/Library/Application Support/WordCraft, on Windows %APPDATA%\WordCraft. <instance> is the Flatpak instance id inside Flatpak, and local-<port> otherwise. On Unix the file has mode 0600.

cat ~/.config/wordcraft/control-key.local-7981   # Linux, outside Flatpak, port 7981
  • Send the key as top-level "key" with every request, or once per connection with {"method": "auth", "key": "…"} (reply {"ok": true, "result": {}}); later requests on that connection may leave key out. Only auth reads params.key; for other methods params.key is an ordinary parameter (ui.key {"key": "Enter"} presses Enter).
  • The key is checked on every request. A missing or wrong key gets {"ok": false, "error": "unauthorized"}, and the app closes the connection.
  • A line over 1 MiB gets {"ok": false, "error": "line too long"}, and the app closes the connection.
  • A connection that sends no authorised request within 30 s of being accepted is closed. That is one limit: blank lines and bad JSON do not extend it. After an authorised request the connection may stay idle.

wordcraft-cli mcp --connect takes the key from WORDCRAFT_CONTROL_KEY, or else from the key file for the port it connects to, and reads it again on every new connection (the app may have restarted). It sends the key only to loopback addresses (127.0.0.0/8, ::1, localhost). For any other host it sends no key, says so, and the app refuses its requests: to drive an app on another machine, forward its port to 127.0.0.1 (for example ssh -L 7981:127.0.0.1:7981 host) and set WORDCRAFT_CONTROL_KEY to that app's key.

Methods

Method Params Result
engine.execute command, params the command's result (any id from engine.commands, including UI commands ui.tab, ui.dialog ({"name": dialog, "part"?: table style part}; part opens New / Modify Table Style (newTableStyle / modifyTableStyle) on that part: wholeTable, headerRow, firstColumn, bandedRows, nwCell… as table.modifyStyle names them), ui.backstage, ui.zotero.* (see docs/zotero.md), ui.language (`{"value": "auto"
engine.execute command, params the command's result (any id from engine.commands, including UI commands ui.tab, ui.dialog ({"name": dialog, "part"?: table style part}; part opens New / Modify Table Style (newTableStyle / modifyTableStyle) on that part: wholeTable, headerRow, firstColumn, bandedRows, nwCell… as table.modifyStyle names them), ui.backstage, ui.zotero.* (see docs/zotero.md), ui.language (`{"value": "auto"
engine.commands — every command: id, label, location, shortcut, params, enabled
document.inspect text? blocks (text, style, runs, lists, tables), parts, sections, selection, pages
ui.inspect — UI state, view, dialog, window size, page rects on screen, caret, comment balloons (balloons: id + screen rect; balloon: the selected one), perf
ui.click x, y, button?, count?, shift?, cmd?, alt? real pointer input at window coordinates
ui.clickText page (0-based), x, y (points from the page's top-left), count? click inside a page
ui.move / ui.drag x,y / x,y,toX,toY,steps? pointer move / drag
ui.press / ui.release x,y, modifiers primary button down / up there (with ui.move between: a drag you can screenshot halfway)
ui.wheel x, y, dy, dx?, unit? (point/line/page), phase? (start/move/end), modifiers mouse-wheel or trackpad scroll at window coordinates (dy < 0 scrolls down)
ui.key key (B, Enter, Left…), shift?, alt?, cmd? key press through egui (shortcuts apply)
ui.text text typed text through egui
ui.screenshot path? PNG of the window: written to path, or without one returned as base64 png in the reply
ui.render path?, page? (1-based), scale? PNG of a page, rendered by the engine: written to path, or without one returned as base64 png in the reply (never a fixed name in the shared temporary folder)
ui.parity — feature-catalog parity
ui.resize / ui.focus / app.quit window control

Any other method name is treated as a command id: {"method": "format.bold"} works.

The CharProps and ParaProps that format.set, para.set and styles.modify take are listed in MCP server.

Positions are {"story": "body" | {"part": n}, "path": [block, row, cell, block, …], "off": byteOffset}.

Keyboard and text input

ui.key and ui.text go to this window's focused widget even when another app is in front (#2420). A focused Find field receives the text; a document receives it only while its canvas owns keyboard focus. Shortcuts, dialogs and document protection still apply. These methods do not activate the native window. ui.focus separately requests native activation.

The {"queued": true} reply acknowledges the input queue, not a completed edit. Input is applied on subsequent UI frames. Inspect the resulting document or widget state before continuing with an operation that depends on it; do not replay an edit simply because its effect is not immediate.

Closing the document

engine.execute with file.close never shows the Save / Don't Save / Cancel prompt. On a document with unsaved changes it replies with an error (the app keeps running and the edits stay); save first (file.save), or pass {"force": true} to discard the changes and close. The bare form {"method": "file.close", "force": true} (a method name that is a command id) is guarded the same way, and only a boolean true counts as force. A document without unsaved changes closes at once (#1268). Closing closes the document, not the app: the window stays and shows no page, and file.new or file.open goes on from there (#1712).

Native printing

engine.execute can call ui.printSettings with {} to inspect the printer list, selected printer, copies, duplex, grayscale, discovery/submission status and backend receipt/error. {"refresh":true} starts background discovery; poll the same command until discovering is false. Set printer to a discovered name, integer copies from 1 to 999, duplex to printerDefault, off, longEdge or shortEdge, and boolean grayscale. Invalid updates are rejected without changing earlier settings. ui.printSubmit explicitly submits the current print-ready PDF without opening a dialog; inspect ui.printSettings until busy becomes false and check status/detail. The browser keeps its own print hook. PDF export remains separate.

ui.printSubmit / ui.print also accept the upstream PDF page settings (range, pages, parity, copies, collate). Native printing renders the selected pages once and passes copies/collation to the spooler; the browser and file.printPdf keep copies in the PDF. Omitting native copies uses the configured ui.printSettings value. collate defaults to File › Print's setting; invalid ranges submit no job. paper (none, or a Layout › Size name such as Letter, Legal, A4) is Scale to paper size (#2108): every page is scaled onto that paper and centred, and native jobs ask the spooler for that media; the document's page size is unchanged. file.printPdf takes it too; an unknown paper submits no job.

engine.commands {"search":"print"} lists the engine print commands and the app-owned ui.printSettings, ui.printSubmit, and legacy ui.print alias, with current enablement and their parameter documentation. Search is case-insensitive across IDs, labels, locations and parameter descriptions. Listing commands never discovers printers, submits a job or opens a dialog. Settings inspection is always available; submission becomes enabled when the native printer is ready, or when the browser host provides its print flow.