Skip to content

Commit 05d9e57

Browse files
committed
all: allow unregister hotkeys (darwin)
Updates #5, #7
1 parent 9cf98f7 commit 05d9e57

File tree

19 files changed

+300
-432
lines changed

19 files changed

+300
-432
lines changed

README.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ keys is not supported at the moment. Furthermore, because of OS
2121
restriction, hotkey events must be handled on the main thread.
2222

2323
Therefore, in order to use this package properly, here is a complete
24-
example that corporates the [mainthread](https://golang.design/s/mainthread)
24+
example that corporates the mainthread:
2525
package:
2626

2727
```go
@@ -31,29 +31,28 @@ import (
3131
"context"
3232

3333
"golang.design/x/hotkey"
34-
"golang.design/x/mainthread"
34+
"golang.design/x/hotkey/mainthread"
3535
)
3636

3737
// initialize mainthread facility so that hotkey can be
3838
// properly registered to the system and handled by the
3939
// application.
40-
func main() { mainthread.Init(fn) }
40+
func main() { mainthread.Run(fn) }
4141
func fn() { // Use fn as the actual main function.
4242
var (
43-
mods = []hotkey.Modifier{hotkey.ModCtrl}
43+
mods = []hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}
4444
k = hotkey.KeyS
4545
)
4646

4747
// Register a desired hotkey.
48-
hk, err := hotkey.Register(mods, k)
49-
if err != nil {
48+
hk := hotkey.New(mods, k)
49+
if err := hk.Register() err != nil {
5050
panic("hotkey registration failed")
5151
}
5252

5353
// Start listen hotkey event whenever you feel it is ready.
54-
triggered := hk.Listen(context.Background())
55-
for range triggered {
56-
println("hotkey ctrl+s is triggered")
54+
for range hk.Listen() {
55+
println("hotkey ctrl+shift+s is triggered")
5756
}
5857
}
5958
```

example/example

1.59 MB
Binary file not shown.

example/main.go

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,43 @@
55
package main
66

77
import (
8-
"context"
8+
"time"
99

1010
"golang.design/x/hotkey"
11-
"golang.design/x/mainthread"
11+
"golang.design/x/hotkey/mainthread"
1212
)
1313

14-
func main() { mainthread.Init(fn) }
14+
func main() { mainthread.Run(fn) }
1515
func fn() {
16-
var (
17-
mods = []hotkey.Modifier{hotkey.ModCtrl}
18-
k = hotkey.KeyS
19-
)
20-
21-
hk, err := hotkey.Register(mods, k)
22-
if err != nil {
23-
panic("hotkey registration failed")
24-
}
25-
26-
triggered := hk.Listen(context.Background())
27-
for range triggered {
28-
println("hotkey ctrl+s is triggered")
29-
}
16+
hk1 := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl}, hotkey.KeyS)
17+
go func() {
18+
println("register")
19+
if err := hk1.Register(); err != nil {
20+
panic(err)
21+
}
22+
for range hk1.Listen() {
23+
println("hotkey ctrl+s is triggered")
24+
}
25+
}()
26+
27+
<-time.After(5 * time.Second)
28+
hk1.Unregister()
29+
println("unregistered")
30+
31+
<-time.After(5 * time.Second)
32+
go func() {
33+
println("register again")
34+
if err := hk1.Register(); err != nil {
35+
panic(err)
36+
}
37+
for range hk1.Listen() {
38+
println("hotkey ctrl+s is triggered")
39+
}
40+
}()
41+
42+
<-time.After(5 * time.Second)
43+
hk1.Unregister()
44+
println("unregistered again")
45+
46+
println("done")
3047
}

go.mod

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
11
module golang.design/x/hotkey
22

33
go 1.17
4-
5-
require (
6-
golang.design/x/mainthread v0.3.0
7-
golang.org/x/sys v0.0.0-20210122093101-04d7465088b8 // indirect
8-
)

go.sum

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +0,0 @@
1-
golang.design/x/mainthread v0.3.0 h1:UwFus0lcPodNpMOGoQMe87jSFwbSsEY//CA7yVmu4j8=
2-
golang.design/x/mainthread v0.3.0/go.mod h1:vYX7cF2b3pTJMGM/hc13NmN6kblKnf4/IyvHeu259L0=
3-
golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
4-
golang.org/x/sys v0.0.0-20210122093101-04d7465088b8 h1:de2yTH1xuxjmGB7i6Z5o2z3RCHVa0XlpSZzjd8Fe6bE=
5-
golang.org/x/sys v0.0.0-20210122093101-04d7465088b8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

