kind of working finally

This commit is contained in:
testing
2026-04-09 22:57:06 -07:00
parent ba0e75c503
commit 25ab3accfa
78 changed files with 6700 additions and 5000 deletions
+80 -99
View File
@@ -1,13 +1,11 @@
// Package hotkey provides global keyboard shortcut detection and handling.
// It uses the gohook library to capture system-wide key events.
package hotkey
import (
"sync"
"time"
"wis-free-v3/internal/logger"
hook "github.com/robotn/gohook"
"wis-free-v3/internal/xhotkey"
)
// Listener handles global hotkey events and triggers callbacks when the
@@ -16,145 +14,128 @@ type Listener struct {
startCallback func()
stopCallback func()
isListening bool
stopChan chan struct{}
triggerKeys []uint16
modifiers [][]uint16
shortcut string
hk *hotkey.Hotkey
mu sync.RWMutex
}
// NewListener creates a new hotkey listener with the specified shortcut and callbacks.
// The shortcut should be in format like "ctrl+k" or "alt+shift+space".
// onStart is called when the shortcut is pressed, onStop when it's released.
func NewListener(shortcut string, onStart, onStop func()) *Listener {
trigger, mods := ParseShortcut(shortcut)
logger.Info("Hotkey listener created: shortcut=%s, trigger=%d, modifiers=%v",
shortcut, trigger, mods)
logger.Info("Hotkey listener created: shortcut=%s", shortcut)
return &Listener{
startCallback: onStart,
stopCallback: onStop,
stopChan: make(chan struct{}),
triggerKeys: trigger,
modifiers: mods,
shortcut: shortcut,
}
}
// UpdateShortcut changes the shortcut without stopping the listener.
// This allows hot-swapping the shortcut while the application is running.
func (l *Listener) UpdateShortcut(shortcut string) {
trigger, mods := ParseShortcut(shortcut)
l.mu.Lock()
l.triggerKeys = trigger
l.modifiers = mods
l.shortcut = shortcut
wasListening := l.isListening
if l.hk != nil {
l.hk.Unregister()
l.hk = nil
l.isListening = false
}
l.mu.Unlock()
logger.Info("Hotkey updated: shortcut=%s, trigger=%d", shortcut, trigger)
logger.Info("Hotkey updated: shortcut=%s", shortcut)
if wasListening {
l.Start()
}
}
// Start begins listening for the configured shortcut in a background goroutine.
// It's safe to call Start multiple times; subsequent calls are ignored.
func (l *Listener) Start() {
if l.isListening {
l.mu.Lock()
defer l.mu.Unlock()
if l.hk != nil {
return
}
l.isListening = true
go l.eventLoop()
key, mods, ok := ParseShortcut(l.shortcut)
if !ok {
logger.Error("Failed to parse shortcut: %s", l.shortcut)
return
}
l.hk = hotkey.New(mods, key)
if err := l.hk.Register(); err != nil {
logger.Error("Failed to register hotkey %s: %v", l.shortcut, err)
l.hk = nil
return
}
l.isListening = true
logger.Info("Hotkey listener started, waiting for %s", l.shortcut)
go l.eventLoop(l.hk)
}
// Stop terminates the hotkey listener.
// It's safe to call Stop multiple times.
func (l *Listener) Stop() {
if !l.isListening {
l.mu.Lock()
defer l.mu.Unlock()
if l.hk == nil {
return
}
err := l.hk.Unregister()
if err != nil {
logger.Error("Failed to unregister hotkey: %v", err)
}
l.hk = nil
l.isListening = false
close(l.stopChan)
logger.Info("Hotkey listener stopped")
}
// eventLoop runs the main keyboard event processing loop.
func (l *Listener) eventLoop() {
logger.Info("Hotkey listener started")
evChan := hook.Start()
defer hook.End()
func (l *Listener) eventLoop(hk *hotkey.Hotkey) {
var isRecording bool
pressedKeys := make(map[uint16]bool)
for {
select {
case <-l.stopChan:
return
case _, ok := <-hk.Keydown():
if !ok {
return // Hotkey was unregistered
}
if !isRecording {
logger.Info("Shortcut activated: starting recording")
go l.startCallback()
isRecording = true
}
case ev := <-evChan:
l.handleKeyEvent(ev, pressedKeys, &isRecording)
}
}
}
// handleKeyEvent processes a single keyboard event.
func (l *Listener) handleKeyEvent(ev hook.Event, pressedKeys map[uint16]bool, isRecording *bool) {
// Update pressed keys state
switch ev.Kind {
case hook.KeyDown, hook.KeyHold:
pressedKeys[ev.Rawcode] = true
case hook.KeyUp:
delete(pressedKeys, ev.Rawcode)
default:
return
}
// Read current configuration
l.mu.RLock()
triggerVariants := l.triggerKeys
modGroups := l.modifiers
l.mu.RUnlock()
if len(triggerVariants) == 0 {
return
}
// 1. Check trigger key first (fast path)
triggerPressed := false
for _, t := range triggerVariants {
if pressedKeys[t] {
triggerPressed = true
break
}
}
// 2. State transition handling
active := false
if triggerPressed {
active = true
for _, group := range modGroups {
groupPressed := false
for _, m := range group {
if pressedKeys[m] {
groupPressed = true
break
case _, ok := <-hk.Keyup():
if !ok {
return // Hotkey was unregistered
}
if isRecording {
// X11 AutoRepeat Debounce logic
// Wait a tiny fraction of a second. If we get a Keydown during this window,
// it's an auto-repeat from holding the key, so we ignore both the Keyup and Keydown.
select {
case _, downOk := <-hk.Keydown():
if !downOk {
return
}
// AutoRepeat detected, skip this release!
case <-time.After(50 * time.Millisecond):
// Key was genuinely physically released
logger.Info("Shortcut released: stopping recording")
go l.stopCallback()
isRecording = false
}
}
if !groupPressed {
active = false
break
}
}
}
if active && !*isRecording {
logger.Info("Shortcut activated: starting recording")
go l.startCallback()
*isRecording = true
} else if !active && *isRecording {
logger.Info("Shortcut released: stopping recording")
go l.stopCallback()
*isRecording = false
}
}
+78 -92
View File
@@ -1,112 +1,98 @@
// Package hotkey provides global keyboard shortcut detection and handling.
package hotkey
import "strings"
import (
"strings"
// Windows Virtual Key Codes
// See: https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
const (
VK_LCTRL = 162 // Left Control
VK_RCTRL = 163 // Right Control
VK_LSHIFT = 160 // Left Shift
VK_RSHIFT = 161 // Right Shift
VK_LALT = 164 // Left Alt (Menu)
VK_RALT = 165 // Right Alt (Menu)
VK_LWIN = 91 // Left Windows key
VK_RWIN = 92 // Right Windows key
"wis-free-v3/internal/xhotkey"
)
// keyMap maps human-readable key names to Windows Virtual Key Codes.
// Each key may have multiple valid codes (e.g., left and right variants).
var keyMap = map[string][]uint16{
// Modifier keys
"ctrl": {VK_LCTRL, VK_RCTRL, 17},
"control": {VK_LCTRL, VK_RCTRL, 17},
"shift": {VK_LSHIFT, VK_RSHIFT, 16},
"alt": {VK_LALT, VK_RALT, 18},
"win": {VK_LWIN, VK_RWIN, 91, 92},
"windows": {VK_LWIN, VK_RWIN, 91, 92},
"meta": {VK_LWIN, VK_RWIN, 91, 92},
"super": {VK_LWIN, VK_RWIN, 91, 92},
// Special keys
"backspace": {8},
"tab": {9},
"enter": {13},
"escape": {27},
"esc": {27},
"space": {32},
"left": {37},
"up": {38},
"right": {39},
"down": {40},
"delete": {46},
"del": {46},
// Number keys (top row)
"0": {48}, "1": {49}, "2": {50}, "3": {51}, "4": {52},
"5": {53}, "6": {54}, "7": {55}, "8": {56}, "9": {57},
// Letter keys
"a": {65}, "b": {66}, "c": {67}, "d": {68}, "e": {69},
"f": {70}, "g": {71}, "h": {72}, "i": {73}, "j": {74},
"k": {75}, "l": {76}, "m": {77}, "n": {78}, "o": {79},
"p": {80}, "q": {81}, "r": {82}, "s": {83}, "t": {84},
"u": {85}, "v": {86}, "w": {87}, "x": {88}, "y": {89},
"z": {90},
// Function keys
"f1": {112}, "f2": {113}, "f3": {114}, "f4": {115},
"f5": {116}, "f6": {117}, "f7": {118}, "f8": {119},
"f9": {120}, "f10": {121}, "f11": {122}, "f12": {123},
// KeyMap maps human-readable key names to golang.design/x/hotkey Key instances.
var KeyMap = map[string]hotkey.Key{
"space": hotkey.KeySpace,
"0": hotkey.Key0,
"1": hotkey.Key1,
"2": hotkey.Key2,
"3": hotkey.Key3,
"4": hotkey.Key4,
"5": hotkey.Key5,
"6": hotkey.Key6,
"7": hotkey.Key7,
"8": hotkey.Key8,
"9": hotkey.Key9,
"a": hotkey.KeyA,
"b": hotkey.KeyB,
"c": hotkey.KeyC,
"d": hotkey.KeyD,
"e": hotkey.KeyE,
"f": hotkey.KeyF,
"g": hotkey.KeyG,
"h": hotkey.KeyH,
"i": hotkey.KeyI,
"j": hotkey.KeyJ,
"k": hotkey.KeyK,
"l": hotkey.KeyL,
"m": hotkey.KeyM,
"n": hotkey.KeyN,
"o": hotkey.KeyO,
"p": hotkey.KeyP,
"q": hotkey.KeyQ,
"r": hotkey.KeyR,
"s": hotkey.KeyS,
"t": hotkey.KeyT,
"u": hotkey.KeyU,
"v": hotkey.KeyV,
"w": hotkey.KeyW,
"x": hotkey.KeyX,
"y": hotkey.KeyY,
"z": hotkey.KeyZ,
"return": hotkey.KeyReturn,
"enter": hotkey.KeyReturn,
"escape": hotkey.KeyEscape,
"esc": hotkey.KeyEscape,
"delete": hotkey.KeyDelete,
"del": hotkey.KeyDelete,
"tab": hotkey.KeyTab,
"left": hotkey.KeyLeft,
"right": hotkey.KeyRight,
"up": hotkey.KeyUp,
"down": hotkey.KeyDown,
"f1": hotkey.KeyF1,
"f2": hotkey.KeyF2,
"f3": hotkey.KeyF3,
"f4": hotkey.KeyF4,
"f5": hotkey.KeyF5,
"f6": hotkey.KeyF6,
"f7": hotkey.KeyF7,
"f8": hotkey.KeyF8,
"f9": hotkey.KeyF9,
"f10": hotkey.KeyF10,
"f11": hotkey.KeyF11,
"f12": hotkey.KeyF12,
}
// modifierKeys identifies which key names are modifiers.
var modifierKeys = map[string]bool{
"ctrl": true, "control": true,
"shift": true,
"alt": true,
"win": true, "windows": true, "meta": true, "super": true,
}
// ParseShortcut parses a shortcut string into a trigger key and modifier keys.
// The shortcut format is "modifier+modifier+key" (e.g., "ctrl+shift+k").
//
// Returns:
// - trigger: the primary key code that activates the shortcut
// - modifiers: slice of modifier key code variants that must be held
func ParseShortcut(shortcut string) (trigger []uint16, modifiers [][]uint16) {
// ParseShortcut parses a shortcut string and returns the hotkey.Key and a list of modifiers.
// Returns false if parsing fails.
func ParseShortcut(shortcut string) (hotkey.Key, []hotkey.Modifier, bool) {
parts := strings.Split(strings.ToLower(strings.TrimSpace(shortcut)), "+")
if len(parts) == 0 {
return nil, nil
return 0, nil, false
}
// Last part is always the trigger key
// The last part is the trigger key
triggerName := strings.TrimSpace(parts[len(parts)-1])
if codes, ok := keyMap[triggerName]; ok && len(codes) > 0 {
trigger = codes
k, exists := KeyMap[triggerName]
if !exists {
return 0, nil, false
}
// Preceding parts are modifiers
// The preceding parts are modifiers
var mods []hotkey.Modifier
for i := 0; i < len(parts)-1; i++ {
modName := strings.TrimSpace(parts[i])
if codes, ok := keyMap[modName]; ok {
modifiers = append(modifiers, codes)
if m, ok := ModMap[modName]; ok {
mods = append(mods, m)
}
}
return trigger, modifiers
return k, mods, true
}
// IsModifier returns true if the given key name is a modifier key.
func IsModifier(keyName string) bool {
return modifierKeys[strings.ToLower(keyName)]
}
// GetKeyCode returns the virtual key code(s) for a given key name.
// Returns nil if the key name is not recognized.
func GetKeyCode(keyName string) []uint16 {
return keyMap[strings.ToLower(keyName)]
}
+17
View File
@@ -0,0 +1,17 @@
//go:build darwin
package hotkey
import "wis-free-v3/internal/xhotkey"
var ModMap = map[string]hotkey.Modifier{
"ctrl": hotkey.ModCtrl,
"control": hotkey.ModCtrl,
"shift": hotkey.ModShift,
"alt": hotkey.ModOption,
"win": hotkey.ModCmd,
"windows": hotkey.ModCmd,
"meta": hotkey.ModCmd,
"super": hotkey.ModCmd,
"cmd": hotkey.ModCmd,
}
+16
View File
@@ -0,0 +1,16 @@
//go:build linux
package hotkey
import "wis-free-v3/internal/xhotkey"
var ModMap = map[string]hotkey.Modifier{
"ctrl": hotkey.ModCtrl,
"control": hotkey.ModCtrl,
"shift": hotkey.ModShift,
"alt": hotkey.Mod1,
"win": hotkey.Mod4,
"windows": hotkey.Mod4,
"meta": hotkey.Mod4,
"super": hotkey.Mod4,
}
+16
View File
@@ -0,0 +1,16 @@
//go:build windows
package hotkey
import "wis-free-v3/internal/xhotkey"
var ModMap = map[string]hotkey.Modifier{
"ctrl": hotkey.ModCtrl,
"control": hotkey.ModCtrl,
"shift": hotkey.ModShift,
"alt": hotkey.ModAlt,
"win": hotkey.ModWin,
"windows": hotkey.ModWin,
"meta": hotkey.ModWin,
"super": hotkey.ModWin,
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

+15
View File
@@ -5,6 +5,7 @@ package tray
import (
_ "embed"
"os"
"strings"
"wis-free-v3/internal/config"
"wis-free-v3/internal/logger"
@@ -16,6 +17,12 @@ import (
//go:embed icon.ico
var iconData []byte
//go:embed icon_recording.png
var iconRecordingData []byte
//go:embed icon_transcribing.png
var iconTranscribingData []byte
// App defines the interface required by the tray package to interact with the main application.
type App interface {
Quit()
@@ -120,6 +127,14 @@ func UpdateStatus(status string) {
if statusMenuItem != nil {
statusMenuItem.SetTitle("Status: " + status)
systray.SetTooltip("wis-free-v3 - " + status)
if strings.Contains(status, "Recording") {
systray.SetIcon(iconRecordingData)
} else if strings.Contains(status, "Transcribing") {
systray.SetIcon(iconTranscribingData)
} else {
systray.SetIcon(iconData)
}
}
}
+12
View File
@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [changkun] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+65
View File
@@ -0,0 +1,65 @@
# Copyright 2021 The golang.design Initiative Authors.
# All rights reserved. Use of this source code is governed
# by a MIT license that can be found in the LICENSE file.
#
# Written by Changkun Ou <changkun.de>
name: hotkey
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
platform_test:
env:
DISPLAY: ':0.0'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
go: ['1.17.x', '1.18.x', '1.19.x']
steps:
- name: Install and run dependencies (xvfb libx11-dev)
if: ${{ runner.os == 'Linux' }}
run: |
sudo apt update
sudo apt install -y xvfb libx11-dev x11-utils libegl1-mesa-dev libgles2-mesa-dev
Xvfb :0 -screen 0 1024x768x24 > /dev/null 2>&1 &
# Wait for Xvfb
MAX_ATTEMPTS=120 # About 60 seconds
COUNT=0
echo -n "Waiting for Xvfb to be ready..."
while ! xdpyinfo -display "${DISPLAY}" >/dev/null 2>&1; do
echo -n "."
sleep 0.50s
COUNT=$(( COUNT + 1 ))
if [ "${COUNT}" -ge "${MAX_ATTEMPTS}" ]; then
echo " Gave up waiting for X server on ${DISPLAY}"
exit 1
fi
done
echo "Done - Xvfb is ready!"
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
stable: 'false'
go-version: ${{ matrix.go }}
- name: Run Tests with CGO_ENABLED=1
if: ${{ runner.os == 'Linux' || runner.os == 'macOS'}}
run: |
CGO_ENABLED=1 go test -v -covermode=atomic .
- name: Run Tests with CGO_ENABLED=0
if: ${{ runner.os == 'Linux' || runner.os == 'macOS'}}
run: |
CGO_ENABLED=0 go test -v -covermode=atomic .
- name: Run Tests on Windows
if: ${{ runner.os == 'Windows'}}
run: |
go test -v -covermode=atomic .
+15
View File
@@ -0,0 +1,15 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Changkun Ou <changkun.de>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+90
View File
@@ -0,0 +1,90 @@
# hotkey [![PkgGoDev](https://pkg.go.dev/badge/golang.design/x/hotkey)](https://pkg.go.dev/golang.design/x/hotkey) ![](https://changkun.de/urlstat?mode=github&repo=golang-design/hotkey) ![hotkey](https://github.com/golang-design/hotkey/workflows/hotkey/badge.svg?branch=main)
cross platform hotkey package in Go
```go
import "golang.design/x/hotkey"
```
## Features
- Cross platform supports: macOS, Linux (X11), and Windows
- Global hotkey registration without focus on a window
## API Usage
Package hotkey provides the basic facility to register a system-level
global hotkey shortcut so that an application can be notified if a user
triggers the desired hotkey. A hotkey must be a combination of modifiers
and a single key.
```go
package main
import (
"log"
"golang.design/x/hotkey"
"golang.design/x/hotkey/mainthread"
)
func main() { mainthread.Init(fn) } // Not necessary when use in Fyne, Ebiten or Gio.
func fn() {
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyS)
err := hk.Register()
if err != nil {
log.Fatalf("hotkey: failed to register hotkey: %v", err)
return
}
log.Printf("hotkey: %v is registered\n", hk)
<-hk.Keydown()
log.Printf("hotkey: %v is down\n", hk)
<-hk.Keyup()
log.Printf("hotkey: %v is up\n", hk)
hk.Unregister()
log.Printf("hotkey: %v is unregistered\n", hk)
}
```
Note platform specific details:
- On macOS, due to the OS restriction (other platforms does not have this
restriction), hotkey events must be handled on the "main thread".
Therefore, in order to use this package properly, one must start an OS
main event loop on the main thread, For self-contained applications,
using [golang.design/x/hotkey/mainthread](https://pkg.go.dev/golang.design/x/hotkey/mainthread)
is possible. It is uncessary or applications based on other GUI frameworks,
such as fyne, ebiten, or Gio. See the "[./examples](./examples)" folder
for more examples.
- On Linux (X11), when AutoRepeat is enabled in the X server, the Keyup
is triggered automatically and continuously as Keydown continues.
- On Linux (X11), some keys may be mapped to multiple Mod keys. To
correctly register the key combination, one must use the correct
underlying keycode combination. For example, a regular Ctrl+Alt+S
might be registered as: Ctrl+Mod2+Mod4+S.
- If this package did not include a desired key, one can always provide
the keycode to the API. For example, if a key code is 0x15, then the
corresponding key is `hotkey.Key(0x15)`.
## Examples
| Description | Folder |
|:------------|:------:|
| A minimum example | [minimum](./examples/minimum/main.go) |
| Register multiple hotkeys | [multiple](./examples/multiple/main.go) |
| A example to use in GLFW | [glfw](./examples/glfw/main.go) |
| A example to use in Fyne | [fyne](./examples/fyne/main.go) |
| A example to use in Ebiten | [ebiten](./examples/ebiten/main.go) |
| A example to use in Gio | [gio](./examples/gio/main.go) |
## Who is using this package?
The main purpose of building this package is to support the
[midgard](https://changkun.de/s/midgard) project.
To know more projects, check our [wiki](https://github.com/golang-design/hotkey/wiki) page.
## License
MIT | &copy; 2021 The golang.design Initiative Authors, written by [Changkun Ou](https://changkun.de).
+181
View File
@@ -0,0 +1,181 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
// Package hotkey provides the basic facility to register a system-level
// global hotkey shortcut so that an application can be notified if a user
// triggers the desired hotkey. A hotkey must be a combination of modifiers
// and a single key.
//
// Note platform specific details:
//
// - On macOS, due to the OS restriction (other platforms does not have
// this restriction), hotkey events must be handled on the "main thread".
// Therefore, in order to use this package properly, one must start an
// OS main event loop on the main thread, For self-contained applications,
// using [mainthread] package.
// is possible. It is uncessary or applications based on other GUI frameworks,
// such as fyne, ebiten, or Gio. See the "[examples]" for more examples.
//
// - On Linux (X11), when AutoRepeat is enabled in the X server, the
// Keyup is triggered automatically and continuously as Keydown continues.
//
// - On Linux (X11), some keys may be mapped to multiple Mod keys. To
// correctly register the key combination, one must use the correct
// underlying keycode combination. For example, a regular Ctrl+Alt+S
// might be registered as: Ctrl+Mod2+Mod4+S.
//
// - If this package did not include a desired key, one can always provide
// the keycode to the API. For example, if a key code is 0x15, then the
// corresponding key is `hotkey.Key(0x15)`.
//
// THe following is a minimum example:
//
// package main
//
// import (
// "log"
//
// "wis-free-v3/internal/xhotkey"
// "wis-free-v3/internal/xhotkey/mainthread"
// )
//
// func main() { mainthread.Init(fn) } // Not necessary when use in Fyne, Ebiten or Gio.
// func fn() {
// hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyS)
// err := hk.Register()
// if err != nil {
// log.Fatalf("hotkey: failed to register hotkey: %v", err)
// }
//
// log.Printf("hotkey: %v is registered\n", hk)
// <-hk.Keydown()
// log.Printf("hotkey: %v is down\n", hk)
// <-hk.Keyup()
// log.Printf("hotkey: %v is up\n", hk)
// hk.Unregister()
// log.Printf("hotkey: %v is unregistered\n", hk)
// }
//
// [mainthread]: https://pkg.go.dev/golang.design/x/hotkey/mainthread
// [examples]: https://github.com/golang-design/hotkey/tree/main/examples
package hotkey
import (
"fmt"
"runtime"
)
// Event represents a hotkey event
type Event struct{}
// Hotkey is a combination of modifiers and key to trigger an event
type Hotkey struct {
platformHotkey
mods []Modifier
key Key
keydownIn chan<- Event
keydownOut <-chan Event
keyupIn chan<- Event
keyupOut <-chan Event
}
// New creates a new hotkey for the given modifiers and keycode.
func New(mods []Modifier, key Key) *Hotkey {
keydownIn, keydownOut := newEventChan()
keyupIn, keyupOut := newEventChan()
hk := &Hotkey{
mods: mods,
key: key,
keydownIn: keydownIn,
keydownOut: keydownOut,
keyupIn: keyupIn,
keyupOut: keyupOut,
}
// Make sure the hotkey is unregistered when the created
// hotkey is garbage collected.
runtime.SetFinalizer(hk, func(x interface{}) {
hk := x.(*Hotkey)
hk.unregister()
close(hk.keydownIn)
close(hk.keyupIn)
})
return hk
}
// Register registers a combination of hotkeys. If the hotkey has
// registered. This function will invalidates the old registration
// and overwrites its callback.
func (hk *Hotkey) Register() error { return hk.register() }
// Keydown returns a channel that receives a signal when the hotkey is triggered.
func (hk *Hotkey) Keydown() <-chan Event { return hk.keydownOut }
// Keyup returns a channel that receives a signal when the hotkey is released.
func (hk *Hotkey) Keyup() <-chan Event { return hk.keyupOut }
// Unregister unregisters the hotkey.
func (hk *Hotkey) Unregister() error {
err := hk.unregister()
if err != nil {
return err
}
// Reset a new event channel.
close(hk.keydownIn)
close(hk.keyupIn)
hk.keydownIn, hk.keydownOut = newEventChan()
hk.keyupIn, hk.keyupOut = newEventChan()
return nil
}
// String returns a string representation of the hotkey.
func (hk *Hotkey) String() string {
s := fmt.Sprintf("%v", hk.key)
for _, mod := range hk.mods {
s += fmt.Sprintf("+%v", mod)
}
return s
}
// newEventChan returns a sender and a receiver of a buffered channel
// with infinite capacity.
func newEventChan() (chan<- Event, <-chan Event) {
in, out := make(chan Event), make(chan Event)
go func() {
var q []Event
for {
e, ok := <-in
if !ok {
close(out)
return
}
q = append(q, e)
for len(q) > 0 {
select {
case out <- q[0]:
q[0] = Event{}
q = q[1:]
case e, ok := <-in:
if ok {
q = append(q, e)
break
}
for _, e := range q {
out <- e
}
close(out)
return
}
}
}
}()
return in, out
}
+176
View File
@@ -0,0 +1,176 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build darwin
package hotkey
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Cocoa -framework Carbon
#include <stdint.h>
#import <Cocoa/Cocoa.h>
#import <Carbon/Carbon.h>
extern void keydownCallback(uintptr_t handle);
extern void keyupCallback(uintptr_t handle);
int registerHotKey(int mod, int key, uintptr_t handle, EventHotKeyRef* ref);
int unregisterHotKey(EventHotKeyRef ref);
*/
import "C"
import (
"errors"
"runtime/cgo"
"sync"
)
// Hotkey is a combination of modifiers and key to trigger an event
type platformHotkey struct {
mu sync.Mutex
registered bool
hkref C.EventHotKeyRef
}
func (hk *Hotkey) register() error {
hk.mu.Lock()
defer hk.mu.Unlock()
if hk.registered {
return errors.New("hotkey already registered")
}
// Note: we use handle number as hotkey id in the C side.
// A cgo handle could ran out of space, but since in hotkey purpose
// we won't have that much number of hotkeys. So this should be fine.
h := cgo.NewHandle(hk)
var mod Modifier
for _, m := range hk.mods {
mod += m
}
ret := C.registerHotKey(C.int(mod), C.int(hk.key), C.uintptr_t(h), &hk.hkref)
if ret == C.int(-1) {
return errors.New("failed to register the hotkey")
}
hk.registered = true
return nil
}
func (hk *Hotkey) unregister() error {
hk.mu.Lock()
defer hk.mu.Unlock()
if !hk.registered {
return errors.New("hotkey is not registered")
}
ret := C.unregisterHotKey(hk.hkref)
if ret == C.int(-1) {
return errors.New("failed to unregister the current hotkey")
}
hk.registered = false
return nil
}
//export keydownCallback
func keydownCallback(h uintptr) {
hk := cgo.Handle(h).Value().(*Hotkey)
hk.keydownIn <- Event{}
}
//export keyupCallback
func keyupCallback(h uintptr) {
hk := cgo.Handle(h).Value().(*Hotkey)
hk.keyupIn <- Event{}
}
// Modifier represents a modifier.
// See: /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h
type Modifier uint32
// All kinds of Modifiers
const (
ModCtrl Modifier = 0x1000
ModShift Modifier = 0x200
ModOption Modifier = 0x800
ModCmd Modifier = 0x100
)
// Key represents a key.
// See: /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h
type Key uint8
// All kinds of keys
const (
KeySpace Key = 49
Key1 Key = 18
Key2 Key = 19
Key3 Key = 20
Key4 Key = 21
Key5 Key = 23
Key6 Key = 22
Key7 Key = 26
Key8 Key = 28
Key9 Key = 25
Key0 Key = 29
KeyA Key = 0
KeyB Key = 11
KeyC Key = 8
KeyD Key = 2
KeyE Key = 14
KeyF Key = 3
KeyG Key = 5
KeyH Key = 4
KeyI Key = 34
KeyJ Key = 38
KeyK Key = 40
KeyL Key = 37
KeyM Key = 46
KeyN Key = 45
KeyO Key = 31
KeyP Key = 35
KeyQ Key = 12
KeyR Key = 15
KeyS Key = 1
KeyT Key = 17
KeyU Key = 32
KeyV Key = 9
KeyW Key = 13
KeyX Key = 7
KeyY Key = 16
KeyZ Key = 6
KeyReturn Key = 0x24
KeyEscape Key = 0x35
KeyDelete Key = 0x33
KeyTab Key = 0x30
KeyLeft Key = 0x7B
KeyRight Key = 0x7C
KeyUp Key = 0x7E
KeyDown Key = 0x7D
KeyF1 Key = 0x7A
KeyF2 Key = 0x78
KeyF3 Key = 0x63
KeyF4 Key = 0x76
KeyF5 Key = 0x60
KeyF6 Key = 0x61
KeyF7 Key = 0x62
KeyF8 Key = 0x64
KeyF9 Key = 0x65
KeyF10 Key = 0x6D
KeyF11 Key = 0x67
KeyF12 Key = 0x6F
KeyF13 Key = 0x69
KeyF14 Key = 0x6B
KeyF15 Key = 0x71
KeyF16 Key = 0x6A
KeyF17 Key = 0x40
KeyF18 Key = 0x4F
KeyF19 Key = 0x50
KeyF20 Key = 0x5A
)
+65
View File
@@ -0,0 +1,65 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build darwin
#include <stdint.h>
#import <Cocoa/Cocoa.h>
#import <Carbon/Carbon.h>
extern void keydownCallback(uintptr_t handle);
extern void keyupCallback(uintptr_t handle);
static OSStatus
keydownHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData) {
EventHotKeyID k;
GetEventParameter(theEvent, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(k), NULL, &k);
keydownCallback((uintptr_t)k.id); // use id as handle
return noErr;
}
static OSStatus
keyupHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData) {
EventHotKeyID k;
GetEventParameter(theEvent, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(k), NULL, &k);
keyupCallback((uintptr_t)k.id); // use id as handle
return noErr;
}
// registerHotkeyWithCallback registers a global system hotkey for callbacks.
int registerHotKey(int mod, int key, uintptr_t handle, EventHotKeyRef* ref) {
__block OSStatus s;
dispatch_sync(dispatch_get_main_queue(), ^{
EventTypeSpec keydownEvent;
keydownEvent.eventClass = kEventClassKeyboard;
keydownEvent.eventKind = kEventHotKeyPressed;
EventTypeSpec keyupEvent;
keyupEvent.eventClass = kEventClassKeyboard;
keyupEvent.eventKind = kEventHotKeyReleased;
InstallApplicationEventHandler(
&keydownHandler, 1, &keydownEvent, NULL, NULL
);
InstallApplicationEventHandler(
&keyupHandler, 1, &keyupEvent, NULL, NULL
);
EventHotKeyID hkid = {.id = handle};
s = RegisterEventHotKey(
key, mod, hkid, GetApplicationEventTarget(), 0, ref
);
});
if (s != noErr) {
return -1;
}
return 0;
}
int unregisterHotKey(EventHotKeyRef ref) {
OSStatus s = UnregisterEventHotKey(ref);
if (s != noErr) {
return -1;
}
return 0;
}
+68
View File
@@ -0,0 +1,68 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build darwin && cgo
package hotkey_test
import (
"context"
"fmt"
"testing"
"time"
"wis-free-v3/internal/xhotkey"
)
// TestHotkey should always run success.
// This is a test to run and for manually testing the registration of multiple
// hotkeys. Registered hotkeys:
// Ctrl+Shift+S
// Ctrl+Option+S
func TestHotkey(t *testing.T) {
tt := time.Second * 5
done := make(chan struct{}, 2)
ctx, cancel := context.WithTimeout(context.Background(), tt)
go func() {
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyS)
if err := hk.Register(); err != nil {
t.Errorf("failed to register hotkey: %v", err)
return
}
for {
select {
case <-ctx.Done():
cancel()
done <- struct{}{}
return
case <-hk.Keydown():
fmt.Println("triggered ctrl+shift+s")
}
}
}()
go func() {
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModOption}, hotkey.KeyS)
if err := hk.Register(); err != nil {
t.Errorf("failed to register hotkey: %v", err)
return
}
for {
select {
case <-ctx.Done():
cancel()
done <- struct{}{}
return
case <-hk.Keydown():
fmt.Println("triggered ctrl+option+s")
}
}
}()
<-done
<-done
}
+76
View File
@@ -0,0 +1,76 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build linux
#include <stdint.h>
#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/XKBlib.h>
#include <unistd.h>
extern void hotkeyDown(uintptr_t hkhandle);
extern void hotkeyUp(uintptr_t hkhandle);
extern int checkCancel(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;
}
// waitHotkey blocks until the hotkey is triggered.
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;
}
// Optional: Ask X server to only send one release at the physical end of auto-repeat.
Bool supported;
XkbSetDetectableAutoRepeat(d, True, &supported);
int keycode = XKeysymToKeycode(d, key);
XGrabKey(d, keycode, mod, DefaultRootWindow(d), False, GrabModeAsync, GrabModeAsync);
XSelectInput(d, DefaultRootWindow(d), KeyPressMask | KeyReleaseMask);
XEvent ev;
while(1) {
if (checkCancel(hkhandle) == 1) {
break;
}
if (XPending(d) > 0) {
XNextEvent(d, &ev);
switch(ev.type) {
case KeyPress:
hotkeyDown(hkhandle);
continue;
case KeyRelease:
hotkeyUp(hkhandle);
continue;
}
} else {
usleep(10000); // Poll every 10ms for snappy responsiveness without high CPU
}
}
XUngrabKey(d, keycode, mod, DefaultRootWindow(d));
XCloseDisplay(d);
return 0;
}
+218
View File
@@ -0,0 +1,218 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build linux
package hotkey
/*
#cgo LDFLAGS: -lX11
#include <stdint.h>
int displayTest();
int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key);
*/
import "C"
import (
"context"
"errors"
"runtime"
"runtime/cgo"
"sync"
)
const errmsg = `Failed to initialize the X11 display, and the clipboard package
will not work properly. Install the following dependency may help:
apt install -y libx11-dev
If the clipboard package is in an environment without a frame buffer,
such as a cloud server, it may also be necessary to install xvfb:
apt install -y xvfb
and initialize a virtual frame buffer:
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
export DISPLAY=:99.0
Then this package should be ready to use.
`
func init() {
if C.displayTest() != 0 {
panic(errmsg)
}
}
type platformHotkey struct {
mu sync.Mutex
registered bool
ctx context.Context
cancel context.CancelFunc
canceled chan struct{}
}
// Nothing needs to do for register
func (hk *Hotkey) register() error {
hk.mu.Lock()
if hk.registered {
hk.mu.Unlock()
return errors.New("hotkey already registered.")
}
hk.registered = true
hk.ctx, hk.cancel = context.WithCancel(context.Background())
hk.canceled = make(chan struct{})
hk.mu.Unlock()
go hk.handle()
return nil
}
// Nothing needs to do for unregister
func (hk *Hotkey) unregister() error {
hk.mu.Lock()
defer hk.mu.Unlock()
if !hk.registered {
return errors.New("hotkey is not registered.")
}
hk.cancel()
hk.registered = false
<-hk.canceled
return nil
}
// handle registers an application global hotkey to the system,
// and returns a channel that will signal if the hotkey is triggered.
func (hk *Hotkey) handle() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// KNOWN ISSUE: if a hotkey is grabbed by others, C side will crash the program
var mod Modifier
for _, m := range hk.mods {
mod = mod | m
}
h := cgo.NewHandle(hk)
defer h.Delete()
for {
// Just call it once! The C code handles its own loop until checkCancel returns 1.
_ = C.waitHotkey(C.uintptr_t(h), C.uint(mod), C.int(hk.key))
close(hk.canceled)
return
}
}
//export checkCancel
func checkCancel(h uintptr) C.int {
hk := cgo.Handle(h).Value().(*Hotkey)
select {
case <-hk.ctx.Done():
return 1
default:
return 0
}
}
//export hotkeyDown
func hotkeyDown(h uintptr) {
hk := cgo.Handle(h).Value().(*Hotkey)
hk.keydownIn <- Event{}
}
//export hotkeyUp
func hotkeyUp(h uintptr) {
hk := cgo.Handle(h).Value().(*Hotkey)
hk.keyupIn <- Event{}
}
// Modifier represents a modifier.
type Modifier uint32
// All kinds of Modifiers
// See /usr/include/X11/X.h
const (
ModCtrl Modifier = (1 << 2)
ModShift Modifier = (1 << 0)
Mod1 Modifier = (1 << 3)
Mod2 Modifier = (1 << 4)
Mod3 Modifier = (1 << 5)
Mod4 Modifier = (1 << 6)
Mod5 Modifier = (1 << 7)
)
// Key represents a key.
// See /usr/include/X11/keysymdef.h
type Key uint16
// All kinds of keys
const (
KeySpace Key = 0x0020
Key1 Key = 0x0030
Key2 Key = 0x0031
Key3 Key = 0x0032
Key4 Key = 0x0033
Key5 Key = 0x0034
Key6 Key = 0x0035
Key7 Key = 0x0036
Key8 Key = 0x0037
Key9 Key = 0x0038
Key0 Key = 0x0039
KeyA Key = 0x0061
KeyB Key = 0x0062
KeyC Key = 0x0063
KeyD Key = 0x0064
KeyE Key = 0x0065
KeyF Key = 0x0066
KeyG Key = 0x0067
KeyH Key = 0x0068
KeyI Key = 0x0069
KeyJ Key = 0x006a
KeyK Key = 0x006b
KeyL Key = 0x006c
KeyM Key = 0x006d
KeyN Key = 0x006e
KeyO Key = 0x006f
KeyP Key = 0x0070
KeyQ Key = 0x0071
KeyR Key = 0x0072
KeyS Key = 0x0073
KeyT Key = 0x0074
KeyU Key = 0x0075
KeyV Key = 0x0076
KeyW Key = 0x0077
KeyX Key = 0x0078
KeyY Key = 0x0079
KeyZ Key = 0x007a
KeyReturn Key = 0xff0d
KeyEscape Key = 0xff1b
KeyDelete Key = 0xffff
KeyTab Key = 0xff1b
KeyLeft Key = 0xff51
KeyRight Key = 0xff53
KeyUp Key = 0xff52
KeyDown Key = 0xff54
KeyF1 Key = 0xffbe
KeyF2 Key = 0xffbf
KeyF3 Key = 0xffc0
KeyF4 Key = 0xffc1
KeyF5 Key = 0xffc2
KeyF6 Key = 0xffc3
KeyF7 Key = 0xffc4
KeyF8 Key = 0xffc5
KeyF9 Key = 0xffc6
KeyF10 Key = 0xffc7
KeyF11 Key = 0xffc8
KeyF12 Key = 0xffc9
KeyF13 Key = 0xffca
KeyF14 Key = 0xffcb
KeyF15 Key = 0xffcc
KeyF16 Key = 0xffcd
KeyF17 Key = 0xffce
KeyF18 Key = 0xffcf
KeyF19 Key = 0xffd0
KeyF20 Key = 0xffd1
)
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build linux && cgo
package hotkey_test
import (
"context"
"fmt"
"testing"
"time"
"wis-free-v3/internal/xhotkey"
)
// TestHotkey should always run success.
// This is a test to run and for manually testing, registered combination:
// Ctrl+Alt+A (Ctrl+Mod2+Mod4+A on Linux)
func TestHotkey(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
}
for {
select {
case <-ctx.Done():
return
case <-hk.Keydown():
fmt.Println("triggered")
}
}
}
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build !windows && !cgo
package hotkey
type platformHotkey struct{}
// Modifier represents a modifier
type Modifier uint32
// Key represents a key.
type Key uint8
func (hk *Hotkey) register() error {
panic("hotkey: cannot use when CGO_ENABLED=0")
}
// unregister deregisteres a system hotkey.
func (hk *Hotkey) unregister() error {
panic("hotkey: cannot use when CGO_ENABLED=0")
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build (linux || darwin) && !cgo
package hotkey_test
import (
"testing"
"wis-free-v3/internal/xhotkey"
)
// TestHotkey should always run success.
// This is a test to run and for manually testing, registered combination:
// Ctrl+Alt+A (Ctrl+Mod2+Mod4+A on Linux)
func TestHotkey(t *testing.T) {
defer func() {
if r := recover(); r != nil {
return
}
t.Fatalf("expect to fail when CGO_ENABLED=0")
}()
hk := hotkey.New([]hotkey.Modifier{}, hotkey.Key(0))
err := hk.Register()
if err != nil {
t.Fatal(err)
}
hk.Unregister()
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
package hotkey_test
import (
"os"
"testing"
"wis-free-v3/internal/xhotkey/mainthread"
)
// The test cannot be run twice since the mainthread loop may not be terminated:
// go test -v -count=1
func TestMain(m *testing.M) {
mainthread.Init(func() { os.Exit(m.Run()) })
}
+226
View File
@@ -0,0 +1,226 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build windows
package hotkey
import (
"errors"
"runtime"
"sync"
"sync/atomic"
"time"
"wis-free-v3/internal/xhotkey/internal/win"
)
type platformHotkey struct {
mu sync.Mutex
hotkeyId uint64
registered bool
funcs chan func()
canceled chan struct{}
}
var hotkeyId uint64 // atomic
// register registers a system hotkey. It returns an error if
// the registration is failed. This could be that the hotkey is
// conflict with other hotkeys.
func (hk *Hotkey) register() error {
hk.mu.Lock()
if hk.registered {
hk.mu.Unlock()
return errors.New("hotkey already registered")
}
mod := uint8(0)
for _, m := range hk.mods {
mod = mod | uint8(m)
}
hk.hotkeyId = atomic.AddUint64(&hotkeyId, 1)
hk.funcs = make(chan func())
hk.canceled = make(chan struct{})
go hk.handle()
var (
ok bool
err error
done = make(chan struct{})
)
hk.funcs <- func() {
ok, err = win.RegisterHotKey(0, uintptr(hk.hotkeyId), uintptr(mod), uintptr(hk.key))
done <- struct{}{}
}
<-done
if !ok {
close(hk.canceled)
hk.mu.Unlock()
return err
}
hk.registered = true
hk.mu.Unlock()
return nil
}
// unregister deregisteres a system hotkey.
func (hk *Hotkey) unregister() error {
hk.mu.Lock()
defer hk.mu.Unlock()
if !hk.registered {
return errors.New("hotkey is not registered")
}
done := make(chan struct{})
hk.funcs <- func() {
win.UnregisterHotKey(0, uintptr(hk.hotkeyId))
done <- struct{}{}
close(hk.canceled)
}
<-done
<-hk.canceled
hk.registered = false
return nil
}
const (
// wmHotkey represents hotkey message
wmHotkey uint32 = 0x0312
wmQuit uint32 = 0x0012
)
// handle handles the hotkey event loop.
func (hk *Hotkey) handle() {
// We could optimize this. So far each hotkey is served in an
// individual thread. If we have too many hotkeys, then a program
// have to create too many threads to serve them.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
tk := time.NewTicker(time.Second / 100)
for range tk.C {
msg := win.MSG{}
if !win.PeekMessage(&msg, 0, 0, 0) {
select {
case f := <-hk.funcs:
f()
case <-hk.canceled:
return
default:
}
continue
}
if !win.GetMessage(&msg, 0, 0, 0) {
return
}
switch msg.Message {
case wmHotkey:
hk.keydownIn <- Event{}
tk := time.NewTicker(time.Second / 100)
for range tk.C {
if win.GetAsyncKeyState(int(hk.key)) == 0 {
hk.keyupIn <- Event{}
break
}
}
case wmQuit:
return
}
}
}
// Modifier represents a modifier.
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey
type Modifier uint8
// All kinds of Modifiers
const (
ModAlt Modifier = 0x1
ModCtrl Modifier = 0x2
ModShift Modifier = 0x4
ModWin Modifier = 0x8
)
// Key represents a key.
// https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
type Key uint16
// All kinds of Keys
const (
KeySpace Key = 0x20
Key0 Key = 0x30
Key1 Key = 0x31
Key2 Key = 0x32
Key3 Key = 0x33
Key4 Key = 0x34
Key5 Key = 0x35
Key6 Key = 0x36
Key7 Key = 0x37
Key8 Key = 0x38
Key9 Key = 0x39
KeyA Key = 0x41
KeyB Key = 0x42
KeyC Key = 0x43
KeyD Key = 0x44
KeyE Key = 0x45
KeyF Key = 0x46
KeyG Key = 0x47
KeyH Key = 0x48
KeyI Key = 0x49
KeyJ Key = 0x4A
KeyK Key = 0x4B
KeyL Key = 0x4C
KeyM Key = 0x4D
KeyN Key = 0x4E
KeyO Key = 0x4F
KeyP Key = 0x50
KeyQ Key = 0x51
KeyR Key = 0x52
KeyS Key = 0x53
KeyT Key = 0x54
KeyU Key = 0x55
KeyV Key = 0x56
KeyW Key = 0x57
KeyX Key = 0x58
KeyY Key = 0x59
KeyZ Key = 0x5A
KeyReturn Key = 0x0D
KeyEscape Key = 0x1B
KeyDelete Key = 0x2E
KeyTab Key = 0x09
KeyLeft Key = 0x25
KeyRight Key = 0x27
KeyUp Key = 0x26
KeyDown Key = 0x28
KeyF1 Key = 0x70
KeyF2 Key = 0x71
KeyF3 Key = 0x72
KeyF4 Key = 0x73
KeyF5 Key = 0x74
KeyF6 Key = 0x75
KeyF7 Key = 0x76
KeyF8 Key = 0x77
KeyF9 Key = 0x78
KeyF10 Key = 0x79
KeyF11 Key = 0x7A
KeyF12 Key = 0x7B
KeyF13 Key = 0x7C
KeyF14 Key = 0x7D
KeyF15 Key = 0x7E
KeyF16 Key = 0x7F
KeyF17 Key = 0x80
KeyF18 Key = 0x81
KeyF19 Key = 0x82
KeyF20 Key = 0x83
)
+42
View File
@@ -0,0 +1,42 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build windows
package hotkey_test
import (
"context"
"fmt"
"testing"
"time"
"wis-free-v3/internal/xhotkey"
)
// TestHotkey should always run success.
// This is a test to run and for manually testing, registered combination:
// Ctrl+Shift+S
func TestHotkey(t *testing.T) {
tt := time.Second * 5
ctx, cancel := context.WithTimeout(context.Background(), tt)
defer cancel()
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyS)
if err := hk.Register(); err != nil {
t.Errorf("failed to register hotkey: %v", err)
return
}
for {
select {
case <-ctx.Done():
return
case <-hk.Keydown():
fmt.Println("triggered")
}
}
}
+122
View File
@@ -0,0 +1,122 @@
// Copyright 2021 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build windows
// +build windows
package win
import (
"syscall"
"unsafe"
)
var (
user32 = syscall.NewLazyDLL("user32")
registerHotkey = user32.NewProc("RegisterHotKey")
unregisterHotkey = user32.NewProc("UnregisterHotKey")
getMessage = user32.NewProc("GetMessageW")
peekMessage = user32.NewProc("PeekMessageA")
sendMessage = user32.NewProc("SendMessageW")
getAsyncKeyState = user32.NewProc("GetAsyncKeyState")
quitMessage = user32.NewProc("PostQuitMessage")
)
// RegisterHotKey defines a system-wide hot key.
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey
func RegisterHotKey(hwnd, id uintptr, mod uintptr, k uintptr) (bool, error) {
ret, _, err := registerHotkey.Call(
hwnd, id, mod, k,
)
return ret != 0, err
}
// UnregisterHotKey frees a hot key previously registered by the calling
// thread.
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregisterhotkey
func UnregisterHotKey(hwnd, id uintptr) (bool, error) {
ret, _, err := unregisterHotkey.Call(hwnd, id)
return ret != 0, err
}
// MSG contains message information from a thread's message queue.
//
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-msg
type MSG struct {
HWnd uintptr
Message uint32
WParam uintptr
LParam uintptr
Time uint32
Pt struct { //POINT
x, y int32
}
}
// SendMessage sends the specified message to a window or windows.
// The SendMessage function calls the window procedure for the specified
// window and does not return until the window procedure has processed
// the message.
// The return value specifies the result of the message processing;
// it depends on the message sent.
//
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessage
func SendMessage(hwnd uintptr, msg uint32, wParam, lParam uintptr) uintptr {
ret, _, _ := sendMessage.Call(
hwnd,
uintptr(msg),
wParam,
lParam,
)
return ret
}
// GetMessage retrieves a message from the calling thread's message
// queue. The function dispatches incoming sent messages until a posted
// message is available for retrieval.
//
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage
func GetMessage(msg *MSG, hWnd uintptr, msgFilterMin, msgFilterMax uint32) bool {
ret, _, _ := getMessage.Call(
uintptr(unsafe.Pointer(msg)),
hWnd,
uintptr(msgFilterMin),
uintptr(msgFilterMax),
)
return ret != 0
}
// PeekMessage dispatches incoming sent messages, checks the thread message
// queue for a posted message, and retrieves the message (if any exist).
//
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-peekmessagea
func PeekMessage(msg *MSG, hWnd uintptr, msgFilterMin, msgFilterMax uint32) bool {
ret, _, _ := peekMessage.Call(
uintptr(unsafe.Pointer(msg)),
hWnd,
uintptr(msgFilterMin),
uintptr(msgFilterMax),
0, // PM_NOREMOVE
)
return ret != 0
}
// PostQuitMessage indicates to the system that a thread has made
// a request to terminate (quit). It is typically used in response
// to a WM_DESTROY message.
//
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postquitmessage
func PostQuitMessage(exitCode int) {
quitMessage.Call(uintptr(exitCode))
}
func GetAsyncKeyState(keycode int) uintptr {
ret, _, _ := getAsyncKeyState.Call(uintptr(keycode))
return ret
}
+10
View File
@@ -0,0 +1,10 @@
// Copyright 2022 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
// Package mainthread wrapps the golang.design/x/mainthread, and
// provides a different implementation for macOS so that it can
// handle main thread events for the NSApplication.
package mainthread
+19
View File
@@ -0,0 +1,19 @@
// Copyright 2022 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build windows || linux || (darwin && !cgo)
package mainthread
import "golang.design/x/mainthread"
// Call calls f on the main thread and blocks until f finishes.
func Call(f func()) { mainthread.Call(f) }
// Init initializes the functionality of running arbitrary subsequent functions be called on the main system thread.
//
// Init must be called in the main.main function.
func Init(main func()) { mainthread.Init(main) }
+69
View File
@@ -0,0 +1,69 @@
// Copyright 2022 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build darwin
package mainthread
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Cocoa
#import <Cocoa/Cocoa.h>
#import <Dispatch/Dispatch.h>
extern void os_main(void);
extern void wakeupMainThread(void);
static bool isMainThread() {
return [NSThread isMainThread];
}
*/
import "C"
import (
"os"
"runtime"
)
func init() {
runtime.LockOSThread()
}
// Call calls f on the main thread and blocks until f finishes.
func Call(f func()) {
if C.isMainThread() {
f()
return
}
go func() {
mainFuncs <- f
C.wakeupMainThread()
}()
}
// Init initializes the functionality of running arbitrary subsequent functions be called on the main system thread.
//
// Init must be called in the main.main function.
func Init(f func()) {
go func() {
f()
os.Exit(0)
}()
C.os_main()
}
var mainFuncs = make(chan func(), 1)
//export dispatchMainFuncs
func dispatchMainFuncs() {
for {
select {
case f := <-mainFuncs:
f()
default:
return
}
}
}
+28
View File
@@ -0,0 +1,28 @@
// Copyright 2022 The golang.design Initiative Authors.
// All rights reserved. Use of this source code is governed
// by a MIT license that can be found in the LICENSE file.
//
// Written by Changkun Ou <changkun.de>
//go:build darwin
#include <stdint.h>
#import <Cocoa/Cocoa.h>
extern void dispatchMainFuncs();
void wakeupMainThread(void) {
dispatch_async(dispatch_get_main_queue(), ^{
dispatchMainFuncs();
});
}
// The following three lines of code must run on the main thread.
// It must handle it using golang.design/x/mainthread.
//
// inspired from here: https://github.com/cehoffman/dotfiles/blob/4be8e893517e970d40746a9bdc67fe5832dd1c33/os/mac/iTerm2HotKey.m
void os_main(void) {
[NSApplication sharedApplication];
[NSApp disableRelaunchOnLogin];
[NSApp run];
}
+3
View File
@@ -0,0 +1,3 @@
# golang.design/x/mainthread v0.3.0
## explicit; go 1.16
golang.design/x/mainthread