Skip to content

Commit 9f83a68

Browse files
committed
all: support keyup event (windows)
Updates #9 Fixes #10
1 parent 258b6bc commit 9f83a68

File tree

6 files changed

+182
-45
lines changed

6 files changed

+182
-45
lines changed

examples/minimum/main.go

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,51 @@
55
package main
66

77
import (
8+
"log"
9+
"sync"
10+
811
"golang.design/x/hotkey"
912
"golang.design/x/hotkey/mainthread"
1013
)
1114

1215
func main() { mainthread.Init(fn) }
1316
func fn() {
14-
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl}, hotkey.KeyS)
15-
var err error
16-
if mainthread.Call(func() { err = hk.Register() }); err != nil {
17-
panic(err)
18-
}
17+
wg := sync.WaitGroup{}
18+
wg.Add(2)
19+
go func() {
20+
defer wg.Done()
1921

20-
for range hk.Keydown() {
21-
println("hotkey ctrl+s is triggered, exit the application, good bye!")
22-
hk.Unregister()
22+
err := listenHotkey(hotkey.KeyS, hotkey.ModCtrl, hotkey.ModShift)
23+
if err != nil {
24+
log.Println(err)
25+
}
26+
}()
27+
go func() {
28+
defer wg.Done()
29+
30+
err := listenHotkey(hotkey.KeyA, hotkey.ModCtrl, hotkey.ModShift)
31+
if err != nil {
32+
log.Println(err)
33+
}
34+
}()
35+
wg.Wait()
36+
}
37+
38+
func listenHotkey(key hotkey.Key, mods ...hotkey.Modifier) (err error) {
39+
ms := []hotkey.Modifier{}
40+
ms = append(ms, mods...)
41+
hk := hotkey.New(ms, key)
42+
43+
mainthread.Call(func() { err = hk.Register() })
44+
if err != nil {
45+
return
2346
}
47+
48+
// Blocks until the hokey is triggered.
49+
<-hk.Keydown()
50+
log.Printf("hotkey: %v is down\n", hk)
51+
<-hk.Keyup()
52+
log.Printf("hotkey: %v is up\n", hk)
53+
hk.Unregister()
54+
return
2455
}

hotkey.go

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
// folder for more examples.
2121
package hotkey
2222

23+
import (
24+
"fmt"
25+
"runtime"
26+
)
27+
2328
// Event represents a hotkey event
2429
type Event struct{}
2530

@@ -30,13 +35,33 @@ type Hotkey struct {
3035
mods []Modifier
3136
key Key
3237

33-
in chan<- Event
34-
out <-chan Event
38+
keydownIn chan<- Event
39+
keydownOut <-chan Event
40+
keyupIn chan<- Event
41+
keyupOut <-chan Event
3542
}
3643

3744
func New(mods []Modifier, key Key) *Hotkey {
38-
in, out := newEventChan()
39-
return &Hotkey{mods: mods, key: key, in: in, out: out}
45+
keydownIn, keydownOut := newEventChan()
46+
keyupIn, keyupOut := newEventChan()
47+
hk := &Hotkey{
48+
mods: mods,
49+
key: key,
50+
keydownIn: keydownIn,
51+
keydownOut: keydownOut,
52+
keyupIn: keyupIn,
53+
keyupOut: keyupOut,
54+
}
55+
56+
// Make sure the hotkey is unregistered when the created
57+
// hotkey is garbage collected.
58+
runtime.SetFinalizer(hk, func(x interface{}) {
59+
hk := x.(*Hotkey)
60+
hk.unregister()
61+
close(hk.keydownIn)
62+
close(hk.keyupIn)
63+
})
64+
return hk
4065
}
4166