hotkey.go

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,85 +12,83 @@
1212
// restriction, hotkey events must be handled on the main thread.
1313
//
1414
// Therefore, in order to use this package properly, here is a complete
15-
// example that corporates the golang.design/x/mainthread package:
15+
// example that corporates the golang.design/x/hotkey/mainthread package:
1616
//
1717
// package main
1818
//
1919
// import (
2020
// "context"
2121
//
2222
// "golang.design/x/hotkey"
23-
// "golang.design/x/mainthread"
23+
// "golang.design/x/hotkey/mainthread"
2424
// )
2525
//
2626
// // initialize mainthread facility so that hotkey can be
2727
// // properly registered to the system and handled by the
2828
// // application.
29-
// func main() { mainthread.Init(fn) }
29+
// func main() { mainthread.Run(fn) }
3030
// func fn() { // Use fn as the actual main function.
3131
// var (
32-
// mods = []hotkey.Modifier{hotkey.ModCtrl}
32+
// mods = []hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}
3333
// k = hotkey.KeyS
3434
// )
3535
//
3636
// // Register a desired hotkey.
37-
// hk, err := hotkey.Register(mods, k)
38-
// if err != nil {
37+
// hk := hotkey.New(mods, k)
38+
// if err := hk.Register(); err != nil {
3939
// panic("hotkey registration failed")
4040
// }
4141
//
4242
// // Start listen hotkey event whenever you feel it is ready.
43-
// triggered := hk.Listen(context.Background())
44-
// for range triggered {
45-
// println("hotkey ctrl+s is triggered")
43+
// for range hk.Listen() {
44+
// println("hotkey ctrl+shift+s is triggered")
4645
// }
4746
// }
4847
package hotkey
4948

50-
import (
51-
"context"
52-
"runtime"
53-
54-
"golang.design/x/mainthread"
55-
)
56-
5749
// Event represents a hotkey event
5850
type Event struct{}
5951

6052
// Hotkey is a combination of modifiers and key to trigger an event
6153
type Hotkey struct {
54+
platformHotkey
55+
6256
mods []Modifier
6357
key Key
6458

6559
in chan<- Event
6660
out <-chan Event
6761
}
6862

