fix(text-insert): drop nut-js 300ms key delay and send paste as one chord
This commit is contained in:
parent
9e5b94ced2
commit
96b24e279c
2 changed files with 312 additions and 21 deletions
|
|
@ -2,6 +2,7 @@
|
|||
// 키 입력 시뮬레이션 포트와 nut-js 어댑터.
|
||||
// nut-js 는 ESM + 네이티브 모듈이므로 첫 사용 시 lazy dynamic import 한다.
|
||||
|
||||
import type { Key as NutKey } from '@nut-tree-fork/nut-js'
|
||||
import { getLogger } from '../LoggerService'
|
||||
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
|
||||
|
||||
|
|
@ -15,46 +16,96 @@ export interface KeyboardPort {
|
|||
type(text: string): Promise<void>
|
||||
}
|
||||
|
||||
type NutModule = typeof import('@nut-tree-fork/nut-js')
|
||||
/**
|
||||
* nut-js 가 키 이벤트마다 넣는 지연(ms).
|
||||
* nut-js 기본값은 300ms 로, pressKey/releaseKey/글자마다 그만큼 잠든다 — Ctrl 이 300ms 동안
|
||||
* 눌린 채 남아 사용자의 다음 키가 단축키가 되고, 60자 타이핑에 18초가 걸린다.
|
||||
* 앱이 붙여넣기를 처리할 시간은 TextInsertService 가 자기 sleep 으로 따로 준다.
|
||||
*/
|
||||
export const NUT_KEY_DELAY_MS = 0
|
||||
|
||||
interface NutKeyboard {
|
||||
pressKey: NutModule['keyboard']['pressKey']
|
||||
releaseKey: NutModule['keyboard']['releaseKey']
|
||||
type: NutModule['keyboard']['type']
|
||||
Key: NutModule['Key']
|
||||
/** nut-js 네이티브 키보드 provider 중 이 어댑터가 쓰는 부분. */
|
||||
export interface NutNativeKeyboard {
|
||||
setKeyboardDelay(delay: number): void
|
||||
/** 문자열 전체를 네이티브 호출 한 번으로 타이핑 */
|
||||
type(input: string): Promise<void>
|
||||
/** modifier down → 키 down/up → modifier up 을 네이티브 호출 한 번(keyTap)으로 */
|
||||
click(...keys: NutKey[]): Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* 이 어댑터가 nut-js 모듈에서 쓰는 부분만 적은 구조 타입.
|
||||
* 실제 모듈이 그대로 맞고, 테스트는 가짜 provider 로 만든 모듈을 넣을 수 있다.
|
||||
*/
|
||||
export interface NutKeyboardModule {
|
||||
keyboard: { config: { autoDelayMs: number } }
|
||||
providerRegistry: {
|
||||
hasKeyboard(): boolean
|
||||
getKeyboard(): NutNativeKeyboard
|
||||
}
|
||||
Key: Pick<typeof NutKey, 'LeftControl' | 'LeftSuper' | 'V'>
|
||||
}
|
||||
|
||||
export type NutModuleLoader = () => Promise<NutKeyboardModule>
|
||||
|
||||
const loadNutModule: NutModuleLoader = () => import('@nut-tree-fork/nut-js')
|
||||
|
||||
/**
|
||||
* nut-js 의 JS 쪽 autoDelay 와 네이티브 keyboardDelay 를 모두 NUT_KEY_DELAY_MS 로 맞춘다.
|
||||
* 네이티브 지연은 키 토글 뒤 메인 스레드를 동기로 막으므로 JS 설정만으로는 부족하다.
|
||||
*/
|
||||
export function configureNutKeyDelay(nut: NutKeyboardModule, delayMs: number = NUT_KEY_DELAY_MS): void {
|
||||
nut.keyboard.config.autoDelayMs = delayMs
|
||||
if (nut.providerRegistry.hasKeyboard()) {
|
||||
nut.providerRegistry.getKeyboard().setKeyboardDelay(delayMs)
|
||||
}
|
||||
}
|
||||
|
||||
interface LoadedNut {
|
||||
native: NutNativeKeyboard
|
||||
Key: NutKeyboardModule['Key']
|
||||
}
|
||||
|
||||
/**
|
||||
* nut-js 네이티브 provider 로 키를 보내는 KeyboardPort 어댑터.
|
||||
* KeyboardClass(pressKey/releaseKey/type)를 거치지 않는다 — 그 경로는 키 이벤트·글자마다
|
||||
* JS setTimeout 을 끼워 넣어, 지연 0 이어도 Windows 타이머 해상도(~15ms)만큼 글자가 늘어진다.
|
||||
*/
|
||||
export class NutKeyboardPort implements KeyboardPort {
|
||||
private _nutKeyboard: NutKeyboard | null = null
|
||||
private _nut: LoadedNut | null = null
|
||||
private _nutLoadPromise: Promise<void> | null = null
|
||||
|
||||
constructor(private readonly _platform: NodeJS.Platform = process.platform) {}
|
||||
constructor(
|
||||
private readonly _platform: NodeJS.Platform = process.platform,
|
||||
private readonly _load: NutModuleLoader = loadNutModule
|
||||
) {}
|
||||
|
||||
/** 붙여넣기 단축키를 한 번의 코드(chord)로 보내 await 사이에 modifier 가 눌린 채 남지 않게 한다. */
|
||||
async paste(): Promise<void> {
|
||||
const nut = await this._ensureNut()
|
||||
const pasteModKey = this._platform === 'darwin' ? nut.Key.LeftSuper : nut.Key.LeftControl
|
||||
await nut.pressKey(pasteModKey, nut.Key.V)
|
||||
await nut.releaseKey(nut.Key.V, pasteModKey)
|
||||
await nut.native.click(pasteModKey, nut.Key.V)
|
||||
}
|
||||
|
||||
/** 텍스트 전체를 네이티브 호출 한 번으로 타이핑한다 (글자 사이 JS 대기 없음). */
|
||||
async type(text: string): Promise<void> {
|
||||
if (!text) return
|
||||
const nut = await this._ensureNut()
|
||||
await nut.type(text)
|
||||
await nut.native.type(text)
|
||||
}
|
||||
|
||||
private async _ensureNut(): Promise<NutKeyboard> {
|
||||
if (this._nutKeyboard) return this._nutKeyboard
|
||||
private async _ensureNut(): Promise<LoadedNut> {
|
||||
if (this._nut) return this._nut
|
||||
|
||||
if (!this._nutLoadPromise) {
|
||||
this._nutLoadPromise = (async () => {
|
||||
try {
|
||||
const nut = await import('@nut-tree-fork/nut-js')
|
||||
this._nutKeyboard = {
|
||||
pressKey: nut.keyboard.pressKey.bind(nut.keyboard),
|
||||
releaseKey: nut.keyboard.releaseKey.bind(nut.keyboard),
|
||||
type: nut.keyboard.type.bind(nut.keyboard),
|
||||
Key: nut.Key
|
||||
const nut = await this._load()
|
||||
if (!nut.providerRegistry.hasKeyboard()) {
|
||||
throw new Error('no native keyboard provider registered')
|
||||
}
|
||||
configureNutKeyDelay(nut)
|
||||
this._nut = { native: nut.providerRegistry.getKeyboard(), Key: nut.Key }
|
||||
logger.info('nut-js loaded successfully')
|
||||
} catch (error) {
|
||||
logger.error(`Failed to load nut-js: ${error instanceof Error ? error.message : String(error)}`)
|
||||
|
|
@ -67,9 +118,9 @@ export class NutKeyboardPort implements KeyboardPort {
|
|||
}
|
||||
|
||||
await this._nutLoadPromise
|
||||
if (!this._nutKeyboard) {
|
||||
if (!this._nut) {
|
||||
throw new D3ROError(ErrorCode.TextInsertKeySimulationFailed, 'nut-js not available')
|
||||
}
|
||||
return this._nutKeyboard
|
||||
return this._nut
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue