Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-0095: A shortcut is built from enums

  • Status: Accepted
  • Date: 2026-08-18
  • Relates to: docs/design-system.md §2.3, docs/ARCHITECTURE.md §7.2

Context

An accelerator had one way in: Shortcut.of("Ctrl+S"). The string was parsed at runtime against a tolerant table of spellings, and a typo — "Crtl+S", "Ctrl-S", "Ctrl+Save" — threw when the line ran, which for a shortcut bound at start-up means at start-up and for one bound lazily means whenever.

Modifiers had the matching problem from the other direction: four positional booleans. new Modifiers(true, false, false, false) is four chances to get the order wrong and nothing to catch it, and 23 call sites wrote them out.

The request was Mod.CTRL | Key.A.

Decision

A Mod enum with a real bitmask, composed with and.

Mod.CTRL.and(Key.S)                    // Ctrl+S
Mod.CTRL.and(Mod.SHIFT).and(Key.Z)     // Ctrl+Shift+Z
Shortcut.of(Key.F5)                    // F5

| is not available and the alternative that is would be worse. | is defined for the integral types and boolean and Java does not allow overloading it. The spelling that would compile — Mod.CTRL.bit() | Mod.SHIFT.bit() passed to a method taking an int — is a mask with nothing checking it, and Key.A.ordinal() | Mod.CTRL.bit() would compile and mean nothing. So the mask is real and private to the arithmetic: bit() exists for the SDL boundary and for tests, and an application composes with and, which can only produce Modifiers or a Shortcut.

Modifiers is one int instead of four booleans, with has(Mod), only(Mod) and set() on top and the four boolean accessors kept so no call site changed. The four-boolean constructor stays as a secondary one — it reads fine where all four are literals and is a trap where they are computed, which is exactly the distinction between a secondary constructor and a canonical one.

Shortcut.of(String) stays. An accelerator has to be printed beside a menu item anyway, and a configuration file has nothing but text. What changed is that it is no longer the only way in.

Host.shortcut and PointerRouter.shortcut take both forms, and the showcase uses the enum one.

Alternatives considered

An int mask parameter — the literal reading of the request. Rejected above: it accepts any int, including ones that mean nothing.

EnumSet<Mod>. Type-safe and idiomatic, and heavier than the thing it describes: a Shortcut is a map key on the keyboard path, and an EnumSet allocation per comparison for four possible bits is a poor trade. set() returns one for callers that want it.

Keep the four booleans and only add Mod for shortcuts. Half the change, and it leaves new Modifiers(true, false, false, false) in 23 places — the exact thing the enum was asked for.

Translate Cmd to Ctrl on macOS. Unchanged from before and still refused: a toolkit that silently remapped them would make Ctrl+C mean two different things depending on where it ran. docs/ARCHITECTURE.md §17.1 still records that design-system.md §2.3 wants the opposite, and this ADR does not settle it.

Consequences

A shortcut built in Java cannot be misspelled. A parsed one still can, and that is the price of keeping the string form for menus and config.

Modifiers is a different record shape, so anything pattern-matching its four components would break. Nothing did — checked before the change.

One mask layout is now load-bearing in two places: Mod.bit() and Modifiers.fromSdl. They are in the same file as each other, and the constructor rejects bits no Mod owns, so a third layout cannot appear quietly.

Key is untouched. It is already an enum and already the type Shortcut holds; only the modifier half needed the work.