Compare commits

...
1 Commits
Author SHA1 Message Date
6668684eef Save As and Export: Browse in WordCraft when the system dialog doesn't appear (#3535) (#3801)
Ported from #3801 by VoiceLessQ onto current main.

Re-resolved against main:
- file_browser.rs tests: rebuilt whole, keeping main's #3859 tests (browser
  above the dialog, Escape, dialog closing into the browser, modeless
  keyboard, no keytips) and the PR's #3535 export tests.
- print_page.rs: EXPORT_FORMATS now carries main's OpenDocument Text
  Template (*.ott) and Flat OpenDocument Text (*.fodt) in main's order;
  main's tests module kept.
- docs/control-protocol.md: ui.fileBrowser purpose "export" added to main's
  current row (keeps ui.displayServer).
- docs/ui-parity.md: File dialogs row merged with main's Linux display
  server row, #3955 history row kept, status line and revision history
  bumped.

Added a test that the ott and fodt export types get their extension and
write the file.

Co-Authored-By: VoiceLessQ <44067308+VoiceLessQ@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCVCCEo7u5wetHaJRU4
2026-11-06 12:53:57 +00:00
9 changed files with 262 additions and 34 deletions
+1
View File
@@ -144,6 +144,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
## Progress log
- 2026-11-06: Save As and Export (also the formats on the Print page) offer Browse in WordCraft… on Linux and the BSDs, as File › Open does, for when the system file dialog doesn't appear (#3535, from VoiceLessQ's #3801). Exporting in the built-in browser lists Export's formats as the types; the chosen type is the format and its extension. Readiness estimates unchanged.
- 2026-11-06: Windows print submissions run in separate helper processes (#5710). A helper acknowledges cancellation before it stops, so a driver stalled in a cancelled job cannot hold another job’s provider lock. A private temporary PDF and bounded settings messages cross the process boundary; normal shutdown reaps helpers and removes their inputs. The helper erases its plaintext inputs before driver work; a hard app crash during startup can still leave private temporary files. Jobs already sending still complete. Readiness estimates unchanged.
- 2026-11-06: File › Print: before sending starts, Cancel printing stops a waiting job, frees the Print page and keeps the settings; a successfully cancelled job is never sent. Cancellation remains available after the last document closes. Quitting cancels waiting jobs and joins jobs already sending (#5710). On Windows, winprint repeats the DEVMODE conversion after sending starts. A stall during that repeat prevents cancellation and can keep shutdown waiting, even before a native job exists. Readiness estimates unchanged.
- 2026-11-06: File › Print on Windows no longer waits on printer drivers to list printers (#5710). The list used to wait until each printer's driver said whether it prints on both sides, and a networked printer asleep in power save could keep it empty with Refresh unavailable; now discovery asks no driver (the Windows spooler still supplies the list), and printing still checks the job's options with the driver. The reporter's sleeping printer has not been retested yet. Readiness estimates unchanged.
+1 -1
View File
@@ -736,7 +736,7 @@ impl WordApp {
json!({"dir": dir})
}
// The built-in file browser (#576), for when the system's file dialog doesn't show up
// (desktop only): `purpose` is document (default), picture, recipients or save. A system
// (desktop only): `purpose` is document (default), picture, recipients, save or export. A system
// dialog still waiting for an answer is given up as cancelled.
#[cfg(not(target_arch = "wasm32"))]
"ui.fileBrowser" => {
+5
View File
@@ -689,6 +689,11 @@ fn save_as_page(app: &mut WordApp, ui: &mut Ui) {
if ui.button(egui::RichText::new(tl!("Browse…")).font(medium(14.0))).clicked() {
app.save_as_dialog();
}
// Linux and the BSDs (#3535): WordCraft's own browser when the system dialog doesn't appear.
#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
if ui.button(egui::RichText::new(tl!("Browse in WordCraft…")).font(medium(14.0))).clicked() {
let _ = app.run("ui.fileBrowser", json!({"purpose": "save"}));
}
// A Word template in the user templates folder, which File › New lists under Personal (#2852).
ui.add_space(8.0);
if ui
+71
View File
@@ -809,6 +809,77 @@ fn save_as_page_shows_browse_and_no_open_list() {
assert!(h.query_by_label("Recent").is_none(), "Save As has no list that opens documents");
}
/// #3467, #3535: a system dialog that never shows up doesn't leave Save As stuck: Browse in
/// WordCraft… gives it up and saves through WordCraft's own browser.
#[test]
#[cfg(all(unix, not(target_os = "macos")))]
fn save_as_saves_in_wordcraft_when_the_system_dialog_never_shows() {
// A host that takes every request and never answers, like a portal that shows nothing.
let senders: std::rc::Rc<std::cell::RefCell<Vec<std::sync::mpsc::Sender<crate::FileDialogAnswer>>>> = Default::default();
let keep = senders.clone();
let services = Services {
file_dialog: Some(Box::new(move |_| {
let (tx, rx) = std::sync::mpsc::channel();
keep.borrow_mut().push(tx);
rx
})),
..Default::default()
};
let dir = std::env::temp_dir().join(format!("wordcraft-backstage-builtin-save-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), services);
a.run("text.insert", json!({"text": "Saved past a silent portal"})).unwrap();
a.run("ui.backstage", json!({"value": true, "page": "saveAs"})).unwrap();
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|ui, app: &mut WordApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
a,
);
h.run_steps(4);
h.get_by_label("Browse…").click();
h.run_steps(4);
assert!(h.state().file_dialog.is_some(), "the system dialog was asked for and never answered");
h.get_by_label("Browse in WordCraft…").click();
h.run_steps(4);
let a = h.state_mut();
assert!(a.file_dialog.is_none(), "the silent system dialog is given up");
let b = a.file_browser.open.as_mut().expect("WordCraft's own browser is open");
b.navigate(dir.clone());
b.name = "kept".into();
let crate::file_browser::Step::Pick(path) = b.accept() else { panic!("picked") };
a.close_file_browser(Some(path));
let saved = dir.join("kept.docx");
assert_eq!(a.session.path.as_deref(), Some(saved.as_path()));
let back = wordcraft_engine::io::open_path(&saved).unwrap();
assert!(back.plain_text(wordcraft_doc::StoryRef::Body).contains("Saved past a silent portal"));
let _ = std::fs::remove_dir_all(&dir);
}
/// #3535: File › Export offers WordCraft's own browser as well, for its formats.
#[test]
#[cfg(all(unix, not(target_os = "macos")))]
fn export_page_offers_wordcrafts_own_browser() {
let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Services::default());
a.run("ui.backstage", json!({"value": true, "page": "export"})).unwrap();
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|ui, app: &mut WordApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
a,
);
h.run_steps(4);
h.get_by_label("Browse in WordCraft…").click();
h.run_steps(4);
let b = h.state().file_browser.open.as_ref().expect("WordCraft's own browser is open");
assert!(matches!(&b.after, crate::file_dialogs::AfterPick::Export { ext } if ext == "pdf"));
}
#[test]
fn backstage_save_navigates_to_save_as_when_unnamed() {
let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Services::default());
+137 -7
View File
@@ -12,6 +12,7 @@ use std::path::{Path, PathBuf};
use egui::{Align2, Rect, Sense, Ui, pos2, vec2};
use crate::file_dialogs::{AfterPick, FileDialogRequest, open_filters};
use crate::print_page::{EXPORT_FORMATS, export_file_ext};
use crate::theme::{Tokens, regular};
use crate::{WordApp, icons};
@@ -138,6 +139,8 @@ pub(crate) struct FileBrowser {
/// The file types, as (label, extensions).
filters: Vec<(String, Vec<String>)>,
filter: usize,
/// Exporting: the format behind each type, which `after` follows; empty otherwise.
exports: Vec<String>,
listing: Result<Listing, String>,
/// The highlighted entry.
selected: Option<PathBuf>,
@@ -158,6 +161,15 @@ impl FileBrowser {
(name.clone(), f)
}
};
// Exporting (#3535): File › Export's formats are the types, starting on the one asked for.
let (filters, filter, exports) = match &after {
AfterPick::Export { ext } => (
EXPORT_FORMATS.iter().map(|&(l, f)| (l.to_string(), vec![export_file_ext(f).to_string()])).collect(),
EXPORT_FORMATS.iter().position(|&(_, f)| f == ext.as_str()).unwrap_or(0),
EXPORT_FORMATS.iter().map(|&(_, f)| f.to_string()).collect(),
),
_ => (filters, 0, Vec::new()),
};
let mut b = FileBrowser {
req,
after,
@@ -166,7 +178,8 @@ impl FileBrowser {
location: String::new(),
name,
filters,
filter: 0,
filter,
exports,
listing: Ok(Listing::default()),
selected: None,
note: Note::None,
@@ -189,6 +202,18 @@ impl FileBrowser {
self.note = Note::None;
}
/// A type was chosen: when exporting it is the format, and the name's export extension follows it.
fn type_changed(&mut self) {
let Some(format) = self.exports.get(self.filter).cloned() else { return };
let name = Path::new(&self.name);
let known =
name.extension().and_then(|e| e.to_str()).is_some_and(|e| self.exports.iter().any(|f| export_file_ext(f).eq_ignore_ascii_case(e)));
if known {
self.name = name.with_extension(export_file_ext(&format)).to_string_lossy().into_owned();
}
self.after = AfterPick::Export { ext: format };
}
fn up(&mut self) {
if let Some(parent) = self.dir.parent().map(Path::to_path_buf) {
self.navigate(parent);
@@ -196,8 +221,8 @@ impl FileBrowser {
}
/// OK: a highlighted folder, or a typed folder, is opened; otherwise the typed name is the
/// pick. Save adds the chosen type's extension to a name without one and asks once more
/// before replacing a file; Open wants a file that exists.
/// pick. Save adds the chosen type's extension to a name without one (an export, to a name
/// without the export's) and asks once more before replacing a file; Open wants a file that exists.
pub(crate) fn accept(&mut self) -> Step {
if let Some(dir) = self.selected.take().filter(|p| p.is_dir()) {
self.navigate(dir);
@@ -218,7 +243,13 @@ impl FileBrowser {
Step::Stay
}
FileDialogRequest::Save { .. } => {
if path.extension().is_none()
if let AfterPick::Export { ext } = &self.after {
// The chosen type is the format, so its extension goes on any name without it.
let want = export_file_ext(ext);
if !path.extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case(want)) {
path.as_mut_os_string().push(format!(".{want}"));
}
} else if path.extension().is_none()
&& let Some(ext) = self.exts().first().filter(|e| e.as_str() != "*")
{
path.set_extension(ext);
@@ -273,10 +304,11 @@ impl WordApp {
self.file_browser.open = Some(Box::new(FileBrowser::new(req, after, start, unavailable)));
}
/// `ui.fileBrowser`: the built-in browser for `purpose` (document, picture, recipients, save),
/// even when a system dialog was asked for and never showed up (it is given up as cancelled).
/// `ui.fileBrowser`: the built-in browser for `purpose` (document, picture, recipients, save,
/// export), even when a system dialog was asked for and never showed up (it is given up as cancelled).
pub(crate) fn browse_builtin(&mut self, purpose: &str) {
self.give_up_file_dialog();
// An export that was waiting on the system dialog keeps its format.
let pending = self.give_up_file_dialog();
if self.file_browser.open.is_some() {
return;
}
@@ -287,6 +319,11 @@ impl WordApp {
FileDialogRequest::Save { name: self.default_save_name(), dir: None },
AfterPick::SaveAs { after: crate::file_dialogs::AfterSave::Nothing, document: self.session.document_id() },
),
"export" => {
let ext = pending.unwrap_or_else(|| "pdf".into());
let name = format!("{}.{}", self.title_stem(), export_file_ext(&ext));
(FileDialogRequest::Save { name, dir: None }, AfterPick::Export { ext })
}
_ => (FileDialogRequest::Open { purpose: "document".into() }, AfterPick::OpenDocument { document: self.session.document_id() }),
};
self.show_file_browser(req, after, false);
@@ -471,6 +508,7 @@ fn body(ui: &mut Ui, b: &mut FileBrowser, t: &Tokens) -> Option<Option<PathBuf>>
refilter = b.filter != before;
});
if refilter {
b.type_changed();
b.navigate(b.dir.clone());
}
ui.add_space(8.0);
@@ -731,6 +769,98 @@ mod tests {
assert!(crate::keytips::suppressed(&a, &ctx));
}
/// #3535: exporting offers File › Export's formats as the types; the chosen one is the format,
/// the name's extension follows it, and the export writes that format.
#[test]
fn export_types_choose_the_format_and_the_extension() {
let dir = scratch("export");
let mut a = WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default());
a.run("text.insert", json!({"text": "Exported in WordCraft"})).unwrap();
a.run("ui.fileBrowser", json!({"purpose": "export"})).unwrap();
let b = a.file_browser.open.as_mut().expect("the built-in browser is open");
assert_eq!(b.filters.len(), EXPORT_FORMATS.len());
assert_eq!(b.filters[b.filter].0, "PDF document (*.pdf)");
assert!(b.name.ends_with(".pdf"), "{}", b.name);
b.filter = b.filters.iter().position(|(l, _)| l == "Plain text (*.txt)").unwrap();
b.type_changed();
assert!(b.name.ends_with(".txt"), "{}", b.name);
assert!(matches!(&b.after, AfterPick::Export { ext } if ext == "txt"));
b.navigate(dir.clone());
// A name typed afterwards with another extension still gets the chosen format's.
b.name = "notes.docx".into();
let Step::Pick(path) = b.accept() else { panic!("picked") };
assert_eq!(path.file_name().unwrap(), "notes.docx.txt");
a.close_file_browser(Some(path.clone()));
assert!(std::fs::read_to_string(&path).unwrap().contains("Exported in WordCraft"));
// Word 2003 XML starts on its own type, though Word XML writes .xml too.
let req = FileDialogRequest::Save { name: "x.xml".into(), dir: None };
let b = FileBrowser::new(req, AfterPick::Export { ext: crate::print_page::EXPORT_WORD_2003.into() }, dir.clone(), false);
assert_eq!(b.filters[b.filter].0, "Word 2003 XML Document (*.xml)");
let _ = std::fs::remove_dir_all(&dir);
}
/// The OpenDocument template and flat-XML exports are types of their own: the name gets
/// their extension, and the export saves by it.
#[test]
fn export_types_include_the_opendocument_template_and_flat_xml() {
let dir = scratch("export_ott");
let mut a = WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default());
a.run("text.insert", json!({"text": "Template text"})).unwrap();
for (label, ext) in [("OpenDocument Text Template (*.ott)", "ott"), ("Flat OpenDocument Text (*.fodt)", "fodt")] {
let req = FileDialogRequest::Save { name: "x.pdf".into(), dir: None };
let mut b = FileBrowser::new(req, AfterPick::Export { ext: "pdf".into() }, dir.clone(), false);
b.filter = b.filters.iter().position(|(l, _)| l == label).unwrap();
b.type_changed();
assert_eq!(b.name, format!("x.{ext}"));
b.navigate(dir.clone());
let Step::Pick(path) = b.accept() else { panic!("picked") };
assert_eq!(path, dir.join(format!("x.{ext}")));
a.finish_pick(AfterPick::Export { ext: ext.into() }, Some(path.to_string_lossy().into_owned())).unwrap();
assert!(path.is_file(), "{ext} written");
}
let _ = std::fs::remove_dir_all(&dir);
}
/// An export waiting on a system dialog that never showed up keeps its format when Browse
/// in WordCraft… takes over, instead of starting over on PDF.
#[test]
fn browse_in_wordcraft_keeps_a_pending_exports_format() {
let services = Services {
file_dialog: Some(Box::new(|_req| {
let (tx, rx) = channel();
// The system dialog never answers.
std::mem::forget(tx);
rx
})),
..Default::default()
};
let mut a = WordApp::new(Session::new(wordcraft_doc::Document::new()), services);
a.export_dialog("rtf");
assert!(a.file_dialog.is_some(), "waiting on the system dialog");
a.run("ui.fileBrowser", json!({"purpose": "export"})).unwrap();
assert!(a.file_dialog.is_none(), "the system dialog is given up");
let b = a.file_browser.open.as_ref().expect("the built-in browser is open");
assert!(matches!(&b.after, AfterPick::Export { ext } if ext == "rtf"));
assert_eq!(b.filters[b.filter].0, "Rich Text Format (*.rtf)");
assert!(b.name.ends_with(".rtf"), "{}", b.name);
}
/// Word XML chosen as the export type wins over a Word 2003 XML file already at that path,
/// whose format a save by path alone would keep.
#[test]
fn exporting_word_xml_over_a_word_2003_file_writes_word_xml() {
let dir = scratch("export_xml");
let path = dir.join("letter.xml").to_string_lossy().into_owned();
let mut a = WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default());
a.run("text.insert", json!({"text": "Dear reader"})).unwrap();
a.finish_pick(AfterPick::Export { ext: crate::print_page::EXPORT_WORD_2003.into() }, Some(path.clone())).unwrap();
assert!(std::fs::read_to_string(&path).unwrap().contains("w:wordDocument"));
a.finish_pick(AfterPick::Export { ext: "xml".into() }, Some(path.clone())).unwrap();
let xml = std::fs::read_to_string(&path).unwrap();
assert!(xml.contains("pkg:package") && !xml.contains("w:wordDocument"), "Word XML, not Word 2003 XML");
let _ = std::fs::remove_dir_all(&dir);
}
/// #3037: an empty folder (or a type filter that matches nothing) says so instead of a blank
/// box; a matching file replaces the hint.
#[test]
+11 -7
View File
@@ -171,12 +171,14 @@ impl WordApp {
}
}
/// Treat the system dialog the host is showing as cancelled, e.g. one that never showed up.
/// Treat the system dialog the host is showing as cancelled, e.g. one that never showed up;
/// returns the format of the export it was for, if any.
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn give_up_file_dialog(&mut self) {
if let Some(d) = self.file_dialog.take() {
let _ = self.finish_pick(d.after, None);
}
pub(crate) fn give_up_file_dialog(&mut self) -> Option<String> {
let d = self.file_dialog.take()?;
let export = if let AfterPick::Export { ext } = &d.after { Some(ext.clone()) } else { None };
let _ = self.finish_pick(d.after, None);
export
}
/// The host couldn't show a system dialog (#576): ask in the built-in browser instead, which
@@ -297,6 +299,9 @@ impl WordApp {
self.run("file.exportPng", json!({"path": path}))
} else if ext == crate::print_page::EXPORT_WORD_2003 {
self.run("file.saveAs", json!({"path": path, "format": ext}))
} else if ext == "xml" {
// Word XML even over a Word 2003 XML file, which a save by path would keep.
self.run("file.saveAs", json!({"path": path, "format": wordcraft_engine::cmd::file::FORMAT_FLAT_OPC}))
} else {
self.run("file.saveAs", json!({"path": path}))
};
@@ -433,8 +438,7 @@ impl WordApp {
/// File › Export: ask where to save a copy in this format, then write it.
pub(crate) fn export_dialog(&mut self, ext: &str) {
let file_ext = if ext == crate::print_page::EXPORT_WORD_2003 { "xml" } else { ext };
let name = format!("{}.{file_ext}", self.title_stem());
let name = format!("{}.{}", self.title_stem(), crate::print_page::export_file_ext(ext));
let _ = self.ask_file(FileDialogRequest::Save { name, dir: None }, AfterPick::Export { ext: ext.to_string() });
}
+31 -15
View File
@@ -454,28 +454,44 @@ fn preview(app: &mut WordApp, ui: &mut Ui, lay: &DocLayout) {
/// The export "format" of a Word 2003 XML Document: an `.xml` file saved with `format: "wordml2003"`.
pub(crate) const EXPORT_WORD_2003: &str = "wordml2003";
/// The "save a copy" formats as (label, format), also the built-in browser's types when exporting.
pub(crate) const EXPORT_FORMATS: &[(&str, &str)] = &[
("PDF document (*.pdf)", "pdf"),
("Word document (*.docx)", "docx"),
("Word XML Document (*.xml)", "xml"),
("Word 2003 XML Document (*.xml)", EXPORT_WORD_2003),
("OpenDocument Text (*.odt)", "odt"),
("OpenDocument Text Template (*.ott)", "ott"),
("Flat OpenDocument Text (*.fodt)", "fodt"),
("Rich Text Format (*.rtf)", "rtf"),
("Web page (*.html)", "html"),
("Markdown (*.md)", "md"),
("LaTeX (*.tex)", "tex"),
("Plain text (*.txt)", "txt"),
("Page image (*.png)", "png"),
];
/// The file extension an export format writes.
pub(crate) fn export_file_ext(format: &str) -> &str {
if format == EXPORT_WORD_2003 { "xml" } else { format }
}
/// The "save a copy" formats (File › Export, and below the settings on the Print page).
pub fn export_formats(app: &mut WordApp, ui: &mut Ui) {
for (label, ext) in [
("PDF document (*.pdf)", "pdf"),
("Word document (*.docx)", "docx"),
("Word XML Document (*.xml)", "xml"),
("Word 2003 XML Document (*.xml)", EXPORT_WORD_2003),
("OpenDocument Text (*.odt)", "odt"),
("OpenDocument Text Template (*.ott)", "ott"),
("Flat OpenDocument Text (*.fodt)", "fodt"),
("Rich Text Format (*.rtf)", "rtf"),
("Web page (*.html)", "html"),
("Markdown (*.md)", "md"),
("LaTeX (*.tex)", "tex"),
("Plain text (*.txt)", "txt"),
("Page image (*.png)", "png"),
] {
for &(label, ext) in EXPORT_FORMATS {
if ui.add(egui::Button::new(egui::RichText::new(tl!(label)).font(medium(13.5))).min_size(vec2(320.0, 34.0))).clicked() {
app.export_dialog(ext);
}
ui.add_space(4.0);
}
// Linux and the BSDs (#3535): WordCraft's own browser when the system dialog doesn't appear.
#[cfg(all(unix, not(target_os = "macos"), not(target_arch = "wasm32")))]
{
ui.add_space(8.0);
if ui.add(egui::Button::new(egui::RichText::new(tl!("Browse in WordCraft…")).font(medium(13.5))).min_size(vec2(320.0, 34.0))).clicked() {
let _ = app.run("ui.fileBrowser", json!({"purpose": "export"}));
}
}
}
#[cfg(test)]
+2 -2
View File
@@ -54,8 +54,8 @@ and set `WORDCRAFT_CONTROL_KEY` to that app's key.
| Method | Params | Result |
|---|---|---|
| `engine.execute` | `command`, `params`, `doc?` | the command's result (any id from `engine.commands`, including UI commands `ui.tab`, `ui.dialog` (`{"name": dialog, "part"?: table style part}`; `part` opens New / Modify Table Style (`newTableStyle` / `modifyTableStyle`) on that part: `wholeTable`, `headerRow`, `firstColumn`, `bandedRows`, `nwCell`… as `table.modifyStyle` names them), `ui.backstage`, `ui.zotero.*` (see `docs/zotero.md`), `ui.language` (`{"value": "auto"\|"en"\|"zh-hans"\|"zh-hant"\|"ja"\|"pt-br"\|"es"\|"uk"\|"sr"\|"sr-latn"\|"et"\|"tr"\|"ru"\|"pl"\|"de"\|"it"\|"cs"\|"hu"\|"fr"}`), `ui.theme` (`{"value": "system"\|"light"\|"dark"}`; `system` follows the OS appearance, light when it reports none), `ui.interfaceSize` (`{"value": "auto"}` or `{"percent": 50–300}`, clamped; no value reads it; `auto` keeps the system display scale, a percentage is absolute, so 100 is one screen pixel per point (#822); the result's `percent` is the size drawn at and `auto` whether it follows the system; document zoom stays `view.zoom`), `ui.animations` (`{"value": "system"\|"on"\|"off"}`; no value reads it; `system` follows the system's animation setting, which the desktop app reads on Windows, macOS, Linux and the BSDs and the web app from `prefers-reduced-motion`, and means on where the system doesn't say; the result's `enabled` is what the setting comes to and `system` what the system reports, `null` when it doesn't say), `ui.fileBrowser` (desktop: `{"purpose": "document"\|"picture"\|"recipients"\|"save"}` opens WordCraft's own file browser, for when the system file dialog doesn't show up, #576), `ui.saveAsTemplate` (File › Save As › Save as Word Template: the Save dialog for a `.dotx`, starting in the user templates folder; the result's `dir` is that folder, `null` on the web, #2852), `ui.caretAnimation` (`{"value": true\|false}`; no value reads it; off, the caret jumps and blinks on and off instead of gliding and fading), `ui.startScreen` (`{"value": true\|false}`; no value reads it; on, the default, the desktop and web apps start on File › Home when launched with nothing to open, #766), `ui.displayServer` (`{"value": "auto"\|"wayland"\|"x11"}`; no value reads it; File › Options › Advanced › Linux display server, which the desktop app reads on Linux and the BSDs at its next launch; `WORDCRAFT_WAYLAND` overrides it, #2184), `ui.alignmentGuides` (`{"value": true\|false}`; no value reads it; on, the default, moving a picture or shape snaps it to the page, the margins and other objects, with guide lines while dragging; Alt moves freely), `ui.quickAccessToolbar` (`{"value": true\|false}`; no value toggles it; false takes the Quick Access Toolbar out of the title bar, as its menu's Hide Quick Access Toolbar does, and true brings it back, #1945), `ui.focusRibbon` (`{"value": true\|false}`; no value toggles it; in Focus, true drops the ribbon down over the page, as the bar's ⋯ does, and false puts it away, as a click on the page does; outside Focus it stays put away, #1678), `ui.contextMenu` (opens the page's context menu below the caret, as Shift+F10 does), `ui.paneDock` (`{"pane": "navigation"\|"clipboard"\|"styles"\|"styleInspector"\|"accessibility"\|"altText"\|"formatShape"\|"restrictEditing"\|"comments"\|"smartArtText", "dock"?: "left"\|"right"\|"float", "before"?: pane\|null}`; no `dock` reads it; docks or floats a task pane as its menu does, kept between runs; Styles is `view.stylesPaneDock`, #1756; `before` puts the pane directly before that pane in the order, outside it on their edge, as a drop over a docked pane's outer third does, and `null` after every pane; the order is kept between runs)…) |
| `engine.execute` | `command`, `params`, `doc?` | the command's result (any id from `engine.commands`, including UI commands `ui.tab`, `ui.dialog` (`{"name": dialog, "part"?: table style part}`; `part` opens New / Modify Table Style (`newTableStyle` / `modifyTableStyle`) on that part: `wholeTable`, `headerRow`, `firstColumn`, `bandedRows`, `nwCell`… as `table.modifyStyle` names them), `ui.backstage`, `ui.zotero.*` (see `docs/zotero.md`), `ui.language` (`{"value": "auto"\|"en"\|"zh-hans"\|"zh-hant"\|"ja"\|"pt-br"\|"es"\|"uk"\|"sr"\|"sr-latn"\|"et"\|"tr"\|"ru"\|"pl"\|"de"\|"it"\|"cs"\|"hu"\|"fr"}`), `ui.theme` (`{"value": "system"\|"light"\|"dark"}`; `system` follows the OS appearance, light when it reports none), `ui.interfaceSize` (`{"value": "auto"}` or `{"percent": 50–300}`, clamped; no value reads it; `auto` keeps the system display scale, a percentage is absolute, so 100 is one screen pixel per point (#822); the result's `percent` is the size drawn at and `auto` whether it follows the system; document zoom stays `view.zoom`), `ui.animations` (`{"value": "system"\|"on"\|"off"}`; no value reads it; `system` follows the system's animation setting, which the desktop app reads on Windows, macOS, Linux and the BSDs and the web app from `prefers-reduced-motion`, and means on where the system doesn't say; the result's `enabled` is what the setting comes to and `system` what the system reports, `null` when it doesn't say), `ui.fileBrowser` (desktop: `{"purpose": "document"\|"picture"\|"recipients"\|"save"}` opens WordCraft's own file browser, for when the system file dialog doesn't show up, #576), `ui.saveAsTemplate` (File › Save As › Save as Word Template: the Save dialog for a `.dotx`, starting in the user templates folder; the result's `dir` is that folder, `null` on the web, #2852), `ui.caretAnimation` (`{"value": true\|false}`; no value reads it; off, the caret jumps and blinks on and off instead of gliding and fading), `ui.startScreen` (`{"value": true\|false}`; no value reads it; on, the default, the desktop and web apps start on File › Home when launched with nothing to open, #766), `ui.alignmentGuides` (`{"value": true\|false}`; no value reads it; on, the default, moving a picture or shape snaps it to the page, the margins and other objects, with guide lines while dragging; Alt moves freely), `ui.quickAccessToolbar` (`{"value": true\|false}`; no value toggles it; false takes the Quick Access Toolbar out of the title bar, as its menu's Hide Quick Access Toolbar does, and true brings it back, #1945), `ui.statusCount` (`{"value": "words"\|"characters"\|"charactersNoSpaces"}`; no value reads it; what the status bar's count shows — words (the default), characters with spaces, or characters without spaces — kept between runs, #2797), `ui.focusRibbon` (`{"value": true\|false}`; no value toggles it; in Focus, true drops the ribbon down over the page, as the bar's ⋯ does, and false puts it away, as a click on the page does; outside Focus it stays put away, #1678), `ui.contextMenu` (opens the page's context menu below the caret, as Shift+F10 does), `ui.paneDock` (`{"pane": "navigation"\|"clipboard"\|"styles"\|"styleInspector"\|"accessibility"\|"altText"\|"formatShape"\|"restrictEditing"\|"comments"\|"smartArtText", "dock"?: "left"\|"right"\|"float", "before"?: pane\|null}`; no `dock` reads it; docks or floats a task pane as its menu does, kept between runs; Styles is `view.stylesPaneDock`, #1756; `before` puts the pane directly before that pane in the order, outside it on their edge, as a drop over a docked pane's outer third does, and `null` after every pane; the order is kept between runs)…) |
| `engine.execute` | `command`, `params`, `doc?` | the command's result (any id from `engine.commands`, including UI commands `ui.tab`, `ui.dialog` (`{"name": dialog, "part"?: table style part}`; `part` opens New / Modify Table Style (`newTableStyle` / `modifyTableStyle`) on that part: `wholeTable`, `headerRow`, `firstColumn`, `bandedRows`, `nwCell`… as `table.modifyStyle` names them), `ui.backstage`, `ui.zotero.*` (see `docs/zotero.md`), `ui.language` (`{"value": "auto"\|"en"\|"zh-hans"\|"zh-hant"\|"ja"\|"pt-br"\|"es"\|"uk"\|"sr"\|"sr-latn"\|"et"\|"tr"\|"ru"\|"pl"\|"de"\|"it"\|"cs"\|"hu"\|"fr"}`), `ui.theme` (`{"value": "system"\|"light"\|"dark"}`; `system` follows the OS appearance, light when it reports none), `ui.interfaceSize` (`{"value": "auto"}` or `{"percent": 50–300}`, clamped; no value reads it; `auto` keeps the system display scale, a percentage is absolute, so 100 is one screen pixel per point (#822); the result's `percent` is the size drawn at and `auto` whether it follows the system; document zoom stays `view.zoom`), `ui.animations` (`{"value": "system"\|"on"\|"off"}`; no value reads it; `system` follows the system's animation setting, which the desktop app reads on Windows, macOS, Linux and the BSDs and the web app from `prefers-reduced-motion`, and means on where the system doesn't say; the result's `enabled` is what the setting comes to and `system` what the system reports, `null` when it doesn't say), `ui.fileBrowser` (desktop: `{"purpose": "document"\|"picture"\|"recipients"\|"save"\|"export"}` opens WordCraft's own file browser, for when the system file dialog doesn't show up, #576; `export` offers File › Export's formats as its types, #3535), `ui.saveAsTemplate` (File › Save As › Save as Word Template: the Save dialog for a `.dotx`, starting in the user templates folder; the result's `dir` is that folder, `null` on the web, #2852), `ui.caretAnimation` (`{"value": true\|false}`; no value reads it; off, the caret jumps and blinks on and off instead of gliding and fading), `ui.startScreen` (`{"value": true\|false}`; no value reads it; on, the default, the desktop and web apps start on File › Home when launched with nothing to open, #766), `ui.displayServer` (`{"value": "auto"\|"wayland"\|"x11"}`; no value reads it; File › Options › Advanced › Linux display server, which the desktop app reads on Linux and the BSDs at its next launch; `WORDCRAFT_WAYLAND` overrides it, #2184), `ui.alignmentGuides` (`{"value": true\|false}`; no value reads it; on, the default, moving a picture or shape snaps it to the page, the margins and other objects, with guide lines while dragging; Alt moves freely), `ui.quickAccessToolbar` (`{"value": true\|false}`; no value toggles it; false takes the Quick Access Toolbar out of the title bar, as its menu's Hide Quick Access Toolbar does, and true brings it back, #1945), `ui.focusRibbon` (`{"value": true\|false}`; no value toggles it; in Focus, true drops the ribbon down over the page, as the bar's ⋯ does, and false puts it away, as a click on the page does; outside Focus it stays put away, #1678), `ui.contextMenu` (opens the page's context menu below the caret, as Shift+F10 does), `ui.paneDock` (`{"pane": "navigation"\|"clipboard"\|"styles"\|"styleInspector"\|"accessibility"\|"altText"\|"formatShape"\|"restrictEditing"\|"comments"\|"smartArtText", "dock"?: "left"\|"right"\|"float", "before"?: pane\|null}`; no `dock` reads it; docks or floats a task pane as its menu does, kept between runs; Styles is `view.stylesPaneDock`, #1756; `before` puts the pane directly before that pane in the order, outside it on their edge, as a drop over a docked pane's outer third does, and `null` after every pane; the order is kept between runs)…) |
| `engine.execute` | `command`, `params`, `doc?` | the command's result (any id from `engine.commands`, including UI commands `ui.tab`, `ui.dialog` (`{"name": dialog, "part"?: table style part}`; `part` opens New / Modify Table Style (`newTableStyle` / `modifyTableStyle`) on that part: `wholeTable`, `headerRow`, `firstColumn`, `bandedRows`, `nwCell`… as `table.modifyStyle` names them), `ui.backstage`, `ui.zotero.*` (see `docs/zotero.md`), `ui.language` (`{"value": "auto"\|"en"\|"zh-hans"\|"zh-hant"\|"ja"\|"pt-br"\|"es"\|"uk"\|"sr"\|"sr-latn"\|"et"\|"tr"\|"ru"\|"pl"\|"de"\|"it"\|"cs"\|"hu"\|"fr"}`), `ui.theme` (`{"value": "system"\|"light"\|"dark"}`; `system` follows the OS appearance, light when it reports none), `ui.interfaceSize` (`{"value": "auto"}` or `{"percent": 50–300}`, clamped; no value reads it; `auto` keeps the system display scale, a percentage is absolute, so 100 is one screen pixel per point (#822); the result's `percent` is the size drawn at and `auto` whether it follows the system; document zoom stays `view.zoom`), `ui.animations` (`{"value": "system"\|"on"\|"off"}`; no value reads it; `system` follows the system's animation setting, which the desktop app reads on Windows, macOS, Linux and the BSDs and the web app from `prefers-reduced-motion`, and means on where the system doesn't say; the result's `enabled` is what the setting comes to and `system` what the system reports, `null` when it doesn't say), `ui.fileBrowser` (desktop: `{"purpose": "document"\|"picture"\|"recipients"\|"save"\|"export"}` opens WordCraft's own file browser, for when the system file dialog doesn't show up, #576; `export` offers File › Export's formats as its types, #3535), `ui.saveAsTemplate` (File › Save As › Save as Word Template: the Save dialog for a `.dotx`, starting in the user templates folder; the result's `dir` is that folder, `null` on the web, #2852), `ui.caretAnimation` (`{"value": true\|false}`; no value reads it; off, the caret jumps and blinks on and off instead of gliding and fading), `ui.startScreen` (`{"value": true\|false}`; no value reads it; on, the default, the desktop and web apps start on File › Home when launched with nothing to open, #766), `ui.alignmentGuides` (`{"value": true\|false}`; no value reads it; on, the default, moving a picture or shape snaps it to the page, the margins and other objects, with guide lines while dragging; Alt moves freely), `ui.quickAccessToolbar` (`{"value": true\|false}`; no value toggles it; false takes the Quick Access Toolbar out of the title bar, as its menu's Hide Quick Access Toolbar does, and true brings it back, #1945), `ui.statusCount` (`{"value": "words"\|"characters"\|"charactersNoSpaces"}`; no value reads it; what the status bar's count shows — words (the default), characters with spaces, or characters without spaces — kept between runs, #2797), `ui.focusRibbon` (`{"value": true\|false}`; no value toggles it; in Focus, true drops the ribbon down over the page, as the bar's ⋯ does, and false puts it away, as a click on the page does; outside Focus it stays put away, #1678), `ui.contextMenu` (opens the page's context menu below the caret, as Shift+F10 does), `ui.paneDock` (`{"pane": "navigation"\|"clipboard"\|"styles"\|"styleInspector"\|"accessibility"\|"altText"\|"formatShape"\|"restrictEditing"\|"comments"\|"smartArtText", "dock"?: "left"\|"right"\|"float", "before"?: pane\|null}`; no `dock` reads it; docks or floats a task pane as its menu does, kept between runs; Styles is `view.stylesPaneDock`, #1756; `before` puts the pane directly before that pane in the order, outside it on their edge, as a drop over a docked pane's outer third does, and `null` after every pane; the order is kept between runs)…) |
| `engine.commands` | — | every command: id, label, location, shortcut, params, enabled |
| `document.inspect` | `text?`, `doc?` | 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; `document`: the active document's id; `documents`: the open documents (`id`, `title`, `path`, `dirty`, `active`) |
+3 -2
View File
File diff suppressed because one or more lines are too long