4267
// Register registers a combination of hotkeys. If the hotkey has
@@ -49,12 +74,10 @@ func New(mods []Modifier, key Key) *Hotkey {
4974
func (hk *Hotkey) Register() error { return hk.register() }
5075

5176
// Keydown returns a channel that receives a signal when hotkey is triggered.
52-
func (hk *Hotkey) Keydown() <-chan Event { return hk.out }
77+
func (hk *Hotkey) Keydown() <-chan Event { return hk.keydownOut }
5378

5479
// Keyup returns a channel that receives a signal when the hotkey is released.
55-
func (hk *Hotkey) Keyup() <-chan Event {
56-
panic("hotkey: unimplemented")
57-
}
80+
func (hk *Hotkey) Keyup() <-chan Event { return hk.keyupOut }
5881

5982
// Unregister unregisters the hotkey.
6083
func (hk *Hotkey) Unregister() error {
@@ -63,14 +86,23 @@ func (hk *Hotkey) Unregister() error {
6386
return err
6487
}
6588

66-
// Setup a new event channel.
67-
close(hk.in)
68-
in, out := newEventChan()
69-
hk.in = in
70-
hk.out = out
89+
// Reset a new event channel.
90+
close(hk.keydownIn)
91+
close(hk.keyupIn)
92+
hk.keydownIn, hk.keydownOut = newEventChan()
93+
hk.keyupIn, hk.keyupOut = newEventChan()
7194
return nil
7295
}
7396

97+
// String returns a string representation of the hotkey.
98+
func (hk *Hotkey) String() string {
99+
s := fmt.Sprintf("%v", hk.key)
100+
for _, mod := range hk.mods {
101+
s += fmt.Sprintf("+%v", mod)
102+
}
103+
return s
104+
}
105+
74106
// newEventChan returns a sender and a receiver of a buffered channel
75107
// with infinite capacity.
76108
func newEventChan() (chan<- Event, <-chan Event) {

hotkey_darwin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func (hk *Hotkey) register() error {
4444
// A cgo handle could ran out of space, but since in hotkey purpose
4545
// we won't have that much number of hotkeys. So this should be fine.
4646

47-
h := cgo.NewHandle(hk.in)
47+
h := cgo.NewHandle(hk.keydownIn)
4848
var mod Modifier
4949
for _, m := range hk.mods {
5050
mod += m

hotkey_linux.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func (hk *Hotkey) handle() {
9696
if ret != 0 {
9797
continue
9898
}
99-
hk.in <- Event{}
99+
hk.keydownIn <- Event{}
100100
}
101101
}
102102
}

hotkey_windows.go

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
package hotkey
1010

1111
import (
12-
"context"
1312
"errors"
1413
"runtime"
1514
"sync"
1615
"sync/atomic"
16+
"time"
1717

1818
"golang.design/x/hotkey/internal/win"
1919
)
@@ -22,8 +22,7 @@ type platformHotkey struct {
2222
mu sync.Mutex
2323
hotkeyId uint64
2424
registered bool
25-
ctx context.Context
26-
cancel context.CancelFunc
25+
funcs chan func()
2726
canceled chan struct{}
2827
}
2928

@@ -36,24 +35,36 @@ func (hk *Hotkey) register() error {
3635
hk.mu.Lock()
3736
if hk.registered {
3837
hk.mu.Unlock()
39-
return errors.New("hotkey already registered.")
38+
return errors.New("hotkey already registered")
4039
}
40+
4141
mod := uint8(0)
4242
for _, m := range hk.mods {
4343
mod = mod | uint8(m)
4444
}
4545

4646
hk.hotkeyId = atomic.AddUint64(&hotkeyId, 1)
47-
ok, err := win.RegisterHotKey(0, uintptr(hk.hotkeyId), uintptr(mod), uintptr(hk.key))
47+
hk.funcs = make(chan func())
48+
hk.canceled = make(chan struct{})
49+
go hk.handle()
50+
51+
var (
52+
ok bool
53+
err error
54+
done = make(chan struct{})
55+
)
56+
hk.funcs <- func() {
57+
ok, err = win.RegisterHotKey(0, uintptr(hk.hotkeyId), uintptr(mod), uintptr(hk.key))
58+
done <- struct{}{}
59+
}
60+
<-done
4861
if !ok {
62+
close(hk.canceled)
63+
hk.mu.Unlock()
4964
return err
5065
}
5166
hk.registered = true
52-
hk.ctx, hk.cancel = context.WithCancel(context.Background())
53-
hk.canceled = make(chan struct{})
5467
hk.mu.Unlock()
55-
56-
go hk.handle()
5768
return nil
5869
}
5970

@@ -64,34 +75,64 @@ func (hk *Hotkey) unregister() error {
6475
if !hk.registered {
6576
return errors.New("hotkey is not registered")
6677
}
67-
hk.cancel()
78+
79+
done := make(chan struct{})
80+
hk.funcs <- func() {
81+
win.UnregisterHotKey(0, uintptr(hk.hotkeyId))
82+
done <- struct{}{}
83+
close(hk.canceled)
84+
}
85+
<-done
86+
6887
<-hk.canceled
6988
hk.registered = false
7089
return nil
7190
}
7291

73-
// msgHotkey represents hotkey message
74-
const msgHotkey uint32 = 0x0312
92+
const (
93+
// wmHotkey represents hotkey message
94+
wmHotkey uint32 = 0x0312
95+
wmQuit uint32 = 0x0012
96+
)
7597

7698
// handle handles the hotkey event loop.
7799
func (hk *Hotkey) handle() {
100+
// We could optimize this. So far each hotkey is served in an
101+
// individual thread. If we have too many hotkeys, then a program
102+
// have to create too many threads to serve them.
78103
runtime.LockOSThread()
79104
defer runtime.UnlockOSThread()
80105

81-
msg := win.MSG{}
82-
// KNOWN ISSUE: This message loop need to press an additional
83-
// hotkey to eventually return if ctx.Done() is ready.
84-
for win.GetMessage(&msg, 0, 0, 0) {
85-
switch msg.Message {
86-
case msgHotkey:
106+
tk := time.NewTicker(time.Second / 100)
107+
for range tk.C {
108+
msg := win.MSG{}
109+
if !win.PeekMessage(&msg, 0, 0, 0) {
87110
select {
88-
case <-hk.ctx.Done():
89-
win.UnregisterHotKey(0, uintptr(hk.hotkeyId))
90-
close(hk.canceled)
111+
case f := <-hk.funcs:
112+
f()
113+
case <-hk.canceled:
91114
return
92115
default:
93-
hk.in <- Event{}
94116
}
117+
continue
118+
}
119+
if !win.GetMessage(&msg, 0, 0, 0) {
120+
return
121+
}
122+
123+
switch msg.Message {
124+
case wmHotkey:
125+
hk.keydownIn <- Event{}
126+
127+
tk := time.NewTicker(time.Second / 100)
128+
for range tk.C {
129+
if win.GetAsyncKeyState(int(hk.key)) == 0 {
130+
hk.keyupIn <- Event{}
131+
break
132+
}
133+
}
134+
case wmQuit:
135+
return
95136
}
96137
}
97138
}

internal/win/hotkey.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ var (
1919
registerHotkey = user32.NewProc("RegisterHotKey")
2020
unregisterHotkey = user32.NewProc("UnregisterHotKey")
2121
getMessage = user32.NewProc("GetMessageW")
22+
peekMessage = user32.NewProc("PeekMessageA")
2223
sendMessage = user32.NewProc("SendMessageW")
24+
getAsyncKeyState = user32.NewProc("GetAsyncKeyState")
25+
quitMessage = user32.NewProc("PostQuitMessage")
2326
)
2427

2528
// RegisterHotKey defines a system-wide hot key.
@@ -87,3 +90,33 @@ func GetMessage(msg *MSG, hWnd uintptr, msgFilterMin, msgFilterMax uint32) bool
8790

8891
return ret != 0
8992
}
93+
94+
// PeekMessage dispatches incoming sent messages, checks the thread message
95+
// queue for a posted message, and retrieves the message (if any exist).
96+
//
97+
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-peekmessagea
98+
func PeekMessage(msg *MSG, hWnd uintptr, msgFilterMin, msgFilterMax uint32) bool {
99+
ret, _, _ := peekMessage.Call(
100+
uintptr(unsafe.Pointer(msg)),
101+
hWnd,
102+
uintptr(msgFilterMin),
103+
uintptr(msgFilterMax),
104+
0, // PM_NOREMOVE
105+
)
106+
107+
return ret != 0
108+
}
109+
110+
// PostQuitMessage indicates to the system that a thread has made
111+
// a request to terminate (quit). It is typically used in response
112+
// to a WM_DESTROY message.
113+
//
114+
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postquitmessage
115+
func PostQuitMessage(exitCode int) {
116+
quitMessage.Call(uintptr(exitCode))
117+
}
118+
119+
func GetAsyncKeyState(keycode int) uintptr {
120+
ret, _, _ := getAsyncKeyState.Call(uintptr(keycode))
121+
return ret
122+
}

0 commit comments

Comments
 (0)