Documentation
¶
Overview ¶
Package hotkey claims system-wide keyboard shortcuts on macOS from pure Go (CGO_ENABLED=0), and falls back to a neighbouring combination when the one you asked for is already taken.
A hot key registered here fires while the user is working in ANOTHER application. That is the whole point: the consumer is an XR virtual-desktop app whose ribbon of screens is turned from the keyboard while the user types inside the applications on those screens. A shortcut that only works when your own window is focused would be useless to it.
No permission is required ¶
The two obvious routes — CGEventTap and -[NSEvent addGlobalMonitorForEventsMatchingMask:] — both demand the Accessibility (TCC) grant, which means a system dialog and a trip to System Settings. This package instead uses Carbon's hot-key API (RegisterEventHotKey), which is still present on macOS 26 and needs no permission at all. Registering ⌥⌘← on macOS 26.6.2 produced no dialog.
Three kinds of conflict, two of them detectable ¶
RegisterEventHotKey does NOT conflict-check against system shortcuts. This is the single most important thing to understand about it, and the reason a naive implementation is useless:
- Another Carbon hot-key holder — DETECTED. Registration returns eventHotKeyExistsErr (-9878), surfaced as ErrComboTaken.
- A macOS system shortcut (⌥⌘Space is the Finder's search window) — NOT detected by registration, which returns 0 for it anyway. This package catches these itself, before registering, with SystemShortcuts. See that type for exactly how dependable that is.
- An ordinary application's own menu key equivalent — for example Safari's ⌥⌘← for "previous tab". NOT DETECTABLE, by this package or any other. Nothing on macOS enumerates other applications' menu shortcuts, and such a shortcut is only live while that application is frontmost. If you claim one, you will win it globally and that application will silently stop seeing it. There is no coverage here and this package does not pretend otherwise.
Portability ¶
Every exported symbol is defined on all platforms so consumers cross-compile. On non-darwin GOOS the registration entry points report ErrUnsupported; the whole policy layer — the fallback ladder, combination formatting, and the parsing of the system-shortcut data — is OS-independent and fully testable anywhere.
Index ¶
- Variables
- func KeyNames() string
- func Resolve(want Combo, ladder []Modifier, reg Registrar, reserved Reserver) (Combo, Claim, error)
- func ResolveBare(want Combo, ladder []Modifier, reg Registrar, reserved Reserver) (Combo, Claim, error)
- type Claim
- type Combo
- type Event
- type Hotkey
- type Key
- type Modifier
- type NoReserved
- type Options
- type Origin
- type Override
- type Registrar
- type Reserver
- type SystemShortcut
- type SystemShortcuts
Constants ¶
This section is empty.
Variables ¶
var ( // ErrUnsupported is returned by the registration entry points on // non-darwin platforms (Carbon is macOS-only). ErrUnsupported = errors.New("hotkey: unsupported on this platform (darwin only)") // ErrComboTaken reports that the combination is already held. It wraps // both detectable conflict kinds: another Carbon hot-key holder // (eventHotKeyExistsErr, -9878) and a macOS system shortcut found by // [SystemShortcuts]. ErrComboTaken = errors.New("hotkey: combination already taken") // ErrNoCandidate reports that neither the wanted combination nor any // rung of the fallback ladder could be claimed. Nothing was registered. ErrNoCandidate = errors.New("hotkey: every candidate combination is taken") // ErrNoModifier reports a combination with no modifier at all. Carbon // accepts one, but claiming a bare key system-wide would swallow that // key everywhere, in every application, which is never what a caller // means. ErrNoModifier = errors.New("hotkey: a system-wide hot key needs at least one modifier") // ErrClosed reports use of a [Hotkey] that has already been released. ErrClosed = errors.New("hotkey: hot key already released") )
Errors reported by the package. They are stable and may be tested with errors.Is.
DefaultLadder is the order in which Resolve tries neighbouring combinations when the wanted one is taken: the same key with Shift added, then with Control added, then with both.
The order is deliberate. Shift first because ⇧ combined with an existing modifier set is the least likely to collide with anything and reads most naturally on a menu; Control last-but-one because ⌃ is heavily used by the terminal and by text-editing key bindings; both together last because it is the most awkward to press.
var ErrParse = errors.New("hotkey: unreadable combination")
ErrParse says a written combination could not be read.
Functions ¶
func KeyNames ¶ added in v0.2.0
func KeyNames() string
KeyNames lists every key name this package accepts, sorted, for an error message that tells a person what they may write instead of what they wrote.
func Resolve ¶
Resolve walks the fallback ladder and claims the first combination that is free, returning which one it got.
Each candidate is first put to reserved (the system-shortcut check, conflict kind 2), and only then to reg.Claim (conflict kind 1). The order matters: a system shortcut registers with status 0, so asking Carbon first would "succeed" at claiming a combination the user cannot actually use.
If every candidate is taken, Resolve returns ErrNoCandidate and nothing is registered. It never silently returns an unusable claim.
func ResolveBare ¶ added in v0.6.0
func ResolveBare(want Combo, ladder []Modifier, reg Registrar, reserved Reserver) (Combo, Claim, error)
ResolveBare is Resolve for a combination with no modifier. See Options.BareKey for when that is a reasonable thing to want, and for the obligation that comes with it.
Types ¶
type Claim ¶
type Claim interface {
Release() error
}
Claim is a held hot key. Release gives the combination back to the system.
type Combo ¶
Combo is a key plus its modifiers — one keyboard shortcut.
func Candidates ¶
Candidates returns the combinations Resolve will try, in order: the wanted one first, then the wanted one with each ladder rung's modifiers added.
A rung that adds nothing new — because the caller already asked for that modifier — is skipped rather than retried, so asking for ⇧⌥⌘← does not try ⇧⌥⌘← twice. Duplicate rungs are likewise collapsed.
func ParseCombo ¶ added in v0.2.0
ParseCombo reads a combination a person wrote down.
It exists because a configuration file is where a shortcut is CHANGED, and a person editing one should not have to reach for the glyph palette. All three of these are the same combination:
option+command+space Option-Command-Space ⌥⌘Space
Modifiers and the key may be separated by "+", "-", or spaces, in any order and any case; the glyph forms need no separator at all. Every name this package prints is accepted, so Combo.String and Combo.Names both round trip through here — which is the property its test asserts, over every key the package names.
A combination with no modifier is refused. Claiming a bare key system-wide takes it away from every application on the machine, including whatever the person is typing into.
func (Combo) Glyphs ¶ added in v0.7.0
Glyphs renders the combination as macOS prints it on a menu -- "⌃⌥⌘=" where Combo.String gives "⌃⌥⌘Equal".
Three renderings rather than two, because they answer three questions. String is what a settings file round-trips. Names is what a font that has no ⌘ can still show. This is what goes on a menu row beside the thing it does, and there a key that says "Equal" is a key somebody looks for and does not find.
⛔ IT ASKS THE KEYBOARD FIRST. A Key is a POSITION, and this package's names are the ANSI legends for those positions -- so on a French layout the key this package calls Equal is printed "-", and a menu row saying "⌃⌥⌘=" would be sending a person to a key that does nothing. Key.Char is what the system says is printed there; the ANSI name is the fallback for a key that prints nothing and for a platform with no layout to ask.
func (Combo) Names ¶
Names renders the combination in words — "Option-Command-←" — for logs and accessibility labels.
func (Combo) String ¶
String renders the combination the way macOS shows it to a person: "⌥⌘←", "⌃⌥⇧⌘Space". This is what you put in front of the user when the fallback gives them something other than what they asked for. A shortcut the user cannot be told about is worse than none.
func (Combo) Valid ¶
Valid reports whether the combination can be claimed system-wide. It requires at least one modifier; see ErrNoModifier.
type Event ¶
type Event struct {
// Combo is the combination that fired.
Combo Combo
// At is when the press was delivered.
At time.Time
}
Event is one press of a hot key. No press is ever delivered on this platform.
type Hotkey ¶
type Hotkey struct {
// contains filtered or unexported fields
}
Hotkey is a live system-wide hot key. On non-darwin platforms one can never be created, so no value of this type is ever handed out by Register; the type exists so that consumer code naming it still compiles.
func Register ¶
Register reports ErrUnsupported: system-wide hot keys here are Carbon's, and Carbon is macOS-only.
func (*Hotkey) Substituted ¶
Substituted reports whether the fallback ladder had to be used.
type Key ¶
type Key uint16
Key is a macOS virtual key code (the kVK_* constants from HIToolbox/Events.h). It is a hardware position, not a character: Key(0) is the key labelled "A" on a US layout and "Q" on a French one.
const ( KeyA Key = 0x00 KeyS Key = 0x01 KeyD Key = 0x02 KeyF Key = 0x03 KeyH Key = 0x04 KeyG Key = 0x05 KeyZ Key = 0x06 KeyX Key = 0x07 KeyC Key = 0x08 KeyV Key = 0x09 KeyB Key = 0x0B KeyQ Key = 0x0C KeyW Key = 0x0D KeyE Key = 0x0E KeyR Key = 0x0F KeyY Key = 0x10 KeyT Key = 0x11 KeyO Key = 0x1F KeyU Key = 0x20 KeyI Key = 0x22 KeyP Key = 0x23 KeyL Key = 0x25 KeyJ Key = 0x26 KeyK Key = 0x28 KeyN Key = 0x2D KeyM Key = 0x2E KeyN1 Key = 0x12 KeyN2 Key = 0x13 KeyN3 Key = 0x14 KeyN4 Key = 0x15 KeyN5 Key = 0x17 KeyN6 Key = 0x16 KeyN7 Key = 0x1A KeyN8 Key = 0x1C KeyN9 Key = 0x19 KeyN0 Key = 0x1D KeySlash Key = 0x2C // KeyMinus and KeyEqual are the two keys either side of the number row's // end, which is where a keyboard puts "smaller" and "larger". The virtual // codes are the US layout's, like every other key here: a hot key is // registered by CODE and the code is a POSITION, so on a French keyboard // these are the same two keys in the same place whatever is printed on them. KeyMinus Key = 0x1B KeyEqual Key = 0x18 // KeyLeftBracket and KeyRightBracket are the pair after P on the top row, // which is where a keyboard puts a matched set of opposites that nothing has // told anybody the meaning of. Same reasoning as Minus and Equal, and the same // caveat: the CODE is a position, so on a French keyboard these are the two // keys in that place whatever is printed on them. KeyLeftBracket Key = 0x21 KeyRightBracket Key = 0x1E // KeyISOSection is the EXTRA key an ISO keyboard has and an ANSI one does // not: the short one between the left Shift and the Z position, which Apple // calls kVK_ISO_Section. // // ⭐ IT IS WHERE A FRENCH MAC PRINTS "@". Measured on this machine: // position 0x0A prints "@" unshifted. There is nowhere else to look for it // -- no ANSI position on a French layout prints one -- so a shortcut on "@" // is this key or it is nothing. // // ⛔ NAMED AS A POSITION, deliberately, like Minus and the brackets. The // name is a WORD, so [onThisKeyboard] leaves it alone: a key named for what // it prints would be moved to the local key printing that legend, and this // key IS the local one. A layout that prints something else here -- "§" on // a Swiss keyboard, "`" on a British one -- gets the same physical key, // which is what a person pointing at their keyboard means. KeyISOSection Key = 0x0A KeyReturn Key = 0x24 KeyTab Key = 0x30 KeySpace Key = 0x31 KeyDelete Key = 0x33 KeyEscape Key = 0x35 KeyF1 Key = 0x7A KeyF2 Key = 0x78 KeyF3 Key = 0x63 KeyF4 Key = 0x76 KeyF5 Key = 0x60 KeyF6 Key = 0x61 KeyF7 Key = 0x62 KeyF8 Key = 0x64 KeyF9 Key = 0x65 KeyF10 Key = 0x6D KeyF11 Key = 0x67 KeyF12 Key = 0x6F KeyF13 Key = 0x69 KeyF14 Key = 0x6B KeyF15 Key = 0x71 KeyLeftArrow Key = 0x7B KeyRightArrow Key = 0x7C KeyDownArrow Key = 0x7D KeyUpArrow Key = 0x7E )
The virtual key codes this package names. Any other code is usable; it simply renders as "key 0x…" in a Combo string.
func KeyForChar ¶ added in v0.8.0
KeyForChar is the key that PRINTS this character on the current keyboard.
The inverse of Key.Char, and the one a settings file needs: somebody writing "=" means the key with "=" printed on it, not the position ANSI keeps "=" at. Matching is case-insensitive, so "a" and "A" find the same key.
It searches the codes this package NAMES, and no others: a layout can put a character on a position with no name here, and claiming one of those would produce a combination that cannot be written down again.
func (Key) Char ¶ added in v0.8.0
Char is what this key PRINTS on the keyboard in front of the person.
A Key is a virtual key code, which is a POSITION, and the names in this package are the ANSI legends for those positions. On a layout that is not ANSI the two come apart -- on French the position ANSI calls Equal prints "-", and "=" is over on the position ANSI calls Slash -- so a name is a claim about a keyboard nobody is typing on.
It answers "" where the system cannot say: a platform with no layout service, an input METHOD rather than a layout, or a key with no printed character at all -- an arrow, Escape, Return. A caller then has Key.String, which is at least a name a person can look up.
func (Key) Glyph ¶ added in v0.7.0
Glyph is the key as macOS prints it on a menu: "=" rather than "Equal".
It is Key.String for every key but the four whose printed character is one a written combination uses for something else. Use it for a MENU and for anything else drawn rather than parsed; use String where the result may be read back.
func (Key) Name ¶ added in v0.3.0
Name spells the key out, where Key.String would print the glyph macOS puts on a menu: "Left" rather than "←", "Return" rather than "↩".
The glyphs are right on a menu and in a terminal, and they are NOT in every font. Rendered in a window with a font that lacks them, "⌥⌘←" comes out as "Option-Command-" and stops — a line whose whole job is to say which combination was granted, saying nothing. So anywhere the font is not known, this is the one to use.
A key with no name of its own still renders as its hexadecimal code, which is honest rather than wrong.
func (Key) String ¶
String renders the key as macOS would print it on a menu — "←" for the left arrow, "Space" for the space bar. An unnamed code renders as its hexadecimal virtual key code, which is honest rather than wrong: this package does not consult the active keyboard layout, so it cannot know what character an arbitrary code produces.
type Modifier ¶
type Modifier uint8
Modifier is a set of modifier keys, as a bit set. It is deliberately NOT the Carbon bitmask nor the Cocoa one; both of those are derived from it, so a caller never has to know either.
The modifier keys, in Apple's canonical display order.
func ParseModifier ¶ added in v0.2.0
ParseModifier reads one modifier name — "shift", "Control", "⌘" — or a sum of them, "control+shift". It is what a fallback ladder is written with.
type NoReserved ¶
type NoReserved struct{}
NoReserved is a Reserver that reserves nothing. Use it to opt out of the system-shortcut check.
type Options ¶
type Options struct {
// Ladder overrides [DefaultLadder]. An explicitly empty (non-nil, len 0)
// ladder disables the fallback entirely: the wanted combination is
// claimed or [ErrNoCandidate] is returned.
Ladder []Modifier
// Reserved overrides the system-shortcut check. Leave it nil to use the
// machine's effective set ([LoadSystemShortcuts]). Set it to
// NoReserved{} to skip the check and let Carbon be the only authority,
// accepting that conflict kind 2 then goes undetected.
Reserved Reserver
// BareKey allows a combination with NO MODIFIER — a plain arrow, Return,
// Escape — which is otherwise refused with [ErrNoModifier].
//
// It is refused by default because a bare key claimed system-wide is taken
// from every application on the machine, and a caller who did that by
// accident would break typing everywhere. That reasoning holds for a claim
// that lasts as long as the program.
//
// It does not hold for a claim that lasts as long as a MODE. go-xrkit/desk
// puts a full-screen gallery on a pair of glasses and wants the arrows to
// move the selection in it — plain arrows, because a person looking at a
// grid should not have to hold three modifiers to walk it — and gives them
// back the moment the gallery closes. The person is not typing into
// anything while a gallery covers their view.
//
// So it is opt-in and named, and a caller who sets it is saying they know
// what they are taking. RELEASE IT: a bare key left claimed is a keyboard
// somebody else cannot use.
BareKey bool
// OnThisKeyboard reads each key's name as the LEGEND PRINTED ON THE KEY, and
// claims whichever position prints it here.
//
// ⛔ WITHOUT IT A SETTINGS FILE MEANS SOMETHING DIFFERENT ON EVERY LAYOUT AND
// SAYS NOTHING ABOUT IT. A [Key] is a virtual key code, which is a POSITION,
// and this package's names are the ANSI legends for those positions. On a
// French Mac the position called Equal prints "-", and "=" is over on the
// position called Slash -- so "ctrl+alt+cmd+Equal" claimed the key printed
// "-", the shortcut was granted, it fired, and the person pressing the key
// printed "=" reached nothing at all. Every check reported it as granted,
// because it was.
//
// It is an OPTION and not the default because the two readings are both
// legitimate: a game wants the position (WASD is a shape under the hand,
// whatever is printed there) and a shortcut wants the legend (a person
// presses what the menu says). This package cannot tell which a caller means.
//
// It applies to [Register] alone, and once -- which is the point of it being
// here rather than a method on a combination. Reading a key's ANSI name and
// moving to the local key that prints it is a ONE-WAY interpretation: the
// result is a position whose own ANSI name says something else, so a
// transform a caller could apply twice would walk. Doing it at the moment a
// combination becomes a claim is the one place it cannot happen twice.
//
// Keys with no printed character of their own -- the arrows, Return, Escape,
// the function keys -- are never moved: no layout moves them, and there is
// nothing to match on. Neither is a key whose legend this keyboard does not
// print anywhere, which is what "[" is on French: nothing is silently
// swapped, the claim stays where it was, and [Combo.Glyphs] then reports what
// that key actually prints.
//
// [Hotkey.Wanted] is the combination as WRITTEN, so a caller can still say
// what was asked for.
OnThisKeyboard bool
}
Options tunes Register. The zero value is the sensible default: the DefaultLadder, and the system-shortcut check switched on.
type Origin ¶
type Origin int
Origin says where a SystemShortcut's binding came from — which is the difference between a fact and a well-informed guess.
const ( // FromDefaults means the binding comes from this package's built-in // table of macOS defaults. It is a hand-maintained LIST, not a query. FromDefaults Origin = iota // FromPreferences means the user rebound the shortcut and the key and // modifiers were read out of com.apple.symbolichotkeys. This is a fact // about this machine. FromPreferences )
type Override ¶
type Override struct {
// ID is the entry number.
ID int
// Enabled is the entry's "enabled" flag.
Enabled bool
// Combo is the rebound combination, valid only when HasCombo is true.
Combo Combo
// HasCombo reports whether the entry carried a usable
// value.parameters triple. Entries with only an "enabled" flag, and
// entries whose parameters are the 65535 "unbound" sentinel, do not.
HasCombo bool
}
Override is one entry read out of the com.apple.symbolichotkeys domain. It is an override of a default, which is why Combo is optional: an entry may say only that a shortcut is switched off, without restating what it is bound to.
func ParseSymbolicHotKeys ¶
ParseSymbolicHotKeys reads the decoded com.apple.symbolichotkeys dictionary — the value of its "AppleSymbolicHotKeys" key — into a list of overrides, sorted by ID.
The shape it expects, confirmed by dumping the real domain on macOS 26.6.2:
{"65": {"enabled": true,
"value": {"type": "standard",
"parameters": [32, 49, 1572864]}}}
parameters is [ASCII character, virtual key code, NSEventModifierFlags mask]. The first element is 65535 when the shortcut has no character equivalent, and an all-65535 triple means the shortcut is unbound; both are handled.
Parsing is deliberately tolerant. This is a preference file a user or a third-party tool may have written, so a malformed entry is skipped rather than failing the whole read: a hot key that cannot be claimed because one preference entry was odd would be a bad trade.
type Registrar ¶
Registrar is the seam between the fallback policy and the operating system. Resolve speaks only to this, so the whole ladder — including the case where every candidate is taken — is testable on any platform with no Carbon at all.
Claim must report ErrComboTaken (or an error wrapping it) when the combination is held by another Carbon hot-key holder. Any other error aborts the ladder, because it means something is wrong with the process rather than with this particular combination.
type Reserver ¶
Reserver reports combinations that are known to be taken WITHOUT asking the operating system to register them — the macOS system shortcuts that RegisterEventHotKey would happily hand out anyway. SystemShortcuts implements it. A nil Reserver means "check nothing", in which case only conflict kind 1 is detected.
type SystemShortcut ¶
type SystemShortcut struct {
// ID is the com.apple.symbolichotkeys entry number.
ID int
// Name is what System Settings > Keyboard > Keyboard Shortcuts calls it.
Name string
// Combo is the key combination it occupies.
Combo Combo
// Enabled reports whether it is currently switched on. A disabled
// shortcut does not reserve its combination.
Enabled bool
// Origin says whether Combo was read from this machine's preferences or
// taken from the built-in defaults list.
Origin Origin
}
SystemShortcut is one macOS system-wide shortcut: a Mission Control or Spotlight or input-source binding, the kind RegisterEventHotKey will hand you without complaint even though the window server will keep swallowing it.
func DefaultShortcuts ¶
func DefaultShortcuts() []SystemShortcut
DefaultShortcuts returns a copy of the built-in defaults list. Every entry has FromDefaults as its origin. Callers may use it to show the user what this package believes is reserved, and to see plainly that it is a list.
func (SystemShortcut) String ¶
func (s SystemShortcut) String() string
String renders the shortcut for a diagnostic listing.
type SystemShortcuts ¶
type SystemShortcuts struct {
// contains filtered or unexported fields
}
SystemShortcuts is the effective set of macOS system shortcuts on a machine: the built-in defaults list with the com.apple.symbolichotkeys overrides layered over it. It implements Reserver.
func LoadSystemShortcuts ¶
func LoadSystemShortcuts() (*SystemShortcuts, error)
LoadSystemShortcuts returns the built-in defaults list alone. There is no com.apple.symbolichotkeys domain to layer over it on this platform, so the result is what macOS reserves BY DEFAULT — useful for showing a user what their Mac would refuse, and for testing the merge, but it describes macOS rather than the machine this is running on.
func Merge ¶
func Merge(defaults []SystemShortcut, overrides []Override) *SystemShortcuts
Merge layers preference overrides over a defaults list and returns the effective set.
Three things happen, and each corresponds to something really seen in the domain on macOS 26.6.2:
- An override with a binding (entry 60: parameters [32, 49, 262144]) replaces the default's combination.
- An override with only an "enabled" flag (entries 79-82) changes only whether the default is on. Its binding still comes from the list.
- An override for an ID the list does not know is kept if it carries a binding, and dropped if it does not — an unknown ID with no binding tells us nothing usable.
Disabled shortcuts are retained in the set but do not reserve their combination; see SystemShortcuts.Reserved.
func (*SystemShortcuts) All ¶
func (s *SystemShortcuts) All() []SystemShortcut
All returns the effective shortcuts, ordered by the defaults list and then by any extra entries found in preferences.
func (*SystemShortcuts) Describe ¶
func (s *SystemShortcuts) Describe() string
Describe renders the effective set as a diagnostic listing, one shortcut per line. It is what to print when a user asks why they did not get the combination they wanted.
func (*SystemShortcuts) Reserved ¶
func (s *SystemShortcuts) Reserved(c Combo) (string, bool)
Reserved implements Reserver. It reports a combination as taken when an ENABLED system shortcut occupies it, and says which one — so the caller can tell the user "⌥⌘Space is the Finder's search window" rather than just "no".
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
hotkeycheck
command
Command hotkeycheck claims a system-wide shortcut and reports what it got.
|
Command hotkeycheck claims a system-wide shortcut and reports what it got. |