Skip to content

Commit f3e9351

Browse files
authored
fix(linux): fire hotkey regardless of NumLock/CapsLock state (#39)
An X11 XGrabKey matches an exact modifier mask, but an active NumLock (Mod2) or CapsLock (Lock) adds bits to the event state, so the grab no longer matches and the hotkey silently stops firing (issue #25). Grab the key once for every on/off combination of the NumLock and CapsLock masks (deduplicated, so we never grab the same key+mask twice and trigger BadAccess). The mask set is computed in Go by lockVariants and unit-tested. Fixes #25.
1 parent 3514a67 commit f3e9351

3 files changed

Lines changed: 90 additions & 5 deletions

File tree

hotkey_linux.c

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ int displayTest() {
4747

4848
// waitHotkey blocks until the hotkey is triggered.
4949
// this function crashes the program if the hotkey already grabbed by others.
50-
int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key) {
50+
//
51+
// mods points to nmods modifier masks: the same hotkey is grabbed once per
52+
// mask so that it still fires while NumLock/CapsLock are active (those locks
53+
// add bits to the event state that an exact-mask grab would not match).
54+
int waitHotkey(uintptr_t hkhandle, unsigned int* mods, int nmods, int key) {
5155
Display* d = NULL;
5256
for (int i = 0; i < 42; i++) {
5357
d = XOpenDisplay(0);
@@ -58,7 +62,9 @@ int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key) {
5862
return -1;
5963
}
6064
int keycode = XKeysymToKeycode(d, key);
61-
XGrabKey(d, keycode, mod, DefaultRootWindow(d), False, GrabModeAsync, GrabModeAsync);
65+
for (int i = 0; i < nmods; i++) {
66+
XGrabKey(d, keycode, mods[i], DefaultRootWindow(d), False, GrabModeAsync, GrabModeAsync);
67+
}
6268
XSelectInput(d, DefaultRootWindow(d), KeyPressMask);
6369
XEvent ev;
6470
while(1) {
@@ -69,7 +75,9 @@ int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key) {
6975
continue;
7076
case KeyRelease:
7177
hotkeyUp(hkhandle);
72-
XUngrabKey(d, keycode, mod, DefaultRootWindow(d));
78+
for (int i = 0; i < nmods; i++) {
79+
XUngrabKey(d, keycode, mods[i], DefaultRootWindow(d));
80+
}
7381
XCloseDisplay(d);
7482
return 0;
7583
}

hotkey_linux.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ package hotkey
1414
#include <stdint.h>
1515
1616
int displayTest();
17-
int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key);
17+
int waitHotkey(uintptr_t hkhandle, unsigned int* mods, int nmods, int key);
1818
*/
1919
import "C"
2020
import (
@@ -95,15 +95,48 @@ func (hk *Hotkey) handle() {
9595
h := cgo.NewHandle(hk)
9696
defer h.Delete()
9797

98+
// Grab the hotkey once per NumLock/CapsLock state so it fires
99+
// regardless of those locks (see lockVariants).
100+
variants := lockVariants(mod)
101+
cmods := make([]C.uint, len(variants))
102+
for i, v := range variants {
103+
cmods[i] = C.uint(v)
104+
}
105+
98106
for {
99107
select {
100108
case <-hk.ctx.Done():
101109
close(hk.canceled)
102110
return
103111
default:
104-
_ = C.waitHotkey(C.uintptr_t(h), C.uint(mod), C.int(hk.key))
112+
_ = C.waitHotkey(C.uintptr_t(h), &cmods[0], C.int(len(cmods)), C.int(hk.key))
113+
}
114+
}
115+
}
116+
117+
// X11 lock modifier masks (see /usr/include/X11/X.h).
118+
const (
119+
x11LockMask Modifier = 1 << 1 // CapsLock
120+
x11Mod2Mask Modifier = 1 << 4 // usually NumLock
121+
)
122+
123+
// lockVariants returns mod combined with every on/off combination of the
124+
// CapsLock and NumLock masks, deduplicated. An XGrabKey uses an exact
125+
// modifier mask, so without these variants a hotkey would stop firing
126+
// whenever NumLock or CapsLock is toggled on. Duplicates are removed so we
127+
// never grab the same key+mask twice (which itself raises BadAccess).
128+
func lockVariants(mod Modifier) []uint32 {
129+
extra := []Modifier{0, x11LockMask, x11Mod2Mask, x11LockMask | x11Mod2Mask}
130+
seen := make(map[uint32]bool, len(extra))
131+
out := make([]uint32, 0, len(extra))
132+
for _, e := range extra {
133+
v := uint32(mod | e)
134+
if !seen[v] {
135+
seen[v] = true
136+
out = append(out, v)
105137
}
106138
}
139+
return out
107140
}
108141

109142
//export hotkeyDown

hotkey_linux_internal_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2021 The golang.design Initiative Authors.
2+
// All rights reserved. Use of this source code is governed
3+
// by a MIT license that can be found in the LICENSE file.
4+
//
5+
// Written by Changkun Ou <changkun.de>
6+
7+
//go:build linux && cgo
8+
9+
package hotkey
10+
11+
import (
12+
"reflect"
13+
"testing"
14+
)
15+
16+
// TestLockVariants verifies that a hotkey is grabbed for every NumLock/CapsLock
17+
// state. Before this fix only the exact modifier mask was grabbed, so a hotkey
18+
// stopped firing whenever NumLock or CapsLock was toggled on (issue #25).
19+
func TestLockVariants(t *testing.T) {
20+
const (
21+
lock = 1 << 1 // CapsLock
22+
num = 1 << 4 // NumLock
23+
)
24+
for _, tt := range []struct {
25+
name string
26+
mod Modifier
27+
want []uint32
28+
}{
29+
{
30+
name: "ctrl+shift grabs all four lock combinations",
31+
mod: ModCtrl | ModShift, // 0b101 = 5
32+
want: []uint32{5, 5 | lock, 5 | num, 5 | lock | num},
33+
},
34+
{
35+
name: "mod already containing NumLock is deduplicated",
36+
mod: ModCtrl | Mod2, // Mod2 == NumLock mask
37+
want: []uint32{uint32(ModCtrl | Mod2), uint32(ModCtrl|Mod2) | lock},
38+
},
39+
} {
40+
if got := lockVariants(tt.mod); !reflect.DeepEqual(got, tt.want) {
41+
t.Errorf("%s: lockVariants(%#b) = %v, want %v", tt.name, tt.mod, got, tt.want)
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)