diff --git a/apps/stage-tamagotchi/package.json b/apps/stage-tamagotchi/package.json index f1c6ccfed..3102eca2a 100644 --- a/apps/stage-tamagotchi/package.json +++ b/apps/stage-tamagotchi/package.json @@ -192,8 +192,10 @@ "h3": "2.0.1-rc.20", "less": "^4.6.4", "mkcert": "catalog:", + "node-abi": "catalog:", "std-env": "^4.1.0", "superjson": "catalog:", + "uiohook-napi": "catalog:", "unocss-preset-scrollbar": "^4.0.0", "unplugin-info": "^1.3.2", "unplugin-yaml": "^4.1.0", diff --git a/apps/stage-tamagotchi/src/main/services/electron/global-shortcut-uiohook.test.ts b/apps/stage-tamagotchi/src/main/services/electron/global-shortcut-uiohook.test.ts new file mode 100644 index 000000000..2b55906b8 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/electron/global-shortcut-uiohook.test.ts @@ -0,0 +1,368 @@ +import type { ShortcutBinding } from '@proj-airi/stage-shared/global-shortcut' + +import { ShortcutFailureReasons } from '@proj-airi/stage-shared/global-shortcut' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +/** + * Builds a binding for the uiohook driver. + * + * Defaults to `receiveKeyUps: true` because that flag is the dispatch + * signal in the orchestrator; the driver itself does not inspect it, + * but tests stay closer to how callers will use the driver this way. + * + * @example + * exampleBinding('ptt') + * // => { id: 'ptt', accelerator: { modifiers: ['shift'], key: 'KeyK' }, + * // scope: 'global', receiveKeyUps: true } + */ +function exampleBinding(id: string, modifiers: ShortcutBinding['accelerator']['modifiers'] = ['shift'], key = 'KeyK'): ShortcutBinding { + return { + id, + accelerator: { modifiers, key }, + scope: 'global', + receiveKeyUps: true, + } +} + +interface KeyboardEvent { + keycode: number + altKey: boolean + ctrlKey: boolean + metaKey: boolean + shiftKey: boolean +} + +function event(partial: Partial & Pick): KeyboardEvent { + return { + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, + ...partial, + } +} + +/** + * Wires mocks for `uiohook-napi` (singleton + listeners) and the + * `electron` `systemPreferences` surface, then imports the driver + * factory under test. + * + * @example + * const m = await setupMocks() + * const driver = m.createUiohookDriver({ ... }) + * m.fire('keydown', event({ keycode: 37 })) + */ +async function setupMocks() { + const onMock = vi.fn() + const removeListenerMock = vi.fn() + const startMock = vi.fn() + const stopMock = vi.fn() + const isTrustedAccessibilityClientMock = vi.fn(() => true) + + const listeners = new Map void>>() + + onMock.mockImplementation((event: string, listener: (e: KeyboardEvent) => void) => { + const arr = listeners.get(event) ?? [] + arr.push(listener) + listeners.set(event, arr) + }) + + removeListenerMock.mockImplementation((event: string, listener: (e: KeyboardEvent) => void) => { + const arr = listeners.get(event) + if (!arr) + return + listeners.set(event, arr.filter(l => l !== listener)) + }) + + // Mirrors the literal subset of `UiohookKey` that exercises the + // mapper. KeyK = 37, KeyA = 30 (matches real upstream constants so + // tests assert real keycodes, not arbitrary numbers). + const UiohookKey = { + K: 37, + A: 30, + Q: 16, + } as const + + vi.doMock('uiohook-napi', () => ({ + uIOhook: { + on: onMock, + removeListener: removeListenerMock, + start: startMock, + stop: stopMock, + }, + UiohookKey, + })) + + vi.doMock('electron', () => ({ + systemPreferences: { + isTrustedAccessibilityClient: isTrustedAccessibilityClientMock, + }, + })) + + const { createUiohookDriver } = await import('./global-shortcut-uiohook') + + function fire(name: 'keydown' | 'keyup', e: KeyboardEvent): void { + for (const listener of listeners.get(name) ?? []) + listener(e) + } + + function createDriver(overrides: { platform?: NodeJS.Platform, sessionType?: string } = {}) { + const broadcastTriggered = vi.fn<(id: string, phase: 'down' | 'up') => void>() + const logger = { + warn: vi.fn(), + withError: vi.fn(() => ({ warn: vi.fn() })), + } + const driver = createUiohookDriver({ + broadcastTriggered, + logger: logger as unknown as Parameters[0]['logger'], + platform: overrides.platform ?? 'darwin', + sessionType: overrides.sessionType, + }) + return { driver, broadcastTriggered, logger } + } + + return { + onMock, + removeListenerMock, + startMock, + stopMock, + isTrustedAccessibilityClientMock, + fire, + createDriver, + } +} + +describe('createUiohookDriver', () => { + beforeEach(() => { + vi.resetModules() + vi.clearAllMocks() + vi.restoreAllMocks() + }) + + it('starts the OS hook lazily and installs keydown/keyup listeners on first registration', async () => { + const m = await setupMocks() + const { driver } = m.createDriver() + + expect(m.startMock).not.toHaveBeenCalled() + expect(m.onMock).not.toHaveBeenCalled() + + const result = driver.tryRegister(exampleBinding('ptt')) + + expect(result).toEqual({ id: 'ptt', ok: true }) + expect(m.startMock).toHaveBeenCalledTimes(1) + expect(m.onMock).toHaveBeenCalledWith('keydown', expect.any(Function)) + expect(m.onMock).toHaveBeenCalledWith('keyup', expect.any(Function)) + }) + + it('stops the OS hook only after the last binding is unregistered', async () => { + const m = await setupMocks() + const { driver } = m.createDriver() + + driver.tryRegister(exampleBinding('a', ['shift'], 'KeyA')) + driver.tryRegister(exampleBinding('b', ['shift'], 'KeyQ')) + expect(m.stopMock).not.toHaveBeenCalled() + + driver.unregisterById('a') + expect(m.stopMock).not.toHaveBeenCalled() + + driver.unregisterById('b') + expect(m.stopMock).toHaveBeenCalledTimes(1) + }) + + it('broadcasts a "down" event when a matching keydown arrives', async () => { + const m = await setupMocks() + const { driver, broadcastTriggered } = m.createDriver() + driver.tryRegister(exampleBinding('ptt', ['shift'], 'KeyK')) + + m.fire('keydown', event({ keycode: 37, shiftKey: true })) + + expect(broadcastTriggered).toHaveBeenCalledTimes(1) + expect(broadcastTriggered).toHaveBeenCalledWith('ptt', 'down') + }) + + it('suppresses OS auto-repeat — repeated keydowns between matching down/up collapse to one broadcast', async () => { + // ROOT CAUSE: + // + // libuiohook reports the OS-level keydown stream verbatim, which + // includes auto-repeat events while the key remains physically + // held. Without per-binding `pressed` tracking, a held PTT key + // would emit hundreds of `down` broadcasts per second and the mic + // would start/stop frantically. + const m = await setupMocks() + const { driver, broadcastTriggered } = m.createDriver() + driver.tryRegister(exampleBinding('ptt', ['shift'], 'KeyK')) + + m.fire('keydown', event({ keycode: 37, shiftKey: true })) + m.fire('keydown', event({ keycode: 37, shiftKey: true })) + m.fire('keydown', event({ keycode: 37, shiftKey: true })) + + const downCalls = broadcastTriggered.mock.calls.filter(c => c[1] === 'down') + expect(downCalls).toHaveLength(1) + }) + + it('broadcasts "up" on matching keyup and re-arms the binding for the next press', async () => { + const m = await setupMocks() + const { driver, broadcastTriggered } = m.createDriver() + driver.tryRegister(exampleBinding('ptt', ['shift'], 'KeyK')) + + m.fire('keydown', event({ keycode: 37, shiftKey: true })) + m.fire('keyup', event({ keycode: 37, shiftKey: false })) + m.fire('keydown', event({ keycode: 37, shiftKey: true })) + + expect(broadcastTriggered).toHaveBeenNthCalledWith(1, 'ptt', 'down') + expect(broadcastTriggered).toHaveBeenNthCalledWith(2, 'ptt', 'up') + expect(broadcastTriggered).toHaveBeenNthCalledWith(3, 'ptt', 'down') + }) + + it('matches keyup by keycode even when modifiers were released before the main key', async () => { + // NOTICE: + // Users routinely release the modifier first (e.g. let Cmd go + // before letting K go). The keyup event for K therefore carries + // `metaKey: false`, which would fail strict modifier matching. + // The driver keys the "up" broadcast off the prior `pressed` + // state rather than the modifier predicate. + const m = await setupMocks() + const { driver, broadcastTriggered } = m.createDriver() + driver.tryRegister(exampleBinding('ptt', ['cmd-or-ctrl'], 'KeyK')) + + m.fire('keydown', event({ keycode: 37, metaKey: true })) + m.fire('keyup', event({ keycode: 37, metaKey: false })) + + expect(broadcastTriggered).toHaveBeenCalledWith('ptt', 'down') + expect(broadcastTriggered).toHaveBeenCalledWith('ptt', 'up') + }) + + it('ignores keyup when no matching keydown was tracked', async () => { + const m = await setupMocks() + const { driver, broadcastTriggered } = m.createDriver() + driver.tryRegister(exampleBinding('ptt', ['shift'], 'KeyK')) + + m.fire('keyup', event({ keycode: 37, shiftKey: true })) + + expect(broadcastTriggered).not.toHaveBeenCalled() + }) + + it('does not match a keydown that carries an extra modifier', async () => { + // Strict matching mirrors Electron's accelerator semantics: a + // `Shift+K` binding must not fire on `Cmd+Shift+K`. + const m = await setupMocks() + const { driver, broadcastTriggered } = m.createDriver() + driver.tryRegister(exampleBinding('ptt', ['shift'], 'KeyK')) + + m.fire('keydown', event({ keycode: 37, shiftKey: true, metaKey: true })) + + expect(broadcastTriggered).not.toHaveBeenCalled() + }) + + it('maps cmd-or-ctrl to metaKey on darwin', async () => { + const m = await setupMocks() + const { driver, broadcastTriggered } = m.createDriver({ platform: 'darwin' }) + driver.tryRegister(exampleBinding('ptt', ['cmd-or-ctrl'], 'KeyK')) + + m.fire('keydown', event({ keycode: 37, metaKey: true })) + m.fire('keydown', event({ keycode: 37, ctrlKey: true })) + + expect(broadcastTriggered).toHaveBeenCalledTimes(1) + expect(broadcastTriggered).toHaveBeenCalledWith('ptt', 'down') + }) + + it('maps cmd-or-ctrl to ctrlKey on non-darwin platforms', async () => { + const m = await setupMocks() + const { driver, broadcastTriggered } = m.createDriver({ platform: 'win32' }) + driver.tryRegister(exampleBinding('ptt', ['cmd-or-ctrl'], 'KeyK')) + + m.fire('keydown', event({ keycode: 37, ctrlKey: true })) + m.fire('keydown', event({ keycode: 37, metaKey: true })) + + // First (ctrl) matches; second (meta) does not — the pressed + // state stays cleared and produces no extra broadcast. + expect(broadcastTriggered).toHaveBeenCalledTimes(1) + expect(broadcastTriggered).toHaveBeenCalledWith('ptt', 'down') + }) + + it('rejects duplicate ids with reason "duplicate-id" and does not double-start the hook', async () => { + const m = await setupMocks() + const { driver } = m.createDriver() + + expect(driver.tryRegister(exampleBinding('ptt'))).toEqual({ id: 'ptt', ok: true }) + const second = driver.tryRegister(exampleBinding('ptt')) + expect(second).toEqual({ id: 'ptt', ok: false, reason: ShortcutFailureReasons.DuplicateId }) + expect(m.startMock).toHaveBeenCalledTimes(1) + }) + + it('returns Unsupported under a native Wayland session', async () => { + const m = await setupMocks() + const { driver } = m.createDriver({ platform: 'linux', sessionType: 'wayland' }) + + const result = driver.tryRegister(exampleBinding('ptt')) + expect(result).toEqual({ id: 'ptt', ok: false, reason: ShortcutFailureReasons.Unsupported }) + expect(m.startMock).not.toHaveBeenCalled() + }) + + it('permits registration on Linux under X11 / XWayland', async () => { + const m = await setupMocks() + const { driver } = m.createDriver({ platform: 'linux', sessionType: 'x11' }) + + expect(driver.tryRegister(exampleBinding('ptt'))).toEqual({ id: 'ptt', ok: true }) + expect(m.startMock).toHaveBeenCalledTimes(1) + }) + + it('returns Denied when macOS Accessibility permission is not granted', async () => { + const m = await setupMocks() + m.isTrustedAccessibilityClientMock.mockReturnValue(false) + const { driver } = m.createDriver({ platform: 'darwin' }) + + const result = driver.tryRegister(exampleBinding('ptt')) + expect(result).toEqual({ id: 'ptt', ok: false, reason: ShortcutFailureReasons.Denied }) + expect(m.isTrustedAccessibilityClientMock).toHaveBeenCalledWith(true) + expect(m.startMock).not.toHaveBeenCalled() + }) + + it('skips the Accessibility check entirely on non-darwin', async () => { + const m = await setupMocks() + const { driver } = m.createDriver({ platform: 'win32' }) + driver.tryRegister(exampleBinding('ptt')) + expect(m.isTrustedAccessibilityClientMock).not.toHaveBeenCalled() + }) + + it('unregisterAll clears every binding and stops the hook in one shot', async () => { + const m = await setupMocks() + const { driver, broadcastTriggered } = m.createDriver() + + driver.tryRegister(exampleBinding('a', ['shift'], 'KeyA')) + driver.tryRegister(exampleBinding('b', ['shift'], 'KeyQ')) + driver.unregisterAll() + + m.fire('keydown', event({ keycode: 30, shiftKey: true })) + m.fire('keydown', event({ keycode: 16, shiftKey: true })) + + expect(broadcastTriggered).not.toHaveBeenCalled() + expect(m.stopMock).toHaveBeenCalledTimes(1) + }) + + it('dispose removes the keydown/keyup listeners', async () => { + const m = await setupMocks() + const { driver } = m.createDriver() + driver.tryRegister(exampleBinding('ptt')) + + driver.dispose() + + expect(m.removeListenerMock).toHaveBeenCalledWith('keydown', expect.any(Function)) + expect(m.removeListenerMock).toHaveBeenCalledWith('keyup', expect.any(Function)) + }) + + it('keeps per-binding pressed state independent across multiple bindings', async () => { + const m = await setupMocks() + const { driver, broadcastTriggered } = m.createDriver() + driver.tryRegister(exampleBinding('a', ['shift'], 'KeyA')) + driver.tryRegister(exampleBinding('b', ['shift'], 'KeyQ')) + + m.fire('keydown', event({ keycode: 30, shiftKey: true })) + m.fire('keydown', event({ keycode: 16, shiftKey: true })) + m.fire('keyup', event({ keycode: 30 })) + + expect(broadcastTriggered).toHaveBeenNthCalledWith(1, 'a', 'down') + expect(broadcastTriggered).toHaveBeenNthCalledWith(2, 'b', 'down') + expect(broadcastTriggered).toHaveBeenNthCalledWith(3, 'a', 'up') + }) +}) diff --git a/apps/stage-tamagotchi/src/main/services/electron/global-shortcut-uiohook.ts b/apps/stage-tamagotchi/src/main/services/electron/global-shortcut-uiohook.ts new file mode 100644 index 000000000..eb0486e96 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/electron/global-shortcut-uiohook.ts @@ -0,0 +1,317 @@ +import type { useLogg } from '@guiiai/logg' +import type { + ShortcutAccelerator, + ShortcutBinding, + ShortcutKey, + ShortcutModifier, + ShortcutRegistrationResult, +} from '@proj-airi/stage-shared/global-shortcut' +import type { UiohookKeyboardEvent } from 'uiohook-napi' + +import process from 'node:process' + +import { ShortcutFailureReasons } from '@proj-airi/stage-shared/global-shortcut' +import { systemPreferences } from 'electron' +import { uIOhook, UiohookKey } from 'uiohook-napi' + +type Logger = ReturnType['useGlobalConfig']> + +interface ModifierMask { + ctrl: boolean + shift: boolean + alt: boolean + meta: boolean +} + +interface UiohookEntry { + binding: ShortcutBinding + predicate: (event: UiohookKeyboardEvent) => boolean + expectedKeycode: number + pressed: boolean +} + +const W3C_TO_UIOHOOK: Readonly> = buildKeycodeMap() + +function buildKeycodeMap(): Record { + const map: Record = {} + + for (let i = 0; i < 26; i++) { + const letter = String.fromCharCode(65 + i) + map[`Key${letter}`] = (UiohookKey as unknown as Record)[letter] + } + + for (let i = 0; i <= 9; i++) { + map[`Digit${i}`] = (UiohookKey as unknown as Record)[String(i)] + } + + for (let i = 1; i <= 24; i++) { + map[`F${i}`] = (UiohookKey as unknown as Record)[`F${i}`] + } + + const named: Record = { + Space: 'Space', + Tab: 'Tab', + Enter: 'Enter', + Escape: 'Escape', + Backspace: 'Backspace', + Delete: 'Delete', + Insert: 'Insert', + ArrowUp: 'ArrowUp', + ArrowDown: 'ArrowDown', + ArrowLeft: 'ArrowLeft', + ArrowRight: 'ArrowRight', + Home: 'Home', + End: 'End', + PageUp: 'PageUp', + PageDown: 'PageDown', + Backquote: 'Backquote', + Minus: 'Minus', + Equal: 'Equal', + BracketLeft: 'BracketLeft', + BracketRight: 'BracketRight', + Backslash: 'Backslash', + Semicolon: 'Semicolon', + Quote: 'Quote', + Comma: 'Comma', + Period: 'Period', + Slash: 'Slash', + } + for (const [w3c, uioName] of Object.entries(named)) + map[w3c] = (UiohookKey as unknown as Record)[uioName as string] + + return map +} + +function resolveModifierMask(modifiers: readonly ShortcutModifier[], platform: NodeJS.Platform): ModifierMask { + const mask: ModifierMask = { ctrl: false, shift: false, alt: false, meta: false } + for (const m of modifiers) { + switch (m) { + case 'cmd-or-ctrl': + if (platform === 'darwin') + mask.meta = true + else + mask.ctrl = true + break + case 'cmd': + case 'super': + // libuiohook surfaces macOS Cmd, Windows key, and X11 Super + // through the same `metaKey` flag. + mask.meta = true + break + case 'ctrl': + mask.ctrl = true + break + case 'alt': + mask.alt = true + break + case 'shift': + mask.shift = true + break + } + } + return mask +} + +function buildPredicate(acc: ShortcutAccelerator, platform: NodeJS.Platform): { predicate: UiohookEntry['predicate'], expectedKeycode: number } | undefined { + const expectedKeycode = W3C_TO_UIOHOOK[acc.key] + if (expectedKeycode === undefined) + return undefined + const required = resolveModifierMask(acc.modifiers, platform) + const predicate: UiohookEntry['predicate'] = e => + e.keycode === expectedKeycode + && e.ctrlKey === required.ctrl + && e.shiftKey === required.shift + && e.altKey === required.alt + && e.metaKey === required.meta + return { predicate, expectedKeycode } +} + +function isNativeWayland(platform: NodeJS.Platform, sessionType: string | undefined): boolean { + return platform === 'linux' && sessionType === 'wayland' +} + +function isMacAccessibilityTrusted(platform: NodeJS.Platform, prompt: boolean): boolean { + if (platform !== 'darwin') + return true + try { + return systemPreferences.isTrustedAccessibilityClient(prompt) + } + catch { + return true + } +} + +export interface UiohookDriverOptions { + broadcastTriggered: (id: string, phase: 'down' | 'up') => void + logger: Logger + /** + * Host platform; injected so tests can exercise cross-platform + * modifier mapping without stubbing `process`. + * + * @default process.platform + */ + platform?: NodeJS.Platform + /** + * `XDG_SESSION_TYPE` value used for the Wayland refusal check; + * injected for the same reason as `platform`. + * + * @default process.env.XDG_SESSION_TYPE + */ + sessionType?: string +} + +export interface UiohookDriver { + tryRegister: (binding: ShortcutBinding) => ShortcutRegistrationResult + unregisterById: (id: string) => void + unregisterAll: () => void + dispose: () => void +} + +/** + * Driver that captures global key-down and key-up events via + * libuiohook (through `uiohook-napi`). + * + * Use when: + * - A binding asks for `receiveKeyUps: true` (push-to-talk and similar + * hold-driven flows) + * + * Lifecycle: + * - The OS hook starts lazily on the first successful registration and + * stops once the last binding is unregistered, so apps that never + * bind a PTT shortcut never pay the permission/perf cost. + * + * Constraints: + * - macOS: requires the Accessibility permission. First registration + * triggers the system prompt; subsequent failures return `Denied`. + * - Linux: requires X11 or XWayland. Native Wayland sessions return + * `Unsupported` because XRecord cannot observe Wayland clients. + */ +export function createUiohookDriver(options: UiohookDriverOptions): UiohookDriver { + const { + broadcastTriggered, + logger, + platform = process.platform, + sessionType = process.env.XDG_SESSION_TYPE, + } = options + const entries = new Map() + let started = false + let listenersInstalled = false + + function ensureListeners(): void { + if (listenersInstalled) + return + listenersInstalled = true + uIOhook.on('keydown', onKeydown) + uIOhook.on('keyup', onKeyup) + } + + function startIfNeeded(): void { + if (started || entries.size === 0) + return + try { + uIOhook.start() + started = true + } + catch (error) { + logger.withError(error).warn('Failed to start uIOhook') + } + } + + function stopIfIdle(): void { + if (!started || entries.size > 0) + return + try { + uIOhook.stop() + } + catch (error) { + logger.withError(error).warn('Failed to stop uIOhook') + } + started = false + } + + function onKeydown(event: UiohookKeyboardEvent): void { + for (const entry of entries.values()) { + if (!entry.predicate(event)) + continue + // Auto-repeat suppression: OS may deliver repeated keydown + // while the key stays physically held. Emit one `down` per + // physical press until the matching keyup clears the flag. + if (entry.pressed) + continue + entry.pressed = true + broadcastTriggered(entry.binding.id, 'down') + } + } + + function onKeyup(event: UiohookKeyboardEvent): void { + // NOTICE: + // Match keyup by keycode alone; modifier flags may be released + // before the main key (e.g. Cmd released before K), in which case + // the strict predicate would not match. Pairing keyup to the + // binding via the prior `pressed` state ensures every `down` + // emits a matching `up`. + for (const entry of entries.values()) { + if (!entry.pressed) + continue + if (event.keycode !== entry.expectedKeycode) + continue + entry.pressed = false + broadcastTriggered(entry.binding.id, 'up') + } + } + + function tryRegister(binding: ShortcutBinding): ShortcutRegistrationResult { + if (entries.has(binding.id)) + return { id: binding.id, ok: false, reason: ShortcutFailureReasons.DuplicateId } + + if (isNativeWayland(platform, sessionType)) { + // libuiohook hooks install but never receive events under + // native Wayland. Refuse rather than register a binding that + // would silently no-op. + return { id: binding.id, ok: false, reason: ShortcutFailureReasons.Unsupported } + } + + if (!isMacAccessibilityTrusted(platform, true)) + return { id: binding.id, ok: false, reason: ShortcutFailureReasons.Denied } + + const built = buildPredicate(binding.accelerator, platform) + if (built === undefined) { + logger.warn(`uiohook driver: no keycode mapping for "${binding.accelerator.key}"`) + return { id: binding.id, ok: false, reason: ShortcutFailureReasons.Unsupported } + } + + entries.set(binding.id, { + binding, + predicate: built.predicate, + expectedKeycode: built.expectedKeycode, + pressed: false, + }) + ensureListeners() + startIfNeeded() + return { id: binding.id, ok: true } + } + + function unregisterById(id: string): void { + if (!entries.delete(id)) + return + stopIfIdle() + } + + function unregisterAll(): void { + if (entries.size === 0) + return + entries.clear() + stopIfIdle() + } + + function dispose(): void { + unregisterAll() + if (listenersInstalled) { + uIOhook.removeListener('keydown', onKeydown) + uIOhook.removeListener('keyup', onKeyup) + listenersInstalled = false + } + } + + return { tryRegister, unregisterById, unregisterAll, dispose } +} diff --git a/apps/stage-tamagotchi/src/main/services/electron/global-shortcut.test.ts b/apps/stage-tamagotchi/src/main/services/electron/global-shortcut.test.ts index e340f2cf5..e4329f4f8 100644 --- a/apps/stage-tamagotchi/src/main/services/electron/global-shortcut.test.ts +++ b/apps/stage-tamagotchi/src/main/services/electron/global-shortcut.test.ts @@ -101,6 +101,19 @@ async function setupMocks() { unregister: unregisterMock, unregisterAll: unregisterAllMock, }, + systemPreferences: { + isTrustedAccessibilityClient: vi.fn(() => true), + }, + })) + + vi.doMock('uiohook-napi', () => ({ + uIOhook: { + on: vi.fn(), + removeListener: vi.fn(), + start: vi.fn(), + stop: vi.fn(), + }, + UiohookKey: new Proxy({}, { get: () => 0 }), })) vi.doMock('@moeru/eventa', async (importOriginal) => { @@ -162,19 +175,15 @@ describe('setupGlobalShortcutService', () => { expect(m.registerMock).toHaveBeenCalledWith('CmdOrCtrl+Shift+K', expect.any(Function)) }) - it('refuses receiveKeyUps with reason "unsupported" and does not call globalShortcut', async () => { - // Electron's `globalShortcut` does not deliver key-release - // events. The driver refuses `receiveKeyUps: true` honestly so - // callers can switch to (or fail back from) the uiohook driver - // path that will handle it. + it('routes receiveKeyUps:true to the uiohook driver and bypasses electron.globalShortcut', async () => { const m = await setupMocks() const service = m.setupGlobalShortcutService() const ctx = createMockContext() registerMockWindow(service, ctx) const handler = ctx.invokeHandlers.get('eventa:invoke:electron:shortcut:register')! - const result = handler({ ...exampleBinding('ptt'), receiveKeyUps: true }) as { id: string, ok: boolean, reason?: string } - expect(result).toEqual({ id: 'ptt', ok: false, reason: ShortcutFailureReasons.Unsupported }) + const result = handler({ ...exampleBinding('ptt'), receiveKeyUps: true }) as { id: string, ok: boolean } + expect(result).toEqual({ id: 'ptt', ok: true }) expect(m.registerMock).not.toHaveBeenCalled() }) diff --git a/apps/stage-tamagotchi/src/main/services/electron/global-shortcut.ts b/apps/stage-tamagotchi/src/main/services/electron/global-shortcut.ts index 910ea9b7a..cbc86268f 100644 --- a/apps/stage-tamagotchi/src/main/services/electron/global-shortcut.ts +++ b/apps/stage-tamagotchi/src/main/services/electron/global-shortcut.ts @@ -15,6 +15,7 @@ import { electronShortcutUnregisterAll, } from '../../../shared/eventa' import { onAppBeforeQuit } from '../../libs/bootkit/lifecycle' +import { createUiohookDriver } from './global-shortcut-uiohook' export type EventaContext = ReturnType['context'] @@ -34,10 +35,10 @@ export interface GlobalShortcutService { dispose: () => void } -interface ActiveBinding { - binding: ShortcutBinding - electronAccelerator: string -} +type ActiveBinding = { binding: ShortcutBinding } & ( + | { driver: 'electron', electronAccelerator: string } + | { driver: 'uiohook' } +) export function setupGlobalShortcutService(): GlobalShortcutService { const log = useLogg('global-shortcut').useGlobalConfig() @@ -56,20 +57,12 @@ export function setupGlobalShortcutService(): GlobalShortcutService { } } - function tryRegister(binding: ShortcutBinding): ShortcutRegistrationResult { - if (binding.receiveKeyUps) { - // Electron's `globalShortcut` only fires on press. A separate - // driver path (uiohook-napi) handles `receiveKeyUps: true`; - // this driver refuses honestly until that path is wired. - return { id: binding.id, ok: false, reason: ShortcutFailureReasons.Unsupported } - } - - if (active.has(binding.id)) { - // Callers must `unregister` first to rebind. Avoids silent overrides - // between unrelated registration sites. - return { id: binding.id, ok: false, reason: ShortcutFailureReasons.DuplicateId } - } + const uiohookDriver = createUiohookDriver({ + broadcastTriggered, + logger: log, + }) + function tryRegisterElectron(binding: ShortcutBinding): ShortcutRegistrationResult { const electronAccelerator = formatElectronAccelerator(binding.accelerator) const ok = globalShortcut.register(electronAccelerator, () => broadcastTriggered(binding.id, 'down')) @@ -78,30 +71,37 @@ export function setupGlobalShortcutService(): GlobalShortcutService { // causes (held by another app, or denied by the OS for media // keys / Accessibility-gated combos on macOS). Electron does not // expose which case applied, so this driver reports `Conflict` - // for both. A future driver path (XDG portal, native macOS) can - // emit `Denied` directly. + // for both. The uiohook driver path can emit `Denied` directly. return { id: binding.id, ok: false, reason: ShortcutFailureReasons.Conflict } } - active.set(binding.id, { binding, electronAccelerator }) + active.set(binding.id, { binding, driver: 'electron', electronAccelerator }) return { id: binding.id, ok: true } } + function tryRegisterUiohook(binding: ShortcutBinding): ShortcutRegistrationResult { + const result = uiohookDriver.tryRegister(binding) + if (result.ok) + active.set(binding.id, { binding, driver: 'uiohook' }) + return result + } + + function tryRegister(binding: ShortcutBinding): ShortcutRegistrationResult { + if (active.has(binding.id)) { + return { id: binding.id, ok: false, reason: ShortcutFailureReasons.DuplicateId } + } + + return binding.receiveKeyUps + ? tryRegisterUiohook(binding) + : tryRegisterElectron(binding) + } + function unregisterById(id: string): void { const entry = active.get(id) if (!entry) return - try { - globalShortcut.unregister(entry.electronAccelerator) - } - catch (error) { - log.withError(error).warn(`Failed to unregister accelerator for "${id}"`) - } - active.delete(id) - } - function unregisterAll(): void { - for (const [id, entry] of active) { + if (entry.driver === 'electron') { try { globalShortcut.unregister(entry.electronAccelerator) } @@ -109,6 +109,24 @@ export function setupGlobalShortcutService(): GlobalShortcutService { log.withError(error).warn(`Failed to unregister accelerator for "${id}"`) } } + else { + uiohookDriver.unregisterById(id) + } + active.delete(id) + } + + function unregisterAll(): void { + for (const [id, entry] of active) { + if (entry.driver !== 'electron') + continue + try { + globalShortcut.unregister(entry.electronAccelerator) + } + catch (error) { + log.withError(error).warn(`Failed to unregister accelerator for "${id}"`) + } + } + uiohookDriver.unregisterAll() active.clear() } @@ -142,6 +160,7 @@ export function setupGlobalShortcutService(): GlobalShortcutService { const dispose: GlobalShortcutService['dispose'] = () => { unregisterAll() + uiohookDriver.dispose() contexts.clear() } diff --git a/apps/stage-tamagotchi/src/renderer/pages/devtools/global-shortcut.vue b/apps/stage-tamagotchi/src/renderer/pages/devtools/global-shortcut.vue index f26ea2c81..746254486 100644 --- a/apps/stage-tamagotchi/src/renderer/pages/devtools/global-shortcut.vue +++ b/apps/stage-tamagotchi/src/renderer/pages/devtools/global-shortcut.vue @@ -144,6 +144,18 @@ function formatTime(ms: number) { return new Date(ms).toLocaleTimeString() } +function deltaForTrigger(index: number): string { + const current = triggers.value[index] + if (!current) + return '' + for (let i = index + 1; i < triggers.value.length; i++) { + const prev = triggers.value[i] + if (prev.id === current.id) + return String(current.time - prev.time) + } + return '' +} + let disposeTriggerListener: (() => void) | undefined onMounted(async () => { @@ -195,7 +207,7 @@ onUnmounted(() => {
@@ -349,6 +361,9 @@ onUnmounted(() => { Phase + + Δ ms + @@ -366,6 +381,9 @@ onUnmounted(() => { {{ t.phase }} + + {{ deltaForTrigger(i) }} + diff --git a/cspell.config.yaml b/cspell.config.yaml index 5c77c815b..2fa12be3f 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -161,6 +161,7 @@ words: - lerp - libsamplerate - libsodium + - libuiohook - lightningcss - lipsync - listhen diff --git a/packages/stage-shared/src/global-shortcut/types.ts b/packages/stage-shared/src/global-shortcut/types.ts index 5a063f0d8..466942c78 100644 --- a/packages/stage-shared/src/global-shortcut/types.ts +++ b/packages/stage-shared/src/global-shortcut/types.ts @@ -62,10 +62,12 @@ export interface ShortcutBinding { /** * Whether the driver should also emit key-release events. * - * Drivers that cannot deliver release events refuse the registration - * with `{ ok: false, reason: ShortcutFailureReasons.Unsupported }`. The Electron - * `globalShortcut` driver currently refuses; a uiohook-based driver - * path is planned to honour this flag. + * Routes the binding through the uiohook driver, which delivers + * both `down` and `up` phases. Required for push-to-talk and any + * hold-driven flow. Drivers that genuinely cannot deliver release + * events under the current session (e.g. native Wayland for the + * uiohook path) refuse with `{ ok: false, reason: ShortcutFailureReasons.Unsupported }`; + * macOS without Accessibility permission refuses with `Denied`. * * @default false */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a6d17a8d..6a50631eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -276,6 +276,9 @@ catalogs: nanoid: specifier: 5.1.6 version: 5.1.6 + node-abi: + specifier: ^4.31.0 + version: 4.31.0 node-pty: specifier: ^1.1.0 version: 1.1.0 @@ -321,6 +324,9 @@ catalogs: tsx: specifier: ^4.21.0 version: 4.21.0 + uiohook-napi: + specifier: ^1.5.5 + version: 1.5.5 uncrypto: specifier: ^0.1.3 version: 0.1.3 @@ -1471,7 +1477,7 @@ importers: version: 3.0.2(electron@41.2.1) '@electron-toolkit/tsconfig': specifier: ^2.0.0 - version: 2.0.0(@types/node@24.12.2) + version: 2.0.0(@types/node@25.6.0) '@electron-toolkit/utils': specifier: ^4.0.0 version: 4.0.0(electron@41.2.1) @@ -1510,7 +1516,7 @@ importers: version: 3.1.0 '@intlify/unplugin-vue-i18n': specifier: ^11.0.7 - version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) + version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) '@modelcontextprotocol/sdk': specifier: 'catalog:' version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) @@ -1546,10 +1552,10 @@ importers: version: link:../../packages/ui-transitions '@proj-airi/unplugin-fetch': specifier: 'catalog:' - version: 0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@proj-airi/unplugin-live2d-sdk': specifier: ^0.1.7 - version: 0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + version: 0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) '@types/audioworklet': specifier: 'catalog:' version: 0.0.97 @@ -1576,7 +1582,7 @@ importers: version: 2.10.3 '@vitejs/plugin-vue': specifier: ^6.0.6 - version: 6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)) + version: 6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)) '@vue-macros/volar': specifier: ^3.1.2 version: 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)) @@ -1609,7 +1615,7 @@ importers: version: 6.8.3 electron-vite: specifier: ^5.0.0 - version: 5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) get-port-please: specifier: 'catalog:' version: 3.2.0 @@ -1622,39 +1628,45 @@ importers: mkcert: specifier: 'catalog:' version: 3.2.0 + node-abi: + specifier: 'catalog:' + version: 4.31.0 std-env: specifier: ^4.1.0 version: 4.1.0 superjson: specifier: 'catalog:' version: 2.2.6 + uiohook-napi: + specifier: 'catalog:' + version: 1.5.5 unocss-preset-scrollbar: specifier: ^4.0.0 - version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) unplugin-info: specifier: ^1.3.2 - version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) unplugin-yaml: specifier: ^4.1.0 - version: 4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.0(@nuxt/kit@3.20.2(magicast@0.5.2))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vite: specifier: 'catalog:' - version: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vite-bundle-visualizer: specifier: ^1.2.1 version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@4.60.1) vite-plugin-mkcert: specifier: 'catalog:' - version: 2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vite-plugin-vue-devtools: specifier: ^8.1.1 - version: 8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)) + version: 8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)) vite-plugin-vue-layouts: specifier: ^0.11.0 - version: 0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) + version: 0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) vue-macros: specifier: ^3.1.2 - version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)) + version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)) vue-tsc: specifier: ^3.2.6 version: 3.2.6(typescript@5.9.3) @@ -15122,8 +15134,8 @@ packages: resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==} engines: {node: '>=10'} - node-abi@4.24.0: - resolution: {integrity: sha512-u2EC1CeNe25uVtX3EZbdQ275c74zdZmmpzrHEQh2aIYqoVjlglfUpOX9YY85x1nlBydEKDVaSmMNhR7N82Qj8A==} + node-abi@4.31.0: + resolution: {integrity: sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==} engines: {node: '>=22.12.0'} node-addon-api@1.7.2: @@ -17213,6 +17225,10 @@ packages: uint4@0.1.2: resolution: {integrity: sha512-lhEx78gdTwFWG+mt6cWAZD/R6qrIj0TTBeH5xwyuDJyswLNlGe+KVlUPQ6+mx5Ld332pS0AMUTo9hIly7YsWxQ==} + uiohook-napi@1.5.5: + resolution: {integrity: sha512-oSlTdnECw2GBfsJPTbBQBeE4v/EXP0EZmX6BJq5nzH/JgFaBE8JpFwEA/kLhiEP7HxQw28FViWiYgdIZzWuuJQ==} + engines: {node: '>= 16'} + unbash@2.2.0: resolution: {integrity: sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==} engines: {node: '>=14'} @@ -19947,9 +19963,9 @@ snapshots: dependencies: electron: 41.2.1 - '@electron-toolkit/tsconfig@2.0.0(@types/node@24.12.2)': + '@electron-toolkit/tsconfig@2.0.0(@types/node@25.6.0)': dependencies: - '@types/node': 24.12.2 + '@types/node': 25.6.0 '@electron-toolkit/utils@4.0.0(electron@41.2.1)': dependencies: @@ -20028,7 +20044,7 @@ snapshots: detect-libc: 2.1.2 got: 11.8.6 graceful-fs: 4.2.11 - node-abi: 4.24.0 + node-abi: 4.31.0 node-api-version: 0.2.1 node-gyp: 11.5.0 ora: 5.4.1 @@ -20861,31 +20877,6 @@ snapshots: - supports-color - typescript - '@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) - '@intlify/bundle-utils': 11.0.7(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3))) - '@intlify/shared': 11.3.2 - '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.3.2)(@vue/compiler-dom@3.5.32)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) - '@rollup/pluginutils': 5.3.0(rollup@4.60.1) - '@typescript-eslint/scope-manager': 8.58.1 - '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3) - debug: 4.4.3(supports-color@10.2.2) - fast-glob: 3.3.3 - pathe: 2.0.3 - picocolors: 1.1.1 - unplugin: 2.3.11 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vue: 3.5.32(typescript@5.9.3) - optionalDependencies: - vue-i18n: 11.3.2(vue@3.5.32(typescript@5.9.3)) - transitivePeerDependencies: - - '@vue/compiler-dom' - - eslint - - rollup - - supports-color - - typescript - '@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1)) @@ -22867,35 +22858,11 @@ snapshots: transitivePeerDependencies: - magicast - '@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - ofetch: 1.5.1 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: ofetch: 1.5.1 vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)': - dependencies: - ofetch: 1.5.1 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - yauzl: 3.3.0 - transitivePeerDependencies: - - '@types/node' - - '@vitejs/devtools' - - esbuild - - jiti - - less - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - '@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)': dependencies: ofetch: 1.5.1 @@ -23796,7 +23763,6 @@ snapshots: '@types/node@25.6.0': dependencies: undici-types: 7.19.2 - optional: true '@types/nprogress@0.2.3': {} @@ -24462,12 +24428,6 @@ snapshots: vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vue: 3.5.32(typescript@5.9.3) - '@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))': - dependencies: - '@rolldown/pluginutils': 1.0.0-rc.13 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vue: 3.5.32(typescript@5.9.3) - '@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.13 @@ -24770,15 +24730,6 @@ snapshots: transitivePeerDependencies: - vue - '@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - sirv: 3.0.2 - vue: 3.5.32(typescript@5.9.3) - optionalDependencies: - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - transitivePeerDependencies: - - typescript - '@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: sirv: 3.0.2 @@ -26920,7 +26871,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-vite@5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + electron-vite@5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) @@ -26928,7 +26879,7 @@ snapshots: esbuild: 0.25.12 magic-string: 0.30.21 picocolors: 1.1.1 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -30257,7 +30208,7 @@ snapshots: dependencies: semver: 7.7.4 - node-abi@4.24.0: + node-abi@4.31.0: dependencies: semver: 7.7.4 @@ -32809,6 +32760,10 @@ snapshots: uint4@0.1.2: {} + uiohook-napi@1.5.5: + dependencies: + node-gyp-build: 4.8.4 + unbash@2.2.0: {} unconfig-core@7.5.0: @@ -32840,8 +32795,7 @@ snapshots: undici-types@7.16.0: {} - undici-types@7.19.2: - optional: true + undici-types@7.19.2: {} undici@6.24.1: {} @@ -32953,6 +32907,11 @@ snapshots: '@unocss/preset-mini': 66.6.8 unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + unocss-preset-scrollbar@4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))): + dependencies: + '@unocss/preset-mini': 66.6.8 + unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@unocss/cli': 66.6.8 @@ -33049,14 +33008,6 @@ snapshots: unplugin: 2.3.11 vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - optionalDependencies: - esbuild: 0.27.2 - rolldown: 1.0.0-rc.16 - rollup: 4.60.1 - unplugin: 2.3.11 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): optionalDependencies: esbuild: 0.27.2 @@ -33091,19 +33042,6 @@ snapshots: transitivePeerDependencies: - supports-color - unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - ci-info: 4.4.0 - git-url-parse: 16.1.0 - simple-git: 3.36.0 - unplugin: 2.3.11 - optionalDependencies: - esbuild: 0.27.2 - rollup: 4.60.1 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - transitivePeerDependencies: - - supports-color - unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: ci-info: 4.4.0 @@ -33213,17 +33151,6 @@ snapshots: rollup: 4.60.1 vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - unplugin-yaml@4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.60.1) - unplugin: 3.0.0 - yaml: 2.8.3 - optionalDependencies: - esbuild: 0.27.2 - rolldown: 1.0.0-rc.16 - rollup: 4.60.1 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 @@ -33455,22 +33382,12 @@ snapshots: - rollup - supports-color - vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - birpc: 2.9.0 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vite-hot-client: 2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: birpc: 2.9.0 vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vite-hot-client: 2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - vite-hot-client@2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vite-hot-client@2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) @@ -33516,21 +33433,6 @@ snapshots: - tsx - yaml - vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - ansis: 4.2.0 - debug: 4.4.3(supports-color@10.2.2) - error-stack-parser-es: 1.0.5 - ohash: 2.0.11 - open: 10.2.0 - perfect-debounce: 2.1.0 - sirv: 3.0.2 - unplugin-utils: 0.3.1 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vite-dev-rpc: 1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - transitivePeerDependencies: - - supports-color - vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: ansis: 4.2.0 @@ -33562,13 +33464,6 @@ snapshots: - typescript - ws - vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - debug: 4.4.3(supports-color@10.2.2) - supports-color: 10.2.2 - undici: 8.1.0 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -33587,20 +33482,6 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)): - dependencies: - '@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3)) - '@vue/devtools-kit': 8.1.1 - '@vue/devtools-shared': 8.1.1 - sirv: 3.0.2 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vite-plugin-inspect: 11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - vite-plugin-vue-inspector: 5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - transitivePeerDependencies: - - '@nuxt/kit' - - supports-color - - vue - vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)): dependencies: '@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3)) @@ -33615,21 +33496,6 @@ snapshots: - supports-color - vue - vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.29.0) - '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.29.0) - '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0) - '@vue/compiler-dom': 3.5.32 - kolorist: 1.8.0 - magic-string: 0.30.21 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - transitivePeerDependencies: - - supports-color - vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@babel/core': 7.29.0 @@ -33645,16 +33511,6 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)): - dependencies: - debug: 4.4.3(supports-color@10.2.2) - fast-glob: 3.3.3 - vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vue: 3.5.32(typescript@5.9.3) - vue-router: 5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) - transitivePeerDependencies: - - supports-color - vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)): dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -33921,54 +33777,6 @@ snapshots: - vue-tsc - webpack - vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)): - dependencies: - '@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/boolean-prop': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/chain-call': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/common': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/config': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/define-emit': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/define-models': 3.1.2(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/define-prop': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/define-props': 3.1.2(@vue-macros/reactivity-transform@3.1.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/define-props-refs': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/define-render': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/define-slots': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/define-stylex': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/devtools': 3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vue-macros/export-expose': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/export-props': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/export-render': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/hoist-static': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/jsx-directive': 3.1.2(typescript@5.9.3) - '@vue-macros/named-template': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/reactivity-transform': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/script-lang': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/setup-block': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/setup-component': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/setup-sfc': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/short-bind': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/short-emits': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/short-vmodel': 3.1.2(vue@3.5.32(typescript@5.9.3)) - '@vue-macros/volar': 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)) - unplugin: 2.3.11 - unplugin-combine: 2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - unplugin-vue-define-options: 3.1.2(vue@3.5.32(typescript@5.9.3)) - vue: 3.5.32(typescript@5.9.3) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - - '@rspack/core' - - '@vueuse/core' - - esbuild - - rolldown - - rollup - - typescript - - vite - - vue-tsc - - webpack - vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)): dependencies: '@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3)) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 639b5d232..03b58187e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -120,6 +120,7 @@ catalog: mkcert: ^3.2.0 nano-staged: ^1.0.2 nanoid: 5.1.6 + node-abi: ^4.31.0 node-pty: ^1.1.0 ofetch: ^1.5.1 oxc-minify: ^0.126.0 @@ -135,6 +136,7 @@ catalog: tinyexec: ^1.1.1 tsdown: ^0.21.9 tsx: ^4.21.0 + uiohook-napi: ^1.5.5 uncrypto: ^0.1.3 unplugin-info: ^1.3.2 unstorage: ^1.17.5 @@ -194,6 +196,7 @@ onlyBuiltDependencies: - sharp - spawn-sync - stockfish + - uiohook-napi - utf-8-validate - vue-demi packageExtensions: