diff --git a/hotkey_linux.c b/hotkey_linux.c index 0cb8b1c..0e2822a 100644 --- a/hotkey_linux.c +++ b/hotkey_linux.c @@ -6,25 +6,26 @@ //go:build linux -#include -#include #include #include +#include +#include // memset extern void hotkeyDown(uintptr_t hkhandle); extern void hotkeyUp(uintptr_t hkhandle); int displayTest() { - Display* d = NULL; - for (int i = 0; i < 42; i++) { - d = XOpenDisplay(0); - if (d == NULL) continue; - break; - } - if (d == NULL) { - return -1; - } - return 0; + Display *d = NULL; + for (int i = 0; i < 42; i++) { + d = XOpenDisplay(0); + if (d == NULL) + continue; + break; + } + if (d == NULL) { + return -1; + } + return 0; } // FIXME: handle bad access properly. @@ -38,40 +39,84 @@ int displayTest() { // pErr->minor_code ); // if( pErr->request_code == 33 ){ // 33 (X_GrabKey) // if( pErr->error_code == BadAccess ){ -// printf("ERROR: key combination already grabbed by another client.\n"); -// return 0; +// printf("ERROR: key combination already grabbed by another +// client.\n"); return 0; // } // } // return 0; // } +Display *openDisplay() { + Display *d = NULL; + for (int i = 0; i < 42; i++) { + d = XOpenDisplay(0); + if (d == NULL) + continue; + break; + } + return d; +} + +// Creates an invisible window, which can receive ClientMessage events. On +// hotkey cancel a ClientMessageEvent is generated on the window. The event is +// catched and the event loop terminates. x: 0 y: 0 w: 1 h: 1 border_width: 1 +// depth: 0 +// class: InputOnly (window will not be drawn) +// visual: default visual of display +// no attributes will be set (0, &attr) +Window createInvisWindow(Display *d) { + XSetWindowAttributes attr; + return XCreateWindow(d, DefaultRootWindow(d), 0, 0, 1, 1, 0, 0, InputOnly, + DefaultVisual(d, 0), 0, &attr); +} + +// Sends a custom ClientMessage of type (Atom) "go_hotkey_cancel_hotkey" +// Passed value 'True' of XInternAtom creates the Atom, if it does not exist yet +void sendCancel(Display *d, Window window) { + Atom atom = XInternAtom(d, "golangdesign_hotkey_cancel_hotkey", True); + XClientMessageEvent clientEvent; + memset(&clientEvent, 0, sizeof(clientEvent)); + clientEvent.type = ClientMessage; + clientEvent.send_event = True; + clientEvent.display = d; + clientEvent.window = window; + clientEvent.message_type = atom; + clientEvent.format = 8; + + XEvent event; + event.type = ClientMessage; + event.xclient = clientEvent; + XSendEvent(d, window, False, 0, &event); + XFlush(d); +} + +// Closes the connection and destroys the invisible 'cancel' window +void cleanupConnection(Display *d, Window w) { + XDestroyWindow(d, w); + XCloseDisplay(d); +} + // waitHotkey blocks until the hotkey is triggered. // this function crashes the program if the hotkey already grabbed by others. -int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key) { - Display* d = NULL; - for (int i = 0; i < 42; i++) { - d = XOpenDisplay(0); - if (d == NULL) continue; - break; - } - if (d == NULL) { - return -1; - } - int keycode = XKeysymToKeycode(d, key); - XGrabKey(d, keycode, mod, DefaultRootWindow(d), False, GrabModeAsync, GrabModeAsync); - XSelectInput(d, DefaultRootWindow(d), KeyPressMask); - XEvent ev; - while(1) { - XNextEvent(d, &ev); - switch(ev.type) { - case KeyPress: - hotkeyDown(hkhandle); - continue; - case KeyRelease: - hotkeyUp(hkhandle); - XUngrabKey(d, keycode, mod, DefaultRootWindow(d)); - XCloseDisplay(d); - return 0; - } - } -} \ No newline at end of file +int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key, Display *d, + Window w) { + int keycode = XKeysymToKeycode(d, key); + XGrabKey(d, keycode, mod, DefaultRootWindow(d), False, GrabModeAsync, + GrabModeAsync); + XSelectInput(d, DefaultRootWindow(d), KeyPressMask); + XEvent ev; + while (1) { + XNextEvent(d, &ev); + switch (ev.type) { + case KeyPress: + hotkeyDown(hkhandle); + continue; + case KeyRelease: + hotkeyUp(hkhandle); + XUngrabKey(d, keycode, mod, DefaultRootWindow(d)); + return 0; + case ClientMessage: + return 0; + } + } +} diff --git a/hotkey_linux.go b/hotkey_linux.go index f3be4fa..7598e12 100644 --- a/hotkey_linux.go +++ b/hotkey_linux.go @@ -12,9 +12,14 @@ package hotkey #cgo LDFLAGS: -lX11 #include +#include int displayTest(); -int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key); +Display *openDisplay(); +Window createInvisWindow(Display *d); +void sendCancel(Display *d, Window window); +void cleanupConnection(Display *d, Window window); +int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key, Display *d, Window w); */ import "C" import ( @@ -50,6 +55,8 @@ type platformHotkey struct { ctx context.Context cancel context.CancelFunc canceled chan struct{} + display *C.Display + window C.Window } // Nothing needs to do for register @@ -76,6 +83,9 @@ func (hk *Hotkey) unregister() error { return errors.New("hotkey is not registered.") } hk.cancel() + if hk.display != nil { + C.sendCancel(hk.display, hk.window) + } hk.registered = false <-hk.canceled return nil @@ -88,24 +98,32 @@ func (hk *Hotkey) handle() { defer runtime.UnlockOSThread() // KNOWN ISSUE: if a hotkey is grabbed by others, C side will crash the program + hk.display = C.openDisplay() + hk.window = C.createInvisWindow(hk.display) + var mod Modifier for _, m := range hk.mods { mod = mod | m } h := cgo.NewHandle(hk) defer h.Delete() - + defer hk.cleanConnection() for { select { case <-hk.ctx.Done(): close(hk.canceled) return default: - _ = C.waitHotkey(C.uintptr_t(h), C.uint(mod), C.int(hk.key)) + _ = C.waitHotkey(C.uintptr_t(h), C.uint(mod), C.int(hk.key), hk.display, hk.window) } } } +func (hk *Hotkey) cleanConnection() { + C.cleanupConnection(hk.display, hk.window) + hk.display = nil +} + //export hotkeyDown func hotkeyDown(h uintptr) { hk := cgo.Handle(h).Value().(*Hotkey) diff --git a/hotkey_linux_test.go b/hotkey_linux_test.go index 5c70d75..240aeb0 100644 --- a/hotkey_linux_test.go +++ b/hotkey_linux_test.go @@ -39,3 +39,26 @@ func TestHotkey(t *testing.T) { } } } + +func TestHotkey_Unregister(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.Mod2, hotkey.Mod4}, hotkey.KeyA) + if err := hk.Register(); err != nil { + t.Errorf("failed to register hotkey: %v", err) + return + } + if err := hk.Unregister(); err != nil { + t.Errorf("failed to unregister hotkey: %v", err) + return + } + + for { + select { + case <-ctx.Done(): + return + case <-hk.Keydown(): + t.Fatalf("hotkey should not be registered but actually triggered.") + } + } +}