fix(stage-tamagotchi): use rdev for keyboard events monitoring (#344)

This commit is contained in:
Neko
2025-08-04 16:14:30 +08:00
committed by GitHub
parent 856625f638
commit 42f42cce39
17 changed files with 5602 additions and 30 deletions
Generated
+52 -3
View File
@@ -187,6 +187,7 @@ dependencies = [
"tauri-plugin-os",
"tauri-plugin-positioner",
"tauri-plugin-prevent-default",
"tauri-plugin-rdev",
"tauri-plugin-window-pass-through-on-hover",
"tauri-plugin-window-router-link",
"tauri-plugin-window-state",
@@ -2760,7 +2761,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
dependencies = [
"bitflags 2.9.1",
"block2 0.6.1",
"dispatch2",
"libc",
"objc2 0.6.1",
]
@@ -2771,10 +2774,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4"
dependencies = [
"bitflags 2.9.1",
"block2 0.6.1",
"dispatch2",
"libc",
"objc2 0.6.1",
"objc2-core-foundation",
"objc2-io-surface",
"objc2-metal 0.3.1",
]
[[package]]
@@ -2850,6 +2856,17 @@ dependencies = [
"objc2-foundation 0.2.2",
]
[[package]]
name = "objc2-metal"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f246c183239540aab1782457b35ab2040d4259175bd1d0c58e46ada7b47a874"
dependencies = [
"bitflags 2.9.1",
"objc2 0.6.1",
"objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-quartz-core"
version = "0.2.2"
@@ -2860,7 +2877,7 @@ dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal",
"objc2-metal 0.2.2",
]
[[package]]
@@ -3593,6 +3610,24 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "rdev"
version = "0.6.0"
source = "git+https://github.com/Narsil/rdev#c14f2dc5c8100a96c5d7e3013de59d6aa0b9eae2"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"dispatch",
"lazy_static",
"libc",
"objc2 0.6.1",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.1",
"serde",
"winapi",
]
[[package]]
name = "realfft"
version = "3.5.0"
@@ -4112,9 +4147,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.141"
version = "1.0.142"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
dependencies = [
"itoa",
"memchr",
@@ -5087,6 +5122,20 @@ dependencies = [
"thiserror 2.0.12",
]
[[package]]
name = "tauri-plugin-rdev"
version = "0.7.0-beta.1"
dependencies = [
"anyhow",
"log",
"rdev",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tokio",
]
[[package]]
name = "tauri-plugin-window-pass-through-on-hover"
version = "0.7.0-beta.1"
+4
View File
@@ -3,6 +3,7 @@ members = [
"crates/tauri-plugin-ipc-audio-transcription-ort",
"crates/tauri-plugin-ipc-audio-vad-ort",
"crates/tauri-plugin-mcp",
"crates/tauri-plugin-rdev",
"crates/tauri-plugin-window-pass-through-on-hover",
"crates/tauri-plugin-window-router-link",
"apps/stage-tamagotchi/src-tauri"
@@ -23,6 +24,9 @@ path = "./crates/tauri-plugin-ipc-audio-vad-ort"
[workspace.dependencies.tauri-plugin-mcp]
path = "./crates/tauri-plugin-mcp"
[workspace.dependencies.tauri-plugin-rdev]
path = "./crates/tauri-plugin-rdev"
[workspace.dependencies.tauri-plugin-window-pass-through-on-hover]
path = "./crates/tauri-plugin-window-pass-through-on-hover"
@@ -36,6 +36,7 @@ tauri-plugin-mcp = { workspace = true }
tauri-plugin-ipc-audio-transcription-ort = { workspace = true }
tauri-plugin-ipc-audio-vad-ort = { workspace = true }
tauri-plugin-prevent-default = "1.3"
tauri-plugin-rdev = { workspace = true }
tauri-plugin-window-pass-through-on-hover = { workspace = true }
tauri-plugin-window-router-link = { workspace = true }
serde = { version = "1.0.219", features = ["derive"] }
+4 -1
View File
@@ -33,6 +33,7 @@ pub fn run() {
.plugin(tauri_plugin_ipc_audio_transcription_ort::init())
.plugin(tauri_plugin_ipc_audio_vad_ort::init())
.plugin(tauri_plugin_window_pass_through_on_hover::init())
.plugin(tauri_plugin_rdev::init())
.plugin(tauri_plugin_window_router_link::init(
WindowMatcher::new()
.register("chat", |app, on_page_load| {
@@ -63,7 +64,9 @@ pub fn run() {
builder = builder.title_bar_style(tauri::TitleBarStyle::Transparent);
}
let _ = builder.build().unwrap();
let window = builder.build().unwrap();
#[cfg(debug_assertions)]
window.open_devtools();
#[cfg(target_os = "macos")]
{
@@ -1,16 +1,17 @@
import type { ShortcutEvent } from '@tauri-apps/plugin-global-shortcut'
import { register, unregisterAll } from '@tauri-apps/plugin-global-shortcut'
import { until } from '@vueuse/shared'
import { useMagicKeys } from '@vueuse/core'
import { until, whenever } from '@vueuse/shared'
import { storeToRefs } from 'pinia'
import { watch } from 'vue'
import { useShortcutsStore } from '../stores/shortcuts'
import { useAppRuntime } from './runtime'
import { useTauriRdevEventTarget } from './tauri-rdev'
export function useTauriGlobalShortcuts() {
const { shortcuts } = storeToRefs(useShortcutsStore())
const { platform, isInitialized } = useAppRuntime()
const eventTarget = useTauriRdevEventTarget()
const keys = useMagicKeys({ target: eventTarget })
watch(shortcuts, async () => {
await until(isInitialized).toBeTruthy()
@@ -18,25 +19,13 @@ export function useTauriGlobalShortcuts() {
return
}
await unregisterAll()
for (const handler of shortcuts.value) {
if (!handler.shortcut) {
return
}
await register(handler.shortcut
.replaceAll('Meta', 'CommandOrControl')
.replaceAll('meta', 'CommandOrControl')
.replaceAll('Ctrl', 'CommandOrControl')
.replaceAll('ctrl', 'CommandOrControl')
.replaceAll('Option', 'OptionOrAlt')
.replaceAll('option', 'OptionOrAlt'), (event: ShortcutEvent) => {
if (event.state !== 'Pressed') {
return
}
handler.handle(event).catch((error) => {
whenever(keys[handler.shortcut], async () => {
handler.handle().catch((error) => {
console.error('Error handling shortcut', error)
})
})
@@ -0,0 +1,76 @@
import type { AiriTamagotchiEvents } from './tauri'
import { onMounted, onUnmounted } from 'vue'
import { KeyCode, mapKeyCode, mapKeyKey } from '../tauri/rdev'
import { useTauriEvent } from './tauri'
export function useTauriRdevEventTarget(): EventTarget {
const unListenFuncs: (() => void)[] = []
const eventTarget = new EventTarget()
const { listen } = useTauriEvent<AiriTamagotchiEvents>()
async function setup() {
unListenFuncs.push(await listen('tauri-plugins:tauri-plugin-rdev:keyup', (event) => {
if (typeof event.payload.event_type.KeyRelease === 'object' && 'Unknown' in event.payload.event_type.KeyRelease) {
if (event.payload.event_type.KeyRelease.Unknown === 62) {
event.payload.event_type = { KeyRelease: KeyCode.ControlLeft }
}
}
if (typeof event.payload.event_type.KeyRelease !== 'string') {
console.warn('unknown key release event:', event.payload.event_type.KeyRelease)
return
}
const e = new KeyboardEvent('keyup', {
key: mapKeyKey[event.payload.event_type.KeyRelease],
code: mapKeyCode[event.payload.event_type.KeyRelease],
metaKey: mapKeyKey[event.payload.event_type.KeyRelease] === 'Meta',
ctrlKey: mapKeyKey[event.payload.event_type.KeyRelease] === 'Control',
altKey: mapKeyKey[event.payload.event_type.KeyRelease] === 'Alt',
shiftKey: mapKeyKey[event.payload.event_type.KeyRelease] === 'Shift',
})
eventTarget.dispatchEvent(e)
}))
unListenFuncs.push(await listen('tauri-plugins:tauri-plugin-rdev:keydown', (event) => {
if (typeof event.payload.event_type.KeyPress === 'object' && 'Unknown' in event.payload.event_type.KeyPress) {
if (event.payload.event_type.KeyPress.Unknown === 62) {
event.payload.event_type = { KeyPress: KeyCode.ControlLeft }
}
}
if (typeof event.payload.event_type.KeyPress !== 'string') {
console.warn('unknown key release event:', event.payload.event_type.KeyPress)
return
}
const e = new KeyboardEvent('keydown', {
key: mapKeyKey[event.payload.event_type.KeyPress],
code: mapKeyCode[event.payload.event_type.KeyPress],
metaKey: mapKeyKey[event.payload.event_type.KeyPress] === 'Meta',
ctrlKey: mapKeyKey[event.payload.event_type.KeyPress] === 'Control',
altKey: mapKeyKey[event.payload.event_type.KeyPress] === 'Alt',
shiftKey: mapKeyKey[event.payload.event_type.KeyPress] === 'Shift',
})
eventTarget.dispatchEvent(e)
}))
}
function cleanup() {
unListenFuncs.forEach(unListen => unListen())
unListenFuncs.length = 0
}
onMounted(() => setup())
onUnmounted(() => cleanup())
if (import.meta.hot) { // For better DX
import.meta.hot.on('vite:beforeUpdate', () => cleanup())
import.meta.hot.on('vite:afterUpdate', async () => await setup())
}
return eventTarget
}
@@ -3,6 +3,7 @@ import type { EventCallback, EventName, UnlistenFn } from '@tauri-apps/api/event
import type { Monitor } from '@tauri-apps/api/window'
import type { InvokeMethods, InvokeMethodShape } from '../tauri/invoke'
import type { KeyCode } from '../tauri/rdev'
import { withRetry } from '@moeru/std'
import { computedAsync, until } from '@vueuse/core'
@@ -51,11 +52,23 @@ interface Events {
}
export interface AiriTamagotchiEvents extends Events {
// from tauri-plugin-window-pass-through-on-hover
'tauri-plugins:tauri-plugin-window-pass-through-on-hover:cursor-position': Point
'tauri-plugins:tauri-plugin-window-pass-through-on-hover:window-frame': WindowFrame
'tauri-plugins:tauri-plugin-window-pass-through-on-hover:pass-through-enabled': boolean
// from tauri-plugin-ipc-audio-transcription-ort
'tauri-plugins:tauri-plugin-ipc-audio-transcription-ort:load-model-silero-vad-progress': [boolean, string, number, number, number]
// from tauri-plugin-ipc-audio-vad-ort
'tauri-plugins:tauri-plugin-ipc-audio-vad-ort:load-model-whisper-progress': [boolean, string, number, number, number]
// from tauri-plugin-rdev
'tauri-plugins:tauri-plugin-rdev:keydown': { time: { secs_since_epoch: number, nanos_since_epoch: number }, name: string, event_type: { KeyPress: KeyCode | { Unknown: number } } } // similar to 'keydown' events from DOM elements
'tauri-plugins:tauri-plugin-rdev:keyup': { time: { secs_since_epoch: number, nanos_since_epoch: number }, name: string, event_type: { KeyRelease: KeyCode | { Unknown: number } } } // similar to 'keyup' events from DOM elements
'tauri-plugins:tauri-plugin-rdev:mousedown': { time: { secs_since_epoch: number, nanos_since_epoch: number }, name: string, event_type: { ButtonPress: string } } // similar to 'mousedown' events from DOM elements
'tauri-plugins:tauri-plugin-rdev:mouseup': { time: { secs_since_epoch: number, nanos_since_epoch: number }, name: string, event_type: { ButtonRelease: string } } // similar to 'mouseup' events from DOM elements
// MCP
'mcp_plugin_destroyed': undefined
}
@@ -301,3 +314,8 @@ export function useTauriWindow() {
closeWindow,
}
}
export function createTauriEventTarget(): EventTarget {
const eventTarget = new EventTarget()
return eventTarget
}
@@ -0,0 +1,134 @@
<script setup lang="ts">
import { useMagicKeys } from '@vueuse/core'
import { computed, defineComponent, h } from 'vue'
import { useTauriRdevEventTarget } from '../../composables/tauri-rdev'
const eventTarget = useTauriRdevEventTarget()
const {
shift,
control,
escape,
tab,
v,
u,
e,
s,
v_u_e,
u_s_e,
Shift_Alt_i,
Shift_Alt_r,
shift_Alt_n,
current,
} = useMagicKeys({ target: eventTarget })
const keys = computed(() => Array.from(current))
const Key = defineComponent({
props: {
value: {
type: Boolean,
required: true,
},
},
render() {
return h('div', {
class: [
'font-mono px-4 py-2 rounded',
this.value
? 'opacity-100 text-primary bg-primary bg-opacity-15'
: 'opacity-50 bg-gray-600 bg-opacity-10 dark:(bg-gray-400 bg-opacity-10)',
],
}, this.$slots.default?.())
},
})
</script>
<template>
<div class="flex flex-col md:flex-row">
<img
class="m-auto h-38 transform py-8 transition duration-500"
:class="{ 'opacity-0': !v_u_e, 'rotate-180': shift }"
>
<div>
<div class="mb-5 mt-0 text-center">
Press the following keys to test out
</div>
<div class="flex justify-center gap-3">
<Key :value="v">
V
</Key>
<Key :value="u">
u
</Key>
<Key :value="e">
e
</Key>
<div class="mx-1" />
<Key :value="u">
U
</Key>
<Key :value="s">
s
</Key>
<Key :value="e">
e
</Key>
</div>
<div class="mt-3 flex justify-center gap-3">
<Key :value="escape">
Escape
</Key>
<Key :value="shift">
Shift
</Key>
<Key :value="control">
Control
</Key>
<Key :value="tab">
Tab
</Key>
</div>
<div class="mt-3 flex justify-center gap-3">
<Key :value="v_u_e">
Vue
</Key>
<Key :value="u_s_e">
Use
</Key>
</div>
<div class="mt-3 flex flex-col items-center gap-3">
<Key :value="Shift_Alt_i">
Shift + Alt + I
</Key>
<Key :value="Shift_Alt_r">
Shift + Alt + R
</Key>
<Key :value="shift_Alt_n">
Shift + Alt + N
</Key>
</div>
<div class="mt-4 text-center">
<div>Keys Pressed</div>
<div class="mt-2 min-h-1.5em flex justify-center space-x-1">
<code
v-for="key in keys"
:key="key"
class="font-mono"
>
{{ key }}
</code>
</div>
</div>
</div>
<img
class="m-auto h-38 transform py-8 transition duration-500"
:class="{ 'opacity-0': !u_s_e, 'rotate-180': shift }"
>
</div>
</template>
@@ -1,4 +1,3 @@
import type { ShortcutEvent } from '@tauri-apps/plugin-global-shortcut'
import type { MaybeRefOrGetter, UseStorageOptions } from '@vueuse/core'
import { useLocalStorage } from '@vueuse/core'
@@ -77,11 +76,7 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
shortcut: useVersionedLocalStorage('shortcuts/window/move', 'Shift+Alt+N'), // Shift + Alt + N
group: 'window',
type: 'move',
handle: async (event: ShortcutEvent) => {
if (event.state !== 'Pressed') {
return
}
handle: async () => {
windowStore.setMode(WindowControlMode.MOVE)
windowStore.toggleControl()
},
@@ -91,7 +86,7 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
shortcut: useVersionedLocalStorage('shortcuts/window/resize', 'Shift+Alt+R'), // Shift + Alt + R
group: 'window',
type: 'resize',
handle: async (_: ShortcutEvent) => {
handle: async () => {
windowStore.setMode(WindowControlMode.RESIZE)
windowStore.toggleControl()
},
@@ -101,7 +96,7 @@ export const useShortcutsStore = defineStore('shortcuts', () => {
shortcut: useVersionedLocalStorage('shortcuts/window/debug', 'Shift+Alt+I'), // Shift + Alt + I
group: 'window',
type: 'ignore-mouse-event',
handle: async (_: ShortcutEvent) => {
handle: async () => {
windowStore.isIgnoringMouseEvent = !windowStore.isIgnoringMouseEvent
if (windowStore.isIgnoringMouseEvent) {
await startClickThrough()
+407
View File
@@ -0,0 +1,407 @@
// rdev enums
export enum KeyCode {
// Alt key on Linux and Windows (option key on macOS)
Alt = 'Alt',
AltGr = 'AltGr',
Backspace = 'Backspace',
CapsLock = 'CapsLock',
ControlLeft = 'ControlLeft',
ControlRight = 'ControlRight',
Delete = 'Delete',
DownArrow = 'DownArrow',
End = 'End',
Escape = 'Escape',
F1 = 'F1',
F10 = 'F10',
F11 = 'F11',
F12 = 'F12',
F13 = 'F13',
F14 = 'F14',
F15 = 'F15',
F16 = 'F16',
F17 = 'F17',
F18 = 'F18',
F19 = 'F19',
F20 = 'F20',
F21 = 'F21',
F22 = 'F22',
F23 = 'F23',
F24 = 'F24',
F2 = 'F2',
F3 = 'F3',
F4 = 'F4',
F5 = 'F5',
F6 = 'F6',
F7 = 'F7',
F8 = 'F8',
F9 = 'F9',
Home = 'Home',
LeftArrow = 'LeftArrow',
/// also known as "windows", "super", and "command"
MetaLeft = 'MetaLeft',
/// also known as "windows", "super", and "command"
MetaRight = 'MetaRight',
PageDown = 'PageDown',
PageUp = 'PageUp',
Return = 'Return',
RightArrow = 'RightArrow',
ShiftLeft = 'ShiftLeft',
ShiftRight = 'ShiftRight',
Space = 'Space',
Tab = 'Tab',
UpArrow = 'UpArrow',
PrintScreen = 'PrintScreen',
ScrollLock = 'ScrollLock',
Pause = 'Pause',
NumLock = 'NumLock',
BackQuote = 'BackQuote',
Num1 = 'Num1',
Num2 = 'Num2',
Num3 = 'Num3',
Num4 = 'Num4',
Num5 = 'Num5',
Num6 = 'Num6',
Num7 = 'Num7',
Num8 = 'Num8',
Num9 = 'Num9',
Num0 = 'Num0',
Minus = 'Minus',
Equal = 'Equal',
KeyQ = 'KeyQ',
KeyW = 'KeyW',
KeyE = 'KeyE',
KeyR = 'KeyR',
KeyT = 'KeyT',
KeyY = 'KeyY',
KeyU = 'KeyU',
KeyI = 'KeyI',
KeyO = 'KeyO',
KeyP = 'KeyP',
LeftBracket = 'LeftBracket',
RightBracket = 'RightBracket',
KeyA = 'KeyA',
KeyS = 'KeyS',
KeyD = 'KeyD',
KeyF = 'KeyF',
KeyG = 'KeyG',
KeyH = 'KeyH',
KeyJ = 'KeyJ',
KeyK = 'KeyK',
KeyL = 'KeyL',
SemiColon = 'SemiColon',
Quote = 'Quote',
BackSlash = 'BackSlash',
IntlBackslash = 'IntlBackslash',
KeyZ = 'KeyZ',
KeyX = 'KeyX',
KeyC = 'KeyC',
KeyV = 'KeyV',
KeyB = 'KeyB',
KeyN = 'KeyN',
KeyM = 'KeyM',
Comma = 'Comma',
Dot = 'Dot',
Slash = 'Slash',
Insert = 'Insert',
KpReturn = 'KpReturn',
KpMinus = 'KpMinus',
KpPlus = 'KpPlus',
KpMultiply = 'KpMultiply',
KpDivide = 'KpDivide',
Kp0 = 'Kp0',
Kp1 = 'Kp1',
Kp2 = 'Kp2',
Kp3 = 'Kp3',
Kp4 = 'Kp4',
Kp5 = 'Kp5',
Kp6 = 'Kp6',
Kp7 = 'Kp7',
Kp8 = 'Kp8',
Kp9 = 'Kp9',
KpDelete = 'KpDelete',
Function = 'Function',
VolumeUp = 'VolumeUp',
VolumeDown = 'VolumeDown',
VolumeMute = 'VolumeMute',
BrightnessUp = 'BrightnessUp',
BrightnessDown = 'BrightnessDown',
PreviousTrack = 'PreviousTrack',
PlayPause = 'PlayPause',
PlayCd = 'PlayCd',
NextTrack = 'NextTrack',
Unknown = 'Unknown',
}
// Follows the exact order
// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values#code_values_on_windows
export const mapKeyCode: Record<KeyCode, string> = {
[KeyCode.Escape]: 'Escape',
[KeyCode.Kp0]: 'Digit0',
[KeyCode.Kp1]: 'Digit1',
[KeyCode.Kp2]: 'Digit2',
[KeyCode.Kp3]: 'Digit3',
[KeyCode.Kp4]: 'Digit4',
[KeyCode.Kp5]: 'Digit5',
[KeyCode.Kp6]: 'Digit6',
[KeyCode.Kp7]: 'Digit7',
[KeyCode.Kp8]: 'Digit8',
[KeyCode.Kp9]: 'Digit9',
[KeyCode.Minus]: 'Minus',
[KeyCode.Equal]: 'Equal',
[KeyCode.Backspace]: 'Backspace',
[KeyCode.Tab]: 'Tab',
[KeyCode.KeyQ]: 'KeyQ',
[KeyCode.KeyW]: 'KeyW',
[KeyCode.KeyE]: 'KeyE',
[KeyCode.KeyR]: 'KeyR',
[KeyCode.KeyT]: 'KeyT',
[KeyCode.KeyY]: 'KeyY',
[KeyCode.KeyU]: 'KeyU',
[KeyCode.KeyI]: 'KeyI',
[KeyCode.KeyO]: 'KeyO',
[KeyCode.KeyP]: 'KeyP',
[KeyCode.LeftBracket]: 'BracketLeft',
[KeyCode.RightBracket]: 'BracketRight',
[KeyCode.Return]: 'Enter',
[KeyCode.ControlLeft]: 'ControlLeft',
[KeyCode.KeyA]: 'KeyA',
[KeyCode.KeyS]: 'KeyS',
[KeyCode.KeyD]: 'KeyD',
[KeyCode.KeyF]: 'KeyF',
[KeyCode.KeyG]: 'KeyG',
[KeyCode.KeyH]: 'KeyH',
[KeyCode.KeyJ]: 'KeyJ',
[KeyCode.KeyK]: 'KeyK',
[KeyCode.KeyL]: 'KeyL',
[KeyCode.SemiColon]: 'SemiColon',
[KeyCode.Quote]: 'Quote',
[KeyCode.BackQuote]: 'BackQuote',
[KeyCode.ShiftLeft]: 'ShiftLeft',
[KeyCode.BackSlash]: 'BackSlash',
[KeyCode.KeyZ]: 'KeyZ',
[KeyCode.KeyX]: 'KeyX',
[KeyCode.KeyC]: 'KeyC',
[KeyCode.KeyV]: 'KeyV',
[KeyCode.KeyB]: 'KeyB',
[KeyCode.KeyN]: 'KeyN',
[KeyCode.KeyM]: 'KeyM',
[KeyCode.Comma]: 'Comma',
[KeyCode.Dot]: 'Period',
[KeyCode.Slash]: 'Slash',
[KeyCode.ShiftRight]: 'ShiftRight',
[KeyCode.KpMultiply]: 'NumpadMultiply',
[KeyCode.Alt]: 'Alt',
[KeyCode.Space]: 'Space',
[KeyCode.CapsLock]: 'CapsLock',
[KeyCode.F1]: 'F1',
[KeyCode.F2]: 'F2',
[KeyCode.F3]: 'F3',
[KeyCode.F4]: 'F4',
[KeyCode.F5]: 'F5',
[KeyCode.F6]: 'F6',
[KeyCode.F7]: 'F7',
[KeyCode.F8]: 'F8',
[KeyCode.F9]: 'F9',
[KeyCode.F10]: 'F10',
[KeyCode.ScrollLock]: 'ScrollLock',
[KeyCode.Num7]: 'Numpad7',
[KeyCode.Num8]: 'Numpad8',
[KeyCode.Num9]: 'Numpad9',
[KeyCode.KpMinus]: 'NumpadSubtract',
[KeyCode.Num4]: 'Numpad4',
[KeyCode.Num5]: 'Numpad5',
[KeyCode.Num6]: 'Numpad6',
[KeyCode.KpPlus]: 'NumpadAdd',
[KeyCode.Num1]: 'Numpad1',
[KeyCode.Num2]: 'Numpad2',
[KeyCode.Num3]: 'Numpad3',
[KeyCode.Num0]: 'Numpad0',
// Numpad Decimal?
[KeyCode.IntlBackslash]: 'IntlBackslash',
[KeyCode.F11]: 'F11',
[KeyCode.F12]: 'F12',
// NumpadEqual?
[KeyCode.F13]: 'F13',
[KeyCode.F14]: 'F14',
[KeyCode.F15]: 'F15',
[KeyCode.F16]: 'F16',
[KeyCode.F17]: 'F17',
[KeyCode.F18]: 'F18',
[KeyCode.F19]: 'F19',
[KeyCode.F20]: 'F20',
[KeyCode.F21]: 'F21',
[KeyCode.F22]: 'F22',
[KeyCode.F23]: 'F23',
[KeyCode.F24]: 'F24',
// NumpadComma?
[KeyCode.PreviousTrack]: 'MediaTrackPrevious',
[KeyCode.NextTrack]: 'MediaTrackNext',
[KeyCode.KpReturn]: 'NumpadEnter',
[KeyCode.ControlRight]: 'ControlRight',
[KeyCode.VolumeMute]: 'VolumeMute',
[KeyCode.PlayPause]: 'MediaPlayPause',
[KeyCode.VolumeDown]: 'VolumeDown',
[KeyCode.VolumeUp]: 'VolumeUp',
[KeyCode.KpDivide]: 'NumpadDivide',
[KeyCode.PrintScreen]: 'PrintScreen',
[KeyCode.AltGr]: 'AltGr',
[KeyCode.NumLock]: 'NumLock',
[KeyCode.Pause]: 'Pause',
[KeyCode.Home]: 'Home',
[KeyCode.UpArrow]: 'ArrowUp',
[KeyCode.PageUp]: 'PageUp',
[KeyCode.LeftArrow]: 'ArrowLeft',
[KeyCode.RightArrow]: 'ArrowRight',
[KeyCode.End]: 'End',
[KeyCode.DownArrow]: 'ArrowDown',
[KeyCode.PageDown]: 'PageDown',
[KeyCode.Insert]: 'Insert',
[KeyCode.Delete]: 'Delete',
// also known as "windows", "super", and "command"
[KeyCode.MetaLeft]: 'MetaLeft',
// also known as "windows", "super", and "command"
[KeyCode.MetaRight]: 'MetaRight',
[KeyCode.KpDelete]: 'NumpadDelete',
[KeyCode.Function]: 'Function',
[KeyCode.BrightnessUp]: 'BrightnessUp',
[KeyCode.BrightnessDown]: 'BrightnessDown',
[KeyCode.PlayCd]: 'PlayCd',
[KeyCode.Unknown]: 'Unknown',
}
export const mapKeyKey: Record<KeyCode, string> = {
[KeyCode.Escape]: 'Escape',
[KeyCode.Kp0]: '0',
[KeyCode.Kp1]: '1',
[KeyCode.Kp2]: '2',
[KeyCode.Kp3]: '3',
[KeyCode.Kp4]: '4',
[KeyCode.Kp5]: '5',
[KeyCode.Kp6]: '6',
[KeyCode.Kp7]: '7',
[KeyCode.Kp8]: '8',
[KeyCode.Kp9]: '9',
[KeyCode.Minus]: '-',
[KeyCode.Equal]: '=',
[KeyCode.Backspace]: 'Backspace',
[KeyCode.Tab]: 'Tab',
[KeyCode.KeyQ]: 'q',
[KeyCode.KeyW]: 'w',
[KeyCode.KeyE]: 'e',
[KeyCode.KeyR]: 'r',
[KeyCode.KeyT]: 't',
[KeyCode.KeyY]: 'y',
[KeyCode.KeyU]: 'u',
[KeyCode.KeyI]: 'i',
[KeyCode.KeyO]: 'o',
[KeyCode.KeyP]: 'p',
[KeyCode.LeftBracket]: '[',
[KeyCode.RightBracket]: ']',
[KeyCode.Return]: 'Enter',
[KeyCode.ControlLeft]: 'Control',
[KeyCode.KeyA]: 'a',
[KeyCode.KeyS]: 's',
[KeyCode.KeyD]: 'd',
[KeyCode.KeyF]: 'f',
[KeyCode.KeyG]: 'g',
[KeyCode.KeyH]: 'h',
[KeyCode.KeyJ]: 'j',
[KeyCode.KeyK]: 'k',
[KeyCode.KeyL]: 'l',
[KeyCode.SemiColon]: ';',
[KeyCode.Quote]: '\'',
[KeyCode.BackQuote]: '`',
[KeyCode.ShiftLeft]: 'Shift',
[KeyCode.BackSlash]: '\\',
[KeyCode.KeyZ]: 'z',
[KeyCode.KeyX]: 'x',
[KeyCode.KeyC]: 'c',
[KeyCode.KeyV]: 'v',
[KeyCode.KeyB]: 'b',
[KeyCode.KeyN]: 'n',
[KeyCode.KeyM]: 'm',
[KeyCode.Comma]: ',',
[KeyCode.Dot]: '.',
[KeyCode.Slash]: '/',
[KeyCode.ShiftRight]: 'Shift',
[KeyCode.KpMultiply]: '*',
[KeyCode.Alt]: 'Alt',
[KeyCode.Space]: ' ',
[KeyCode.CapsLock]: 'CapsLock',
[KeyCode.F1]: 'F1',
[KeyCode.F2]: 'F2',
[KeyCode.F3]: 'F3',
[KeyCode.F4]: 'F4',
[KeyCode.F5]: 'F5',
[KeyCode.F6]: 'F6',
[KeyCode.F7]: 'F7',
[KeyCode.F8]: 'F8',
[KeyCode.F9]: 'F9',
[KeyCode.F10]: 'F10',
[KeyCode.ScrollLock]: 'ScrollLock',
[KeyCode.Num7]: '7',
[KeyCode.Num8]: '8',
[KeyCode.Num9]: '9',
[KeyCode.KpMinus]: '-',
[KeyCode.Num4]: '4',
[KeyCode.Num5]: '5',
[KeyCode.Num6]: '6',
[KeyCode.KpPlus]: '+',
[KeyCode.Num1]: '1',
[KeyCode.Num2]: '2',
[KeyCode.Num3]: '3',
[KeyCode.Num0]: '0',
// Numpad Decimal?
[KeyCode.IntlBackslash]: 'IntlBackslash',
[KeyCode.F11]: 'F11',
[KeyCode.F12]: 'F12',
// NumpadEqual?
[KeyCode.F13]: 'F13',
[KeyCode.F14]: 'F14',
[KeyCode.F15]: 'F15',
[KeyCode.F16]: 'F16',
[KeyCode.F17]: 'F17',
[KeyCode.F18]: 'F18',
[KeyCode.F19]: 'F19',
[KeyCode.F20]: 'F20',
[KeyCode.F21]: 'F21',
[KeyCode.F22]: 'F22',
[KeyCode.F23]: 'F23',
[KeyCode.F24]: 'F24',
// NumpadComma?
[KeyCode.PreviousTrack]: 'MediaTrackPrevious',
[KeyCode.NextTrack]: 'MediaTrackNext',
[KeyCode.KpReturn]: 'Enter',
[KeyCode.ControlRight]: 'Control',
[KeyCode.VolumeMute]: 'VolumeMute',
[KeyCode.PlayPause]: 'MediaPlayPause',
[KeyCode.VolumeDown]: 'VolumeDown',
[KeyCode.VolumeUp]: 'VolumeUp',
[KeyCode.KpDivide]: '/',
[KeyCode.PrintScreen]: 'PrintScreen',
[KeyCode.AltGr]: 'Alt',
[KeyCode.NumLock]: 'NumLock',
[KeyCode.Pause]: 'Pause',
[KeyCode.Home]: 'Home',
[KeyCode.UpArrow]: 'ArrowUp',
[KeyCode.PageUp]: 'PageUp',
[KeyCode.LeftArrow]: 'ArrowLeft',
[KeyCode.RightArrow]: 'ArrowRight',
[KeyCode.End]: 'End',
[KeyCode.DownArrow]: 'ArrowDown',
[KeyCode.PageDown]: 'PageDown',
[KeyCode.Insert]: 'Insert',
[KeyCode.Delete]: 'Delete',
// also known as "windows", "super", and "command"
[KeyCode.MetaLeft]: 'Meta',
// also known as "windows", "super", and "command"
[KeyCode.MetaRight]: 'Meta',
[KeyCode.KpDelete]: 'Delete',
[KeyCode.Function]: 'Function',
[KeyCode.BrightnessUp]: 'BrightnessUp',
[KeyCode.BrightnessDown]: 'BrightnessDown',
[KeyCode.PlayCd]: 'PlayCd',
[KeyCode.Unknown]: 'Unknown',
}
@@ -8,6 +8,7 @@ repository = "https://github.com/moeru-ai/airi"
edition = "2024"
rust-version = "1.85.0"
links = "tauri-plugin-ipc-audio-transcription-ort"
publish = false
[lib]
name = "tauri_plugin_ipc_audio_transcription_ort"
@@ -8,6 +8,7 @@ repository = "https://github.com/moeru-ai/airi"
edition = "2024"
rust-version = "1.85.0"
links = "tauri-plugin-ipc-audio-vad-ort"
publish = false
[lib]
name = "tauri_plugin_ipc_audio_vad_ort"
+4732
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
[package]
name = "tauri-plugin-rdev"
version.workspace = true
description = "Integration of github.com/Narsil/rdev for Tauri"
authors = [ "Neko Ayaka <neko@ayaka.moe>" ]
license = "MIT"
repository = "https://github.com/moeru-ai/airi"
edition = "2024"
rust-version = "1.85.0"
links = "tauri-plugin-rdev"
publish = false
[lib]
name = "tauri_plugin_rdev"
crate-type = [
"staticlib",
"cdylib",
"rlib"
]
[dependencies]
tauri = "2.3.1"
tokio = { version = "1", features = [ "full" ] }
anyhow = "1"
rdev = { git = "https://github.com/Narsil/rdev", version = "0.6.0", features = ["serialize"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.142"
log = "0.4.27"
[build-dependencies]
tauri-plugin = { version = "2.2", features = [ "build" ] }
+1
View File
@@ -0,0 +1 @@
fn main() {}
+126
View File
@@ -0,0 +1,126 @@
use std::sync::{Mutex, atomic::Ordering};
use log::error;
use rdev::{Event, EventType, listen};
use serde::Serialize;
use serde_json::Value;
use tauri::{
Emitter,
Manager,
Runtime,
plugin::{Builder as PluginBuilder, TauriPlugin},
};
#[derive(Debug, Clone, Serialize)]
pub enum DeviceKind {
MousePress,
MouseRelease,
MouseMove,
KeyboardPress,
KeyboardRelease,
}
#[derive(Debug, Clone, Serialize)]
pub struct DeviceEvent {
kind: DeviceKind,
value: Value,
}
#[derive(Default)]
struct PluginState {
window_labels: Vec<String>,
}
const PLUGIN_NAME: &str = "rdev";
// Code referenced from
// https://github.com/liwenka1/bongo-cat-next/blob/891c3380e1d4a8cfe8452668632be8bd5086b46c/src-tauri/src/core/device.rs#L7
static IS_RUNNING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
fn start_listen<R: tauri::Runtime>(app: tauri::AppHandle<R>) {
#[cfg(target_os = "macos")]
rdev::set_is_main_thread(false);
// Events needed by
// https://github.com/vueuse/vueuse/blob/f8c4e0a128e2d8df52cca0299a08bfd4dd8638da/packages/core/useMagicKeys/index.ts#L153C3-L153C19
_ = listen(move |event: Event| {
let event_name = match event.event_type {
EventType::KeyPress(_) => "tauri-plugins:tauri-plugin-rdev:keydown",
EventType::KeyRelease(_) => "tauri-plugins:tauri-plugin-rdev:keyup",
EventType::ButtonPress(_) => "tauri-plugins:tauri-plugin-rdev:mousedown",
EventType::ButtonRelease(_) => "tauri-plugins:tauri-plugin-rdev:mouseup",
_ => return,
};
let state = app.state::<Mutex<PluginState>>();
let state = match state.lock() {
Ok(s) => s,
Err(e) => {
error!("PluginState mutex is poisoned: {}", e);
return;
},
};
for label in &state.window_labels {
if let Err(e) = app.emit_to(label, event_name, &event) {
error!(
"Failed to emit rdev event '{}' to window '{}': {}",
event_name, label, e
);
}
}
})
.map_err(|e| {
error!("Failed to start rdev listener: {:?}", e);
});
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
PluginBuilder::new(PLUGIN_NAME)
.setup(|app, _| {
app.manage(Mutex::new(PluginState {
window_labels: vec![],
}));
// Code referenced from
// https://github.com/liwenka1/bongo-cat-next/blob/891c3380e1d4a8cfe8452668632be8bd5086b46c/src-tauri/src/core/device.rs#L7
if IS_RUNNING.load(Ordering::SeqCst) {
return Ok(());
}
IS_RUNNING.store(true, Ordering::SeqCst);
let app = app.clone();
std::thread::spawn(move || {
start_listen(app);
});
Ok(())
})
.on_window_ready(|window| {
let app = window.app_handle();
let state = app.state::<Mutex<PluginState>>();
let mut state = state.lock().unwrap();
state
.window_labels
.push(window.label().to_string());
let window_cloned = window.clone();
window.on_window_event(move |event| match event {
tauri::WindowEvent::CloseRequested { .. } => {
let app = window_cloned.app_handle();
let state = app.state::<Mutex<PluginState>>();
let mut state = state.lock().unwrap();
state
.window_labels
.retain(|label| label != window_cloned.label());
},
_ => {},
});
})
.build()
}
+4
View File
@@ -110,6 +110,7 @@ words:
- jszip
- Kawaii
- kebabcase
- keyup
- Keyyable
- kirakira
- kwaa
@@ -142,6 +143,8 @@ words:
- modnet
- moeru
- Morioki
- mousedown
- mouseup
- msvc
- Myriam
- ndarray
@@ -186,6 +189,7 @@ words:
- pthread
- quanlai
- qwen
- rdev
- rehype
- reka
- Replayable