* --help and -h print the usage for the app and every CLI command (#968)
wordcraft --help / -h printed nothing useful: the app tried to open a
file named --help and started. wordcraft-cli <command> --help failed with
"unknown option --help" for every command, and -h was taken as a file
name or ignored.
The app now prints its options and returns before the window opens.
The CLI checks for --help / -h after any known command before options
are validated or files and folders opened, and prints the shared usage.
Tests run both binaries for each command and flag; the app's test runs
without a display, so a regression fails instead of opening a window.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #968: test bare --help and -h on wordcraft-cli
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The traffic lights grew in macOS 26, so the zoom button overlapped
AutoSave. Measure the zoom button through AppKit at start-up and size the
bar and its left inset from it, keeping the old layout as the minimum and
the fallback.
Claude-Session: https://claude.ai/code/session_01JTqaRSzgjKv75TzSC55HEY
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* proof: Turkish spelling, suggestions, grammar and hyphenation
* Turkish: dictionary tests skip when no Turkish Hunspell dictionary is installed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* #1445: Save in the close prompt that can't write the file says so instead of looking like Cancel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1445: a failed save from the close prompt brings the prompt back with the reason, Save As included
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1315: web Save before New/Open waits for the save picker; cancel or failure keeps the document
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1315: overlapping saves hold the replacement; cancel and waiting messages
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #1381: WIP
* #1381: HTML import: bold <th>, cell text-align, region paper for HTML/MD/TXT
Header cells are bold unless their own CSS says otherwise. text-align on a
td/th, or inherited from its tr/row group/table, aligns the cell's
paragraphs; a paragraph's own alignment wins, and a table's align attribute
(which places the table) no longer counts. Files that give no page size
(HTML, Markdown, plain text) open on the region's paper, as a new blank
document does: file.open uses the session's region, wordcraft-cli reads the
system region the way the app does (Region::from_locale_vars, shared with
ui-egui). Tables size to that page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1381: the CLI uses the system region everywhere; help mentions WORDCRAFT_REGION
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #816: copied equations reach other programs as MathML
A copy or cut whose selection holds an equation now also puts HTML on the
system clipboard, with each equation as Presentation MathML (wrapped in
<semantics> with the linear format as an application/x-unicodemath
annotation), so Word, LibreOffice, Google Docs and browsers paste a
formatted equation instead of `y=2x_s`.
- formats: new mathml module, a writer from the equation tree (runs,
fractions, scripts and pre-scripts, radicals, n-ary operators, delimiters,
functions, limits, accents, bars, border boxes, group characters, equation
arrays, matrices, phantoms) and a bounded reader (our annotation, else the
presentation elements, else a TeX annotation). HTML export writes
equations as MathML; HTML import (open and paste) reads <math> back.
- engine: edit.copy/edit.cut (and column copy) return `html` and keep
Session::clipboard_html when the fragment holds an equation.
- ui-egui: Services::set_clipboard_html; copy sites put HTML with the text
through it, falling back to egui's text copy.
- wordcraft: arboard set().html() with the text alternative; on Wayland
without data-control the text-only path is kept.
OMML on the clipboard (Word's "Office Open XML") needs custom clipboard
formats arboard can't set; left as a follow-up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #816: honest clipboard docs; MathML reader fixes (token cap, function names, column vectors)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Write WordprocessingML 2003 by rewriting the DOCX writer's package into the 2003 vocabulary; file.save/saveAs take format wordml2003, File > Export lists the format, CLI convert --format wordml2003, a plain save over a 2003 file keeps it.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
WMF and EMF pictures already drew (crates/metafile) when they came inside a
document, but Insert › Picture refused them ("not a supported image"), and the
file dialog, drag-and-drop and paste lists left them out. A metafile is now
placed at its own frame size (points for placeable WMF and EMF; very large
frames scaled to 22 inches on the longer side) and stored as .wmf or .emf, so a
save writes it as such. render gains metafile_size().
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #766: start on File › Home (Start screen) when launched with nothing to open
Desktop and web start on Backstage Home (Blank document, templates, Recent)
when no file, --sample or --control is given; Esc/back leaves the empty blank
document. File › Options › General › Start-up checkbox (ui.startScreen, kept
between runs, default on). WordApp::new is unchanged; hosts opt in via
WordApp::start_screen. ui_shot: {"empty":true,"startScreen":true}.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: Hungarian and French strings for this change
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #766: Bulgarian for the Start screen setting
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(windows): start 32-bit builds on OpenGL so launch no longer exits
DirectX 12 graphics pipeline creation fails on 32-bit Windows, so the app quit before the window appeared.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add @odedigo to the contributor names list
The About window credits this username; no real name or display name was chosen.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #838: new documents follow the system region (A4/cm/decimal comma outside the US)
- geom: Region (from a locale tag; CLDR Letter and US-measurement territories,
decimal separator), Unit::format_with; parse_measure takes either separator
and a Unicode minus.
- engine: Session.region (plain value, en-US headless), Session::units()
(explicit Measurement units choice wins, else the region's),
blank_document(); file.new blank uses the region's paper; file.options
takes "region" and units "auto" and reports unitsFromRegion/region.
Prefs.units is now Option and saved as measurementUnits (old saved values
were the inch default, not a choice).
- ui: region from WORDCRAFT_REGION, LC_PAPER/LC_MEASUREMENT/LC_NUMERIC, the
platform locale (browser on the web) and Windows' home location; length
fields show and read the region's decimal separator (MeasureExt); Size
menu lists sizes in the measurement unit.
- Tests: engine new_documents_follow_the_region, geom region/format/parse,
ui length_fields_follow_the_region. Docs: localization parity, roadmap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review fixes for #857: keep a measurement unit chosen before the rename
Settings saved before #838 kept the unit under `editing.units`, defaulting to inches. On load,
a value there other than inches was the user's own choice, so it now carries over to
`measurementUnits` (unless that key is already present); inches, junk and non-strings follow
the region. Test: `a_unit_chosen_before_regions_survives_the_rename`.
Also merged main (ROADMAP.md, engine lib.rs re-exports: `blank_document` and main's
`set_system_locale`). The Windows region code (`GlobalizationPreferences::HomeGeographicRegion`)
was cross-checked with clippy -D warnings for x86_64-pc-windows-gnu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #838 review: saved region choice, Unix conventions apart, the browser's locale
- File › Options › Advanced › Region: Automatic (the system's) or a chosen region, saved with
the preferences (`Prefs::region`; `file.options {"region": "pl-PL" | "auto"}`). The session
keeps the platform's region as `system_region`; `Session::region()` is the one in effect.
Measurement units gains an Automatic entry (follows the region).
- Unix: numbers follow LC_NUMERIC (else LANG), paper LC_PAPER and measurement LC_MEASUREMENT,
each on its own (`Region::with_paper_and_measurement`); WORDCRAFT_REGION and LC_ALL still set
all three.
- Web: sys-locale's `js` feature, so the browser's language reaches the region (and the
interface language, as its comment already said).
- Runs without preferences (WORDCRAFT_NO_PREFS) stay en-US unless WORDCRAFT_REGION is set.
- docs/localization-parity.md: where the region comes from, the Windows home location vs. Word's
regional format, and where the decimal separator comes from.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #838 review: rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #838: rustfmt after merging main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: Hungarian and French strings for this change
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #687, #833: paste keeps other programs' formatting; Wayland clipboard pictures
- Paste (Ctrl+V, Home > Paste) reads the system clipboard's HTML, else RTF,
and keeps its formatting; our own copy still wins while the clipboard holds it.
Windows CF_HTML headers are understood; oversized or empty payloads fall back
to the text.
- Merge Formatting inserts the content in the destination's font and paragraph,
keeping bold, italic, underline and strikethrough (it did nothing before).
- Paste Special lists what the system clipboard holds (HTML, RTF, picture,
text); Mod+Alt+V opens it even with only a picture there.
- Services gain clipboard_html, clipboard_rtf and clipboard_formats hooks; the
host reads RTF on Windows (clipboard-win) and Wayland (wl-clipboard-rs).
- Linux: arboard's wayland-data-control feature, so pictures copied in a
browser or Spectacle reach a native Wayland window (KWin only syncs to
Xwayland while an X11 window has focus); other picture types are read
from Wayland directly. A paste that finds nothing usable names the formats
on the clipboard, or why its picture couldn't be read, instead of "empty".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review fixes for #847: translate the Paste Special picture strings; Windows/macOS unused import
- `Picture` and `Pastes the clipboard's picture.` translated in all 15 interface catalogs.
- clipboard.rs: `MAX_FILE_BYTES` is used only by the Linux/Wayland module, so it is named
there; the top-level import was unused (a warning, and a clippy -D warnings error) on Windows
and macOS.
- Merged main (ROADMAP.md, docs/gaps.md, docs/file-format-parity.md, main.rs services: keeps
both the clipboard hooks and the default-app hook from #295). Cargo.lock adds only the
wl-clipboard-rs set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: Hungarian and French strings for this change
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Persist the custom dictionary across sessions (#40)
* Review fixes for #796: session-only Ignore All, desktop-only persistence, capped atomic I/O
- review.ignoreAll no longer adds to (and saves) the custom dictionary: ignored words go to a
separate session-only list, as Ignore All does in Word.
- Only the desktop app wires the dictionary file. Headless MCP and `wordcraft-cli run` keep
words in memory, so an agent never reads or changes the person's dictionary (and nothing
writes outside an automation write root, #769).
- Loading reads through `take(1 MB)`, so a huge file is never read whole; lines that aren't a
single word (spaces, control characters, invalid UTF-8, over 64 characters) are skipped.
- review.addToDictionary applies the same word rule (proof::custom_word), so a word added is
never dropped on the next load.
- Saving merges the words on disk first (another process's additions survive; words just
removed stay out), creates the folder, and replaces the file atomically (temp + rename).
The file I/O lives in the proof crate, so the engine doesn't touch std::fs itself.
- Tests for each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Engine: check automation read and write roots on every path a command reads or writes (#769)
* MCP: confine the headless session to the automation roots (#769)
* CLI: add --automation-read-root and --automation-write-root to mcp (#769)
* Docs: document --automation-read-root and --automation-write-root for mcp (#769)
* Review fixes for #798: confine file.printPdf; guard all of engine/src
- print.rs::print_pdf_to_file (file.printPdf, added on main) wrote with std::fs::write
directly, so a confined agent could print a PDF anywhere. It now goes through
io::write_file, which checks the automation write root.
- The guard test now walks every file under crates/engine/src (not only cmd/), except
io.rs, file_access.rs and the tests*.rs modules, and also flags OpenOptions.
- Tests: an engine test that file.printPdf is refused outside the write root, works inside
and is unchanged without roots; the MCP integration test covers it too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review fixes for #798: list file.printPdf among the confined writes in docs/mcp.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Font files added by path go through io, inside the automation read root (#769)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* web: use File System Access API showSaveFilePicker with download fallback
* Review fixes for #728: mark the document saved only after the picker writes the file
A cancelled or failed save in the browser's save picker marks the document unsaved again, and
the beforeunload guard keeps warning while a picker save is in flight. The picker's file types use
our own neutral format names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Open Word documents with WordCraft from the system (#295)
Register WordCraft for Word, RTF and ODT files on Windows and Linux without
taking over anyone's defaults, and add File > Options > General > "Make
WordCraft the default for Word documents..." where the system allows it.
- Windows MSI: a ProgId per type (.docx .docm .dotx .dotm .doc .dot .rtf
.odt, plus .md and .txt as Open With candidates only) with the app icon
and `"wordcraft.exe" "%1"`, OpenWithProgids, Applications\wordcraft.exe
SupportedTypes, and Capabilities + RegisteredApplications so WordCraft
shows in Settings > Default apps. New component with its own GUID.
- Linux: the desktop entry declares the macro-enabled, template and legacy
Word types and text/rtf; AppStream lists the media types. deb, rpm,
AppImage and both Flatpak manifests use this same file.
- macOS: Info.plist also lists legacy .doc.
- The Options button runs `ui.makeDefaultApp` through a new
Services::make_default_app hook: Windows opens
ms-settings:defaultapps on WordCraft's page; Linux and BSD run
`xdg-mime default ai.storyteller.wordcraft.desktop <types>` (refused
inside Flatpak or without an installed desktop entry); macOS shows the
Finder Get Info hint; the web shows nothing. The status bar reports the
outcome. Strings translated in every catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review fixes for #304: keep Make default out of programmatic dispatch
ui.makeDefaultApp is refused when run programmatically (control channel, MCP, macros,
scripts); only a click on File > Options > General > Make WordCraft the default… reaches
the host's hook, so no agent can change the user's system default apps or open system
settings. Test: programmatic_calls_cannot_change_the_default_apps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Default-app strings in the newer catalogs; the button's test looks for a General-pane label
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Linux: the display theme asks the desktop, not winit
winit reports no system theme on Linux — `ActiveEventLoop::system_theme()` is
`None` there on every desktop — so "Use system setting" never saw the desktop's
choice and fell back to light. A new `desktop_theme` module asks the XDG
Desktop Portal's Settings interface for
`org.gnome.desktop.interface color-scheme` on a thread of its own, so no
D-Bus call is ever made from the frame loop, and the app follows the answer,
including a change made while it runs.
The colour scheme is asked for under its GNOME schema name first, which
GNOME's portal and XFCE's both answer, then under the older freedesktop one,
which KDE answers. A desktop that answers neither leaves the user's own choice
alone, exactly as before. macOS and Windows are untouched: winit answers there.
* Review fixes for #298: one D-Bus connection; watcher started by the app
- The watcher keeps one session-bus connection for its lifetime instead of opening a new
one every 4 s; a connection whose calls all fail is dropped and reopened on the next poll.
- WordApp::new no longer starts the watcher. The desktop app calls
WordApp::watch_desktop_theme at start-up, so unit tests, kittest and ui_shot don't spawn
D-Bus pollers and what they draw doesn't depend on the host's desktop theme (test:
a_new_app_does_not_watch_the_desktop).
- A change wakes the window (request_repaint), so an idle app follows a theme switch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test: name the empty Vec's type (a new dependency adds a PartialEq impl for char)
---------
Co-authored-by: GyuriX <gyurix@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Options: Animation effects setting that follows the system
The app eased wheel scrolling, Ctrl+wheel zoom and its fades whatever the
system's animation setting was: with Windows' Animation effects off, the
page still smooth-scrolled.
File > Options > Accessibility > Animation effects: Use system setting
(the default), On or Off; `ui.animations` for agents; kept in ui.json.
- Off: a wheel notch scrolls the page its whole distance at once, egui's
own scroll areas get the notch whole instead of eased in, Ctrl+wheel
zoom steps at once, and egui's fades and scroll-to glides are off.
Touchpad momentum and the caret blink are unchanged.
- Use system setting asks the host (`Services::animations_enabled`) at
start-up and when the window regains focus. Windows: the Windows
Runtime's UISettings, a safe API of the `windows` crate already in the
build. Web: `prefers-reduced-motion`. macOS and Linux/BSD aren't read
yet (docs/gaps.md); there it means on.
- The eight new strings are in all eleven interface catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Animation effects off: zoom steps land finished in one frame
Follow-up to the Animation effects setting: zooming could still look
animated with the setting off.
- Ctrl+wheel zooms in whole steps, each at once. A mouse-wheel notch is
two steps in its frame, the same zoom as before. Turns that arrive in
small pieces (a touchpad pinch that Windows reports as Ctrl+wheel, a
high-resolution or free-spinning wheel) add up to a step of about 10%
first, where each piece used to zoom a little and the zoom glided.
- A page re-rendered for a zoom step is drawn on the pixel grid in that
frame. It used to be drawn off the grid, slightly soft, and snap sharp
a frame later.
- The frame a zoom changes in asks for the next one at once, so the
status bar's percentage and slider follow in the next frame. They
could lag until the caret's next blink, up to half a second.
The last two apply whatever the setting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Animation effects: follow the system's setting on macOS, Linux and the BSDs
"Use system setting" only read Windows and the browser; elsewhere it
meant on.
- macOS: System Settings > Accessibility > Display > Reduce motion,
through NSWorkspace (a safe call in objc2-app-kit 0.3, already in the
build).
- Linux and the BSDs: the XDG settings portal over D-Bus (zbus, already
in the build through AccessKit). Three settings say whether animations
play, and any one saying no turns them off: the cross-desktop
reduced-motion preference, GNOME's enable-animations and KDE Plasma's
animation speed. A thread reads them at start-up and follows the
portal's change signal. Where no portal answers, or it knows none of
them, KDE's kdeglobals and GTK's settings.ini are read instead.
- The UI now asks the host every frame instead of at start-up and on
focus, so a change in the system's settings is followed while the app
runs. Hosts answer from memory: Windows and macOS are asked again at
most once a second, the web keeps its media query, and Linux/BSD is
kept current by its thread.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Animation setting on Linux: ask every setting again when one changes
KDE's portal announces a new animation speed only as the cross-desktop
reduced-motion preference, not under its own kdeglobals name. Keeping
each setting's last announced value left KDE's own one stale: after
going from instant back to normal speed, animations stayed off.
Every setting is now read afresh on any relevant change, and the
settings files' answer stands whenever the portal has none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Caret glide follows Animation effects; its canvas test turns the setting off, then on
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Linux: built-in file browser when the portal file dialog is unavailable (#576)
rfd 0.17 on Linux/BSD asks the xdg-desktop-portal FileChooser and falls back to
zenity; with neither (labwc with only xdg-desktop-portal-wlr, no zenity) it
returns None, the same as Cancel, so Open/Save As/Insert Picture silently did
nothing. The dialog thread now checks first (zenity on PATH, or the portal's
FileChooser interface via dbus-send) and answers with an error when neither is
there; the app then finishes the job in its own egui file browser (folder
field, Up/Home, folders and files of the chosen type, file name, type filter,
replace confirmation) with a hint on how to get the system dialog.
File > Open > Browse in WordCraft... (Linux/BSD) and `ui.fileBrowser` reach
it at any time, even while a system dialog never showed up;
WORDCRAFT_FILE_DIALOG=builtin forces it. Native only (not wasm32).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Turkish and Russian strings for the file browser (#576)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* MacOS window close
The red close button was aborting inside AppKit. The close button now cancels that in-loop window drop, saves preferences, and quits on the next turn of the main queue the same way Command-Q does.
* Review fixes for #539: remove the control key file before the macOS quit
Merge main. The new close path quits through terminate:/exit, which skips
on_exit, so it now removes the control key file as on_exit does (prefs were
already saved). Clippy-checked for aarch64-apple-darwin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Ribbon and context-menu Paste insert the system clipboard's text (#584)
* Review fixes for #593: ribbon Paste of copied picture files inserts the pictures
Picture files copied in a file manager reach the clipboard as their paths;
ribbon/context-menu Paste now pastes the pictures (as Ctrl+V does) instead
of the path text. Test included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Registers assets/app-icon/wordcraft.ico in apps/wordcraft-web/index.html
using Trunk's rel=icon asset pipeline.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Add fonts at run time: Add Font…, drop a font file, font.add (#582)
Rebased onto main as one commit (the branch had merge commits from main).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #582: font test no longer depends on the system's fallback font
The FreeBSD runner has no system fonts, so a missing family fell back to
Source Serif 4, the font the test compared against. The test now adds a
renamed JetBrains Mono under a name without Serif/Sans/Mono (which picks a
fallback of that kind), with narrow-letter sample text: monospaced, it
measures unlike any proportional fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- docx: Flat OPC (`pkg:package`) read by unpacking the parts into an
in-memory package for the DOCX reader; written from the DOCX writer's
parts (XML inline, binary parts in base64). Part count, part size,
total size and base64 length are capped.
- docx: Word 2003 XML (`w:wordDocument`) imported by rewriting it as the
corresponding package (hyphenated names, w:hlink, w:listPr, w:lists,
inline headers/footers, notes, aml annotations, w:binData pictures,
o:DocumentProperties) and reading that with the DOCX reader.
- `.xml` told apart by root element; other XML is refused with
"not a Word XML document". File > Export gets "Word XML Document (*.xml)"
(all 9 catalogs); .xml saves keep the document like .docx.
- Tests: Flat OPC round trip equals DOCX round trip, hand-written Flat OPC
package, Word 2003 import, hostile inputs, engine io.
Fixes#548
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
wgpu 30's default DX12 compiler (Auto) uses DXC and loads whichever
dxcompiler.dll the DLL search path finds first. WordCraft ships none, so
that copy belongs to another program; without a matching dxil.dll, DXC
warns that the DXIL is unsigned, wgpu treats the warning as a compile
error, its indirect-validation pipeline fails, the device is lost and
eframe exits before the first frame ("Parent device is lost").
The window's wgpu instance (and the DirectX 12 adapter probe) now pins
the DX12 shader compiler to FXC (d3dcompiler_47.dll, part of Windows)
unless WGPU_DX12_COMPILER names another. Same approach as
storytold/lightcraft#484. README troubleshooting note and ROADMAP log.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
File › Options is now a pane list (General, Display, Proofing, Save,
Language, Accessibility, Advanced, Agents), one function per pane in
crates/ui-egui/src/options.rs. Every setting shown is wired:
- General: theme, interface size (#475), mini toolbar on selection, dark page, community
button, user name and initials (comments use the initials).
- Display: tab characters, spaces, paragraph marks and hidden text shown
with Show/Hide ¶ off; rulers; print the page colour; update fields
before printing.
- Proofing: ignore words in capitals / with numbers / web and file
addresses (proof::Options, passed through layout), check spelling and
mark grammar as you type, custom dictionary add/remove (kept in ui.json).
- Save: AutoSave. Language: interface language. Accessibility: Read Aloud
speed and citations. Advanced: typing replaces selection, Insert key
overtype, overtype (text.overtype), recent documents count, measurement
units (dialogs, Layout ribbon indents and rulers).
file.options reads and sets the engine-side settings headlessly and only
opens the page when given no setting. ui.json loads leniently: a bad value
falls back to its default instead of dropping every setting.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Select Recipients › Use an Existing List… now reads .xlsx, .xlsm and .ods
workbooks besides CSV. A new read-only SpreadsheetML / ODF spreadsheet
reader in wordcraft-formats lists the sheets and turns one sheet into rows
of display strings: shared, inline and rich strings, numbers, booleans,
cached formula results, dates and times as ISO 8601 from the cell's number
format, sparse cells, ODS value types, repeated rows/columns and covered
cells. Zip entries, rows, columns, cells, string lengths and repeat counts
are capped, and nothing recurses.
mailings.recipients takes "data" (base64) and "sheet" and "headers"; a
workbook with several sheets and no sheet answers with the sheet names, and
the app shows a Select Table dialog with "First row of data contains column
headers". The desktop picker, dropped files and the web inbox route
spreadsheets to the recipient list.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
`println!` panics when stdout is a closed pipe, so `wordcraft-cli commands
--json | head` aborted with exit 101. Every stdout write now goes through a
locked handle with `writeln!` and propagates `io::Error`; a BrokenPipe becomes
`Failure::StdoutClosed`, which `main` treats as a normal exit 0. The MCP stdio
server's BrokenPipe takes the same path, so a client that hangs up ends the
server cleanly. Real errors still go to stderr with exit 1.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Since Windows defaults to DirectX 12 only (#50), PCs whose graphics chip has
no DirectX 12 driver (Intel 4th generation on Windows 11) fall back to WARP,
Microsoft's software renderer, and the app lags. Before eframe creates its
instance, WordCraft now probes DirectX 12 with a short-lived instance: a
hardware adapter that meets eframe's device limits keeps DirectX 12 only;
WARP only enables OpenGL next to DirectX 12 (GPU preferred, WARP as last
resort); no adapter, or only GPUs below the limits, uses OpenGL alone.
WGPU_BACKEND still overrides everything. The choice is logged, and the
decision is a pure function with unit tests.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
egui's paste event only carries text, so a clipboard holding just a picture
sent no paste at all on Ctrl+V, and the ribbon/context-menu Paste only knew
WordCraft's own copy ("the clipboard is empty").
Desktop: the app reads the clipboard's picture through a new
Services::clipboard_picture (arboard, already in the build via egui-winit):
copied files first, else image pixels. Mod+V whose release arrives without a
paste event, and Paste from the ribbon or menus, insert that picture through
insert.picture (one undo step); text naming copied picture files inserts the
pictures. Pixel sizes are hostile: at most 16,000 px a side and 50 Mpx, the
buffer must match, files at most 200 MB.
Web: a document paste listener reads image files from the paste event when
there's no text and delivers them through the inbox like dropped pictures.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
On desktop, the Print button on File › Print now renders the document
to PDF, writes it to a fresh wordcraft-print-*.pdf in the OS temp
folder and opens it without blocking the window: in Preview on macOS,
and in the default PDF app on Linux/BSD (xdg-open, gio, ...) and
Windows (via the `open` crate, already in the tree). The user prints
from there through the system print dialog. If no viewer can be
started, the status bar says so and the file is removed. Print files
older than an hour are cleaned up off the UI thread at start-up and
after each print.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: shapes and pictures can be clicked, resized, dragged and nudged
Fixed by #46 but only text boxes were covered. These walk the editor's
path for shapes (rectangle, ellipse, star, arrow) and pictures: hit test,
select, resize with arrange.bounds, drag to float with Square wrap,
nudge, undo. Covers #142 and #82.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Font list offers every weight a family installs, named as Word names them (#219)
Fonts were listed and matched only by their typographic family name
(name ID 16, e.g. "Roboto"), so a face other than Regular, Bold or Italic
(Light, Medium, SemiBold, Black, Condensed...) never appeared in the font
list, and a document asking for "Roboto Light" fell back to a substitute.
Each face now also carries its legacy family name, the one Word lists and
stores in documents: name ID 1 for static fonts ("Source Sans 3
Semibold"), or the family plus the style words other than Regular, Bold
and Italic for a variable font's named instances ("Inter Light"). The
font list includes these names, has_family and face() resolve them (bold
and italic still choose or synthesise within the faces the name covers),
and asking for a legacy name loads the whole installed family. The
system font scan now also reads fvar, so a variable font's instances are
listed before it is loaded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Absolutely positioned VML text boxes float instead of sitting inline (#149)
A legacy VML shape (w:pict > v:shape / v:rect) with position:absolute in its
style was read as an inline object, so a tall text box in a header made the
header as tall as the box and pushed the body down the page.
The reader now maps the VML style to the same float model DrawingML anchors
use: left/margin-left and top/margin-top offsets, mso-position-*-relative
reference areas, mso-position-horizontal/vertical alignment, mso-wrap-distance-*
and w10:wrap (square, tight, through, topAndBottom; none or absent is in front
of the text, or behind it at a negative z-index). Shapes without
position:absolute stay inline. Saving writes the boxes as DrawingML anchors
with the same placement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Reject All removes paragraph breaks inserted with Track Changes on (#229)
Enter under Track Changes already recorded the new paragraph mark as an
insertion, but accepting or rejecting only cleared that flag, so Reject All
left the paragraphs split. Rejecting an inserted mark (or accepting a deleted
one) now joins the paragraph with the next one again; Accept All, Reject All,
Accept/Reject at the caret or over a selection, Next/Previous Change and the
Reviewing Pane all see paragraph marks.
Backspace at a paragraph start and Delete at a paragraph end now record a
tracked deletion of the mark (or remove a mark the same author inserted)
instead of only moving the caret; tracked deletions across paragraphs mark
the paragraph marks too.
DOCX reads and writes paragraph-mark revisions (w:ins / w:del in the
mark's w:rPr), and a tracked mark is drawn in its author's colour when
formatting marks are shown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Table Layout: Text Direction turns cell text (#226)
The Text Direction button only flipped a flag that layout and rendering
ignored (and docx always wrote it as btLr), so pressing it did nothing
visible.
Cells now carry a text direction (horizontal, top-to-bottom tbRl,
bottom-to-top btLr). The button cycles through them like Word's, for every
selected cell. Layout runs turned text along the cell's height: an exact
row height sets the line length, otherwise the row grows to fit the
unwrapped text. Turned lines are drawn in a turned frame on screen and in
PDF, and clicks, caret, and selection follow the turn. docx reads and
writes w:textDirection with the matching value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* File dialogs no longer freeze the window on Linux (#94)
Open, Save As, Export and Insert/Change Picture used rfd's blocking
FileDialog on the UI thread with no parent window. On Linux the
xdg-desktop-portal call blocked the event loop until the dialog closed,
and the unparented dialog could open behind WordCraft, so the app looked
frozen.
The desktop app now shows rfd::AsyncFileDialog parented to the window
(set_parent with eframe's Frame, no unsafe) and waits for the answer on
a background thread, which hands it back through a channel and wakes the
UI. ui-egui gains a backend-agnostic Services::file_dialog hook: the app
remembers what the dialog was for and finishes it when the answer
arrives (open the file, save, export, insert or change the picture, turn
AutoSave on). "Save changes?" -> Save on a new document now runs the
held-back New/Open/Close only once the later Save As has saved; a
cancelled Save As cancels it. Only one file dialog is open at a time;
further clicks are ignored until it is answered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Select Recipients opens a recipient list instead of a parameters error (#240)
Clicking Mailings › Select Recipients ran mailings.recipients with no data, so the
status bar showed "`csv`, `path` or `rows` required" and nothing opened.
The button is now a menu: Type a New List… opens a Recipient List dialog (an
editable table of fields and entries), and Use an Existing List… opens the file
picker for a CSV/TSV/text file (desktop: loaded by path; web: the picked file
comes through the inbox and loads as recipients instead of opening as a
document). User-run commands that lack their input open a dialog instead of
failing: Select Recipients and Edit Recipient List open the Recipient List,
Insert Merge Field and Find Recipient open small dialogs of their own. Scripts
and agents still get the command's error, never a dialog.
mailings.recipients and mailings.editRecipients take an optional `fields` list,
so a typed list keeps its column order and rows can be arrays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Chinese interface: embed the Chinese UI font in releases, fall back to an installed one (#241)
The release workflow pinned storytold/craft-fonts at abb8331, the commit that added the
Japanese fonts. Noto Sans CJK SC came one commit later (8dcdacd), so release builds embedded
only the Japanese faces and the Simplified Chinese interface fell back to BIZ UDPGothic, which
lacks about 157 simplified-only hanzi (页, 选, 删 …) and drew them as boxes. Document text was
unaffected because it uses the system font fallback.
- Bump the craft-fonts pin in the release and FreeBSD workflows to 8dcdacd.
- With CRAFT_FONTS_REQUIRED, a native build now fails when the craft-fonts manifest has no
Jpan or Hans font, so an outdated pin can't ship silently again.
- When no embedded face covers the interface language, the UI font families get an installed
CJK font (desktop only): a short per-platform list of files in the system font folders
(Windows Deng/msyh/simhei/simsun, macOS PingFang/STHeiti, Linux Noto Sans CJK/WenQuanYi),
header-checked, size-capped, and the face chosen by coverage (a SC face in a pan-CJK
collection), so egui never sees a file it can't parse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add a System interface theme that follows the OS appearance (#115)
The interface theme is now a three-way choice: Use system setting,
Light or Dark, in File > Options > General and in a new Interface Theme
menu in View > Dark Mode. System follows the light/dark appearance egui
reports (winit on macOS, Windows and Linux; prefers-color-scheme on the
web) at launch and live, and falls back to light when the OS reports
none. egui's own theme is now pinned to ours, so an OS change no longer
swaps in egui's unstyled style.
Saved preferences migrate: the old "dark" switch becomes Dark or Light
and is never written again. Light stays the default for new installs.
New ui.theme command (light/dark/system); ui.dark still sets Light/Dark.
The dark page (View > Switch Modes) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #248: read the installed CJK UI font only when CJK text is shown
A build without craft-fonts read a large system CJK font on the first frame even
for an English interface. Now it is loaded only for a CJK interface language, or
once File > Options shows the language list (names in their own scripts).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #247 on #246: Use an Existing List… goes through the non-blocking file dialog
#246 removed the desktop's blocking pick_open; the recipient list picker now asks
the host's file dialog hook (AfterPick::Recipients → load_recipients), and the
desktop dialog offers a Recipient lists filter (csv/tsv/txt).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: translate the recipient list and file dialog strings (#247, #246)
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>
* Tests: shapes and pictures can be clicked, resized, dragged and nudged
Fixed by #46 but only text boxes were covered. These walk the editor's
path for shapes (rectangle, ellipse, star, arrow) and pictures: hit test,
select, resize with arrange.bounds, drag to float with Square wrap,
nudge, undo. Covers #142 and #82.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Font list offers every weight a family installs, named as Word names them (#219)
Fonts were listed and matched only by their typographic family name
(name ID 16, e.g. "Roboto"), so a face other than Regular, Bold or Italic
(Light, Medium, SemiBold, Black, Condensed...) never appeared in the font
list, and a document asking for "Roboto Light" fell back to a substitute.
Each face now also carries its legacy family name, the one Word lists and
stores in documents: name ID 1 for static fonts ("Source Sans 3
Semibold"), or the family plus the style words other than Regular, Bold
and Italic for a variable font's named instances ("Inter Light"). The
font list includes these names, has_family and face() resolve them (bold
and italic still choose or synthesise within the faces the name covers),
and asking for a legacy name loads the whole installed family. The
system font scan now also reads fvar, so a variable font's instances are
listed before it is loaded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Absolutely positioned VML text boxes float instead of sitting inline (#149)
A legacy VML shape (w:pict > v:shape / v:rect) with position:absolute in its
style was read as an inline object, so a tall text box in a header made the
header as tall as the box and pushed the body down the page.
The reader now maps the VML style to the same float model DrawingML anchors
use: left/margin-left and top/margin-top offsets, mso-position-*-relative
reference areas, mso-position-horizontal/vertical alignment, mso-wrap-distance-*
and w10:wrap (square, tight, through, topAndBottom; none or absent is in front
of the text, or behind it at a negative z-index). Shapes without
position:absolute stay inline. Saving writes the boxes as DrawingML anchors
with the same placement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Reject All removes paragraph breaks inserted with Track Changes on (#229)
Enter under Track Changes already recorded the new paragraph mark as an
insertion, but accepting or rejecting only cleared that flag, so Reject All
left the paragraphs split. Rejecting an inserted mark (or accepting a deleted
one) now joins the paragraph with the next one again; Accept All, Reject All,
Accept/Reject at the caret or over a selection, Next/Previous Change and the
Reviewing Pane all see paragraph marks.
Backspace at a paragraph start and Delete at a paragraph end now record a
tracked deletion of the mark (or remove a mark the same author inserted)
instead of only moving the caret; tracked deletions across paragraphs mark
the paragraph marks too.
DOCX reads and writes paragraph-mark revisions (w:ins / w:del in the
mark's w:rPr), and a tracked mark is drawn in its author's colour when
formatting marks are shown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Table Layout: Text Direction turns cell text (#226)
The Text Direction button only flipped a flag that layout and rendering
ignored (and docx always wrote it as btLr), so pressing it did nothing
visible.
Cells now carry a text direction (horizontal, top-to-bottom tbRl,
bottom-to-top btLr). The button cycles through them like Word's, for every
selected cell. Layout runs turned text along the cell's height: an exact
row height sets the line length, otherwise the row grows to fit the
unwrapped text. Turned lines are drawn in a turned frame on screen and in
PDF, and clicks, caret, and selection follow the turn. docx reads and
writes w:textDirection with the matching value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* File dialogs no longer freeze the window on Linux (#94)
Open, Save As, Export and Insert/Change Picture used rfd's blocking
FileDialog on the UI thread with no parent window. On Linux the
xdg-desktop-portal call blocked the event loop until the dialog closed,
and the unparented dialog could open behind WordCraft, so the app looked
frozen.
The desktop app now shows rfd::AsyncFileDialog parented to the window
(set_parent with eframe's Frame, no unsafe) and waits for the answer on
a background thread, which hands it back through a channel and wakes the
UI. ui-egui gains a backend-agnostic Services::file_dialog hook: the app
remembers what the dialog was for and finishes it when the answer
arrives (open the file, save, export, insert or change the picture, turn
AutoSave on). "Save changes?" -> Save on a new document now runs the
held-back New/Open/Close only once the later Save As has saved; a
cancelled Save As cancels it. Only one file dialog is open at a time;
further clicks are ignored until it is answered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Chinese interface: embed the Chinese UI font in releases, fall back to an installed one (#241)
The release workflow pinned storytold/craft-fonts at abb8331, the commit that added the
Japanese fonts. Noto Sans CJK SC came one commit later (8dcdacd), so release builds embedded
only the Japanese faces and the Simplified Chinese interface fell back to BIZ UDPGothic, which
lacks about 157 simplified-only hanzi (页, 选, 删 …) and drew them as boxes. Document text was
unaffected because it uses the system font fallback.
- Bump the craft-fonts pin in the release and FreeBSD workflows to 8dcdacd.
- With CRAFT_FONTS_REQUIRED, a native build now fails when the craft-fonts manifest has no
Jpan or Hans font, so an outdated pin can't ship silently again.
- When no embedded face covers the interface language, the UI font families get an installed
CJK font (desktop only): a short per-platform list of files in the system font folders
(Windows Deng/msyh/simhei/simsun, macOS PingFang/STHeiti, Linux Noto Sans CJK/WenQuanYi),
header-checked, size-capped, and the face chosen by coverage (a SC face in a pan-CJK
collection), so egui never sees a file it can't parse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add a System interface theme that follows the OS appearance (#115)
The interface theme is now a three-way choice: Use system setting,
Light or Dark, in File > Options > General and in a new Interface Theme
menu in View > Dark Mode. System follows the light/dark appearance egui
reports (winit on macOS, Windows and Linux; prefers-color-scheme on the
web) at launch and live, and falls back to light when the OS reports
none. egui's own theme is now pinned to ours, so an OS change no longer
swaps in egui's unstyled style.
Saved preferences migrate: the old "dark" switch becomes Dark or Light
and is never written again. Light stays the default for new installs.
New ui.theme command (light/dark/system); ui.dark still sets Light/Dark.
The dark page (View > Switch Modes) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #248: read the installed CJK UI font only when CJK text is shown
A build without craft-fonts read a large system CJK font on the first frame even
for an English interface. Now it is loaded only for a CJK interface language, or
once File > Options shows the language list (names in their own scripts).
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>
file.setAuthor now documents {"name": string} in the command list, trims the
name, strips control characters, caps it at 255 characters and refuses a blank
name. wordcraft-cli run and headless mcp take --author NAME. review.trackChanges
lists {"value"?: bool} so scripts find the explicit, idempotent form.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* web: print straight to the system print dialog, not just export
The web build's Print backstage page only let you save a copy in another
format — there was no way to actually send the document to a printer short
of exporting a PDF and opening it yourself. Add a dedicated Print button
that exports to PDF in memory and opens it in a hidden iframe, then calls
the iframe's own print() once it has loaded, which is the standard way to
drive the browser's native print dialog on a PDF without a download or a
popup window.
- ui-egui: new Services::print hook (web: hand PDF bytes to the browser's
print flow directly, no download).
- ui-egui backstage: "Print" button on the Print page when the host
implements the hook; goes through wordcraft_engine::io::save_bytes
directly rather than the existing save/download command path, since we
want raw bytes in memory, not a file write.
- wordcraft-web: implement the hook (Blob -> object URL -> hidden iframe ->
iframe.contentWindow.print() on load).
Desktop builds are unaffected (no Services::print set there, so the
button doesn't appear - file.print still opens the Print page as before).
* web print: make printing the ui.print command, dispatched by id
The backstage Print button now runs `ui.print` instead of exporting and
calling the host hook inline, so the control channel, MCP bridge and command
search reach the same path. Like ui.openFileDialog it is the one explicit
call that opens system UI; without a print hook it is an error. Status
strings get rows in all six catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* web print: drop the previous print's iframe and object URL
Each Print appended a hidden iframe holding a fresh blob URL of the whole
document's PDF and never released it, so copies piled up in the page. The
frame now has a fixed id; the next print revokes its URL and removes it
first, and a failure part-way through cleans up after itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* web print: test that Print needs the host's print hook
ui.print errors without Services::print and hands the hook a PDF with it;
a failing hook reports why. The Print page shows the Print button only when
the hook is set, and the button prints through the command.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>