* chore(lints): satisfy nonminimal_bool on Rust 1.96 Clippy Four boolean expressions rewritten into their equivalent minimal forms (De Morgan / is_none_or) so `cargo xtask ci` passes `clippy -D warnings` on current stable. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * i18n(tr): Turkish interface (Türkçe) - crates/ui-egui/src/i18n/tr.tsv: complete catalog, all 1,307 keys of es.tsv, written clean-room from the plain meaning of the English labels; placeholders, escapes and "…" kept. A noun after a number stays singular in Turkish, so the context-free format needs no plural forms. - Registry entry `tr` / "Türkçe": tr-TR, tr-CY and tr_*.UTF-8 locales resolve by the generic prefix match; saved preference and English fallback use the existing machinery. - Command search folds case the Turkish way (İ→i, I→ı) for translated labels and locations, while English labels and ASCII command ids keep their existing Unicode case-insensitive matching ("italik" finds "İtalik", "INSERT.TABLE" still finds insert.table). - The Font dialog's preview sample is "AaBbYyZz ĞğİıŞş": it has no lowercase i, so the All Caps preview (to_uppercase) gets the Turkish capitals right. - Tests: catalog key and placeholder parity, locale resolution, saved preference, Turkish folding, the sample's capitals, a real egui command-dialog regression, and ÇçĞğİıÖöŞşÜü in the bundled UI fonts. - Docs: README, ROADMAP, localization-parity, control-protocol, new localization-tr.md (terminology, scope); ATTRIBUTION row for tr.tsv. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(paste): an absolute picture path that is absolute on Windows too `/tmp/b.JPG` has no drive, so `Path::is_absolute` is false on Windows and `paste_inserts_clipboard_pixels` failed there; the test uses `C:\tmp\b.JPG` on Windows. Test-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
4.8 KiB
Control protocol
Start the app with a control port: wordcraft --control 7981 (or WORDCRAFT_CONTROL_PORT=7981).
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. <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 leavekeyout. Onlyauthreadsparams.key; for other methodsparams.keyis 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, 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.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 |
ui.render |
path, page? (1-based), scale? |
PNG of a page, rendered by the engine |
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.
Positions are {"story": "body" | {"part": n}, "path": [block, row, cell, block, …], "off": byteOffset}.