“Tap Option, then type H, V, V” sounds simple. But Option is also a key people hold while typing, and a shortcut tool sits between your keyboard and your live workbook. If it guesses wrong, a stray letter lands in a cell.
KeyTips handles this with a small state machine. It has three states, and it’s always in exactly one:
| State | Meaning | What moves it on |
|---|---|---|
| Idle | Nothing is happening, and keys pass straight through | Option goes down, moving it to armed |
| Armed | Option is down, and this might become a tap | Releasing Option moves it to active; anything else returns it to idle |
| Active | The tap happened, and it’s collecting a sequence | A full match runs the command and returns to idle |
If you’ve built a month-end checklist with statuses, you already know this idea. Each item is in one status, and only certain events can move it to the next.
A tap has to be a clean tap
The armed state is what separates a tap from a hold. If you press Option and then another key, as you would to type a special character, it isn’t a tap. The tool goes back to idle and your typing is left alone.
A typo shouldn’t reach the cell
In active mode, a key that doesn’t match anything is swallowed and you stay in the mode:
if holdPrefixes.contains(candidate) || map.commands.contains(where: { $0.keys.hasPrefix(candidate) }) {
state = .active(prefix: candidate)
}
// An unknown key stays in the mode, so a typo can't land in a cell.
return .swallow
Press the wrong letter, and nothing is typed into your model. It’s a small line that protects something a finance person cares about a lot.
Getting back out
Esc removes the last key typed, and at the start of a sequence it exits. Press another key, such as an arrow, Return or a Command shortcut, and the sequence ends and that key goes through as normal.
Why keep it separate
This logic has no connection to macOS at all. It takes inputs and returns decisions, so it can be tested without opening Office. A test can feed it “Option down, Option up, H” and check the result in an instant.
It’s the same reason a good model keeps inputs and calculations apart: you can check the logic without touching anything live.