Moved from PaperCraft so other Craft apps can share them. Controls take a
ControlStyle built from the app's tokens and paint through material.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add shared config.toml storage while keeping settings types, defaults and UI policy in each app.
Includes isolated in-memory operation, configurable portable paths, atomic saves with private backups, damaged-file preservation, schema migrations, and documented best-effort handling of external edits.
Validation: native macOS formatting, strict workspace clippy, 172 workspace tests and the craft-settings WASM check. Independent backup-permission and failed external-migration repros fail before the fixes and pass afterward.
Hosted CI: all seven UI and printing jobs pass across macOS, Linux, Windows and UI WASM.
Refs #12
retry_rename also retries ERROR_SHARING_VIOLATION (32) and
ERROR_LOCK_VIOLATION (33), which a rename can report while a scanner
holds the file, and caps each pause at 200 ms.
A rename needs only a writable folder, so a read-only file would be
replaced where writing it in place would fail. Before staging, the
existing file is opened for writing without truncating; when that fails
the save fails with a read-only error.
The target is found by reading links one hop at a time, resolving
relative links against the link's folder. A dangling link writes the
file it names and stays a link, and a loop of links is an error.
A save into a folder that does not exist fails with an error naming the
folder, so a save to an unmounted network share does not create a local
folder in its place.
write_new_with gives the staged file its name with a hard link on every
platform. Where hard links fail for any reason other than an existing
target (FAT/exFAT, some SMB and FUSE volumes), it copies the staged
content into a file opened with create_new. It shares the temporary-file
retry, error context and folder flush with the replace path, and its
errors only mention an original file when there is one.
atomic_write, atomic_write_with and atomic_write_custom all run through
one internal function that takes the AtomicFs, the RenameRetry and a
writer callback, so fault injection also covers streaming writes.
AtomicFs creates and flushes the temporary file (create_new, sync)
instead of writing a byte slice.
install_app_menu printed the error and carried on, so an app could not
tell that ⌘Q had fallen back to winit's Quit, which skips the prompt.
It now returns the error; the app decides whether it is fatal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apps that build their own muda app menu can't use install_app_menu: it
replaces the whole menu and sets muda's process-wide event handler.
QUIT_MENU_ID and on_menu_event(ctx, id) let them give their own Quit
item the id and route its event through the app's close handling.
Neither needs muda, so any muda version works. install_app_menu now
uses both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
winit's default app menu sends Quit (Cmd+Q) straight to
-[NSApplication terminate:] and winit doesn't implement
applicationShouldTerminate:, so a Craft app quits without its
unsaved-changes prompt. The new opt-in `menu` feature adds
`install_app_menu(ctx, app_name)`, which replaces that menu with a
muda-built copy whose Quit sends ViewportCommand::Close. The app's own
close handling then runs, as for the close button, and
on_close_request quits.
muda is a macOS-only, optional dependency; no unsafe code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
craft-print's Windows backend ran an embedded PowerShell script for the printer list and every
job. Under App Control for Business or AppLocker script enforcement that script runs in
Constrained Language Mode and every job fails (storytold/pdfcraft#1107). The backend now runs
in this process and starts no program:
- Printers come from EnumPrinters and the default from GetDefaultPrinter; Printer::two_sided
from each driver's PrintCapabilities. The list is sorted, each name once.
- A job is drawn as vectors by the in-box Windows.Data.Pdf with Direct2D into the XPS print
API (winprint). Copies, collation, duplex (left out for duplex: None), colour, orientation
and the driver's own paper option go in a PrintTicket the driver validates; a ticket the
driver changed (paper, orientation, scaling) is refused before anything is sent. The driver
paper named like the standard size is preferred (pdfcraft#599).
- A job has one ticket, so one paper shape. A small lenient reader (sheets.rs) reads the
print-ready PDF's sheets and, when needed, appends an incremental update: /Rotate for sheets
of the other orientation, and for Scaling::ShrinkToFit a reframe onto the driver's matching
or default paper (shrunk inside its widest hardware margin, never enlarged, centred).
Exact size refuses mixed paper sizes and paper the driver lacks.
- command_preview describes the in-process job on Windows ("xps-print", key=value options).
- windows_print.ps1 and the PowerShell helpers are gone. open_printer_preferences (rundll32)
and the CUPS path are unchanged, as is the public API.
The end-to-end test puts a paused Generic / Text Only printer on LPT1: (no port to remove
afterwards; a paused FILE: queue never takes the job) and checks a mixed-orientation exact job
and a shrink-to-fit job each land as one job with all sheets. The reader is tested on every
platform with classic and compressed cross-references and thousands of truncated and corrupted
files. Windows-only dependencies: winprint =0.2.1 (default features off), winsafe 0.0.29,
roxmltree 0.20, miniz_oxide 0.8.
Ported from PdfCraft's native printing branch (pdfcraft#1132), which carries #599's approach.
Co-authored-by: durtyrobbin <76844303+durtyrobbin@users.noreply.github.com>
Co-authored-by: Dimitar Klaturov <7221384+bulgariamitko@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eframe calls logic before ui, so on_close_request goes after all of the
app's own close handling, at the end of ui when the app handles closes
there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closing an eframe window on macOS can crash in AppKit's Touch Bar finder
after eframe drops the window while the run loop is still running (#3).
craft-quit routes a window close through -[NSApplication terminate:],
the path Cmd+Q already takes, so the process ends before another display
cycle runs.
- quit(): schedules terminate: on the main queue on macOS, no-op elsewhere
- on_close_request() (feature "egui"): cancels the root window's close
and quits through the platform path instead
Also commits Cargo.lock and ignores .idea/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>