diff --git a/ROADMAP.md b/ROADMAP.md index b203a3bef..3d5e9cda6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (Behind Text pictures and shapes can be dragged across overlaid text, in the body and headers, without taking ordinary text clicks; previously trivial: progress log: Styles gallery fallback for DOCX files without qFormat, #1238; previously trivial: progress log: Insert › Bookmark deletes and goes to bookmarks, #1236; previously trivial: progress log: an option to show only the selected text's comment, #1055; previously trivial: #778 mail-merge recipient filter/sort, .xls and legacy encodings; previously trivial: progress log: clicking in a comment's text moves its caret, #1013; previously trivial: progress log: a press on the ribbon no longer scrolls the document, #1019; previously trivial: title rename and account settings; previously trivial: progress log: Down and Up no longer skip a table, #1007; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (Behind Text pictures and shapes can be dragged across overlaid text, in the body and headers, without taking ordinary text clicks; previously trivial: progress log: `--control` checks its port, #1426; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 > **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below. @@ -154,6 +154,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | 2026-10-13 | Replace All respects locked content controls with Track Changes on or off; it refuses the entire batch before editing when any match would change locked content or remove a control locked against deletion | | 2026-10-13 | Windows Magnifier follows the caret onto a new line: after Enter or Shift+Enter the view moves to the empty line at once, instead of staying on the line above until two more letters are typed (#1119) | | 2026-10-13 | A window too big for its screen shrinks to fit when it opens (#1215): on a laptop screen the first window reached under the taskbar or past the screen edge, with the end of the scroll bar, so dragging the scroll bar stopped where the pointer met the edge. A window saved that size is fitted on its next launch too | +| 2026-10-13 | `wordcraft --control` checks its port (#1426): a missing port, one outside 1–65535 or one that isn't a number prints an error naming it and exits with status 2, instead of starting without the control channel (and dropping `WORDCRAFT_CONTROL_PORT`); a flag right after it, such as `--sample` or `--version`, is read as a flag rather than as the port | | 2026-10-12 | Text boxes from older Word files (VML) keep their internal margins, Resize shape to fit text and vertical alignment, so letterhead address blocks and form headers sit where Word puts them instead of 7 pt in and 4 pt down. Hidden drawings stay hidden (they were drawn as empty boxes) and are kept on save | | 2026-10-13 | Word's privacy options are kept and honoured when saving .docx (#1062): with Remove personal information, comments and tracked changes are saved by "Author" and the file has no creator or last-modified-by; with Remove date and time, they are saved without dates. The open document keeps the real names and dates; `file.privacyOptions` sets both | | 2026-10-13 | A selected comment balloon's Delete, Reply and Resolve buttons work again (#1085): pressing them no longer counts as a click on the page that closes the balloon first, so a new empty comment, or any other, can be deleted from its balloon | @@ -375,6 +376,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | 2026-10-13 | trivial | Replace All checks content-control locks before editing; a refused batch leaves the document and undo history unchanged; existing deletion-lock rules reused, with tracked/untracked regression coverage | | 2026-10-13 | trivial | Progress log: Windows Magnifier follows the caret onto a new line (#1119) | | 2026-10-13 | trivial | Progress log: a window too big for its screen shrinks to fit (#1215) | +| 2026-10-13 | trivial | Progress log: `--control` checks its port (#1426) | | 2026-10-12 | trivial | Progress log: VML text box margins, fit and alignment; hidden drawings | | 2026-10-13 | trivial | Progress log: comment balloon buttons work, empty comments can be deleted (#1085) | | 2026-10-12 | trivial | Progress log: sample-text formulas on Enter (#1067) | diff --git a/apps/wordcraft/src/main.rs b/apps/wordcraft/src/main.rs index f6d78a9a0..e03b12465 100644 --- a/apps/wordcraft/src/main.rs +++ b/apps/wordcraft/src/main.rs @@ -289,10 +289,18 @@ fn main() -> eframe::Result { let mut control_port: Option = std::env::var("WORDCRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok()); let mut files = Vec::new(); let mut sample = false; - let mut args = std::env::args().skip(1); + let mut bad_control: Option> = None; + let mut args = std::env::args().skip(1).peekable(); while let Some(a) = args.next() { match a.as_str() { - "--control" => control_port = args.next().and_then(|p| p.parse().ok()), + // A flag after `--control` stays a flag; a bad or missing port is an error (#1426). + "--control" => { + let port = args.next_if(|p| p.strip_prefix('-').is_none_or(|n| n.starts_with(|c: char| c.is_ascii_digit()))); + match port.as_deref().and_then(|p| p.parse::().ok()) { + Some(p) => control_port = Some(p.get()), + None => bad_control = Some(port), + } + } "--sample" => sample = true, "--version" => { println!("wordcraft {} ({})", env!("CARGO_PKG_VERSION"), build_sha()); @@ -306,6 +314,12 @@ fn main() -> eframe::Result { _ => files.push(a), } } + // After the loop, so `--version` later on the line still wins. Unlike `eprintln!`, never panics. + if let Some(port) = bad_control { + let got = port.map(|p| format!(", got {p:?}")).unwrap_or_default(); + let _ = std::io::Write::write_fmt(&mut std::io::stderr(), format_args!("wordcraft: --control needs a port number from 1 to 65535{got}\n")); + std::process::exit(2); + } // The log file lives in the settings directory, next to the preferences; opened after the // arguments, so `--version` leaves no file behind. Records logged until now are written to it // first. Runs without preferences (agents' test runs) log to standard error only, so they diff --git a/apps/wordcraft/tests/control_arg.rs b/apps/wordcraft/tests/control_arg.rs new file mode 100644 index 000000000..8e8f6a2a4 --- /dev/null +++ b/apps/wordcraft/tests/control_arg.rs @@ -0,0 +1,58 @@ +//! `wordcraft --control` checks its port before the window opens (#1426). + +use std::process::{Command, Output, Stdio}; +use std::time::{Duration, Instant}; + +fn app(args: &[&str]) -> Output { + let mut child = Command::new(env!("CARGO_BIN_EXE_wordcraft")) + .args(args) + // With no display, an app that tries to open a window fails at once instead. + .env_remove("DISPLAY") + .env_remove("WAYLAND_DISPLAY") + .env_remove("WAYLAND_SOCKET") + // A bad `--control` must not fall back to this port either. + .env("WORDCRAFT_CONTROL_PORT", "7981") + .env("WORDCRAFT_NO_PREFS", "1") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let deadline = Instant::now() + Duration::from_secs(30); + while child.try_wait().unwrap().is_none() { + if Instant::now() > deadline { + let _ = child.kill(); + panic!("wordcraft {args:?} was still running after 30 s: it started the app"); + } + std::thread::sleep(Duration::from_millis(50)); + } + child.wait_with_output().unwrap() +} + +#[test] +fn a_bad_port_is_an_error_naming_it() { + let cases: [(&[&str], &str); 8] = [ + (&["--control", "-99999999999999999999"], ", got \"-99999999999999999999\""), + (&["--control", "abc"], ", got \"abc\""), + (&["--control", "79811"], ", got \"79811\""), + (&["--control", "0"], ", got \"0\""), + (&["--control", "-5"], ", got \"-5\""), + (&["--control", "abc", "--control", "7981"], ", got \"abc\""), + (&["--control"], ""), + (&["--control", "--sample"], ""), + ]; + for (args, got) in cases { + let out = app(args); + let stderr = String::from_utf8_lossy(&out.stderr); + assert_eq!(out.status.code(), Some(2), "{args:?}: stderr: {stderr}"); + assert_eq!(stderr, format!("wordcraft: --control needs a port number from 1 to 65535{got}\n"), "{args:?}"); + } +} + +#[test] +fn a_flag_after_control_is_read_as_a_flag() { + let out = app(&["--control", "--version"]); + let (stdout, stderr) = (String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr)); + assert!(out.status.success(), "status {:?}, stderr: {stderr}", out.status); + assert!(stdout.starts_with("wordcraft "), "stdout: {stdout}"); +} diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 422f40a76..9104d17f0 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -1,6 +1,7 @@ # Control protocol Start the app with a control port: `wordcraft --control 7981` (or `WORDCRAFT_CONTROL_PORT=7981`). +A missing or invalid port after `--control` (not 1–65535) prints an error and exits with status 2. It listens on `127.0.0.1` only. Send one JSON object per line, with the window's key (see [Keys](#keys)); each gets one reply line: