Add Word's common keyboard shortcuts (#330) (#338)

Bind Word's documented default shortcuts that were missing to existing
commands, plus a few small new commands: Reset Paragraph Formatting
(Ctrl+Q), Reset Character Formatting (Ctrl+Space, which used to clear all
formatting), em dash, copyright, registered, trademark and ellipsis, DATE /
TIME / PAGE fields (Alt+Shift+D/T/P), Print Layout / Outline / Draft /
Split views, Word Count (Ctrl+Shift+G), Bookmark, Alt+F7, Ctrl+Page Up/Down
find browsing, Shift+F12 / Ctrl+F12 / Ctrl+Shift+F12 / Ctrl+F2 / Ctrl+F4,
Mark Index Entry, Mark Citation and the mail merge keys.

Where macOS claims Word's key (Cmd+Q quits, Cmd+Space is Spotlight, Cmd+Shift+G
is Find Previous, Option+letter types symbols), the binding follows Word for
Mac or is left off there. F4 Repeat, Q and F2 now reach the registry from the
keyboard, and an Alt shortcut no longer also types its key's character.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-11 11:19:59 -04:00
1 parent 6129bfeee5
commit eb068d3823
29 files changed
+278 -42

No files matched your search

+2
View File
@@ -131,6 +131,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| Date | What landed |
|---|---|
| 2026-10-11 | Word's common keyboard shortcuts (#330): 101 → 125 of ~250 |
| 2026-10-11 | Turkish (`tr`) interface: all 1,307 catalog entries, `tr-TR`/`tr-CY` system locales and a saved language choice; command search keeps Turkish dotted and dotless I apart (`italik` finds İtalik); locale, catalog, search and bundled-font tests; no Turkish proofing resources |
| 2026-10-11 | Citations and bibliography in all 12 of Word's styles: Harvard, ISO 690 (author-date and numerical), Turabian, GB/T 7714, GOST (name and title sort) and SIST02 join APA, MLA, Chicago and IEEE; numbered styles count in order of first citation (#383) |
| 2026-10-11 | Interface size setting (#475): File › Options › General scales the interface 80–200 % on top of the system display scale, kept between runs; `ui.interfaceSize` for agents |
@@ -150,6 +151,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | trivial | Word's common keyboard shortcuts (#330): 125 of ~250 |
| 2026-10-11 | trivial | Collapsed duplicate rows left by parallel merges |
| 2026-10-11 | trivial | Turkish interface (`tr`): progress log and shipped languages |
| 2026-10-11 | trivial | Progress log: eight more bibliography styles (#383) |
+4 -2
View File
@@ -331,7 +331,8 @@ pub fn specs() -> Vec<CommandSpec> {
s.doc.insert_object(&b, InlineObject::Field { instr: format!("XE \"{}\"", entry.replace('"', "")), result: String::new(), locked: false }, &CharProps { hidden: Some(true), ..Default::default() })?;
Ok(json!({"entry": entry}))
})
.params(r#"{"entry"?: string}"#),
.params(r#"{"entry"?: string}"#)
.key(super::non_mac("Alt+Shift+X")),
CommandSpec::new("references.index", "Insert Index", "References › Index", |s, _| {
generated_list(s, "INDEX", "Index")?;
sel_result(s)
@@ -349,7 +350,8 @@ pub fn specs() -> Vec<CommandSpec> {
s.doc.insert_object(&b, InlineObject::Field { instr: format!("TA \\l \"{}\"", entry.replace('"', "")), result: String::new(), locked: false }, &CharProps { hidden: Some(true), ..Default::default() })?;
Ok(json!({"entry": entry}))
})
.params(r#"{"entry"?: string}"#),
.params(r#"{"entry"?: string}"#)
.key(super::non_mac("Alt+Shift+I")),
CommandSpec::new("references.tableOfAuthorities", "Insert Table of Authorities", "References › Table of Authorities", |s, _| {
generated_list(s, "TOA", "Table of Authorities")?;
sel_result(s)
+1 -1
View File
@@ -144,7 +144,7 @@ pub(crate) fn dispatch(s: &mut Session, id: &str, mutates: bool, run: fn(&mut Se
delete(s, &segs)?;
super::sel_result(s)
}
"format.changeCase" | "format.clear" => per_row(s, run, v, &segs),
"format.changeCase" | "format.clear" | "format.resetChar" => per_row(s, run, v, &segs),
_ if id.starts_with("format.") || id.starts_with("para.") || id == "select.column" => run(s, v),
_ if mutates && (id.starts_with("text.") || id.starts_with("edit.paste")) => {
delete(s, &segs)?;
+5 -2
View File
@@ -64,8 +64,11 @@ pub fn specs() -> Vec<CommandSpec> {
CommandSpec::new("edit.advancedFind", "Advanced Find", "Home › Editing › Find", advanced_find)
.params(r#"{"text"?: string, "matchCase"?: bool, "wholeWord"?: bool, "regex"?: bool, "in"?: "main|selection", "highlight"?: bool}"#)
.pure(),
CommandSpec::new("edit.findNext", "Find Next", "Home › Editing › Find", |s, _| step(s, 1)).key("Mod+G / F3").pure(),
CommandSpec::new("edit.findPrevious", "Find Previous", "Home › Editing › Find", |s, _| step(s, -1)).key("Mod+Shift+G").pure(),
CommandSpec::new("edit.findNext", "Find Next", "Home › Editing › Find", |s, _| step(s, 1)).key("Mod+G / F3 / Mod+PageDown").pure(),
CommandSpec::new("edit.findPrevious", "Find Previous", "Home › Editing › Find", |s, _| step(s, -1))
// Elsewhere Ctrl+Shift+G is Word Count; Ctrl+Page Up/Down browse the find results.
.key(super::mac_or("Mod+Shift+G / Mod+PageUp", "Mod+PageUp"))
.pure(),
CommandSpec::new("edit.replace", "Replace", "Home › Editing", replace).key("Mod+H").params(r#"{"text": string, "with": string}"#),
CommandSpec::new("edit.replaceAll", "Replace All", "Home › Editing › Replace", replace_all)
.params(r#"{"text": string, "with": string, "matchCase"?: bool, "wholeWord"?: bool, "regex"?: bool}"#),
+4 -4
View File
@@ -30,11 +30,11 @@ pub fn specs() -> Vec<CommandSpec> {
vec![
CommandSpec::new("file.new", "New", "File", new).key("Mod+N").params(r#"{"template"?: "blank|sample|letter|resume|report"}"#).pure(),
CommandSpec::new("file.open", "Open", "File", open)
.key("Mod+O")
.key("Mod+O / Mod+F12")
.params(r#"{"path": string, "data"?: base64 (the file's bytes; `path` then only names it), "password"?: string (for a password-protected Word document)}"#)
.pure(),
CommandSpec::new("file.save", "Save", "File", save)
.key("Mod+S")
.key("Mod+S / Shift+F12")
.params(r#"{"path"?: string, "password"?: string (encrypt this and later saves with it, as file.encrypt does)}"#)
.pure(),
CommandSpec::new("file.saveAs", "Save As", "File", save_as).key("F12").params(r#"{"path": string, "password"?: string}"#).pure(),
@@ -58,13 +58,13 @@ pub fn specs() -> Vec<CommandSpec> {
s.ui_requests.push(json!({"open": "print"}));
sel_result(s)
})
.key("Mod+P")
.key("Mod+P / Mod+Shift+F12 / Mod+F2")
.pure(),
CommandSpec::new("file.close", "Close", "File", |s, _| {
s.ui_requests.push(json!({"close": true}));
sel_result(s)
})
.key("Mod+W")
.key("Mod+W / Mod+F4")
.pure(),
CommandSpec::new("file.properties", "Properties", "File › Info", properties)
.params(r#"{"title"?, "subject"?, "author"?, "keywords"?, "comments"?, "category"?, "custom"?: {name: string | null (removes)}}"#),
+18 -4
View File
@@ -101,9 +101,12 @@ pub fn specs() -> Vec<CommandSpec> {
CommandSpec::new("format.growFont1", "Grow Font 1 Point", "Home › Font", |s, _| nudge_size(s, 1.0)).key("Mod+]"),
CommandSpec::new("format.shrinkFont1", "Shrink Font 1 Point", "Home › Font", |s, _| nudge_size(s, -1.0)).key("Mod+["),
CommandSpec::new("format.color", "Font Color", "Home › Font", color).params(r#"{"color": "RRGGBB" | "auto"}"#),
CommandSpec::new("format.highlight", "Text Highlight Color", "Home › Font", highlight).params(
r#"{"color": "yellow|brightGreen|turquoise|pink|blue|red|darkBlue|teal|green|violet|darkRed|darkYellow|gray50|gray25|black|none"}"#,
),
CommandSpec::new("format.highlight", "Text Highlight Color", "Home › Font", highlight)
.params(
r#"{"color": "yellow|brightGreen|turquoise|pink|blue|red|darkBlue|teal|green|violet|darkRed|darkYellow|gray50|gray25|black|none"}"#,
)
// ⌘⌥H hides other apps on macOS.
.key(super::non_mac("Mod+Alt+H")),
CommandSpec::new("format.shading", "Character Shading", "Home › Font", |s, v| {
let c = p::str(v, "color").and_then(Rgb::parse);
apply(s, &|x| x.shading = c)
@@ -112,7 +115,9 @@ pub fn specs() -> Vec<CommandSpec> {
CommandSpec::new("format.changeCase", "Change Case", "Home › Font", change_case)
.key("Shift+F3")
.params(r#"{"mode"?: "sentence|lower|upper|title|toggle"}"#),
CommandSpec::new("format.clear", "Clear All Formatting", "Home › Font", clear).key("Mod+Space"),
CommandSpec::new("format.clear", "Clear All Formatting", "Home › Font", clear),
// Word's Ctrl+Space; ⌘Space is Spotlight, so Word for Mac uses ⌃Space.
CommandSpec::new("format.resetChar", "Reset Character Formatting", "Home › Font", reset_char).key(super::mac_or("Ctrl+Space", "Mod+Space")),
CommandSpec::new("format.spacing", "Character Spacing", "Home › Font › Font › Advanced", |s, v| {
let x = p::req_f32(v, "points")?.clamp(-100.0, 100.0);
apply(s, &|c| c.spacing = Some(x))
@@ -454,6 +459,15 @@ fn clear(s: &mut Session, _: &Value) -> CmdResult {
sel_result(s)
}
/// Remove direct character formatting, keeping the paragraph and any character style.
fn reset_char(s: &mut Session, _: &Value) -> CmdResult {
match target(s) {
Some((a, b)) => s.doc.format_range(&a, &b, &|c| *c = CharProps { style: c.style.clone(), ..c.cleared() })?,
None => s.pending = Some(CharProps::default()),
}
sel_result(s)
}
fn char_style(s: &mut Session, v: &Value) -> CmdResult {
let st = p::req_str(v, "style")?;
let id = s.doc.styles.find(st).map(|x| x.id.clone()).ok_or_else(|| CmdError::Params(format!("no style `{st}`")))?;
+8 -1
View File
@@ -41,7 +41,7 @@ pub fn specs() -> Vec<CommandSpec> {
c.style = None;
}
})),
CommandSpec::new("insert.bookmark", "Bookmark", "Insert › Links", bookmark).params(r#"{"name": string}"#),
CommandSpec::new("insert.bookmark", "Bookmark", "Insert › Links", bookmark).params(r#"{"name": string}"#).key("Mod+Shift+F5"),
CommandSpec::new("insert.header", "Header", "Insert › Header & Footer", |s, v| header_footer(s, v, true)).params(r#"{"text"?: string, "preset"?: "blank|blankThree|title"}"#),
CommandSpec::new("insert.footer", "Footer", "Insert › Header & Footer", |s, v| header_footer(s, v, false)).params(r#"{"text"?: string, "preset"?: "blank|blankThree|pageNumber"}"#),
CommandSpec::new("insert.pageNumber", "Page Number", "Insert › Header & Footer", page_number).params(r#"{"position"?: "top|bottom|current", "align"?: "left|center|right", "format"?: "x of y"}"#),
@@ -71,6 +71,13 @@ pub fn specs() -> Vec<CommandSpec> {
CommandSpec::new("insert.dateTime", "Date & Time", "Insert › Text", date_time).params(r#"{"format"?: "M/d/yyyy", "update"?: bool}"#),
CommandSpec::new("insert.symbol", "Symbol", "Insert › Symbols", symbol).params(r#"{"char": string, "font"?: family (the symbol's own font)}"#),
CommandSpec::new("insert.field", "Field", "Insert › Text › Quick Parts", field).key("Mod+F9").params(r#"{"instr": string, "result"?: string}"#),
// Word's Alt+Shift+D / T / P (Control+Shift+… in Word for Mac): DATE, TIME and PAGE fields.
CommandSpec::new("insert.dateField", "Insert Date Field", "Insert › Text › Quick Parts", |s, _| field(s, &json!({"instr": "DATE"})))
.key(super::mac_or("Ctrl+Shift+D", "Alt+Shift+D")),
CommandSpec::new("insert.timeField", "Insert Time Field", "Insert › Text › Quick Parts", |s, _| field(s, &json!({"instr": "TIME"})))
.key(super::mac_or("Ctrl+Shift+T", "Alt+Shift+T")),
CommandSpec::new("insert.pageField", "Insert Page Field", "Insert › Text › Quick Parts", |s, _| field(s, &json!({"instr": "PAGE"})))
.key(super::mac_or("Ctrl+Shift+P", "Alt+Shift+P")),
CommandSpec::new("insert.dropCap", "Drop Cap", "Insert › Text", |s, v| {
let lines = p::u64(v, "lines").unwrap_or(3).min(10) as u8;
super::para::fmt(s, &|p| p.drop_cap = Some(lines))
+5 -1
View File
@@ -56,13 +56,15 @@ pub fn specs() -> Vec<CommandSpec> {
Ok(json!({"headers": s.merge.headers, "rows": s.merge.rows}))
})
.params(r#"{"rows"?: [{field: value}] | [[value]], "fields"?: [string]}"#)
.key(super::non_mac("Alt+Shift+E"))
.when(has_list)
.pure(),
CommandSpec::new("mailings.insertField", "Insert Merge Field", "Mailings › Write & Insert Fields", |s, v| {
let f = p::req_str(v, "field")?.to_string();
insert_field(s, &format!("MERGEFIELD {}", quote(&f)), &format!("«{f}»"))
})
.params(r#"{"field": string}"#),
.params(r#"{"field": string}"#)
.key(super::non_mac("Alt+Shift+F")),
CommandSpec::new("mailings.addressBlock", "Address Block", "Mailings › Write & Insert Fields", |s, _| {
insert_field(s, "ADDRESSBLOCK", "«AddressBlock»")
}),
@@ -102,6 +104,7 @@ pub fn specs() -> Vec<CommandSpec> {
refresh(s);
Ok(json!({"preview": s.merge.preview, "record": s.merge.record + 1}))
})
.key(super::non_mac("Alt+Shift+K"))
.when(has_recipients),
CommandSpec::new("mailings.next", "Next Record", "Mailings › Preview Results", |s, _| step(s, 1)).when(has_recipients),
CommandSpec::new("mailings.previous", "Previous Record", "Mailings › Preview Results", |s, _| step(s, -1)).when(has_recipients),
@@ -131,6 +134,7 @@ pub fn specs() -> Vec<CommandSpec> {
.pure(),
CommandSpec::new("mailings.finish", "Finish & Merge", "Mailings › Finish", finish)
.params(r#"{"path"?: string (save the merged document), "from"?: n, "to"?: n}"#)
.key(super::non_mac("Alt+Shift+N"))
.when(has_recipients),
CommandSpec::new("mailings.envelopes", "Envelopes", "Mailings › Create", envelopes)
.params(r#"{"delivery": string, "return"?: string, "size"?: "Envelope #10|Envelope DL"}"#),
+11
View File
@@ -66,6 +66,17 @@ pub fn registry() -> Registry {
Registry::new(v)
}
/// A shortcut that differs on macOS, where Word for Mac moves it (⌘Q quits, ⌘Space is
/// Spotlight, ⌘⌥H hides other apps…): `mac` there, `other` everywhere else.
pub const fn mac_or(mac: &'static str, other: &'static str) -> &'static str {
if cfg!(target_os = "macos") { mac } else { other }
}
/// A shortcut with no macOS equivalent (Option+letter types the character there).
pub const fn non_mac(k: &'static str) -> &'static str {
mac_or("", k)
}
/// Parse a position from JSON (`{"story":"body","path":[0],"off":3}` or `{"block":0,"off":3}`).
pub fn parse_pos(v: &Value) -> Option<Pos> {
if let Ok(p) = serde_json::from_value::<Pos>(v.clone()) {
+8
View File
@@ -94,6 +94,14 @@ pub fn specs() -> Vec<CommandSpec> {
let has = cur(s).space_after > 0.0;
fmt(s, &|p| p.space_after = Some(if has { 0.0 } else { 8.0 }))
}),
// Back to the paragraph style: direct indents, spacing, alignment… go; the style, list
// membership and drop cap stay. ⌘Q quits on macOS, so Word for Mac uses ⌃Q.
CommandSpec::new("para.reset", "Reset Paragraph Formatting", "Home › Paragraph", |s, _| {
fmt(s, &|p| {
*p = ParaProps { style: p.style.take(), numbering: p.numbering.take(), drop_cap: p.drop_cap.take(), ..Default::default() }
})
})
.key(super::mac_or("Ctrl+Q", "Mod+Q")),
CommandSpec::new("para.style", "Apply Style", "Home › Styles", apply_style).params(r#"{"style": string (name or id)}"#),
CommandSpec::new("para.normal", "Normal Style", "Home › Styles", |s, _| apply_style(s, &json!({"style": "Normal"}))).key("Mod+Shift+N"),
CommandSpec::new("para.heading1", "Heading 1", "Home › Styles", |s, _| apply_style(s, &json!({"style": "Heading1"}))).key("Mod+Alt+1"),
+6 -2
View File
@@ -46,9 +46,13 @@ pub fn specs() -> Vec<CommandSpec> {
r#"{"comments"?: bool, "ink"?: bool, "insertionsDeletions"?: bool, "formatting"?: bool, "balloons"?: "revisions|inline|commentsAndFormatting", "insertMark"?: "underline|doubleUnderline|bold|italic|strikethrough|colorOnly|none", "insertColor"?: "byAuthor" | "RRGGBB", "deleteMark"?: "strikethrough|doubleStrikethrough|hidden|caret|hash|underline|colorOnly", "deleteColor"?: "byAuthor" | "RRGGBB", "changedLines"?: "outside|left|right|none", "changedLinesColor"?: "auto" | "RRGGBB", "trackFormatting"?: bool} → the options (omit all to read them)"#,
)
.pure(),
CommandSpec::new("review.wordCount", "Word Count", "Review › Proofing", word_count).params(r#"{"includeTextBoxes"?: bool}"#).pure(),
CommandSpec::new("review.wordCount", "Word Count", "Review › Proofing", word_count)
.params(r#"{"includeTextBoxes"?: bool}"#)
// ⌘⇧G is Find Previous on macOS.
.key(super::non_mac("Mod+Shift+G"))
.pure(),
CommandSpec::new("review.changes", "Reviewing Pane", "Review › Tracking", list_changes).pure(),
CommandSpec::new("review.spelling", "Spelling & Grammar", "Review › Proofing", next_issue).key("F7").pure(),
CommandSpec::new("review.spelling", "Spelling & Grammar", "Review › Proofing", next_issue).key("F7 / Alt+F7").pure(),
CommandSpec::new("review.issues", "Proofing Issues", "Review › Proofing", all_issues).pure(),
CommandSpec::new("review.suggestions", "Spelling Suggestions", "Review › Proofing", suggestions).params(r#"{"pos"?: Pos}"#).pure(),
CommandSpec::new("review.addToDictionary", "Add to Dictionary", "Review › Proofing", |s, v| {
+7 -1
View File
@@ -5,7 +5,7 @@ use wordcraft_doc::para::{COLUMN_BREAK, NB_HYPHEN, NBSP, PAGE_BREAK, SOFT_HYPHEN
use wordcraft_doc::props::{Border, BorderStyle, NumRef};
use wordcraft_doc::{Block, ListKind, Pos};
use super::{delete_selection, sel_result, split_para, type_text};
use super::{delete_selection, non_mac, sel_result, split_para, type_text};
use crate::{CmdError, CmdResult, CommandSpec, Selection, Session, p};
pub fn specs() -> Vec<CommandSpec> {
@@ -24,6 +24,12 @@ pub fn specs() -> Vec<CommandSpec> {
CommandSpec::new("text.nbsp", "Nonbreaking Space", "Insert › Symbols", |s, _| ins_char(s, NBSP)).key("Mod+Shift+Space"),
CommandSpec::new("text.nbHyphen", "Nonbreaking Hyphen", "Insert › Symbols", |s, _| ins_char(s, NB_HYPHEN)).key("Mod+Shift+-"),
CommandSpec::new("text.optionalHyphen", "Optional Hyphen", "Insert › Symbols", |s, _| ins_char(s, SOFT_HYPHEN)).key("Mod+-"),
CommandSpec::new("text.emDash", "Em Dash", "Insert › Symbols", |s, _| ins_char(s, '—')).key("Mod+Alt+-"),
// On macOS Option+G / R / 2 / ; type these directly, so Word for Mac has no Cmd shortcut.
CommandSpec::new("text.copyright", "Copyright Sign", "Insert › Symbols", |s, _| ins_char(s, '©')).key(non_mac("Mod+Alt+C")),
CommandSpec::new("text.registered", "Registered Sign", "Insert › Symbols", |s, _| ins_char(s, '®')).key(non_mac("Mod+Alt+R")),
CommandSpec::new("text.trademark", "Trademark Sign", "Insert › Symbols", |s, _| ins_char(s, '™')).key(non_mac("Mod+Alt+T")),
CommandSpec::new("text.ellipsis", "Ellipsis", "Insert › Symbols", |s, _| ins_char(s, '…')).key(non_mac("Mod+Alt+.")),
]
}
+4 -2
View File
@@ -21,14 +21,15 @@ pub fn specs() -> Vec<CommandSpec> {
})
.key("Mod+Shift+8 / Mod+8")
.pure(),
CommandSpec::new("view.printLayout", "Print Layout", "View › Views", |s, _| mode(s, ViewMode::Print, false)).pure(),
CommandSpec::new("view.printLayout", "Print Layout", "View › Views", |s, _| mode(s, ViewMode::Print, false)).key("Mod+Alt+P").pure(),
CommandSpec::new("view.webLayout", "Web Layout", "View › Views", |s, _| mode(s, ViewMode::Web, false)).pure(),
CommandSpec::new("view.draft", "Draft", "View › Views", |s, _| mode(s, ViewMode::Draft, false)).pure(),
CommandSpec::new("view.draft", "Draft", "View › Views", |s, _| mode(s, ViewMode::Draft, false)).key("Mod+Alt+N").pure(),
CommandSpec::new("view.outline", "Outline", "View › Views", |s, _| {
mode(s, ViewMode::Draft, false)?;
s.view.nav_pane = true;
sel_result(s)
})
.key("Mod+Alt+O")
.pure(),
CommandSpec::new("view.readMode", "Read Mode", "View › Views", |s, _| mode(s, ViewMode::Print, true)).pure(),
CommandSpec::new("view.focus", "Focus", "View › Immersive", |s, v| toggle(s, v, |x| &mut x.focus_mode)).pure(),
@@ -71,6 +72,7 @@ pub fn specs() -> Vec<CommandSpec> {
s.ui_requests.push(json!({"toggle": "split"}));
sel_result(s)
})
.key("Mod+Alt+S")
.pure(),
CommandSpec::new("view.state", "View State", "View", |s, _| serde_json::to_value(&s.view).map_err(|e| CmdError::Failed(e.to_string())))
.pure(),
+52
View File
@@ -73,6 +73,58 @@ fn every_shortcut_runs_one_command() {
assert_eq!(reg.by_shortcut("Shift+F3").map(|c| c.id), Some("format.changeCase"));
}
#[test]
fn word_default_shortcuts_resolve() {
// #330: Word's documented default shortcuts reach the matching commands, with Word for
// Mac's keys where macOS takes Word's (⌘Q quits, ⌘Space is Spotlight, ⌘⇧G is Find Previous).
let reg = cmd::registry();
let id = |k: &str| reg.by_shortcut(k).map(|c| c.id);
assert_eq!(id("Mod+Alt+1"), Some("para.heading1"));
assert_eq!(id("Mod+Alt+P"), Some("view.printLayout"));
assert_eq!(id("Mod+Alt+O"), Some("view.outline"));
assert_eq!(id("Mod+Alt+N"), Some("view.draft"));
assert_eq!(id("Mod+Alt+S"), Some("view.split"));
assert_eq!(id("Mod+Alt+-"), Some("text.emDash"));
assert_eq!(id("Mod+Shift+F5"), Some("insert.bookmark"));
assert_eq!(id("Alt+F7"), Some("review.spelling"));
assert_eq!(id("Shift+F12"), Some("file.save"));
assert_eq!(id(cmd::mac_or("Ctrl+Q", "Mod+Q")), Some("para.reset"));
assert_eq!(id(cmd::mac_or("Ctrl+Space", "Mod+Space")), Some("format.resetChar"));
assert_eq!(id(cmd::mac_or("Ctrl+Shift+D", "Alt+Shift+D")), Some("insert.dateField"));
if cfg!(target_os = "macos") {
assert_eq!(id("Mod+Shift+G"), Some("edit.findPrevious"));
} else {
assert_eq!(id("Mod+Shift+G"), Some("review.wordCount"));
assert_eq!(id("Mod+Alt+C"), Some("text.copyright"));
assert_eq!(id("Mod+Alt+H"), Some("format.highlight"));
}
assert_eq!(id("Mod+PageUp"), Some("edit.findPrevious"));
}
#[test]
fn reset_shortcuts_remove_direct_formatting_but_keep_styles() {
let mut s = s();
run(&mut s, "text.insert", json!({"text": "Hello world"}));
run(&mut s, "para.heading1", json!({}));
run(&mut s, "para.indent", json!({}));
run(&mut s, "para.alignCenter", json!({}));
run(&mut s, "para.reset", json!({}));
let props = s.doc.para_at(&Pos::body(0, 0)).unwrap().props.clone();
assert_eq!(props.style.as_deref(), Some("Heading1"), "Ctrl+Q keeps the paragraph style");
assert_eq!((props.indent_left, props.align), (None, None), "Ctrl+Q drops direct paragraph formatting");
run(&mut s, "select.all", json!({}));
run(&mut s, "format.bold", json!({}));
run(&mut s, "format.resetChar", json!({}));
let p = s.doc.para_at(&Pos::body(0, 0)).unwrap();
assert_eq!(p.props_of_char(1).bold, None, "Ctrl+Space drops direct character formatting");
assert_eq!(p.props.style.as_deref(), Some("Heading1"), "Ctrl+Space leaves the paragraph alone");
run(&mut s, "caret.docEnd", json!({}));
run(&mut s, "text.emDash", json!({}));
assert_eq!(text(&s), "Hello world—");
}
#[test]
fn typing_enter_undo() {
let mut s = s();
+4
View File
@@ -73,6 +73,9 @@ pub struct CanvasState {
pub table_tool: Option<crate::table_pen::TableTool>,
/// The Draw Table stroke (or eraser press) in progress.
pub(crate) table_stroke: Option<crate::table_pen::PenStroke>,
/// The last key ran an Alt shortcut: the text its key sends (it may arrive a frame later)
/// isn't typed. The next key press clears it.
pub(crate) swallow_text: bool,
}
impl CanvasState {
@@ -109,6 +112,7 @@ impl Default for CanvasState {
context_menu_open: false,
mini_anchor: None,
pasted: false,
swallow_text: false,
wheel: Default::default(),
scroll_offset: egui::Vec2::ZERO,
scroll_max: egui::Vec2::ZERO,
+10 -2
View File
@@ -549,6 +549,16 @@
Open Abrir
OpenDocument Text (*.odt) Texto de OpenDocument (*.odt)
Optional Hyphen Guion opcional
Copyright Sign Signo de copyright
Ellipsis Puntos suspensivos
Em Dash Raya
Insert Date Field Insertar campo de fecha
Insert Page Field Insertar campo de página
Insert Time Field Insertar campo de hora
Registered Sign Signo de marca registrada
Reset Character Formatting Restablecer formato de caracteres
Reset Paragraph Formatting Restablecer formato de párrafo
Trademark Sign Signo de marca comercial
Options Opciones
Orientation Orientación
Outline Esquema
@@ -1155,8 +1165,6 @@
Document Information Información del documento
Double Closing Quote Comilla doble de cierre
Double Opening Quote Comilla doble de apertura
Ellipsis Puntos suspensivos
Em Dash Raya
En Dash Guion medio
Em Space Espacio eme
En Space Espacio ene
Can't render this file because it contains an unexpected character in line 6 and column 25.
+8
View File
@@ -1323,6 +1323,14 @@
Type the field's name and its switches, for example PAGE \\* roman. Sisesta välja nimi ja lülitid, näiteks PAGE \\* roman.
Width and spacing Laius ja vahe
from Unicode (hex) Unicode (kuueteistkümnend)
Copyright Sign Autoriõiguse märk
Insert Date Field Lisa kuupäevaväli
Insert Page Field Lisa leheküljeväli
Insert Time Field Lisa kellaajaväli
Registered Sign Registreeritud kaubamärgi märk
Reset Character Formatting Lähtesta märgivorming
Reset Paragraph Formatting Lähtesta lõiguvorming
Trademark Sign Kaubamärgi märk
Formatted Vormindatud
Interface size: Kasutajaliidese suurus:
Makes the ribbon, menus and dialogs larger or smaller, on top of your system's display scaling. Document zoom is separate. Muudab menüülindi, menüüd ja dialoogid suuremaks või väiksemaks, lisaks süsteemi ekraani skaleerimisele. Dokumendi suum on eraldi.
Can't render this file because it contains an unexpected character in line 421 and column 13.
+10 -2
View File
@@ -539,6 +539,16 @@
Open 開く
OpenDocument Text (*.odt) OpenDocument テキスト (*.odt)
Optional Hyphen 任意指定のハイフン
Copyright Sign 著作権記号
Ellipsis 省略記号
Em Dash エム ダッシュ
Insert Date Field 日付フィールドの挿入
Insert Page Field ページ フィールドの挿入
Insert Time Field 時刻フィールドの挿入
Registered Sign 登録商標記号
Reset Character Formatting 文字書式のリセット
Reset Paragraph Formatting 段落書式のリセット
Trademark Sign 商標記号
Options オプション
Orientation 印刷の向き
Outline アウトライン
@@ -1139,8 +1149,6 @@
Document Information 文書の情報
Double Closing Quote 二重引用符 (閉じ)
Double Opening Quote 二重引用符 (開き)
Ellipsis 省略記号
Em Dash em ダッシュ
En Dash en ダッシュ
Em Space em スペース
En Space en スペース
Can't render this file because it contains an unexpected character in line 496 and column 13.
+10 -2
View File
@@ -539,6 +539,16 @@
Open Abrir
OpenDocument Text (*.odt) Texto OpenDocument (*.odt)
Optional Hyphen Hífen opcional
Copyright Sign Sinal de copyright
Ellipsis Reticências
Em Dash Travessão
Insert Date Field Inserir campo de data
Insert Page Field Inserir campo de página
Insert Time Field Inserir campo de hora
Registered Sign Sinal de marca registrada
Reset Character Formatting Redefinir formatação de caracteres
Reset Paragraph Formatting Redefinir formatação de parágrafo
Trademark Sign Sinal de marca comercial
Options Opções
Orientation Orientação
Outline Estrutura de tópicos
@@ -1151,8 +1161,6 @@
Document Information Informações do documento
Double Closing Quote Aspas duplas de fechamento
Double Opening Quote Aspas duplas de abertura
Ellipsis Reticências
Em Dash Travessão
En Dash Meia-risca
Em Space Espaço eme
En Space Espaço ene
Can't render this file because it contains an unexpected character in line 6 and column 25.
+10 -2
View File
@@ -587,6 +587,16 @@
Open Otvori
OpenDocument Text (*.odt) OpenDocument tekst (*.odt)
Optional Hyphen Opcionalna crtica
Copyright Sign Znak autorskog prava
Ellipsis Tri tačke
Em Dash Duga crta
Insert Date Field Umetni polje datuma
Insert Page Field Umetni polje strane
Insert Time Field Umetni polje vremena
Registered Sign Znak registrovanog žiga
Reset Character Formatting Poništi formatiranje znakova
Reset Paragraph Formatting Poništi formatiranje pasusa
Trademark Sign Znak žiga
Options Opcije
Orientation Orijentacija
Outline Kontura
@@ -1254,8 +1264,6 @@
Document Information Informacije o dokumentu
Double Closing Quote Zatvoreni dvostruki navodnik
Double Opening Quote Otvoreni dvostruki navodnik
Ellipsis Tri tačke
Em Dash Duga crta
En Dash Srednja crta
Em Space Em razmak
En Space En razmak
Can't render this file because it contains an unexpected character in line 6 and column 25.
+10 -2
View File
@@ -591,6 +591,16 @@
Open Отвори
OpenDocument Text (*.odt) OpenDocument текст (*.odt)
Optional Hyphen Опционална цртица
Copyright Sign Знак ауторског права
Ellipsis Три тачке
Em Dash Дуга црта
Insert Date Field Уметни поље датума
Insert Page Field Уметни поље стране
Insert Time Field Уметни поље времена
Registered Sign Знак регистрованог жига
Reset Character Formatting Поништи форматирање знакова
Reset Paragraph Formatting Поништи форматирање пасуса
Trademark Sign Знак жига
Options Опције
Orientation Оријентација
Outline Контура
@@ -1258,8 +1268,6 @@
Document Information Информације о документу
Double Closing Quote Затворени двоструки наводник
Double Opening Quote Отворени двоструки наводник
Ellipsis Три тачке
Em Dash Дуга црта
En Dash Средња црта
Em Space Em размак
En Space En размак
Can't render this file because it contains an unexpected character in line 11 and column 25.
+10 -2
View File
@@ -553,6 +553,16 @@
Open Відкрити
OpenDocument Text (*.odt) Текст OpenDocument (*.odt)
Optional Hyphen М'який перенос
Copyright Sign Знак авторського права
Ellipsis Три крапки
Em Dash Довге тире
Insert Date Field Вставити поле дати
Insert Page Field Вставити поле сторінки
Insert Time Field Вставити поле часу
Registered Sign Знак зареєстрованої торгової марки
Reset Character Formatting Скинути форматування символів
Reset Paragraph Formatting Скинути форматування абзацу
Trademark Sign Знак торгової марки
Options Параметри
Orientation Орієнтація
Outline План
@@ -1151,8 +1161,6 @@
Document Information Відомості про документ
Double Closing Quote Закривні подвійні лапки
Double Opening Quote Відкривні подвійні лапки
Ellipsis Три крапки
Em Dash Довге тире
En Dash Коротке тире
Em Space Широкий пробіл
En Space Пробіл середньої ширини
Can't render this file because it contains an unexpected character in line 6 and column 31.
+10 -2
View File
@@ -550,6 +550,16 @@
Open 打开
OpenDocument Text (*.odt) OpenDocument 文本 (*.odt)
Optional Hyphen 可选连字符
Copyright Sign 版权符号
Ellipsis 省略号
Em Dash 长破折号
Insert Date Field 插入日期域
Insert Page Field 插入页码域
Insert Time Field 插入时间域
Registered Sign 注册商标符号
Reset Character Formatting 重置字符格式
Reset Paragraph Formatting 重置段落格式
Trademark Sign 商标符号
Options 选项
Orientation 纸张方向
Outline 大纲
@@ -1150,8 +1160,6 @@
Document Information 文档信息
Double Closing Quote 右双引号
Double Opening Quote 左双引号
Ellipsis 省略号
Em Dash 长破折号
En Dash 短破折号
Em Space 全角空格
En Space 半角空格
Can't render this file because it contains an unexpected character in line 6 and column 25.
+10 -2
View File
@@ -540,6 +540,16 @@
Open 開啟
OpenDocument Text (*.odt) OpenDocument 文字 (*.odt)
Optional Hyphen 選擇性連字號
Copyright Sign 著作權符號
Ellipsis 刪節號
Em Dash 長破折號
Insert Date Field 插入日期欄位
Insert Page Field 插入頁碼欄位
Insert Time Field 插入時間欄位
Registered Sign 註冊商標符號
Reset Character Formatting 重設字元格式
Reset Paragraph Formatting 重設段落格式
Trademark Sign 商標符號
Options 選項
Orientation 方向
Outline 大綱
@@ -1140,8 +1150,6 @@
Document Information 文件資訊
Double Closing Quote 右雙引號
Double Opening Quote 左雙引號
Ellipsis 刪節號
Em Dash 長破折號
En Dash 短破折號
Em Space 全形空格
En Space 半形空格
Can't render this file because it contains an unexpected character in line 497 and column 13.
+10 -1
View File
@@ -35,7 +35,9 @@ pub fn key_name(k: Key) -> Option<&'static str> {
Key::Num5 => "5",
Key::Num8 => "8",
Key::Num9 => "9",
Key::F2 => "F2",
Key::F3 => "F3",
Key::F4 => "F4",
Key::F5 => "F5",
Key::F7 => "F7",
Key::F8 => "F8",
@@ -57,6 +59,7 @@ pub fn key_name(k: Key) -> Option<&'static str> {
Key::N => "N",
Key::O => "O",
Key::P => "P",
Key::Q => "Q",
Key::R => "R",
Key::S => "S",
Key::T => "T",
@@ -148,6 +151,12 @@ pub fn canvas_events(app: &mut WordApp, ctx: &egui::Context) {
}
let events = ctx.input(|i| i.events.clone());
for e in events {
// An Alt shortcut (Alt+Shift+D, Alt+F7…) may also send its key's text; it isn't typed.
match &e {
egui::Event::Key { pressed: true, .. } => app.canvas.swallow_text = false,
egui::Event::Text(_) if std::mem::take(&mut app.canvas.swallow_text) => continue,
_ => {}
}
if let egui::Event::Paste(t) = &e {
app.canvas.pasted = true;
// Copied picture files arrive as their paths: insert the pictures (#45).
@@ -260,7 +269,7 @@ pub fn canvas_events(app: &mut WordApp, ctx: &egui::Context) {
let _ = app.run("insert.closeHeader", json!({}));
continue;
}
dispatch(app, key, modifiers);
app.canvas.swallow_text = dispatch(app, key, modifiers) && modifiers.alt;
}
_ => {}
}
+33
View File
@@ -1134,6 +1134,9 @@ fn input_dialog(id: &str, params: &Value) -> Option<&'static str> {
// Define New Multilevel List and Track Changes Options without settings show their dialogs.
"list.define" if params.get("levels").is_none() => Some("defineList"),
"review.trackingOptions" if params.as_object().is_none_or(|m| m.is_empty()) => Some("trackChangesOptions"),
"insert.bookmark" if !has("name") => Some("bookmark"),
// Word Count from the keyboard or command search shows its dialog, as the ribbon does.
"review.wordCount" if params.as_object().is_none_or(|m| m.is_empty()) => Some("wordCount"),
_ => None,
}
}
@@ -1755,6 +1758,36 @@ mod tests {
assert_eq!(reported.get(), Some(true), "the host was told before the frame ended");
}
/// #330: Word's shortcuts work from the keyboard — Ctrl+Alt+1 (⌘⌥1) applies Heading 1, and
/// an Alt shortcut doesn't also type the character its key would produce.
#[test]
fn word_shortcuts_run_from_the_keyboard() {
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);
},
app(),
);
for _ in 0..6 {
h.step();
}
let key = |key, modifiers| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers };
h.event(egui::Event::Text("Kilns".into()));
h.step();
h.event(key(egui::Key::Num1, egui::Modifiers::COMMAND | egui::Modifiers::ALT));
h.step();
let style =
|h: &egui_kittest::Harness<WordApp>| h.state().session.doc.para_at(&wordcraft_doc::Pos::body(0, 0)).and_then(|p| p.props.style.clone());
assert_eq!(style(&h).as_deref(), Some("Heading1"));
// Alt+F7 (next misspelling) can come with key text on some platforms; none is typed.
h.event(key(egui::Key::F7, egui::Modifiers::ALT));
h.event(egui::Event::Text("x".into()));
h.step();
assert_eq!(h.state().session.doc.plain_text(wordcraft_doc::StoryRef::Body), "Kilns");
}
/// Save writes the document first, then carries on with what the user asked for.
#[test]
fn save_then_continue() {
+2 -1
View File
@@ -34,7 +34,7 @@ first (`gh pr list`), and update this file and the parity doc when a gap closes.
| 21 | **Mail merge data sources**: no Excel, Outlook/Contacts, email merge; CSV and typed lists only (#247) | | | Office administrators | 6–10 | [target-app-parity.md](target-app-parity.md) |
| 22 | **Ecosystem**: no VBA execution (macros preserved only), no Office add-ins, no cloud storage, no real-time co-authoring | | `.docm` keeps macros (#172) | Enterprise users with macro templates; teams | 40–80 + owner decisions | [target-app-parity.md](target-app-parity.md) |
| 23 | **PDF Reflow** (open a PDF as an editable document, Word for Windows) | | | Users who edit PDFs they receive | 30–50 | [file-format-parity.md](file-format-parity.md) |
| 24 | **Customize Ribbon / Keyboard**, Quick Access Toolbar customization; ~100 of Word's ~250 shortcuts | | [ui-parity.md](ui-parity.md) | Power users | 10–16 | [ui-parity.md](ui-parity.md) |
| 24 | **Customize Ribbon / Keyboard**, Quick Access Toolbar customization; ~125 of Word's ~250 shortcuts (#330) | | [ui-parity.md](ui-parity.md) | Power users | 10–16 | [ui-parity.md](ui-parity.md) |
| 25 | **In-app AI assistant** (Copilot-style draft, rewrite, summarize) | | Chat PR #178 open | Users expecting Copilot | 15–30 + owner (provider) | [target-app-parity.md](target-app-parity.md) |
| 26 | **Minor formats**: Flat OPC XML, Word 2003 XML, MHT, Works/WordPerfect | | | Rare | 20–35 | [file-format-parity.md](file-format-parity.md) |
| 27 | **View windows**: Side by Side, Synchronous Scrolling, Arrange All, Switch Windows | | [parity-checklist.md](parity-checklist.md) (View 25/29) | Comparing documents | 3–5 | [ui-parity.md](ui-parity.md) |
@@ -64,6 +64,7 @@ The same gaps grouped the way the parity documents are, for agents working in on
| Date | Change | Summary |
|---|---|---|
| 2026-10-10 | trivial | #24: 125 of ~250 keyboard shortcuts after #330 |
| 2026-10-11 | trivial | Collapsed duplicate rows left by parallel merges |
| 2026-10-11 | trivial | All 12 bibliography styles landed (#383); removed from References depth |
| 2026-10-11 | trivial | Formatting revisions landed (#41); gap #5 narrowed to move tracking and content controls, 8–12 h |
+3 -2
View File
@@ -103,7 +103,7 @@ h), pictures and tables (12–18 h), spelling depth (8–12 h), polish (10–20
| Discount | Mainstream | Essentials | Evidence |
|---|---|---|---|
| Interaction fidelity | ×0.93 | — | Typing pinned to Word (`tests_typing.rs`), ribbon, keytips, mini-toolbar; but 16 modal dialogs against Word's ~100, 101 of ~250 shortcuts, no table column drag on the canvas (#217, #49), picture/shape handling complaints (#82, #142), lines wrap differently without an Aptos-metric font |
| Interaction fidelity | ×0.93 | — | Typing pinned to Word (`tests_typing.rs`), ribbon, keytips, mini-toolbar; but 16 modal dialogs against Word's ~100, 125 of ~250 shortcuts, no table column drag on the canvas (#217, #49), picture/shape handling complaints (#82, #142), lines wrap differently without an Aptos-metric font |
| Discoverability and UI clarity | — | ×0.95 | Word-style ribbon a Word user already knows, command search, keytips; some newer pane strings and tooltips terse or untranslated |
| Stability on real machines | ×0.90 | ×0.90 (launch and install) | Never-crash standard, hostile-param fuzzing, panic guard; but open startup crash on Intel UHD (#170), freezes (#77, #59, #31), Windows writes fail without admin (#218), installer conflicts with other Crafting Apps (#64, #73, #106, #51); 5 days of field history |
| Exchanging files with Word users | ×0.90 | ×0.92 (opening files people send) | Word opens our .docx; `.doc` import; but no real-world corpus test, charts/SmartArt/OLE dropped silently, move tracking lost, pagination differs (Aptos, no column balancing or footnote continuation). Simple documents, which are most of what a casual user receives, come through well; double-click open on macOS is fixed on main but not yet released (#223, #279) |
@@ -190,7 +190,7 @@ Word. Their weighted sum is the **full ready for real work** number.
| Features (depth, table above) | 30% | 68% | 210–335 | this file | Weighted table above. Hours: the area table sums to 290–460 h; the Page layout, Footnotes, RTL and East Asian rows (~50–85 h) are counted under Layout and the Proofing row (25–45 h) under Localization, so they aren't counted twice |
| File formats (DOCX fidelity first) | 20% | 60% | 185–300 | [file-format-parity.md](file-format-parity.md) | DOCX opens in Word and round-trips our own tests, but no real-world corpus test yet; charts, SmartArt, OLE, content controls (unwrapped), move tracking and encrypted files are lost or refused |
| Layout and pagination fidelity | 15% | 55% | 90–145 | [layout-parity.md](layout-parity.md) | Aptos has no metric-matched substitute so lines break differently from Word; no column balancing, footnote continuation or document grid; compatibility modes beyond 15 partial |
| UI/UX fidelity | 10% | 65% | 65–100 | [ui-parity.md](ui-parity.md) | Ribbon, keytips, mini-toolbar, Backstage, 101 shortcuts; 16 modal dialogs against Word's ~100; no ribbon/keyboard customization |
| UI/UX fidelity | 10% | 65% | 65–100 | [ui-parity.md](ui-parity.md) | Ribbon, keytips, mini-toolbar, Backstage, 125 shortcuts; 16 modal dialogs against Word's ~100; no ribbon/keyboard customization |
| Stability | 10% | 50% | 25–45 | [gaps.md](gaps.md) | Never-crash standard and hostile-param fuzzing in place, but open reports of a startup crash on Intel UHD (#170), freezes (#77, #59, #31) and Windows write failure without admin (#218); no soak test; 5 days of field history |
| Performance | 5% | 75% | 10–20 | [layout-parity.md](layout-parity.md) | 61 ms cold layout and 1.4 ms relayout on a 188-page document (measured 2026-10-06, not re-run); font picker freezes fixed in #233; no large real-world documents measured |
| Localization | 4% | 25% | 80–130 + native review | [localization-parity.md](localization-parity.md) | 9 UI languages (Word 30); 4 of the 12 key languages partly done; English-only proofing |
@@ -249,6 +249,7 @@ can be used without committing Word output.
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | trivial | Shortcut count 101 → 125 (#330) |
| 2026-10-11 | trivial | Equation fixes: removing numbers keeps every row (#444), caret moves keep imported OMML (#446), build-up on a caret move is an undoable edit (#447), LaTeX `\text{}` keeps its spaces (#448); no percentage change |
| 2026-10-11 | trivial | English spelling (#520): common modern words ("desktop", "download", "email"…) recognised via our supplement list; Proofing row updated, no percentage change |
| 2026-10-11 | trivial | References: all 12 of Word's bibliography styles (eight more: Harvard, ISO 690 ×2, Turabian, GB/T 7714, GOST ×2, SIST02) (#383); no percentage change |
+3 -2
View File
@@ -1,6 +1,6 @@
# UI parity with Microsoft Word
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (Word's common default keyboard shortcuts bound, #330: 101 → 125 commands; previously major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
How WordCraft looks and feels next to Word: ribbon, dialogs, panes, keyboard, mouse and touch.
Typing behaviour has its own checklist ([`typing-parity.md`](typing-parity.md)). All assets are our
@@ -15,7 +15,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
| Mini-toolbar on selection | ✅ (#43) | ✅ | 80% | 1–2 |
| Quick Access Toolbar | Undo, Redo, Repeat, AutoSave | customizable | 50% | 2–3 |
| Customize Ribbon / Customize Keyboard | ❌ | ✅ | 0% | 6–10 |
| Keyboard shortcuts | **101** bound in `CommandSpec::key` (measured) | ~250 default shortcuts | ~60% | 4–6 |
| Keyboard shortcuts | **125** commands bound in `CommandSpec::key`, 144 key combinations (measured on Linux/Windows); styles, spacing, symbols, fields, views, mail merge (#330). Missing: field-code toggles (Shift+F9, Alt+F9), unlink/lock fields, en dash on the numeric keypad, move/promote paragraph, Go Back | ~250 default shortcuts | ~70% | 2–4 |
| Modal dialogs | **35** (`Dialog` enum: Font, Paragraph, Tabs, Borders and Shading, Find, Go To, Insert Table, Table Properties, Page Setup, Link, Bookmark, Word Count, Zoom, Watermark, New Style, Modify Style, Manage Styles, Table Style, Commands, Paste Special, About, Save Changes, Password, Encrypt with Password, Define New Multilevel List, Track Changes Options; Columns, Symbol with Special Characters, Field (#321); mail merge Recipients, Insert Merge Field, Find Recipient, Rules, Match Fields, Check for Errors) plus panes and menus for the rest | ~100 (Caption, Index, TOC options, Citation, Compare, Protect, Language, Hyphenation, Line Numbers, Envelopes, Labels, AutoCorrect, Options panes…) | ~40% | 20–30 |
| Task panes (Navigation, Styles, Comments, Reviewing, Format Picture/Shape, Thesaurus, Accessibility, Clipboard, Selection) | Navigation, Styles, Comments, Thesaurus, Accessibility, Read Aloud, Zotero; Style Inspector (#236); Clipboard pane (#264) open | all | 70% | 4–6 |
| Context menus (right-click) | text, spelling suggestions; objects and tables partial | rich per context | 55% | 3–5 |
@@ -37,6 +37,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | Keyboard shortcuts 101 → 125 commands (#330): Reset Paragraph/Character Formatting, symbols, DATE/TIME/PAGE fields, view switches, Word Count, mail merge, F4 Repeat now reachable |
| 2026-10-11 | trivial | Collapsed duplicate rows left by parallel merges |
| 2026-10-11 | trivial | Dialogs keep their top edge in place when switching tabs (Paragraph, Font, Borders and Shading…): only the bottom edge moves (#507) |
| 2026-10-11 | trivial | Interface size setting in File › Options › General (80–200 %), kept between runs and applied on top of the system's display scale; `ui.interfaceSize` for agents (#475) |