Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 25 additions & 18 deletions src/main/core/doubleTapManager.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { uIOhook, UiohookKey } from 'uiohook-napi'
import { UiohookKey } from 'uiohook-napi'
import globalInputManager from './globalInputManager.js'

interface DoubleTapHandler {
modifier: string
callback: () => void
}

const INPUT_CONSUMER = 'double-tap'

// uiohook keycode → 修饰键名称映射
const MODIFIER_KEYCODES: Record<number, string> = {
[UiohookKey.Meta]: 'Command',
Expand Down Expand Up @@ -82,33 +85,30 @@ class DoubleTapManager {
// 只注册一次事件监听器,避免重复注册导致事件多次触发
if (!this.listenersRegistered) {
this.listenersRegistered = true
uIOhook.on('keydown', (e) => this.handleKeyDown(e))
uIOhook.on('keyup', (e) => this.handleKeyUp(e))
globalInputManager.on(INPUT_CONSUMER, 'keydown', (e) => this.handleKeyDown(e))
globalInputManager.on(INPUT_CONSUMER, 'keyup', (e) => this.handleKeyUp(e))
}

try {
uIOhook.start()
if (globalInputManager.acquire(INPUT_CONSUMER)) {
console.log('[DoubleTapManager] 全局键盘监听已启动')
} catch (error) {
console.error('[DoubleTapManager] 启动全局键盘监听失败:', error)
} else {
this.started = false
}
}

private stop(): void {
if (!this.started) return
try {
uIOhook.stop()
console.log('[DoubleTapManager] 全局键盘监听已停止')
} catch (error) {
console.error('[DoubleTapManager] 停止全局键盘监听失败:', error)
}
globalInputManager.release(INPUT_CONSUMER)
console.log('[DoubleTapManager] 全局键盘监听已停止')
this.started = false
this.listenersRegistered = false
this.lastModifierUp = null
this.nonModifierPressed = false
}

private handleKeyDown(e: { keycode: number }): void {
if (!this.started) return

const modifier = MODIFIER_KEYCODES[e.keycode]
if (modifier) {
if (this.modifierDownTime === 0) {
Expand All @@ -122,6 +122,8 @@ class DoubleTapManager {
}

private handleKeyUp(e: { keycode: number }): void {
if (!this.started) return

const modifier = MODIFIER_KEYCODES[e.keycode]
if (!modifier) {
this.nonModifierPressed = false
Expand Down Expand Up @@ -164,11 +166,16 @@ class DoubleTapManager {
private fireHandlers(modifier: string): void {
for (const handler of this.handlers) {
if (handler.modifier === modifier) {
try {
handler.callback()
} catch (error) {
console.error(`[DoubleTapManager] 回调执行失败 (${modifier}):`, error)
}
// 避免在 uiohook 的 keyup 调用栈里直接 show/focus 窗口,降低 Windows 焦点竞争概率。
setTimeout(() => {
if (!this.started) return

try {
handler.callback()
} catch (error) {
console.error(`[DoubleTapManager] 回调执行失败 (${modifier}):`, error)
}
}, 0)
}
}
}
Expand Down
84 changes: 84 additions & 0 deletions src/main/core/globalInputManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
uIOhook,
type UiohookKeyboardEvent,
type UiohookMouseEvent,
type UiohookWheelEvent
} from 'uiohook-napi'
import type { EventEmitter } from 'events'

type GlobalInputEventMap = {
input: UiohookKeyboardEvent | UiohookMouseEvent | UiohookWheelEvent
keydown: UiohookKeyboardEvent
keyup: UiohookKeyboardEvent
mousedown: UiohookMouseEvent
mouseup: UiohookMouseEvent
mousemove: UiohookMouseEvent
click: UiohookMouseEvent
wheel: UiohookWheelEvent
}

class GlobalInputManager {
// uIOhook 是进程级单例。用 consumer 引用计数管理 start/stop,避免一个模块 stop 掉其他模块的监听。
private consumers = new Set<string>()
// listener 按 consumer 归属记录,release 时只 off 当前模块注册的事件。
private listenersByConsumer = new Map<
string,
Array<{
event: keyof GlobalInputEventMap
listener: (...args: unknown[]) => void
}>
>()
private started = false

public on<K extends keyof GlobalInputEventMap>(
consumer: string,
event: K,
listener: (event: GlobalInputEventMap[K]) => void
): void {
const eventListener = listener as (...args: unknown[]) => void
;(uIOhook as EventEmitter).on(event, eventListener)

const listeners = this.listenersByConsumer.get(consumer) ?? []
listeners.push({ event, listener: eventListener })
this.listenersByConsumer.set(consumer, listeners)
}

public acquire(consumer: string): boolean {
this.consumers.add(consumer)
if (this.started) return true

try {
uIOhook.start()
this.started = true
console.log('[GlobalInput] 全局输入监听已启动')
return true
} catch (error) {
this.consumers.delete(consumer)
console.error('[GlobalInput] 启动全局输入监听失败:', error)
return false
}
}

public release(consumer: string): void {
const listeners = this.listenersByConsumer.get(consumer) ?? []
for (const { event, listener } of listeners) {
;(uIOhook as EventEmitter).off(event, listener)
}
this.listenersByConsumer.delete(consumer)

this.consumers.delete(consumer)
// 仍有其他模块依赖全局输入时,不能停止底层 uIOhook。
if (!this.started || this.consumers.size > 0) return

try {
uIOhook.stop()
console.log('[GlobalInput] 全局输入监听已停止')
} catch (error) {
console.error('[GlobalInput] 停止全局输入监听失败:', error)
} finally {
this.started = false
}
}
}

export default new GlobalInputManager()
Loading