Say that `photocraft --help` lists the flags and their variables, that
a flag wins and an empty variable is unset, and what is a usage error;
that a bad --present-mode or --frame-latency is one while a bad
variable is still ignored; list PHOTOCRAFT_IN_WINDOW_MENUS; and that a
token with a token file is a usage error for the control server too.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With the environment fallbacks, clap's conflicts_with on the token pair
made a stale exported PHOTOCRAFT_CONTROL_TOKEN and
PHOTOCRAFT_CONTROL_TOKEN_FILE a usage error for every mcp and serve, though
headless mcp and stdio serve read no token. As on main, the pair is now
refused only by serve on TCP and mcp bridging, still as clap's either/or
usage error naming each half as it was given.
An automation root on the command line with PHOTOCRAFT_CONTROL_PORT set
is a usage error too: the variable would switch mcp to bridging, which
drops the roots, with only a note on stderr.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Say that PHOTOCRAFT_CONTROL_PORT is the fallback for serve --port and
mcp --bridge (with the stderr notice and the empty-is-unset escape),
and list the CLI's variables on the book's CLI page, which also says
the automation root variables stay the desktop app's.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Step 4 of the issue: the control token flags and the automation roots
move to photocraft-automation's new args module as two clap structs,
which mcp and serve flatten and the desktop app can flatten in step 5
without moving them again. clap is a plain dependency of the crate.
- ControlArgs holds --control-token and --control-token-file;
AutomationArgs holds the roots, and AutomationArgs::workspace opens
them (replacing the CLI's automation_workspace).
- The port value parser is photocraft_automation::args::control_port,
with its unit tests, for serve --port and --bridge now and the
desktop app's --control later. It ignores surrounding spaces and
refuses anything else with a typed NotAPort.
- --bridge parses to Bridge::Port or Bridge::Addr: it takes a bare
port, meaning 127.0.0.1:<port>, or any value containing a ':', which
the bridge client then validates. Anything else is a usage error
(exit 2) instead of a bridge error.
- 127.0.0.1 addresses are built from Ipv4Addr::LOCALHOST.
mcp_bridge_takes_a_port_or_an_address is new: `--bridge nope` is a
usage error, and `--bridge 1` bridges to 127.0.0.1:1, which a tool
call names when nothing listens there.
No new environment behaviour: the token variables are still read at
run time through security::token_inputs, and the roots stay flag-only.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The bridge forwards Replace Contents to the app, which runs it on its
active document, so a given `index` was silently dropped and could replace
another document's contents. It is now refused before anything reaches the
app, as bridge previews and saves already do; documented in the schema and
control-protocol.md. A fake-app test checks the indexed call never arrives.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- crates/ui-egui/src/layer_style.rs: values_of keeps the PR's method/smoothness keys and source-stop ends, and main's (#7072) unconditional From/To and geometry fields.
- crates/compose/src/cmyk_tests.rs (no textual conflict): add the PR's new `interpolation: None` field to main's CMYK test GradientOverlay literal.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts:
- crates/ui-egui/src/i18n/{cs,de,el,es,fr,hu,id,it,ja,ko,nl,pl,pt-br,ru,sr,tr,uk,vi,zh-hans,zh-hant}.tsv: kept every key of both sides (main's block, then the PR's Color panel block); no duplicate keys.
- crates/ui-egui/src/panels.rs: took the PR's removal of color_picker/hsb_sliders/rgb_sliders (moved to color_panel_ui.rs); kept main's color_field_height doc comment; main's #5842 slider recolour tests ported to color_panel_tests.rs for the new HSB/RGB slider modes; main's black-HSB test dropped as the PR's hsb_sliders_take_values_on_black covers it.
- crates/ui-egui/src/color_panel_ui.rs (no textual conflict, adapted): set_color now goes through apply_color::foreground_changed with the gesture (was type_tool::foreground_changed), so every mode (sliders, ramp, alerts, wheel) recolours selected type/shapes tool-gated in one undo step per gesture; panel() calls apply_color::track_focus.
- crates/ui-egui/src/color_panel_tests.rs (adapted): new every_slider_mode_recolours_the_selected_shape test.
- docs/gaps.md, docs/ui-parity.md: union of both sides' status lines, rows and revision history; the PR's done row replaces the old "missing" Color panel row.
- docs/scorecard.md: generated, took b7-base's version.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts resolved:
- crates/ui-egui/src/i18n/hu.tsv: kept base's Bevel & Emboss keys, then the PR's update-check keys
- crates/ui-egui/src/i18n/ru.tsv: kept base's Bevel & Emboss keys, then the PR's update-check keys
- crates/ui-egui/src/i18n/sr.tsv: kept base's added sections, then the PR's update-check keys
- crates/ui-egui/src/i18n/tr.tsv: kept base's Bevel & Emboss keys, then the PR's update-check keys
- crates/ui-egui/src/menus.rs: kept base's saved-workspace entries, then the PR's Help › Check for Updates reordering
- docs/gaps.md: base status line with the PR's G17 clause prepended; PR revision row above base's rows
- docs/target-app-parity.md: base status line (updated 2026-10-22) with the PR's clause prepended; PR revision row above base's rows
- docs/scorecard.md: generated, took b7-base's version
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The rpc test only checked the returned layer id, so a no-op that echoed
{"layer": id} passed, its undo check passed on doc.place's own step, and
the bad-layer case asserted on an empty reason. It now checks the smart
object's file name and rendered centre pixel (42 -> 200 after the
replace, back to 42 on undo, 200 on redo), that every refusal changes
nothing, and that layer 999999 fails with "no such layer".
The bridge row in docs/control-protocol.md claimed doc_replace_smart_object
maps onto layer.smartObjects.replaceContents; the desktop refuses that
command (ambient path), like doc_place's file.placeEmbedded. The row now
says so and points to the headless / workspace server method.
Co-authored-by: VishnuByrraju <112673081+VishnuByrraju@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Main moved about 1,860 commits since the PR's last merge. i18n catalogs keep both
sides' rows; control.rs keeps updatesOpen next to main's strokeTrace (39 fields);
Services keeps check_updates next to main's place_inbox; Cargo.lock is main's,
re-resolved for ureq and semver; docs/gaps.md, docs/target-app-parity.md and
docs/scorecard.md are main's (the PR's entries come back in the next commit).
Co-authored-by: ZhuanTou33212 <285931757+ZhuanTou33212@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's Color panel HSB/RGB slider switch (#7172, #7317) is replaced by this
branch's panel-menu modes, which include RGB Sliders and HSB Sliders; its
control test now sets colorPanel.mode. The panel takes main's shared
photocraft_color HSB conversions and ui.set's f32-range checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crates/ui-egui/src/lib.rs: replay_save doc comment names both Export As and JPEG; replay_save uses the PR's FormatChoices::from_step, after main's export_dialog::replay branch.
docs/file-format-parity.md: header keeps main's text plus the JPEG Options item (last updated 2026-10-19); TIFF row is main's, JPEG row is the PR's; change-log rows once each, newest first.
docs/gaps.md: header keeps main's text plus the G10 item (last updated 2026-10-19).
crates/ui-egui/src/lib.rs: union in the PhotocraftApp fields, the initialiser and set_wgpu (main's window_device/window_lost/window_recovery_retry and set_available, the PR's gpu_render_state/gpu_fault).
docs/control-protocol.md: ui.gpu.simulateLoss line takes the PR's text and keeps main's destroy: true sentences.
crates/paint/src/brush_tests.rs: both sides added tests at the same spot; kept main's four smoothing tests and added the PR's three pose_axes tests after them.
docs/brush-parity.md: kept main's header and added the PR's change item.
docs/gaps.md: kept main's header, added the PR's change item; G4 progress notes and the change-log row kept from both sides.
docs/control-protocol.md: kept main's eyedropper line (with the document.sampleColor wording) and added the PR's Brush Pose line before it.
docs/control-protocol.md: kept main's table rows (document.inspect with snapshots, layer.list) and added the PR's four-floats wording to document.pixel.
openingRaw is {name, path, demosaic}; ui.demosaic is accepted only while a raw
opens; rawDefaults.demosaic is the method the app's raw opens develop with; Open
re-develops when the chosen method differs from the one the open used.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts only in the parity docs: status lines take main's text with the
JPEG Options entry (FILE-215-19) on top; scorecard takes main's rows with
FILE-215-19 done and File compatibility 10 / 12 / 5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
layer.smartObjects.replaceContents takes an ambient path and is refused in
serve/mcp. Add the capability-scoped sibling of doc.place: the file is read
beneath --automation-read-root and handed to the command's own core, as an
RPC method (doc.replaceSmartObject) and an MCP tool (doc_replace_smart_object).
Main added DDS saving (#5993): DDS Options, and FormatChoices carrying the
Targa and DDS answers through every save path (this branch's SaveChoices
carried Targa and JPEG). Resolved by keeping main's name and making it
the one struct for all three answers.
- lib.rs: FormatChoices { tga_bits, dds, jpeg } with from_params (strict),
from_step (lenient JPEG read of a recorded step), apply and record;
main's separate block is folded in. Both prompts (dds_options and
jpeg_options) are kept everywhere: save_to asks Targa, then DDS, then
JPEG Options; save_options_open covers all four. save_in_place takes
main's (path, choices) signature and records the plain-Save step with
plain_save_step for JPEG plus the Targa/DDS answers (save_in_place_with
folded into it). journal_as_plain_save relabels a parked Targa, DDS or
JPEG prompt and keeps tgaBits/dds*/quality/jpegFormat when dropping the
path. replay_save reads choices with from_step.
- SAVE_OPTIONS_OPEN names all four prompts; DDS Options uses it too.
- targa_options_ui: main's plain-Save step (no path) through this
branch's write_answered_save. JPEG Options gets main's Enter-on-a-
focused-Cancel fix.
- view_cmds Save a Copy asks Targa, DDS, then JPEG Options; write_copy
takes FormatChoices.
- control.rs: both app.save tests (JPEG and DDS) kept.
- engine file_cmds: SaveOpts keeps jpeg_format and dds; param docs list
both jpegFormat and ddsCompression/ddsMipmaps. CLI: both --jpeg-format
and --dds-compression/--dds-mipmaps. codecs re-exports JpegFormat and
DdsCompression/DdsOptions.
- docs/book: both entries kept (control-protocol app.save lists both).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DDS textures can now be saved, not just opened: uncompressed (BGRA, BGRX, L8, A8L8; lossless) or BC1/BC2/BC3 (DXT1/3/5) from a clean-room block encoder, with an optional box-filtered mip chain, in the legacy header. Save As and Save a Copy ask with DDS Options; scripts pass `ddsCompression`/`ddsMipmaps` (file commands, control `app.save`, RPC `doc.save`, MCP `doc_save`/`doc_export`), and the CLI takes `--dds-compression`/`--dds-mipmaps`. Targa and DDS answers travel together as `FormatChoices` through the Save As flow.
Review follow-ups:
- File › Save behind DDS or Targa Options records a plain Save with its answer, and replaying `file.save` applies it (#3531).
- The block encoder finds the principal axis from every channel, so a channel that varies on its own can't hide the others' variation.
- DDS joins the shared round-trip suite; the header is built before the output buffer is allocated.
- Enter on a focused Cancel in DDS, Targa or TIFF Options cancels instead of saving.
Save As and Save a Copy to a JPEG now ask for JPEG Options before writing,
as Photoshop does. Re-creates #4330 on current main and folds in the
unique parts of #5580 (#5563).
- codecs: `JpegFormat` (Baseline, Optimized, Progressive) on
`EncodeOptions`. Progressive writes SOF2. Baseline Optimized fits the
Huffman tables only at 4:4:4 (Gray, CMYK) or with Progressive: fitted
tables make the baseline scans non-interleaved, which zune-jpeg 0.5.15
decodes wrongly with 4:2:0 chroma (zune-image#419), as Save for Web
already works around.
- engine: a `jpegFormat` save param (file.saveACopy, Batch, Image
Processor, Layers to Files and every SaveOpts user; anything but the
three names is BadParams before writing) and a shared 0–12 -> 1–100
`jpeg_quality()` that maps NaN/inf to 100.
- ui-egui: `jpeg_options_ui` (Quality 0–12 with Low/Medium/High/Maximum
presets, Format Options), parked like Targa Options. It starts at the
opened JPEG's own quality (`file_encoding`, #4032), else the Export
preference; left there, OK writes that quality exactly and the step
records no `quality`. `SaveChoices` (Targa bits + JPEG) replaces the
bare `tga_bits` through file.save / file.saveAs / app.save, replay and
Save a Copy; automation never prompts. `write_answered_save` is shared
with Targa Options. `park_at` + `Start` let any save seed the prompt.
A File › Save that confirms the document's own flat JPEG is still
journalled as a plain `file.save` (#3531).
- CLI: `--jpeg-format`.
- i18n: five new strings in all 20 catalogs (#4330's duplicate de/es/zh
`Low`/`High` rows dropped: main has them since #4008).
- Docs: book raster-formats JPEG bullet, control protocol, file-format
parity, gaps G10, target-app parity; scorecard FILE-215-19 done.
Co-authored-by: SckraT <70698629+SckraT@users.noreply.github.com>
Co-authored-by: PierrunoYT <95778421+PierrunoYT@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(linux): keep crash-recovery autosaves in $XDG_STATE_HOME, out of the config folder (#6612)
* test(linux): cover the XDG state recovery folder (#6612)
* Curves: Cmd/Ctrl-click on the image adds a point at the pixel's tone (part of #7325)
With Image > Adjustments > Curves open, a Cmd-click (Ctrl-click off the Mac) on
the image adds a point to the channel shown in the dialog, at the pixel's tone
and at the curve's current output there; Shift adds one point on each of Red,
Green and Blue. Only the press adds, an armed eyedropper still wins, and the
control channel's ui.pointer does the same with command: true.
* Curves: Cmd/Ctrl-click adds the point with the Hand tool too; Space and middle button still pan
The Hand tool took the Cmd-click press for a pan, so no point was added; the
press now counts before the Hand. On the control channel, ui.pointer with
space or the middle button pans as the mouse does instead of adding a point.
* Curves: the Cmd/Ctrl-click point's comment says where the curve can still change
* Type: right-click on committed text opens the type layer menu (#7324)
With the Type tool, a right-click only opened a menu while text was being
edited; over committed type nothing opened. The menu was keyed on the edit
session alone (`editing_text`).
Over committed type, a Type tool (not a Type Mask tool) now opens
TYPE_LAYER_MENU: the type edit menu's layer rows, without the edit rows
(Undo, Cut, Copy, Paste, Select All), which would act on the document, and
without Faux Bold/Italic, which act on the characters being edited. Type
outside the selected layers becomes the target first, as a click with the
Type tool makes it. The control channel's right button takes the same path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Type layer menu: the clicked type becomes the active layer in a multiselection (#7324)
The structural rows (Rasterize Type, Convert to Shape, Create Work Path, the
paragraph/point conversions) act on the active layer. When the right-clicked
type was already selected but another selected type layer was active, they
acted on that other layer. Make the clicked type the active layer while
keeping the selection (layer_multi_cmds::set_selection), so the formatting
rows still act on every selected type layer. Found by CodeRabbit on #7430.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
After an error on a device that still works (out of memory, validation), Retry GPU builds a new GPU canvas on the same render state with a fresh health flag, and the views go back to the GPU. After a loss or a failed GPU start it still schedules the retry for the next launch. The dialog says which. Two new strings, translated in all 20 catalogs.
When the window's own wgpu device was lost (a driver reset, e.g. after sleep), egui-wgpu could
not draw another frame: a frame with shapes panicked inside it ("Failed to create staging buffer
for index data", #1215, exit 101, unsaved work lost), and a frozen window's close button opened
an unsaved-changes prompt nobody could see (#3438). The #243 fallback only covered the canvas.
- DeviceHealth::device_lost (wgpu reported the loss, not a recorded fault) and ::probe (a
non-blocking poll that keeps running after a recorded fault).
- RecoveryStore::flush: finish queued autosaves and wait for them.
- gpu_status::check_window, at the end of every frame: once the window's device is lost the
frame draws nothing; the first time every unsaved document is written to crash recovery
(Services::flush_autosaves) and the app closes without asking. The next launch reopens them.
- The app watches the window device on the CPU canvas too; ui.gpu.simulateLoss {destroy: true}
destroys the real device to drive the path.
Verified on an RX 6900 XT, Vulkan and DX12: destroying the device in the running app exited 101
with the egui-wgpu panic before, and now exits 0 with the document recovered on the next launch.