mirror of
https://github.com/jahruz67/wisp-open.git
synced 2026-08-08 18:14:08 +00:00
feat: implement cross-platform hotkey management and update frontend build infrastructure
This commit is contained in:
+44
-25
@@ -2,10 +2,12 @@
|
||||
package hotkey
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"wis-free-v3/internal/logger"
|
||||
"wis-free-v3/internal/xhotkey"
|
||||
xhk "wis-free-v3/internal/xhotkey"
|
||||
)
|
||||
|
||||
// Listener handles global hotkey events and triggers callbacks when the
|
||||
@@ -15,7 +17,8 @@ type Listener struct {
|
||||
stopCallback func()
|
||||
isListening bool
|
||||
shortcut string
|
||||
hk *hotkey.Hotkey
|
||||
hk *xhk.Hotkey
|
||||
stopModPoll chan struct{}
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
@@ -37,12 +40,7 @@ func (l *Listener) UpdateShortcut(shortcut string) {
|
||||
l.mu.Lock()
|
||||
l.shortcut = shortcut
|
||||
wasListening := l.isListening
|
||||
|
||||
if l.hk != nil {
|
||||
l.hk.Unregister()
|
||||
l.hk = nil
|
||||
l.isListening = false
|
||||
}
|
||||
l.stopListeningLocked()
|
||||
l.mu.Unlock()
|
||||
|
||||
logger.Info("Hotkey updated: shortcut=%s", shortcut)
|
||||
@@ -55,26 +53,43 @@ func (l *Listener) UpdateShortcut(shortcut string) {
|
||||
// Start begins listening for the configured shortcut in a background goroutine.
|
||||
func (l *Listener) Start() {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
if l.hk != nil {
|
||||
if l.isListening {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
key, mods, ok := ParseShortcut(l.shortcut)
|
||||
key, mods, modOnly, ok := ParseShortcut(l.shortcut)
|
||||
if !ok {
|
||||
logger.Error("Failed to parse shortcut: %s", l.shortcut)
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.hk = hotkey.New(mods, key)
|
||||
if modOnly {
|
||||
if runtime.GOOS != "windows" {
|
||||
logger.Error("Modifier-only shortcuts like %q are only supported on Windows", l.shortcut)
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
ch := make(chan struct{})
|
||||
l.stopModPoll = ch
|
||||
l.isListening = true
|
||||
l.mu.Unlock()
|
||||
logger.Info("Hotkey listener started (modifier poll), waiting for %s", l.shortcut)
|
||||
go l.modifierPollLoop(mods, ch)
|
||||
return
|
||||
}
|
||||
|
||||
l.hk = xhk.New(mods, key)
|
||||
if err := l.hk.Register(); err != nil {
|
||||
logger.Error("Failed to register hotkey %s: %v", l.shortcut, err)
|
||||
l.hk = nil
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.isListening = true
|
||||
l.mu.Unlock()
|
||||
logger.Info("Hotkey listener started, waiting for %s", l.shortcut)
|
||||
|
||||
go l.eventLoop(l.hk)
|
||||
@@ -85,22 +100,26 @@ func (l *Listener) Stop() {
|
||||
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
|
||||
l.stopListeningLocked()
|
||||
logger.Info("Hotkey listener stopped")
|
||||
}
|
||||
|
||||
func (l *Listener) stopListeningLocked() {
|
||||
if l.stopModPoll != nil {
|
||||
close(l.stopModPoll)
|
||||
l.stopModPoll = nil
|
||||
}
|
||||
if l.hk != nil {
|
||||
if err := l.hk.Unregister(); err != nil {
|
||||
logger.Error("Failed to unregister hotkey: %v", err)
|
||||
}
|
||||
l.hk = nil
|
||||
}
|
||||
l.isListening = false
|
||||
}
|
||||
|
||||
// eventLoop runs the main keyboard event processing loop.
|
||||
func (l *Listener) eventLoop(hk *hotkey.Hotkey) {
|
||||
func (l *Listener) eventLoop(hk *xhk.Hotkey) {
|
||||
var isRecording bool
|
||||
|
||||
for {
|
||||
|
||||
+120
-80
@@ -3,96 +3,136 @@ package hotkey
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"wis-free-v3/internal/xhotkey"
|
||||
xhk "wis-free-v3/internal/xhotkey"
|
||||
)
|
||||
|
||||
// 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,
|
||||
var KeyMap = map[string]xhk.Key{
|
||||
"space": xhk.KeySpace,
|
||||
"0": xhk.Key0,
|
||||
"1": xhk.Key1,
|
||||
"2": xhk.Key2,
|
||||
"3": xhk.Key3,
|
||||
"4": xhk.Key4,
|
||||
"5": xhk.Key5,
|
||||
"6": xhk.Key6,
|
||||
"7": xhk.Key7,
|
||||
"8": xhk.Key8,
|
||||
"9": xhk.Key9,
|
||||
"a": xhk.KeyA,
|
||||
"b": xhk.KeyB,
|
||||
"c": xhk.KeyC,
|
||||
"d": xhk.KeyD,
|
||||
"e": xhk.KeyE,
|
||||
"f": xhk.KeyF,
|
||||
"g": xhk.KeyG,
|
||||
"h": xhk.KeyH,
|
||||
"i": xhk.KeyI,
|
||||
"j": xhk.KeyJ,
|
||||
"k": xhk.KeyK,
|
||||
"l": xhk.KeyL,
|
||||
"m": xhk.KeyM,
|
||||
"n": xhk.KeyN,
|
||||
"o": xhk.KeyO,
|
||||
"p": xhk.KeyP,
|
||||
"q": xhk.KeyQ,
|
||||
"r": xhk.KeyR,
|
||||
"s": xhk.KeyS,
|
||||
"t": xhk.KeyT,
|
||||
"u": xhk.KeyU,
|
||||
"v": xhk.KeyV,
|
||||
"w": xhk.KeyW,
|
||||
"x": xhk.KeyX,
|
||||
"y": xhk.KeyY,
|
||||
"z": xhk.KeyZ,
|
||||
"return": xhk.KeyReturn,
|
||||
"enter": xhk.KeyReturn,
|
||||
"escape": xhk.KeyEscape,
|
||||
"esc": xhk.KeyEscape,
|
||||
"delete": xhk.KeyDelete,
|
||||
"del": xhk.KeyDelete,
|
||||
"tab": xhk.KeyTab,
|
||||
"left": xhk.KeyLeft,
|
||||
"right": xhk.KeyRight,
|
||||
"up": xhk.KeyUp,
|
||||
"down": xhk.KeyDown,
|
||||
"f1": xhk.KeyF1,
|
||||
"f2": xhk.KeyF2,
|
||||
"f3": xhk.KeyF3,
|
||||
"f4": xhk.KeyF4,
|
||||
"f5": xhk.KeyF5,
|
||||
"f6": xhk.KeyF6,
|
||||
"f7": xhk.KeyF7,
|
||||
"f8": xhk.KeyF8,
|
||||
"f9": xhk.KeyF9,
|
||||
"f10": xhk.KeyF10,
|
||||
"f11": xhk.KeyF11,
|
||||
"f12": xhk.KeyF12,
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// ParseShortcut parses a shortcut string and returns the trigger key, modifiers, whether the
|
||||
// shortcut is modifier-only (no letter/key, e.g. "ctrl+win"), and whether parsing succeeded.
|
||||
//
|
||||
// For shortcuts that include a non-modifier key, the trigger key is the rightmost segment in
|
||||
// KeyMap; every other segment must be in ModMap (so "win+ctrl+k" and "ctrl+win+k" both work).
|
||||
//
|
||||
// If no segment matches KeyMap but every segment is a known modifier, modifierOnly is true
|
||||
// (supported on Windows via key-state polling; RegisterHotKey cannot represent these chords).
|
||||
func ParseShortcut(shortcut string) (key xhk.Key, mods []xhk.Modifier, modifierOnly bool, ok bool) {
|
||||
parts := strings.Split(strings.ToLower(strings.TrimSpace(shortcut)), "+")
|
||||
if len(parts) == 0 {
|
||||
return 0, nil, false
|
||||
return 0, nil, false, false
|
||||
}
|
||||
|
||||
// The last part is the trigger key
|
||||
triggerName := strings.TrimSpace(parts[len(parts)-1])
|
||||
k, exists := KeyMap[triggerName]
|
||||
if !exists {
|
||||
return 0, nil, false
|
||||
}
|
||||
|
||||
// The preceding parts are modifiers
|
||||
var mods []hotkey.Modifier
|
||||
for i := 0; i < len(parts)-1; i++ {
|
||||
modName := strings.TrimSpace(parts[i])
|
||||
if m, ok := ModMap[modName]; ok {
|
||||
mods = append(mods, m)
|
||||
keyIdx := -1
|
||||
var triggerKey xhk.Key
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
name := strings.TrimSpace(parts[i])
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if k, exists := KeyMap[name]; exists {
|
||||
keyIdx = i
|
||||
triggerKey = k
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return k, mods, true
|
||||
if keyIdx >= 0 {
|
||||
mods = nil
|
||||
for i := 0; i < len(parts); i++ {
|
||||
if i == keyIdx {
|
||||
continue
|
||||
}
|
||||
modName := strings.TrimSpace(parts[i])
|
||||
if modName == "" {
|
||||
continue
|
||||
}
|
||||
m, valid := ModMap[modName]
|
||||
if !valid {
|
||||
return 0, nil, false, false
|
||||
}
|
||||
mods = append(mods, m)
|
||||
}
|
||||
return triggerKey, mods, false, true
|
||||
}
|
||||
|
||||
mods = nil
|
||||
for i := 0; i < len(parts); i++ {
|
||||
modName := strings.TrimSpace(parts[i])
|
||||
if modName == "" {
|
||||
continue
|
||||
}
|
||||
m, valid := ModMap[modName]
|
||||
if !valid {
|
||||
return 0, nil, false, false
|
||||
}
|
||||
mods = append(mods, m)
|
||||
}
|
||||
if len(mods) < 2 {
|
||||
// Single-modifier "chords" would fire on every press of that key alone.
|
||||
return 0, nil, false, false
|
||||
}
|
||||
return 0, mods, true, true
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
//go:build !windows
|
||||
|
||||
package hotkey
|
||||
|
||||
import xhk "wis-free-v3/internal/xhotkey"
|
||||
|
||||
func (l *Listener) modifierPollLoop(required []xhk.Modifier, stop <-chan struct{}) {
|
||||
// Listener.Start rejects modifier-only shortcuts on non-Windows; never called.
|
||||
_, _ = required, stop
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//go:build windows
|
||||
|
||||
package hotkey
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"wis-free-v3/internal/logger"
|
||||
xhk "wis-free-v3/internal/xhotkey"
|
||||
)
|
||||
|
||||
const (
|
||||
vkShift = 0x10
|
||||
vkControl = 0x11
|
||||
vkMenu = 0x12
|
||||
vkLWin = 0x5B
|
||||
vkRWin = 0x5C
|
||||
)
|
||||
|
||||
func containsMod(mods []xhk.Modifier, want xhk.Modifier) bool {
|
||||
for _, m := range mods {
|
||||
if m == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// modifiersExactMatch reports whether ctrl/shift/alt/win match the required set exactly
|
||||
// (each required modifier is held, and no extra modifiers among those four are held).
|
||||
func modifiersExactMatch(required []xhk.Modifier) bool {
|
||||
needCtrl := containsMod(required, xhk.ModCtrl)
|
||||
needShift := containsMod(required, xhk.ModShift)
|
||||
needAlt := containsMod(required, xhk.ModAlt)
|
||||
needWin := containsMod(required, xhk.ModWin)
|
||||
|
||||
ctrlDown := xhk.AsyncKeyDown(vkControl)
|
||||
shiftDown := xhk.AsyncKeyDown(vkShift)
|
||||
altDown := xhk.AsyncKeyDown(vkMenu)
|
||||
winDown := xhk.AsyncKeyDown(vkLWin) || xhk.AsyncKeyDown(vkRWin)
|
||||
|
||||
return needCtrl == ctrlDown &&
|
||||
needShift == shiftDown &&
|
||||
needAlt == altDown &&
|
||||
needWin == winDown
|
||||
}
|
||||
|
||||
// modifierPollLoop implements modifier-only shortcuts (e.g. ctrl+win), which Windows
|
||||
// RegisterHotKey cannot represent.
|
||||
func (l *Listener) modifierPollLoop(required []xhk.Modifier, stop <-chan struct{}) {
|
||||
ticker := time.NewTicker(15 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
chordHeld := false
|
||||
isRecording := false
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
if isRecording {
|
||||
l.stopCallback()
|
||||
}
|
||||
logger.Info("Modifier hotkey poll stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
on := modifiersExactMatch(required)
|
||||
switch {
|
||||
case on && !chordHeld:
|
||||
chordHeld = true
|
||||
if !isRecording {
|
||||
logger.Info("Modifier chord activated: starting recording")
|
||||
go l.startCallback()
|
||||
isRecording = true
|
||||
}
|
||||
case !on && chordHeld:
|
||||
chordHeld = false
|
||||
if isRecording {
|
||||
logger.Info("Modifier chord released: stopping recording")
|
||||
go l.stopCallback()
|
||||
isRecording = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
package hotkey
|
||||
|
||||
import "wis-free-v3/internal/xhotkey"
|
||||
import xhk "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,
|
||||
var ModMap = map[string]xhk.Modifier{
|
||||
"ctrl": xhk.ModCtrl,
|
||||
"control": xhk.ModCtrl,
|
||||
"shift": xhk.ModShift,
|
||||
"alt": xhk.ModOption,
|
||||
"win": xhk.ModCmd,
|
||||
"windows": xhk.ModCmd,
|
||||
"meta": xhk.ModCmd,
|
||||
"super": xhk.ModCmd,
|
||||
"cmd": xhk.ModCmd,
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
package hotkey
|
||||
|
||||
import "wis-free-v3/internal/xhotkey"
|
||||
import xhk "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,
|
||||
var ModMap = map[string]xhk.Modifier{
|
||||
"ctrl": xhk.ModCtrl,
|
||||
"control": xhk.ModCtrl,
|
||||
"shift": xhk.ModShift,
|
||||
"alt": xhk.Mod1,
|
||||
"win": xhk.Mod4,
|
||||
"windows": xhk.Mod4,
|
||||
"meta": xhk.Mod4,
|
||||
"super": xhk.Mod4,
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
package hotkey
|
||||
|
||||
import "wis-free-v3/internal/xhotkey"
|
||||
import xhk "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,
|
||||
var ModMap = map[string]xhk.Modifier{
|
||||
"ctrl": xhk.ModCtrl,
|
||||
"control": xhk.ModCtrl,
|
||||
"shift": xhk.ModShift,
|
||||
"alt": xhk.ModAlt,
|
||||
"win": xhk.ModWin,
|
||||
"windows": xhk.ModWin,
|
||||
"meta": xhk.ModWin,
|
||||
"super": xhk.ModWin,
|
||||
}
|
||||
|
||||
@@ -28,8 +28,12 @@ type App interface {
|
||||
Quit()
|
||||
GetConfig() *config.Config
|
||||
ShowSettings()
|
||||
Version() string
|
||||
}
|
||||
|
||||
// trayLabel is the short product name shown in the tray (includes version when set at link time).
|
||||
var trayLabel = "wis-free-v3"
|
||||
|
||||
// Menu item references for dynamic updates
|
||||
var statusMenuItem *systray.MenuItem
|
||||
|
||||
@@ -42,11 +46,21 @@ func Start(app App) {
|
||||
)
|
||||
}
|
||||
|
||||
func appDisplayName(app App) string {
|
||||
v := app.Version()
|
||||
if v != "" && v != "dev" {
|
||||
return "wis-free-v3 v" + v
|
||||
}
|
||||
return "wis-free-v3"
|
||||
}
|
||||
|
||||
// onReady is called when the system tray is ready to be configured.
|
||||
func onReady(app App) {
|
||||
trayLabel = appDisplayName(app)
|
||||
|
||||
// Configure tray icon and tooltip
|
||||
systray.SetIcon(iconData)
|
||||
systray.SetTitle("wis-free-v3")
|
||||
systray.SetTitle(trayLabel)
|
||||
systray.SetTooltip(buildTooltip(app))
|
||||
|
||||
// Build menu structure
|
||||
@@ -119,14 +133,14 @@ func buildTooltip(app App) string {
|
||||
if cfg := app.GetConfig(); cfg != nil && cfg.Shortcut != "" {
|
||||
shortcut = cfg.Shortcut
|
||||
}
|
||||
return "wis-free-v3 - " + shortcut + " to record"
|
||||
return trayLabel + " - " + shortcut + " to record"
|
||||
}
|
||||
|
||||
// UpdateStatus updates the status text displayed in the tray menu.
|
||||
func UpdateStatus(status string) {
|
||||
if statusMenuItem != nil {
|
||||
statusMenuItem.SetTitle("Status: " + status)
|
||||
systray.SetTooltip("wis-free-v3 - " + status)
|
||||
systray.SetTooltip(trayLabel + " - " + status)
|
||||
|
||||
if strings.Contains(status, "Recording") {
|
||||
systray.SetIcon(iconRecordingData)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
//go:build !windows
|
||||
|
||||
package hotkey
|
||||
|
||||
// AsyncKeyDown is a stub on non-Windows platforms.
|
||||
func AsyncKeyDown(vk int) bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//go:build windows
|
||||
|
||||
package hotkey
|
||||
|
||||
import "wis-free-v3/internal/xhotkey/internal/win"
|
||||
|
||||
// AsyncKeyDown reports whether the virtual key is currently held (high bit of GetAsyncKeyState).
|
||||
func AsyncKeyDown(vk int) bool {
|
||||
return win.GetAsyncKeyState(vk)&0x8000 != 0
|
||||
}
|
||||
Reference in New Issue
Block a user