63+
func New(mods []Modifier, key Key) *Hotkey {
64+
in, out := newEventChan()
65+
return &Hotkey{mods: mods, key: key, in: in, out: out}
66+
}
67+
6968
// Register registers a combination of hotkeys. If the hotkey has
7069
// registered. This function will invalidates the old registration
7170
// and overwrites its callback.
72-
func Register(mods []Modifier, key Key) (*Hotkey, error) {
73-
in, out := newEventChan()
74-
hk := &Hotkey{mods, key, in, out}
75-
76-
var err error
77-
mainthread.Call(func() { err = hk.register() })
78-
if err != nil {
79-
return nil, err
80-
}
81-
82-
runtime.SetFinalizer(hk, func(hk *Hotkey) {
83-
hk.unregister()
84-
})
85-
86-
return hk, nil
71+
func (hk *Hotkey) Register() error {
72+
return hk.register()
8773
}
8874

8975
// Listen handles a hotkey event and triggers a call to fn.
9076
// The hotkey listen hook terminates when the context is canceled.
91-
func (hk *Hotkey) Listen(ctx context.Context) <-chan Event {
92-
mainthread.Go(func() { hk.handle(ctx) })
93-
return hk.out
77+
func (hk *Hotkey) Listen() <-chan Event { return hk.out }
78+
79+
// Unregister unregisters the hotkey.
80+
func (hk *Hotkey) Unregister() error {
81+
err := hk.unregister()
82+
if err != nil {
83+
return err
84+
}
85+
86+
// Setup a new event channel.
87+
close(hk.in)
88+
in, out := newEventChan()
89+
hk.in = in
90+
hk.out = out
91+
return nil
9492
}
9593

9694
// newEventChan returns a sender and a receiver of a buffered channel

hotkey_darwin.go

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,32 @@ package hotkey
1616
#import <Carbon/Carbon.h>
1717
1818
extern void hotkeyCallback(uintptr_t handle);
19-
20-
int registerHotKey(int mod, int key, uintptr_t handle);
21-
void runApp();
22-
void stopApp();
19+
int registerHotKey(int mod, int key, uintptr_t handle, EventHotKeyRef* ref);
20+
int unregisterHotKey(EventHotKeyRef ref);
2321
*/
2422
import "C"
2523
import (
26-
"context"
2724
"errors"
2825
"runtime/cgo"
26+
"sync"
27+
28+
"golang.design/x/hotkey/mainthread"
2929
)
3030

31-
// handle handles the hotkey event loop.
32-
func (hk *Hotkey) handle(ctx context.Context) {
33-
// Note: This call never returns.
34-
C.runApp()
31+
// Hotkey is a combination of modifiers and key to trigger an event
32+
type platformHotkey struct {
33+
mu sync.Mutex
34+
registered bool
35+
hkref C.EventHotKeyRef
3536
}
3637

3738
func (hk *Hotkey) register() error {
39+
hk.mu.Lock()
40+
defer hk.mu.Unlock()
41+
if hk.registered {
42+
return errors.New("hotkey already registered")
43+
}
44+
3845
// Note: we use handle number as hotkey id in the C side.
3946
// A cgo handle could ran out of space, but since in hotkey purpose
4047
// we won't have that much number of hotkeys. So this should be fine.
@@ -45,18 +52,31 @@ func (hk *Hotkey) register() error {
4552
mod += m
4653
}
4754

48-
ret := C.registerHotKey(C.int(mod), C.int(hk.key), C.uintptr_t(h))
55+
var ret C.int
56+
mainthread.Call(func() {
57+
ret = C.registerHotKey(C.int(mod), C.int(hk.key), C.uintptr_t(h), &hk.hkref)
58+
})
4959
if ret == C.int(-1) {
50-
return errors.New("register failed")
60+
return errors.New("failed to register the hotkey")
5161
}
62+
63+
hk.registered = true
5264
return nil
5365
}
5466

55-
func (hk *Hotkey) unregister() {
56-
// TODO: unregister registered hotkeys.
67+
func (hk *Hotkey) unregister() error {
68+
hk.mu.Lock()
69+
defer hk.mu.Unlock()
70+
if !hk.registered {
71+
return errors.New("hotkey is not registered")
72+
}
5773

58-
// KNOWN ISSUE: This call seems does not terminate the app.
59-
C.stopApp()
74+
ret := C.unregisterHotKey(hk.hkref)
75+
if ret == C.int(-1) {
76+
return errors.New("failed to unregister the current hotkey")
77+
}
78+
hk.registered = false
79+
return nil
6080
}
6181

6282
//export hotkeyCallback

hotkey_darwin.m

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
}
2222

2323
// registerHotkeyWithCallback registers a global system hotkey for callbacks.
24-
int registerHotKey(int mod, int key, uintptr_t handle) {
24+
int registerHotKey(int mod, int key, uintptr_t handle, EventHotKeyRef* ref) {
2525
EventTypeSpec eventType;
2626
eventType.eventClass = kEventClassKeyboard;
2727
eventType.eventKind = kEventHotKeyPressed;
@@ -30,27 +30,19 @@ int registerHotKey(int mod, int key, uintptr_t handle) {
3030
);
3131

3232
EventHotKeyID hkid = {.id = handle};
33-
EventHotKeyRef ref;
3433
OSStatus s = RegisterEventHotKey(
35-
key, mod, hkid, GetApplicationEventTarget(), 0, &ref
34+
key, mod, hkid, GetApplicationEventTarget(), 0, ref
3635
);
3736
if (s != noErr) {
3837
return -1;
3938
}
4039
return 0;
4140
}
4241

43-
44-
// The following three lines of code must run on the main thread.
45-
// It must handle it using golang.design/x/mainthread.
46-
//
47-
// inspired from here: https://github.com/cehoffman/dotfiles/blob/4be8e893517e970d40746a9bdc67fe5832dd1c33/os/mac/iTerm2HotKey.m
48-
void runApp() {
49-
[NSApplication sharedApplication];
50-
[NSApp disableRelaunchOnLogin];
51-
[NSApp run];
42+
int unregisterHotKey(EventHotKeyRef ref) {
43+
OSStatus s = UnregisterEventHotKey(ref);
44+
if (s != noErr) {
45+
return -1;
46+
}
47+
return 0;
5248
}
53-
54-
void stopApp() {
55-
[NSApp stop: nil];
56-
}

0 commit comments

Comments
 (0)