From 2aaeb047d46a4ad3c389fa6e1771b47a03e27241 Mon Sep 17 00:00:00 2001 From: Niklas Loeser <51879435+data-niklas@users.noreply.github.com> Date: Fri, 6 May 2022 18:55:00 +0200 Subject: [PATCH 1/4] adds rudimentary test --- hotkey_linux.c | 93 ++++++++++++++++++++++++++----------------------- hotkey_linux.go | 6 ++-- test/go.mod | 7 ++++ test/test.go | 26 ++++++++++++++ 4 files changed, 87 insertions(+), 45 deletions(-) create mode 100644 test/go.mod create mode 100644 test/test.go diff --git a/hotkey_linux.c b/hotkey_linux.c index 0cb8b1c..8d525ef 100644 --- a/hotkey_linux.c +++ b/hotkey_linux.c @@ -4,27 +4,28 @@ // // Written by Changkun Ou -//go:build linux +// go:build linux -#include -#include #include #include +#include +#include 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,8 +39,8 @@ 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; @@ -47,31 +48,37 @@ int displayTest() { // 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, + char *is_registered) { + 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); + if (!(*is_registered)) { + return -2; + } + switch (ev.type) { + case KeyPress: + hotkeyDown(hkhandle); + continue; + case KeyRelease: + hotkeyUp(hkhandle); + XUngrabKey(d, keycode, mod, DefaultRootWindow(d)); + XCloseDisplay(d); + return 0; + } + } +} diff --git a/hotkey_linux.go b/hotkey_linux.go index f3be4fa..07268a9 100644 --- a/hotkey_linux.go +++ b/hotkey_linux.go @@ -14,7 +14,7 @@ package hotkey #include int displayTest(); -int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key); +int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key, char* is_registered); */ import "C" import ( @@ -23,6 +23,7 @@ import ( "runtime" "runtime/cgo" "sync" + "unsafe" ) const errmsg = `Failed to initialize the X11 display, and the clipboard package @@ -101,7 +102,8 @@ func (hk *Hotkey) handle() { close(hk.canceled) return default: - _ = C.waitHotkey(C.uintptr_t(h), C.uint(mod), C.int(hk.key)) + var registered_ptr *C.char = (*C.char)(unsafe.Pointer(&hk.registered)) + _ = C.waitHotkey(C.uintptr_t(h), C.uint(mod), C.int(hk.key), registered_ptr) } } } diff --git a/test/go.mod b/test/go.mod new file mode 100644 index 0000000..af61825 --- /dev/null +++ b/test/go.mod @@ -0,0 +1,7 @@ +module test + +replace golang.design/x/hotkey => github.com/data-niklas/hotkey v0.3.1-0.20220427080358-ba5663284c79 + +require ( + golang.design/x/hotkey v0.3.0 +) diff --git a/test/test.go b/test/test.go new file mode 100644 index 0000000..855cf81 --- /dev/null +++ b/test/test.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + h "golang.design/x/hotkey" + "time" +) + +func main() { + hk := h.New([]h.Modifier{h.ModCtrl}, h.KeyM) + hk.Register() + var cancel chan bool = make(chan bool) + go func() { + time.Sleep(time.Second * 1) + fmt.Println("Hotkey will be unregistered") + hk.Unregister() + fmt.Println("Hotkey unregistered") + hk.Register() + fmt.Println("Registered again") + cancel <- true + }() + select { + case <-hk.Keydown(): + case <-cancel: + } +} From 819194638771fe3bd55f0c6c7c8509539039daa6 Mon Sep 17 00:00:00 2001 From: Niklas Loeser <51879435+data-niklas@users.noreply.github.com> Date: Mon, 9 May 2022 17:55:08 +0200 Subject: [PATCH 2/4] adds custom clientmessage to break event loop --- hotkey_linux.c | 63 +++++++++++++++++++++++++++++++++++++++---------- hotkey_linux.go | 19 +++++++++++---- test/go.mod | 7 ------ test/test.go | 26 -------------------- 4 files changed, 65 insertions(+), 50 deletions(-) delete mode 100644 test/go.mod delete mode 100644 test/test.go diff --git a/hotkey_linux.c b/hotkey_linux.c index 8d525ef..70f52d2 100644 --- a/hotkey_linux.c +++ b/hotkey_linux.c @@ -9,7 +9,8 @@ #include #include #include -#include +// Needed for memset() +#include extern void hotkeyDown(uintptr_t hkhandle); extern void hotkeyUp(uintptr_t hkhandle); @@ -46,10 +47,7 @@ int displayTest() { // return 0; // } -// 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, - char *is_registered) { +Display *openDisplay() { Display *d = NULL; for (int i = 0; i < 42; i++) { d = XOpenDisplay(0); @@ -57,9 +55,52 @@ int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key, continue; break; } - if (d == NULL) { - return -1; - } + 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, "go_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, + Window w) { int keycode = XKeysymToKeycode(d, key); XGrabKey(d, keycode, mod, DefaultRootWindow(d), False, GrabModeAsync, GrabModeAsync); @@ -67,9 +108,6 @@ int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key, XEvent ev; while (1) { XNextEvent(d, &ev); - if (!(*is_registered)) { - return -2; - } switch (ev.type) { case KeyPress: hotkeyDown(hkhandle); @@ -77,7 +115,8 @@ int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key, case KeyRelease: hotkeyUp(hkhandle); XUngrabKey(d, keycode, mod, DefaultRootWindow(d)); - XCloseDisplay(d); + return 0; + case ClientMessage: return 0; } } diff --git a/hotkey_linux.go b/hotkey_linux.go index 07268a9..6d4a38f 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, char* is_registered); +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 ( @@ -23,7 +28,6 @@ import ( "runtime" "runtime/cgo" "sync" - "unsafe" ) const errmsg = `Failed to initialize the X11 display, and the clipboard package @@ -51,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 +82,7 @@ func (hk *Hotkey) unregister() error { if !hk.registered { return errors.New("hotkey is not registered.") } + C.sendCancel(hk.display, hk.window) hk.cancel() hk.registered = false <-hk.canceled @@ -89,21 +96,23 @@ 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 C.cleanupConnection(hk.display, hk.window) for { select { case <-hk.ctx.Done(): close(hk.canceled) return default: - var registered_ptr *C.char = (*C.char)(unsafe.Pointer(&hk.registered)) - _ = C.waitHotkey(C.uintptr_t(h), C.uint(mod), C.int(hk.key), registered_ptr) + _ = C.waitHotkey(C.uintptr_t(h), C.uint(mod), C.int(hk.key), hk.display, hk.window) } } } diff --git a/test/go.mod b/test/go.mod deleted file mode 100644 index af61825..0000000 --- a/test/go.mod +++ /dev/null @@ -1,7 +0,0 @@ -module test - -replace golang.design/x/hotkey => github.com/data-niklas/hotkey v0.3.1-0.20220427080358-ba5663284c79 - -require ( - golang.design/x/hotkey v0.3.0 -) diff --git a/test/test.go b/test/test.go deleted file mode 100644 index 855cf81..0000000 --- a/test/test.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "fmt" - h "golang.design/x/hotkey" - "time" -) - -func main() { - hk := h.New([]h.Modifier{h.ModCtrl}, h.KeyM) - hk.Register() - var cancel chan bool = make(chan bool) - go func() { - time.Sleep(time.Second * 1) - fmt.Println("Hotkey will be unregistered") - hk.Unregister() - fmt.Println("Hotkey unregistered") - hk.Register() - fmt.Println("Registered again") - cancel <- true - }() - select { - case <-hk.Keydown(): - case <-cancel: - } -} From 50dc9cbccd580c5df9ac9987552d2cc98caed83c Mon Sep 17 00:00:00 2001 From: data-niklas Date: Tue, 10 May 2022 09:45:32 +0200 Subject: [PATCH 3/4] Update hotkey_linux.c Co-authored-by: Changkun Ou --- hotkey_linux.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hotkey_linux.c b/hotkey_linux.c index 70f52d2..d09d0f6 100644 --- a/hotkey_linux.c +++ b/hotkey_linux.c @@ -74,7 +74,7 @@ Window createInvisWindow(Display *d) { // 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, "go_hotkey_cancel_hotkey", True); + Atom atom = XInternAtom(d, "golangdesign_hotkey_cancel_hotkey", True); XClientMessageEvent clientEvent; memset(&clientEvent, 0, sizeof(clientEvent)); clientEvent.type = ClientMessage; From e6b4a794101278b5c3230869a8d2eb61aa9c657d Mon Sep 17 00:00:00 2001 From: Niklas Loeser <51879435+data-niklas@users.noreply.github.com> Date: Tue, 10 May 2022 10:08:19 +0200 Subject: [PATCH 4/4] stabilizes event loop canceling --- hotkey_linux.c | 5 ++--- hotkey_linux.go | 11 +++++++++-- hotkey_linux_test.go | 23 +++++++++++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/hotkey_linux.c b/hotkey_linux.c index d09d0f6..0e2822a 100644 --- a/hotkey_linux.c +++ b/hotkey_linux.c @@ -4,13 +4,12 @@ // // Written by Changkun Ou -// go:build linux +//go:build linux #include #include #include -// Needed for memset() -#include +#include // memset extern void hotkeyDown(uintptr_t hkhandle); extern void hotkeyUp(uintptr_t hkhandle); diff --git a/hotkey_linux.go b/hotkey_linux.go index 6d4a38f..7598e12 100644 --- a/hotkey_linux.go +++ b/hotkey_linux.go @@ -82,8 +82,10 @@ func (hk *Hotkey) unregister() error { if !hk.registered { return errors.New("hotkey is not registered.") } - C.sendCancel(hk.display, hk.window) hk.cancel() + if hk.display != nil { + C.sendCancel(hk.display, hk.window) + } hk.registered = false <-hk.canceled return nil @@ -105,7 +107,7 @@ func (hk *Hotkey) handle() { } h := cgo.NewHandle(hk) defer h.Delete() - defer C.cleanupConnection(hk.display, hk.window) + defer hk.cleanConnection() for { select { case <-hk.ctx.Done(): @@ -117,6 +119,11 @@ func (hk *Hotkey) handle() { } } +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.") + } + } +}