fix(plot): never fail silently on PDF export; add EXPORTPDF <path> (#369)

Export to PDF could end in complete silence: the flow hands off to an
async rfd save dialog, and when that future resolves to None the app
mapped it straight to Task::none(). None does not only mean the user
cancelled — on Linux the XDG portal SaveFile call can fail outright
(rfd then tries a zenity fallback, which may not be installed) and the
future still just resolves to None. rfd reports those failures only
through the `log` crate, and logging is opt-in (--log / RUST_LOG), so
the reporter saw exactly what #369 describes: no dialog, no error, no
console output, no file.

- EXPORT / EXPORTPDF now accept an optional path argument that exports
  straight to the file with no dialog — both the workaround for systems
  where the native save dialog cannot open and a headless-testable
  entry point. A relative path lands next to the drawing; a missing
  extension gets .pdf.
- The dialog-None arms (PlotExportPath / PlotWindowExportPath) now push
  a command-line message pointing at EXPORTPDF <path> instead of doing
  nothing.
- The success message prints the full output path, not just the file
  name, so users can find the PDF.

Verified end to end on the real binary via --serve: LINE + CIRCLE then
EXPORTPDF <path> writes a valid PDF with the expected geometry. Both
new tests fail on the previous code.

Fixes #369

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Griffin 2026-07-14 13:38:16 -07:00
commit d2af4c0b34
5 changed files with 116 additions and 11 deletions

View file

@ -365,8 +365,40 @@ impl OpenCADStudio {
"PLOT" | "PRINT" => {
return Some(Task::done(Message::PlotDialogOpen));
}
"EXPORT" | "EXPORTPDF" => {
return Some(Task::done(Message::PlotExport));
// With no argument these open the save dialog; with a path they
// export straight to it. The direct form is also the workaround for
// systems where the native save dialog cannot open at all (broken
// XDG portal / missing zenity on Linux) — there the dialog future
// resolves to None and no export would happen. (#369)
cmd if cmd == "EXPORT"
|| cmd == "EXPORTPDF"
|| cmd.starts_with("EXPORT ")
|| cmd.starts_with("EXPORTPDF ") =>
{
// Strip the keyword actually typed — EXPORTPDF first, since
// EXPORT is its prefix (see #295 for the failure mode).
let arg = cmd
.strip_prefix("EXPORTPDF")
.or_else(|| cmd.strip_prefix("EXPORT"))
.unwrap_or("")
.trim()
.trim_matches('"');
if arg.is_empty() {
return Some(Task::done(Message::PlotExport));
}
let mut path = std::path::PathBuf::from(arg);
if path.extension().is_none() {
path.set_extension("pdf");
}
// A bare filename lands next to the drawing when it has a home.
if path.is_relative() {
if let Some(dir) =
self.tabs[i].current_path.as_deref().and_then(|p| p.parent())
{
path = dir.join(path);
}
}
return Some(self.on_plot_export_path_some(path));
}
// PLOTSTYLE — load or clear CTB/STB plot style table
cmd if cmd == "PLOTSTYLE" || cmd.starts_with("PLOTSTYLE ") => {