mirror of
https://github.com/jahruz67/wisp-open.git
synced 2026-08-08 18:14:08 +00:00
kind of working finally
This commit is contained in:
+80
-99
@@ -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
@@ -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)]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user