⚡ Optimize hotkey service
This commit is contained in:
194
internal/common/hotkey/linux/hotkey.c
Normal file
194
internal/common/hotkey/linux/hotkey.c
Normal file
@@ -0,0 +1,194 @@
|
||||
//go:build linux
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
|
||||
extern void hotkeyDown(uintptr_t hkhandle);
|
||||
extern void hotkeyUp(uintptr_t hkhandle);
|
||||
|
||||
// Global error flag for BadAccess detection
|
||||
static int grab_error_occurred = 0;
|
||||
|
||||
// Error handler for X11 errors
|
||||
static int handleXError(Display* dpy, XErrorEvent* pErr) {
|
||||
// Check if it's a GrabKey error (request_code == 33)
|
||||
if (pErr->request_code == 33) {
|
||||
// BadAccess means the key is already grabbed by another client
|
||||
if (pErr->error_code == BadAccess) {
|
||||
grab_error_occurred = 1;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// For other errors, use default handler
|
||||
return 0;
|
||||
}
|
||||
|
||||
// displayTest checks if X11 display is available
|
||||
int displayTest() {
|
||||
Display* d = NULL;
|
||||
for (int i = 0; i < 42; i++) {
|
||||
d = XOpenDisplay(0);
|
||||
if (d == NULL) continue;
|
||||
XCloseDisplay(d);
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// DisplayContext represents a persistent X11 display connection
|
||||
typedef struct {
|
||||
Display* display;
|
||||
int keycode;
|
||||
unsigned int mod;
|
||||
Window root;
|
||||
} DisplayContext;
|
||||
|
||||
// openDisplay opens and initializes a display context
|
||||
// Returns NULL on failure
|
||||
DisplayContext* openDisplay(unsigned int mod, int key) {
|
||||
Display* d = NULL;
|
||||
for (int i = 0; i < 42; i++) {
|
||||
d = XOpenDisplay(0);
|
||||
if (d != NULL) break;
|
||||
}
|
||||
if (d == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
DisplayContext* ctx = (DisplayContext*)malloc(sizeof(DisplayContext));
|
||||
if (ctx == NULL) {
|
||||
XCloseDisplay(d);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ctx->display = d;
|
||||
ctx->keycode = XKeysymToKeycode(d, key);
|
||||
ctx->mod = mod;
|
||||
ctx->root = DefaultRootWindow(d);
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
// closeDisplay closes the display and frees the context
|
||||
void closeDisplay(DisplayContext* ctx) {
|
||||
if (ctx == NULL) return;
|
||||
if (ctx->display != NULL) {
|
||||
XCloseDisplay(ctx->display);
|
||||
}
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
// grabHotkey attempts to grab the hotkey with error handling
|
||||
// Returns: 0 on success, -1 on BadAccess (conflict), -2 on other errors
|
||||
int grabHotkey(DisplayContext* ctx) {
|
||||
if (ctx == NULL || ctx->display == NULL) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
// Set custom error handler
|
||||
grab_error_occurred = 0;
|
||||
XErrorHandler old_handler = XSetErrorHandler(handleXError);
|
||||
|
||||
// Attempt to grab the key
|
||||
XGrabKey(ctx->display, ctx->keycode, ctx->mod, ctx->root,
|
||||
False, GrabModeAsync, GrabModeAsync);
|
||||
|
||||
// Flush to ensure the grab request is processed
|
||||
XSync(ctx->display, False);
|
||||
|
||||
// Restore old error handler
|
||||
XSetErrorHandler(old_handler);
|
||||
|
||||
// Check if error occurred
|
||||
if (grab_error_occurred) {
|
||||
return -1; // BadAccess - hotkey conflict
|
||||
}
|
||||
|
||||
// Select input for both KeyPress and KeyRelease
|
||||
XSelectInput(ctx->display, ctx->root, KeyPressMask | KeyReleaseMask);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ungrabHotkey releases the grabbed hotkey
|
||||
void ungrabHotkey(DisplayContext* ctx) {
|
||||
if (ctx == NULL || ctx->display == NULL) return;
|
||||
XUngrabKey(ctx->display, ctx->keycode, ctx->mod, ctx->root);
|
||||
XFlush(ctx->display);
|
||||
}
|
||||
|
||||
// registerHotkey attempts to register the hotkey and returns a display context
|
||||
// Returns: context pointer on success, NULL on failure
|
||||
// Sets *error_code: 0 = success, -1 = BadAccess (conflict), -2 = other error
|
||||
DisplayContext* registerHotkey(unsigned int mod, int key, int* error_code) {
|
||||
DisplayContext* ctx = openDisplay(mod, key);
|
||||
if (ctx == NULL) {
|
||||
*error_code = -2;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int grab_result = grabHotkey(ctx);
|
||||
if (grab_result != 0) {
|
||||
*error_code = grab_result;
|
||||
closeDisplay(ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*error_code = 0;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
// unregisterHotkey ungrab the hotkey and closes the display
|
||||
void unregisterHotkey(DisplayContext* ctx) {
|
||||
if (ctx == NULL) return;
|
||||
ungrabHotkey(ctx);
|
||||
closeDisplay(ctx);
|
||||
}
|
||||
|
||||
// waitHotkeyEvent waits for the next hotkey event on an already-registered hotkey
|
||||
// Returns: 1 for KeyPress, 2 for KeyRelease, 0 for other events, -1 on error
|
||||
int waitHotkeyEvent(DisplayContext* ctx, uintptr_t hkhandle) {
|
||||
if (ctx == NULL || ctx->display == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
XEvent ev;
|
||||
XNextEvent(ctx->display, &ev);
|
||||
|
||||
switch(ev.type) {
|
||||
case KeyPress:
|
||||
hotkeyDown(hkhandle);
|
||||
return 1;
|
||||
case KeyRelease:
|
||||
hotkeyUp(hkhandle);
|
||||
return 2;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy waitHotkey for compatibility - now uses the new API internally
|
||||
// Returns: 0 on KeyRelease, -1 on BadAccess (conflict), -2 on other error
|
||||
int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key) {
|
||||
int error_code = 0;
|
||||
DisplayContext* ctx = registerHotkey(mod, key, &error_code);
|
||||
if (ctx == NULL) {
|
||||
return error_code;
|
||||
}
|
||||
|
||||
// Event loop
|
||||
while(1) {
|
||||
int result = waitHotkeyEvent(ctx, hkhandle);
|
||||
if (result == 2) { // KeyRelease
|
||||
unregisterHotkey(ctx);
|
||||
return 0;
|
||||
}
|
||||
if (result < 0) { // Error
|
||||
unregisterHotkey(ctx);
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
275
internal/common/hotkey/linux/hotkey.go
Normal file
275
internal/common/hotkey/linux/hotkey.go
Normal file
@@ -0,0 +1,275 @@
|
||||
//go:build linux
|
||||
|
||||
package linux
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lX11
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
// DisplayContext is defined in hotkey.c
|
||||
typedef struct DisplayContext DisplayContext;
|
||||
|
||||
int displayTest();
|
||||
DisplayContext* registerHotkey(unsigned int mod, int key, int* error_code);
|
||||
void unregisterHotkey(DisplayContext* ctx);
|
||||
int waitHotkeyEvent(DisplayContext* ctx, uintptr_t hkhandle);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const errmsg = `Failed to initialize the X11 display. Install the following dependency may help:
|
||||
|
||||
apt install -y libx11-dev
|
||||
If you are in an environment without a frame buffer (e.g., cloud server),
|
||||
you may also need 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
|
||||
`
|
||||
|
||||
var x11Available bool
|
||||
|
||||
func init() {
|
||||
x11Available = C.displayTest() == 0
|
||||
}
|
||||
|
||||
func checkX11() error {
|
||||
if !x11Available {
|
||||
return fmt.Errorf("%w: %s", ErrPlatformUnavailable, errmsg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Standard errors
|
||||
var (
|
||||
ErrAlreadyRegistered = errors.New("hotkey: already registered")
|
||||
ErrNotRegistered = errors.New("hotkey: not registered")
|
||||
ErrPlatformUnavailable = errors.New("hotkey: platform support unavailable")
|
||||
ErrHotkeyConflict = errors.New("hotkey: hotkey conflict with other applications")
|
||||
ErrFailedToRegister = errors.New("hotkey: failed to register")
|
||||
)
|
||||
|
||||
type PlatformHotkey struct {
|
||||
mu sync.Mutex
|
||||
registered bool
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
canceled chan struct{}
|
||||
cgoHandle cgo.Handle // 改名避免与方法名冲突
|
||||
displayCtx *C.DisplayContext // Persistent X11 display connection
|
||||
}
|
||||
|
||||
func (ph *PlatformHotkey) Register(mods []Modifier, key Key, keydownIn, keyupIn chan<- interface{}) error {
|
||||
// Check X11 availability first
|
||||
if err := checkX11(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ph.mu.Lock()
|
||||
if ph.registered {
|
||||
ph.mu.Unlock()
|
||||
return ErrAlreadyRegistered
|
||||
}
|
||||
|
||||
// Calculate combined modifier
|
||||
var mod Modifier
|
||||
for _, m := range mods {
|
||||
mod = mod | m
|
||||
}
|
||||
|
||||
// Try to register the hotkey and check for conflicts
|
||||
var errorCode C.int
|
||||
displayCtx := C.registerHotkey(C.uint(mod), C.int(key), &errorCode)
|
||||
if displayCtx == nil {
|
||||
ph.mu.Unlock()
|
||||
switch errorCode {
|
||||
case -1:
|
||||
return fmt.Errorf("%w: key combination already grabbed by another application", ErrHotkeyConflict)
|
||||
default:
|
||||
return fmt.Errorf("%w: failed to open X11 display or register hotkey", ErrFailedToRegister)
|
||||
}
|
||||
}
|
||||
|
||||
ph.registered = true
|
||||
ph.displayCtx = displayCtx
|
||||
ph.ctx, ph.cancel = context.WithCancel(context.Background())
|
||||
ph.canceled = make(chan struct{})
|
||||
|
||||
// Store callbacks info
|
||||
callbacks := &callbackData{
|
||||
keydownIn: keydownIn,
|
||||
keyupIn: keyupIn,
|
||||
}
|
||||
ph.cgoHandle = cgo.NewHandle(callbacks)
|
||||
ph.mu.Unlock()
|
||||
|
||||
go ph.eventLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ph *PlatformHotkey) Unregister() error {
|
||||
ph.mu.Lock()
|
||||
defer ph.mu.Unlock()
|
||||
if !ph.registered {
|
||||
return ErrNotRegistered
|
||||
}
|
||||
ph.cancel()
|
||||
ph.registered = false
|
||||
<-ph.canceled
|
||||
|
||||
// Clean up CGO handle and X11 display
|
||||
ph.cgoHandle.Delete()
|
||||
if ph.displayCtx != nil {
|
||||
C.unregisterHotkey(ph.displayCtx)
|
||||
ph.displayCtx = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// eventLoop continuously waits for hotkey events on the registered display.
|
||||
// The display connection is kept open for the lifetime of the registration,
|
||||
// avoiding repeated XOpenDisplay/XCloseDisplay calls.
|
||||
func (ph *PlatformHotkey) eventLoop() {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ph.ctx.Done():
|
||||
close(ph.canceled)
|
||||
return
|
||||
default:
|
||||
// Wait for the next event on the persistent display connection
|
||||
result := C.waitHotkeyEvent(ph.displayCtx, C.uintptr_t(ph.cgoHandle))
|
||||
// result: 1 = KeyPress, 2 = KeyRelease, 0 = other event, -1 = error
|
||||
if result < 0 {
|
||||
// Error occurred, likely display connection issue
|
||||
// The context will be canceled by Unregister, so we'll exit
|
||||
continue
|
||||
}
|
||||
// Events are handled by the C callback (hotkeyDown/hotkeyUp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type callbackData struct {
|
||||
keydownIn chan<- interface{}
|
||||
keyupIn chan<- interface{}
|
||||
}
|
||||
|
||||
//export hotkeyDown
|
||||
func hotkeyDown(h uintptr) {
|
||||
cb := cgo.Handle(h).Value().(*callbackData)
|
||||
cb.keydownIn <- struct{}{}
|
||||
}
|
||||
|
||||
//export hotkeyUp
|
||||
func hotkeyUp(h uintptr) {
|
||||
cb := cgo.Handle(h).Value().(*callbackData)
|
||||
cb.keyupIn <- struct{}{}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// ModAlt is typically mapped to Mod1 on most Linux systems
|
||||
ModAlt = Mod1
|
||||
)
|
||||
|
||||
// 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
|
||||
)
|
||||
77
internal/common/hotkey/linux/mainthread.go
Normal file
77
internal/common/hotkey/linux/mainthread.go
Normal file
@@ -0,0 +1,77 @@
|
||||
//go:build linux
|
||||
|
||||
package linux
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Call calls f on the main thread and blocks until f finishes.
|
||||
func Call(f func()) {
|
||||
done := donePool.Get().(chan error)
|
||||
defer donePool.Put(done)
|
||||
|
||||
data := funcData{fn: f, done: done}
|
||||
funcQ <- data
|
||||
if err := <-done; err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
done := donePool.Get().(chan error)
|
||||
defer donePool.Put(done)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
done <- nil
|
||||
}()
|
||||
main()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case f := <-funcQ:
|
||||
func() {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if f.done != nil {
|
||||
if r != nil {
|
||||
f.done <- fmt.Errorf("%v", r)
|
||||
} else {
|
||||
f.done <- nil
|
||||
}
|
||||
} else {
|
||||
if r != nil {
|
||||
select {
|
||||
case erroQ <- fmt.Errorf("%v", r):
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
f.fn()
|
||||
}()
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
funcQ = make(chan funcData, runtime.GOMAXPROCS(0))
|
||||
erroQ = make(chan error, 42)
|
||||
donePool = sync.Pool{New: func() interface{} {
|
||||
return make(chan error)
|
||||
}}
|
||||
)
|
||||
|
||||
type funcData struct {
|
||||
fn func()
|
||||
done chan error
|
||||
}
|
||||
Reference in New Issue
Block a user