mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-19 02:10:15 -05:00
* --help and -h print the usage for the app and every CLI command (#968) wordcraft --help / -h printed nothing useful: the app tried to open a file named --help and started. wordcraft-cli <command> --help failed with "unknown option --help" for every command, and -h was taken as a file name or ignored. The app now prints its options and returns before the window opens. The CLI checks for --help / -h after any known command before options are validated or files and folders opened, and prints the shared usage. Tests run both binaries for each command and flag; the app's test runs without a display, so a regression fails instead of opening a window. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * #968: test bare --help and -h on wordcraft-cli Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
5 files changed
+144
-2
No files matched your search
+3
-1
@@ -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 (progress log: All Caps text in PDFs, #1473; previously trivial: progress log: ODT tracked changes, gap 19; previously trivial: progress log: RTF tracked changes, gap 19; previously trivial: progress log: 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 (progress log: All Caps text in PDFs, #1473; previously trivial: progress log: `--help` for the app and every CLI subcommand, #968; 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.
|
||||
|
||||
@@ -184,6 +184,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
| 2026-10-14 | Table deletes, Change Case and Unlink Fields respect Track Changes and locked content controls (#1329): with tracking on, Delete Table, Rows, Columns and Cells mark the cells' text deleted and keep the table (like Word), and Change Case is a tracked deletion and insertion, so Reject All restores the original; all of them refuse to delete a control locked against deletion or change content locked against editing |
|
||||
| 2026-10-14 | Draw: Ink to Shape recognises arrows. A line and a V-shaped head drawn in one stroke (shaft first or head first, any direction) becomes a straight arrow connector with the head at the tip, in the stroke's colour and width; a lone V, a line with one barb and scribbles stay as they are. Our own recogniser: Word for Mac has no Ink to Shape to compare. Two-stroke arrows and arrows with a closed triangle head not yet. |
|
||||
| 2026-10-12 | RTF import now uses the RTF spec's defaults where a file names none (#902): no space before or after paragraphs, single line spacing, and the `\deff` default font, instead of WordCraft's Normal style; RTF files that relied on the old defaults open tighter, like in Word |
|
||||
| 2026-10-12 | `--help` and `-h` print the usage and exit (#968): `wordcraft --help` lists the app's options without opening a window (it used to try to open a file named `--help`), and `wordcraft-cli <command> --help` works for every command instead of failing with `unknown option --help` |
|
||||
| 2026-10-11 | Right-clicking a table cell offers the table's commands, and as in Word they follow the selection: with the caret in a cell Insert, Delete Cells, Split Cells, Text Direction and Table Properties…; with several cells Merge Cells instead of Split; with whole columns or rows Delete Columns or Rows, Merge Cells and Distribute Rows and Columns; with the whole table Delete Table, Distribute, AutoFit and Insert Caption…. A right-click beside selected cells moves the caret there instead of keeping them |
|
||||
| 2026-10-11 | Drags, Shift+arrows and Shift+click over table cells select whole cells, as in Word: from one cell into another the rectangle between them (bold, alignment and Delete leave the other cells alone), into or out of a table whole rows; extending again goes on from where the selection started |
|
||||
| 2026-10-11 | More of the table mouse: arrows above a column, left of a row and along a cell's left edge select it (a drag selects more; a selected column is edited cell by cell, numbering it makes one list), "+" buttons beside the borders insert a row or column, the move handle past the top-left corner selects the table or drags it to float where it's dropped, and a double-click on a column border fits the column; Select Column in the ribbon; `table.selectAt`, `table.insertAt`, `table.moveDrag`, `table.fitColumn` and `table.selectColumn` for agents |
|
||||
@@ -364,6 +365,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
| 2026-10-14 | trivial | Progress log: table deletes, Change Case and Unlink Fields respect Track Changes and locked content controls (#1329) |
|
||||
| 2026-10-14 | trivial | Progress log: Ink to Shape recognises single-stroke arrows |
|
||||
| 2026-10-12 | trivial | Progress log: Accept All and Reject All keep the right paragraph marks (#864) |
|
||||
| 2026-10-12 | trivial | Progress log: `--help` for the app and every CLI subcommand (#968) |
|
||||
| 2026-10-13 | trivial | Progress log: equation pictures keep the theme colours on Windows (#1002) |
|
||||
| 2026-10-11 | trivial | Progress log: Ctrl, Alt, left-edge and selected-cells table border drags (#711) |
|
||||
| 2026-10-11 | trivial | Progress log: table markers on the rulers (#700) |
|
||||
|
||||
@@ -42,6 +42,7 @@ USAGE:
|
||||
running): addEditCitation, addEditBibliography,
|
||||
addNote, refresh, removeCodes, setDocPrefs
|
||||
(--cmd runs first, e.g. --cmd caret.docEnd)
|
||||
wordcraft-cli [<command>] -h | --help this help
|
||||
wordcraft-cli --version
|
||||
|
||||
Paper: documents created here, and HTML, Markdown and text files (which give no page size), use
|
||||
@@ -50,6 +51,13 @@ LC_ALL / LC_PAPER / LANG or the platform. Set WORDCRAFT_REGION=en-US (or another
|
||||
same output on every machine.
|
||||
";
|
||||
|
||||
/// The subcommands, in `USAGE` order; `--help` or `-h` after any of them prints `USAGE` (#968).
|
||||
const SUBCOMMANDS: &[&str] = &["convert", "info", "text", "inspect", "render", "run", "commands", "parity", "mcp", "zotero"];
|
||||
|
||||
fn wants_help(cmd: &str, rest: &[String]) -> bool {
|
||||
SUBCOMMANDS.contains(&cmd) && rest.iter().any(|a| a == "--help" || a == "-h")
|
||||
}
|
||||
|
||||
fn open(path: &str) -> Result<Session, String> {
|
||||
// A file that gives no page size (HTML, Markdown, plain text) opens on the system region's
|
||||
// paper, as in the app: A4 in Italy, Letter in the US (#1381).
|
||||
@@ -205,6 +213,10 @@ fn write_stdout(write: impl FnOnce(&mut std::io::StdoutLock<'static>) -> std::io
|
||||
fn run(args: &[String]) -> Result<(), Failure> {
|
||||
let cmd = args.first().map(String::as_str).unwrap_or("");
|
||||
let rest: Vec<String> = args.iter().skip(1).cloned().collect();
|
||||
// Before options are checked or files and folders opened.
|
||||
if wants_help(cmd, &rest) {
|
||||
return write_stdout(|o| write!(o, "{USAGE}"));
|
||||
}
|
||||
let pos = |i: usize| rest.iter().filter(|a| !a.starts_with("--")).nth(i).cloned().ok_or_else(|| format!("missing argument\n\n{USAGE}"));
|
||||
match cmd {
|
||||
"convert" => {
|
||||
@@ -502,6 +514,26 @@ mod tests {
|
||||
assert_eq!(run(&args(&["zotero", "refresh", "missing.docx", "--prot", "1"])), Err("zotero: unknown option --prot".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_or_h_after_a_subcommand_asks_for_the_usage() {
|
||||
for cmd in SUBCOMMANDS {
|
||||
assert!(wants_help(cmd, &args(&["in.docx", "--help"])), "{cmd}");
|
||||
assert!(wants_help(cmd, &args(&["-h"])), "{cmd}");
|
||||
assert!(!wants_help(cmd, &args(&["in.docx", "--hepl"])), "{cmd}");
|
||||
}
|
||||
assert!(!wants_help("frobnicate", &args(&["--help"])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usage_lists_exactly_the_subcommands() {
|
||||
let listed: Vec<&str> = USAGE
|
||||
.lines()
|
||||
.filter_map(|line| line.strip_prefix(" wordcraft-cli ")?.split_whitespace().next())
|
||||
.filter(|word| !word.starts_with(['-', '[']))
|
||||
.collect();
|
||||
assert_eq!(listed, SUBCOMMANDS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_keeps_bullet_and_number_labels() {
|
||||
let doc = wordcraft_engine::io::open_bytes("lists.md", b"- alpha\n- beta\n\n1. first\n2. second\n").unwrap();
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
//! `--help` or `-h` after any subcommand prints the usage and exits 0, before options are checked
|
||||
//! or files and folders opened (#968).
|
||||
|
||||
use std::process::{Command, Output};
|
||||
|
||||
const SUBCOMMANDS: [&str; 10] = ["convert", "info", "text", "inspect", "render", "run", "commands", "parity", "mcp", "zotero"];
|
||||
|
||||
fn cli(args: &[&str]) -> Output {
|
||||
// `output()` gives the child no stdin, so `mcp` can't wait for a client.
|
||||
Command::new(env!("CARGO_BIN_EXE_wordcraft-cli")).args(args).output().unwrap()
|
||||
}
|
||||
|
||||
fn assert_prints_usage(args: &[&str]) {
|
||||
let out = cli(args);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(out.status.success(), "{args:?}: status {:?}, stderr: {stderr}", out.status);
|
||||
assert!(String::from_utf8_lossy(&out.stdout).starts_with("wordcraft-cli — "), "{args:?}: stdout: {}", String::from_utf8_lossy(&out.stdout));
|
||||
assert!(stderr.is_empty(), "{args:?}: stderr: {stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_subcommand_prints_usage_for_help_and_h() {
|
||||
assert_prints_usage(&["--help"]);
|
||||
assert_prints_usage(&["-h"]);
|
||||
for sub in SUBCOMMANDS {
|
||||
for flag in ["--help", "-h"] {
|
||||
assert_prints_usage(&[sub, flag]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_comes_before_opening_files_and_folders() {
|
||||
assert_prints_usage(&["info", "missing.docx", "--help"]);
|
||||
assert_prints_usage(&["convert", "missing.docx", "out.pdf", "-h"]);
|
||||
assert_prints_usage(&["mcp", "--automation-read-root", "/definitely/not/a/folder", "--help"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn misspelled_options_and_unknown_commands_still_fail() {
|
||||
let out = cli(&["info", "--hepl"]);
|
||||
assert!(!out.status.success());
|
||||
assert!(String::from_utf8_lossy(&out.stderr).contains("info: unknown option --hepl"));
|
||||
let out = cli(&["frobnicate", "--help"]);
|
||||
assert!(!out.status.success());
|
||||
assert!(String::from_utf8_lossy(&out.stderr).contains("unknown command `frobnicate`"));
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//! WordCraft desktop app.
|
||||
//!
|
||||
//! Usage: `wordcraft [--control <port>] [--sample] [files…]`
|
||||
//! Usage: `wordcraft [--control <port>] [--sample] [files…]`; `--help` prints `USAGE`.
|
||||
//!
|
||||
//! `--control <port>` (or `WORDCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server:
|
||||
//! `{"id":1,"key":"…","method":"engine.execute","params":{"command":"text.insert","params":{"text":"Hi"}}}`.
|
||||
@@ -230,6 +230,20 @@ fn app_icon() -> Option<egui::IconData> {
|
||||
eframe::icon_data::from_png_bytes(png).map_err(|e| log::warn!("app icon: {e}")).ok()
|
||||
}
|
||||
|
||||
const USAGE: &str = "\
|
||||
wordcraft — the WordCraft desktop app
|
||||
|
||||
USAGE:
|
||||
wordcraft [--sample] [--control PORT] [FILE…]
|
||||
|
||||
OPTIONS:
|
||||
--sample open the sample document
|
||||
--control PORT start the localhost control server on PORT, for agents and scripts
|
||||
(or set WORDCRAFT_CONTROL_PORT); see docs/control-protocol.md
|
||||
--version print the version
|
||||
-h, --help print this help
|
||||
";
|
||||
|
||||
/// The commit the build came from (short), or `dev`.
|
||||
fn build_sha() -> &'static str {
|
||||
option_env!("WORDCRAFT_BUILD_SHA").map(|s| s.get(..8).unwrap_or(s)).unwrap_or("dev")
|
||||
@@ -251,6 +265,11 @@ fn main() -> eframe::Result {
|
||||
println!("wordcraft {} ({})", env!("CARGO_PKG_VERSION"), build_sha());
|
||||
return Ok(());
|
||||
}
|
||||
// Before the window opens (#968). Unlike `print!`, a closed stdout can't panic.
|
||||
"--help" | "-h" => {
|
||||
let _ = std::io::Write::write_all(&mut std::io::stdout(), USAGE.as_bytes());
|
||||
return Ok(());
|
||||
}
|
||||
_ => files.push(a),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
//! `wordcraft --help` and `-h` print the options and exit without opening a window (#968).
|
||||
|
||||
use std::process::{Command, Output, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
fn app(flag: &str) -> Output {
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_wordcraft"))
|
||||
.arg(flag)
|
||||
// With no display, an app that tries to open a window fails at once instead.
|
||||
.env_remove("DISPLAY")
|
||||
.env_remove("WAYLAND_DISPLAY")
|
||||
.env_remove("WORDCRAFT_CONTROL_PORT")
|
||||
.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 {flag} was still running after 30 s: it started the app");
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
child.wait_with_output().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_and_h_print_the_options_and_exit() {
|
||||
for flag in ["--help", "-h"] {
|
||||
let out = app(flag);
|
||||
let (stdout, stderr) = (String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
|
||||
assert!(out.status.success(), "{flag}: status {:?}, stderr: {stderr}", out.status);
|
||||
assert!(stdout.starts_with("wordcraft — "), "{flag}: stdout: {stdout}");
|
||||
for option in ["--sample", "--control PORT", "--version", "-h, --help"] {
|
||||
assert!(stdout.contains(option), "{flag}: {option} missing from: {stdout}");
|
||||
}
|
||||
assert!(stderr.is_empty(), "{flag}: stderr: {stderr}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user