Merge pull request #319 from vangel76/fix/excel-shortcuts

Keyboard: Excel shortcut audit, add the missing ones (#110)

# Conflicts:
#	docs/ui-parity.md
This commit is contained in:
Brandon Thomas 2026-10-11 12:06:40 -04:00
commit 95660dc132
9 changed files with 415 additions and 9 deletions

View File

@ -29,7 +29,7 @@ pub fn specs() -> Vec<CommandSpec> {
"data.filter",
"Filter",
["Data", "Sort & Filter"],
Some("Cmd+Shift+F"),
Some("Ctrl+Shift+L"),
"{range?} toggles AutoFilter on the current region",
has_doc,
toggle_filter

View File

@ -92,7 +92,7 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
fill_series
),
cmd!("edit.flashFill", "Flash Fill", ["Home", "Editing", "Fill"], Some("Cmd+E"), "{range?}", has_doc, flash_fill),
cmd!("edit.flashFill", "Flash Fill", ["Home", "Editing", "Fill"], Some("Ctrl+E"), "{range?}", has_doc, flash_fill),
cmd!(noundo "edit.find", "Find…", ["Home", "Editing", "Find & Select"], Some("Cmd+F"), "{what, matchCase?, wholeCell?, lookIn?: formulas|values, byColumns?, all?: bool, within?: sheet|workbook}", has_doc, find),
cmd!(
"edit.replace",

View File

@ -89,7 +89,7 @@ pub fn specs() -> Vec<CommandSpec> {
// Alignment
cmd!("home.alignLeft", "Align Left", ["Home", "Alignment"], Some("Cmd+L"), "{range?}", has_doc, |s, p| halign(s, p, HAlign::Left)),
cmd!("home.alignCenter", "Center", ["Home", "Alignment"], Some("Cmd+E"), "{range?}", has_doc, |s, p| halign(s, p, HAlign::Center)),
cmd!("home.alignRight", "Align Right", ["Home", "Alignment"], Some("Cmd+R"), "{range?}", has_doc, |s, p| halign(s, p, HAlign::Right)),
cmd!("home.alignRight", "Align Right", ["Home", "Alignment"], None, "{range?}", has_doc, |s, p| halign(s, p, HAlign::Right)),
cmd!("home.justify", "Justify", ["Home", "Alignment"], None, "{range?}", has_doc, |s, p| halign(s, p, HAlign::Justify)),
cmd!("home.alignTop", "Top Align", ["Home", "Alignment"], None, "{range?}", has_doc, |s, p| valign(s, p, VAlign::Top)),
cmd!("home.alignMiddle", "Middle Align", ["Home", "Alignment"], None, "{range?}", has_doc, |s, p| valign(s, p, VAlign::Center)),

View File

@ -157,8 +157,10 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
insert_link
),
cmd!("review.newNote", "New Note", ["Review", "Notes"], None, "{cell?, text, author?}", has_doc, |s, p| comment(s, p, false)),
cmd!("review.newComment", "New Comment", ["Review", "Comments"], None, "{cell?, text, author?}", has_doc, |s, p| comment(s, p, true)),
cmd!("review.newNote", "New Note", ["Review", "Notes"], Some("Shift+F2"), "{cell?, text, author?}", has_doc, |s, p| comment(s, p, false)),
cmd!("review.newComment", "New Comment", ["Review", "Comments"], Some("Cmd+Shift+F2"), "{cell?, text, author?}", has_doc, |s, p| comment(
s, p, true
)),
cmd!("review.replyComment", "Reply", [], None, "{cell?, text, author?}", has_doc, reply_comment),
cmd!("review.resolveComment", "Resolve Thread", [], None, "{cell?, resolved?: true}", has_doc, resolve_comment),
cmd!("review.deleteComment", "Delete Comment", ["Review", "Comments"], None, "{cell?}", has_doc, delete_comment),

View File

@ -1949,8 +1949,11 @@ fn keyboard(app: &mut SheetApp, ctx: &egui::Context, resp: &egui::Response, geo:
return;
}
}
egui::Event::Key { key, pressed: true, modifiers: m, .. } => {
egui::Event::Key { key, physical_key, pressed: true, modifiers: m, .. } => {
use egui::Key;
if crate::keymap::dispatch(app, ctx, key, physical_key, m, geo, sh) {
continue;
}
let cmd = m.command;
let shift = m.shift;
let mv = |app: &mut SheetApp, dr: i64, dc: i64| {

View File

@ -0,0 +1,239 @@
//! Excel keyboard shortcuts beyond the plain Ctrl/Cmd letters in [`crate::ribbon::shortcut`]:
//! number-format and border combinations, F-keys, Alt/Option combinations and the Excel for Mac
//! variants. Every entry maps to an existing command or dialog. `docs/ui-parity.md` holds the
//! audit against Microsoft's published Windows and Mac shortcut lists.
use egui::{Key, Modifiers};
use gridcraft_engine::core::{CellRef, RangeRef};
use gridcraft_engine::model::Sheet;
use serde_json::{Value as Json, json};
use crate::SheetApp;
use crate::grid::Geo;
/// What a shortcut does.
#[derive(Clone, Debug, PartialEq)]
pub enum Shortcut {
/// Run a command with these parameters.
Run(&'static str, Json),
/// Open a dialog.
Dialog(&'static str, Json),
/// Start editing with the current date or time (Ctrl+; and Ctrl+Shift+;).
Date,
Time,
/// Ctrl+' / Ctrl+Shift+": the formula / value of the cell above, ready to edit.
FormulaAbove,
ValueAbove,
/// Extend the selection to A1, to the last used cell, or to the start of the row.
ExtendToStart,
ExtendToEnd,
ExtendToRowStart,
/// Alt+Page Down / Alt+Page Up: one screen right / left.
ScreenRight,
ScreenLeft,
/// Excel for Mac Control+U: edit the active cell (like F2).
Edit,
/// Shift+F10: the cell context menu.
ContextMenu,
/// Alt+Down: the active cell's drop-down list.
DropDown,
/// Ctrl+Alt+= / Ctrl+Alt+-: zoom in / out.
Zoom(i32),
}
fn run(id: &'static str) -> Shortcut {
Shortcut::Run(id, json!({}))
}
fn number_format(code: &str) -> Shortcut {
Shortcut::Run("home.numberFormat", json!({"code": code}))
}
fn borders(preset: &str) -> Shortcut {
Shortcut::Run("home.borders", json!({"preset": preset}))
}
/// The shortcut for a key press in the grid, if any. `physical` is the key's position on a US
/// layout (as Excel matches Ctrl+Shift+4 rather than the `$` character); `mac` selects the Excel
/// for Mac variants where they differ.
pub fn lookup(key: Key, physical: Option<Key>, m: Modifiers, mac: bool) -> Option<Shortcut> {
use Key::*;
let k = physical.unwrap_or(key);
// Control itself (Excel for Mac also uses Control for these), Ctrl on Windows / Cmd on Mac,
// and Alt/Option alone.
let ctrl = m.ctrl && !m.alt;
let cmd = m.command && !m.alt;
let alt = m.alt && !m.ctrl && !m.command;
let ctrl_alt = m.alt && (m.ctrl || m.command);
let plain = !m.ctrl && !m.command && !m.alt;
let s = m.shift;
Some(match k {
// Ctrl+Shift+~ ! @ # $ % ^ & _ : number formats and borders (Windows and Mac).
Backtick if ctrl && s => Shortcut::Run("home.numberFormat", json!({"format": "General"})),
Num1 if ctrl && s => number_format("#,##0.00"),
Num2 if ctrl && s => number_format("h:mm AM/PM"),
Num3 if ctrl && s => number_format("d-mmm-yy"),
Num4 if ctrl && s => number_format("$#,##0.00_);($#,##0.00)"),
Num5 if ctrl && s => number_format("0%"),
Num6 if ctrl && s => number_format("0.00E+00"),
Num7 if ctrl && s => borders("outside"),
Minus if ctrl && s => borders("none"),
Num8 if ctrl && s => run("selection.currentRegion"),
Semicolon if ctrl && s => Shortcut::Time,
Quote if ctrl && s => Shortcut::ValueAbove,
Quote if ctrl => Shortcut::FormulaAbove,
// Windows Ctrl+; is in `ribbon::shortcut`; Excel for Mac uses Control+; and Command+;.
Semicolon if mac && ctrl => Shortcut::Date,
Semicolon if mac && cmd && !s => Shortcut::Time,
// Ctrl+2..5 (Windows).
Num2 if ctrl && !mac => run("home.bold"),
Num3 if ctrl && !mac => run("home.italic"),
Num4 if ctrl && !mac => run("home.underline"),
Num5 if ctrl && !mac => run("home.strikethrough"),
// Letters that differ between Windows and Mac.
L if (cmd || ctrl) && s => run("data.filter"),
L if cmd && mac => run("home.alignLeft"),
L if ctrl && mac => Shortcut::Dialog("defineName", json!({})),
U if ctrl && mac && !s => Shortcut::Edit,
L if cmd => run("insert.table"),
E if cmd && mac && !s => run("home.alignCenter"),
E if ctrl && mac && !s => run("edit.flashFill"),
F if cmd && s && !mac => Shortcut::Dialog("formatCells", json!({"tab": "Font"})),
G if cmd && s && !mac => Shortcut::Dialog("statistics", json!({})),
H if cmd && s && mac => Shortcut::Dialog("find", json!({"replace": true})),
R if cmd && s && mac => Shortcut::Dialog("sort", json!({})),
K if cmd && s && mac => run("data.group"),
J if cmd && s && mac => run("data.ungroup"),
P if cmd && !s => run("file.print"),
Period if cmd && s => run("home.increaseFontSize"),
Comma if cmd && s => run("home.decreaseFontSize"),
Space if cmd && s => run("edit.selectAll"),
Home if cmd && s => Shortcut::ExtendToStart,
End if cmd && s => Shortcut::ExtendToEnd,
Home if plain && s => Shortcut::ExtendToRowStart,
// Ctrl+Alt / Cmd+Option combinations.
V if ctrl_alt || (mac && m.ctrl && m.command) => Shortcut::Dialog("pasteSpecial", json!({})),
Equals | Plus if ctrl_alt => Shortcut::Zoom(10),
Minus if ctrl_alt && !mac => Shortcut::Zoom(-10),
F9 if ctrl_alt => run("formulas.calculateNow"),
R if ctrl_alt && mac => run("view.collapseRibbon"),
Num0 if ctrl_alt && mac => borders("outside"),
Minus if ctrl_alt && mac => borders("none"),
ArrowLeft if ctrl_alt && mac => borders("left"),
ArrowRight if ctrl_alt && mac => borders("right"),
ArrowUp if ctrl_alt && mac => borders("top"),
ArrowDown if ctrl_alt && mac => borders("bottom"),
// Alt / Option alone.
Equals if alt && !s && !mac => run("formulas.autoSum"),
ArrowDown if alt && !s => Shortcut::DropDown,
PageDown if alt => Shortcut::ScreenRight,
PageUp if alt => Shortcut::ScreenLeft,
ArrowRight if alt && s && !mac => run("data.group"),
ArrowLeft if alt && s && !mac => run("data.ungroup"),
ArrowRight if alt && !s && mac => run("sheet.next"),
ArrowLeft if alt && !s && mac => run("sheet.previous"),
F1 if alt && s => run("home.insertSheet"),
F1 if alt => run("insert.chart"),
F10 if alt && s => Shortcut::Dialog("errorChecking", json!({})),
// Function keys.
F1 if cmd && !s => run("view.collapseRibbon"),
F2 if (cmd || ctrl) && s => Shortcut::Dialog("comment", json!({"threaded": true})),
F2 if plain && s => Shortcut::Dialog("comment", json!({"threaded": false})),
F3 if cmd && s => run("formulas.createFromSelection"),
F3 if cmd && mac => Shortcut::Dialog("defineName", json!({})),
F3 if cmd => Shortcut::Dialog("nameManager", json!({})),
F3 if plain && s => Shortcut::Dialog("insertFunction", json!({})),
F4 if cmd && !s => run("file.close"),
F5 if plain && s => Shortcut::Dialog("find", json!({})),
F5 if plain => Shortcut::Dialog("goTo", json!({})),
F7 if plain && !s => Shortcut::Dialog("spelling", json!({})),
F9 if plain && s => run("formulas.calculateSheet"),
F10 if plain && s => Shortcut::ContextMenu,
F11 if plain && s => run("home.insertSheet"),
F12 if plain && !s => run("file.saveAs"),
_ => return None,
})
}
/// Handles a key press in the grid. Returns whether it was a shortcut.
pub(crate) fn dispatch(app: &mut SheetApp, ctx: &egui::Context, key: Key, physical: Option<Key>, m: Modifiers, geo: &Geo, sh: &Sheet) -> bool {
let mac = ctx.os() == egui::os::OperatingSystem::Mac;
// AltGr arrives as Ctrl+Alt on Windows: a key that also typed a character (AltGr+= is `~`
// on a German layout) is text, not a shortcut.
if !mac && m.alt && m.ctrl && ctx.input(|i| i.events.iter().any(|e| matches!(e, egui::Event::Text(_)))) {
return false;
}
let Some(shortcut) = lookup(key, physical, m, mac) else { return false };
let (active, anchor) = app.session.active().map(|d| (d.selection.active, d.selection.anchor)).unwrap_or_default();
match shortcut {
Shortcut::Run(id, params) => {
app.run_or_alert(id, params);
app.grid.ensure_visible = true;
}
Shortcut::Dialog(name, params) => app.open_dialog(name, params),
Shortcut::Date | Shortcut::Time => {
let now = gridcraft_engine::calc::now_serial();
let (value, format) = if shortcut == Shortcut::Date { (now.floor(), "m/d/yyyy") } else { (now.fract(), "h:mm AM/PM") };
let text = app
.session
.active()
.map(|d| gridcraft_engine::display::format(&gridcraft_engine::core::Value::Number(value), format, &d.wb).text)
.unwrap_or_default();
edit_with(app, text);
}
Shortcut::FormulaAbove | Shortcut::ValueAbove => {
let Some(above) = active.row.checked_sub(1).map(|r| CellRef::new(r, active.col)) else { return true };
let text = if shortcut == Shortcut::FormulaAbove {
sh.cell(above).map(|c| c.input_text()).unwrap_or_default()
} else {
app.session
.active()
.and_then(|d| sh.value_ref(above).map(|v| gridcraft_engine::display::format(v, "General", &d.wb).text))
.unwrap_or_default()
};
edit_with(app, text);
}
Shortcut::ExtendToStart => extend(app, anchor, active, CellRef::new(0, 0)),
Shortcut::ExtendToEnd => {
let end = sh.used_range().map(|u| u.end).unwrap_or_default();
extend(app, anchor, active, end);
}
Shortcut::ExtendToRowStart => extend(app, anchor, active, CellRef::new(active.row, 0)),
Shortcut::ScreenRight | Shortcut::ScreenLeft => {
let width = ((geo.cells.width() - geo.frozen_w) / geo.z).max(1.0);
let cols = (width / sh.default_col_width.max(1.0)).floor().max(1.0) as i64;
let dc = if shortcut == Shortcut::ScreenRight { cols } else { -cols };
let _ = app.session.run("selection.move", json!({"dr": 0, "dc": dc, "extend": m.shift}));
app.grid.ensure_visible = true;
}
Shortcut::Edit => app.begin_edit(None, false),
Shortcut::ContextMenu => app.grid.context_menu = Some(geo.cell_rect(sh, active).center()),
Shortcut::DropDown => {
if sh.validations.iter().any(|d| d.in_cell_dropdown && d.ranges.iter().any(|r| r.contains(active))) {
app.grid.list_picker = Some(active);
}
}
Shortcut::Zoom(step) => {
let percent = (i32::from(sh.zoom).saturating_add(step)).clamp(10, 400);
app.run_or_alert("view.zoom", json!({"percent": percent}));
}
}
true
}
/// Starts editing the active cell with `text` and the caret at its end.
fn edit_with(app: &mut SheetApp, text: String) {
app.begin_edit(Some(text), false);
if let Some(ed) = app.editor.as_mut() {
ed.caret = ed.text.chars().count();
}
}
/// Selects `anchor`..`to`, keeping the anchor and the active cell.
fn extend(app: &mut SheetApp, anchor: CellRef, active: CellRef, to: CellRef) {
let _ = app.session.run("selection.set", json!({"range": RangeRef::new(anchor, to).a1(), "active": active.a1()}));
if let Some(d) = app.session.active_mut() {
d.selection.anchor = anchor;
}
app.grid.ensure_visible = true;
}

View File

@ -18,6 +18,7 @@ mod formula_locale;
pub mod grid;
pub mod i18n;
pub mod icons;
pub mod keymap;
pub mod keytips;
pub mod panes;
pub mod pivot_pane;

View File

@ -0,0 +1,108 @@
//! Excel keyboard shortcuts (crate::keymap) dispatched through real egui key events.
use egui::{Event, Key, Modifiers};
use gridcraft_engine::Session;
use gridcraft_engine::core::CellRef;
use gridcraft_engine::model::BorderStyle;
use gridcraft_ui_egui::SheetApp;
use gridcraft_ui_egui::keymap::{Shortcut, lookup};
use serde_json::json;
type Harness = egui_kittest::Harness<'static, SheetApp>;
fn harness(session: Session) -> Harness {
let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state(
|ui, app: &mut SheetApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
SheetApp::new(session, Default::default()),
);
h.run_steps(4);
h
}
fn book(selected: &str) -> Session {
let mut s = Session::new();
s.new_workbook();
s.execute("range.setValues", json!({"range": "A1", "values": [[1.5, 2], [3, 4], [5, 6]]})).unwrap();
s.execute("cell.set", json!({"cell": "C1", "input": "=A1*2"})).unwrap();
s.execute("selection.set", json!({"range": selected})).unwrap();
s
}
/// A key press as a US keyboard reports it (the physical key is the unshifted one).
fn key(h: &mut Harness, k: Key, m: Modifiers) {
h.input_mut().events.push(Event::Key { key: k, physical_key: Some(k), pressed: true, repeat: false, modifiers: m });
h.input_mut().events.push(Event::Key { key: k, physical_key: Some(k), pressed: false, repeat: false, modifiers: m });
h.run_steps(2);
}
fn style(h: &Harness, a1: &str) -> gridcraft_engine::model::Style {
let d = h.state().session.active().unwrap();
let sh = d.wb.active().unwrap();
d.wb.styles.get(sh.style_id(CellRef::parse(a1).unwrap())).clone()
}
/// Ctrl on Windows and Linux sets both `ctrl` and `command`.
const CTRL: Modifiers = Modifiers { alt: false, ctrl: true, shift: false, mac_cmd: false, command: true };
const CTRL_SHIFT: Modifiers = Modifiers { shift: true, ..CTRL };
#[test]
fn ctrl_shift_symbols_apply_number_formats_and_borders() {
let mut h = harness(book("A1:B2"));
key(&mut h, Key::Num4, CTRL_SHIFT);
assert_eq!(style(&h, "B2").num_fmt.as_str(), "$#,##0.00_);($#,##0.00)");
key(&mut h, Key::Num5, CTRL_SHIFT);
assert_eq!(style(&h, "A1").num_fmt.as_str(), "0%");
key(&mut h, Key::Backtick, CTRL_SHIFT);
assert_eq!(style(&h, "A1").num_fmt.as_str(), "General");
key(&mut h, Key::Num7, CTRL_SHIFT);
assert!(style(&h, "A1").borders.top.style != BorderStyle::None && style(&h, "B2").borders.right.style != BorderStyle::None);
key(&mut h, Key::Minus, CTRL_SHIFT);
assert!(style(&h, "A1").borders.top.style == BorderStyle::None);
// Ctrl+Shift+End extends from the anchor to the last used cell.
key(&mut h, Key::End, CTRL_SHIFT);
assert_eq!(h.state().session.active().unwrap().selection.current().a1(), "A1:C3");
}
#[test]
fn copy_formula_from_above_and_function_keys() {
let mut h = harness(book("C2"));
key(&mut h, Key::Quote, CTRL);
assert_eq!(h.state().editor.as_ref().map(|e| e.text.clone()).as_deref(), Some("=A1*2"));
key(&mut h, Key::Escape, Modifiers::NONE);
assert!(h.state().editor.is_none());
let sheets = h.state().session.active().unwrap().wb.sheets.len();
key(&mut h, Key::F11, Modifiers::SHIFT);
assert_eq!(h.state().session.active().unwrap().wb.sheets.len(), sheets + 1);
// Back on the data: Alt+F1 charts the current region, Ctrl+Alt+= zooms in.
h.state_mut().session.run("sheet.previous", json!({})).unwrap();
h.state_mut().session.run("selection.set", json!({"range": "A1:B3"})).unwrap();
key(&mut h, Key::F1, Modifiers::ALT);
assert_eq!(h.state().session.active().unwrap().wb.sheets[0].charts.len(), 1);
key(&mut h, Key::Equals, Modifiers { alt: true, ..CTRL });
assert_eq!(h.state().session.active().unwrap().wb.sheets[0].zoom, 110);
}
#[test]
fn windows_and_mac_variants() {
let cmd = CTRL;
let mac_cmd = Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE };
let ctrl = Modifiers::CTRL;
let run = |id: &'static str| Some(Shortcut::Run(id, json!({})));
assert_eq!(lookup(Key::L, Some(Key::L), cmd, false), run("insert.table"));
assert_eq!(lookup(Key::L, Some(Key::L), mac_cmd, true), run("home.alignLeft"));
assert_eq!(lookup(Key::E, Some(Key::E), mac_cmd, true), run("home.alignCenter"));
assert_eq!(lookup(Key::E, Some(Key::E), ctrl, true), run("edit.flashFill"));
assert_eq!(lookup(Key::Equals, Some(Key::Equals), Modifiers::ALT, false), run("formulas.autoSum"));
// The shifted character doesn't matter: Ctrl+Shift+1 is the Number format wherever `!` is.
assert_eq!(
lookup(Key::Exclamationmark, Some(Key::Num1), CTRL_SHIFT, false),
Some(Shortcut::Run("home.numberFormat", json!({"code": "#,##0.00"})))
);
// Plain keys stay with the grid (typing, navigation).
assert_eq!(lookup(Key::A, Some(Key::A), Modifiers::NONE, false), None);
assert_eq!(lookup(Key::F2, Some(Key::F2), Modifiers::NONE, false), None);
}

View File

@ -57,10 +57,62 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes
| F2, F4, F5, F9 family, Shift+F3, F11 | yes | most; F11 chart sheet missing (no chart sheets) |
| Mac-specific (Cmd variants, Fn keys) | yes | Cmd mapping done |
| Delete selected charts, pictures and shapes | yes | done (#213) |
| Shortcuts bound to commands | 200+ documented shortcuts per platform | audited below (#110): shortcuts whose feature exists are bound, with the exceptions listed; the rest wait for their features |
| Ctrl+arrow, Ctrl+Shift+arrow, Ctrl+Home/End, Ctrl+Shift+Home/End, Page Up/Down, Alt+Page | yes | done |
| Ctrl+1, Ctrl+; and Ctrl+Shift+; , Ctrl+D/R, Ctrl+E, Ctrl+T/L, Ctrl+K, Ctrl+Shift+L, Ctrl+Shift+~!@#$%^&_ | yes | done |
| F2, F4, F5, F7, F9 family, Shift+F2/F3/F10/F11, F12 | yes | done; F11 chart sheet, F8 extend mode, F3 Paste Name, F4 Repeat missing |
| Mac-specific (Cmd variants, Control variants, Fn keys) | yes | done where a feature exists (`keymap.rs` picks the Mac variant) |
| Requests | — | #110 (more shortcuts and hot keys), #33 (sequential Alt; initial support landed in #43) |
Work: a shortcut table generated from the command catalog checked against Microsoft's published
shortcut lists for Windows and Mac, then fill the gaps: 6–10 h.
### Shortcut audit (#110)
Checked against Microsoft's published "Keyboard shortcuts in Excel" (Windows and Mac tables,
2026-10-11). Alt key-tip sequences are tracked separately (#33). Bindings live in
`crates/ui-egui/src/grid.rs` (navigation, editing keys), `ribbon::shortcut` (Ctrl/Cmd letters),
`keymap.rs` (everything added in #110, with a `lookup` table that tests cover) and
`formula_bar.rs` (keys while editing). "Ctrl" means Cmd on a Mac unless the Mac column says
otherwise. Number-format and border keys match the physical key (Ctrl+Shift+4, wherever `$` is).
| Shortcut (Windows) | Mac | Action | Status |
|---|---|---|---|
| Ctrl+C / X / V, Ctrl+Z / Y, Ctrl+A, Ctrl+B / I / U | same with Cmd | clipboard, undo/redo, select all, bold/italic/underline | present |
| Ctrl+S, Ctrl+Shift+S, Ctrl+O, Ctrl+N, Ctrl+W | Cmd | save, save as, open, new, close | present |
| Ctrl+F, Ctrl+H, Ctrl+G, Ctrl+K, Ctrl+T | Cmd (Ctrl+H) | find, replace, go to, hyperlink, table | present |
| Ctrl+D, Ctrl+R, Ctrl+E, Ctrl+1, Ctrl+9 / 0, Ctrl+Shift+9 / 0 | Cmd (Ctrl+E on Mac: see below) | fill down/right, Flash Fill, Format Cells, hide/unhide | present |
| Ctrl+- , Ctrl+Shift+= | Cmd | Delete / Insert cells dialog | present |
| Ctrl+; , Ctrl+` | Control+; | current date, show formulas | present (Mac Control+; added) |
| Arrows, Shift / Ctrl / Ctrl+Shift+arrows, Home, Ctrl+Home / End, Page Up / Down, Ctrl+Page Up / Down | Cmd+arrows | navigation and extension | present |
| Tab, Shift+Tab, Enter, Shift+Enter, Ctrl+Space, Shift+Space, Delete, Esc | same | entry and selection | present |
| F2, F9, F10 (key tips), F4 / Alt+Enter / Ctrl+Enter / Ctrl+Shift+Enter while editing | same | edit, calculate, references, new line, fill, array | present |
| Ctrl+Shift+~ ! @ # $ % ^ | Control+Shift | General, Number, Time, Date, Currency, Percent, Scientific | **added** |
| Ctrl+Shift+& / Ctrl+Shift+_ | Cmd+Option+0 / Cmd+Option+- (and Cmd+Option+arrows for one side) | outline border / remove borders | **added** |
| Ctrl+2 / 3 / 4 / 5 | — | bold / italic / underline / strikethrough | **added** |
| Ctrl+Shift+: | Cmd+; | current time | **added** |
| Ctrl+' / Ctrl+Shift+" | Control | formula / value from the cell above | **added** |
| Ctrl+L | Cmd+L is Align Left, Cmd+E Center, Control+E Flash Fill | create table | **added** |
| Ctrl+Shift+L | Cmd+Shift+F (present) / Control+Shift+L | filter | **added** |
| Ctrl+Shift+F, Ctrl+Shift+G | — | Format Cells Font tab, Workbook Statistics | **added** |
| — | Cmd+Shift+H, Cmd+Shift+R, Cmd+Shift+K / J, Control+L, Control+U | replace, sort, group / ungroup, Define Name, edit cell | **added** |
| Alt+Shift+Right / Left | Option+Right / Left is next/previous sheet | group / ungroup | **added** |
| Ctrl+Shift+* (Ctrl+Shift+8), Ctrl+Shift+Space | Cmd+Shift+Space | current region, select all | **added** |
| Ctrl+Shift+Home / End, Shift+Home | same | extend to A1 / last used cell / row start | **added** |
| Ctrl+P | Cmd+P | print | **added** |
| Ctrl+Shift+> / < | Cmd+Shift+> / < | font size | **added** |
| Ctrl+Alt+V | Cmd+Control+V, Cmd+Option+V | Paste Special | **added** |
| Ctrl+Alt+= / Ctrl+Alt+- | — | zoom in / out (10% steps) | **added** |
| Alt+=, Alt+Down, Alt+Page Down / Up | Option+Down, Option+Page Down / Up | AutoSum, drop-down list, one screen right / left | **added** |
| Alt+F1, Alt+Shift+F1, Shift+F11 | Shift+F11 | embedded chart, new sheet | **added** |
| Shift+F2, Ctrl+Shift+F2 | Shift+F2, Cmd+Shift+F2 | note, threaded comment | **added** |
| Shift+F3, Ctrl+F3, Ctrl+Shift+F3 | Shift+F3, Cmd+F3 (Define Name), Cmd+Shift+F3 | Insert Function, Name Manager, Create from Selection | **added** |
| F5, Shift+F5 (Mac), F7, Shift+F9, Ctrl+Alt+F9, F12, Ctrl+F4 | same | Go To, Find, Spelling, calculate sheet / all, Save As, close | **added** |
| Shift+F10, Alt+Shift+F10, Ctrl+F1 | Shift+F10, Cmd+Option+R | context menu, Error Checking, ribbon | **added** |
| F1, F3, F4 (repeat), F6 / Shift+F6, F8 / Shift+F8, F11, End mode | same | help, Paste Name, Repeat, pane cycling, extend/add mode, chart sheet | missing (no such feature yet) |
| Ctrl+6, Ctrl+8, Ctrl+Q, Ctrl+Shift+A, Ctrl+A after a function name, Ctrl+Shift+U, Ctrl+Alt+P | same | objects, outline symbols, Quick Analysis, argument names, Function Arguments, formula bar height, tooltips | missing |
| Ctrl+Shift+Page Up / Down, Ctrl+Alt+5, Ctrl+Shift+F1, Ctrl+F5–F10, Alt+F8 / F11 / F12 | same | group sheets, shape cycling, full screen, window keys, macros, VBA, Power Query | missing |
| — | Cmd+= (calculate), Cmd+T (references while editing; F4 works), Shift+F7, Cmd+Shift+L | Mac-only extras | missing (Cmd+= is egui's zoom key; no thesaurus or Modify Cell Style) |
On Windows, AltGr arrives as Ctrl+Alt: a Ctrl+Alt key that also types a character is treated as
text, so AltGr symbols (e.g. `~` on a German layout) never trigger a shortcut.
## Dialogs
@ -84,7 +136,7 @@ by rule and will never match Office's, by design.
## Work (50–80 h)
Key tips 3–5 · shortcut audit 6–10 · context menus and Quick Analysis 5–8 · Page Layout view
Key tips 3–5 · remaining shortcuts with their features 2–4 · context menus and Quick Analysis 5–8 · Page Layout view
8–12 · Options dialog and ribbon/QAT customization 6–10 · accessibility 12–18 · selection and
scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
@ -95,5 +147,6 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
| 2026-10-11 | minor | Delete and Backspace now remove the selected chart, picture or shape (#213) |
| 2026-10-10 | minor | Keep formula helpers inside the worksheet when scrolling (#200) |
| 2026-10-11 | trivial | Theme: title-bar light/dark switch, File › Display theme, system appearance on first run (#194, #239) |
| 2026-10-11 | minor | Keyboard shortcut audit against Microsoft's Windows and Mac lists; missing shortcuts for existing features added (#110) |
| 2026-10-10 | minor | Chart resize bug (#190) |
| 2026-10-10 | major | Created: chrome, grid, keyboard, dialogs, accessibility; key tips, Page Layout view and accessibility tree flagged |