// src/main/services/text-insert/nut-keyboard-port.ts // 키 입력 시뮬레이션 포트와 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' const logger = getLogger('TextInsertService') /** 텍스트 삽입에 필요한 키 입력만 노출하는 포트. */ export interface KeyboardPort { /** 플랫폼 붙여넣기 단축키 (macOS ⌘+V, 그 외 Ctrl+V) */ paste(): Promise /** 글자 단위 타이핑 (클립보드 비파괴) */ type(text: string): Promise } /** * nut-js 가 키 이벤트마다 넣는 지연(ms). * nut-js 기본값은 300ms 로, pressKey/releaseKey/글자마다 그만큼 잠든다 — Ctrl 이 300ms 동안 * 눌린 채 남아 사용자의 다음 키가 단축키가 되고, 60자 타이핑에 18초가 걸린다. * 앱이 붙여넣기를 처리할 시간은 TextInsertService 가 자기 sleep 으로 따로 준다. */ export const NUT_KEY_DELAY_MS = 0 /** nut-js 네이티브 키보드 provider 중 이 어댑터가 쓰는 부분. */ export interface NutNativeKeyboard { setKeyboardDelay(delay: number): void /** 문자열 전체를 네이티브 호출 한 번으로 타이핑 */ type(input: string): Promise /** modifier down → 키 down/up → modifier up 을 네이티브 호출 한 번(keyTap)으로 */ click(...keys: NutKey[]): Promise } /** * 이 어댑터가 nut-js 모듈에서 쓰는 부분만 적은 구조 타입. * 실제 모듈이 그대로 맞고, 테스트는 가짜 provider 로 만든 모듈을 넣을 수 있다. */ export interface NutKeyboardModule { keyboard: { config: { autoDelayMs: number } } providerRegistry: { hasKeyboard(): boolean getKeyboard(): NutNativeKeyboard } Key: Pick } export type NutModuleLoader = () => Promise 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 _nut: LoadedNut | null = null private _nutLoadPromise: Promise | null = null constructor( private readonly _platform: NodeJS.Platform = process.platform, private readonly _load: NutModuleLoader = loadNutModule ) {} /** 붙여넣기 단축키를 한 번의 코드(chord)로 보내 await 사이에 modifier 가 눌린 채 남지 않게 한다. */ async paste(): Promise { const nut = await this._ensureNut() const pasteModKey = this._platform === 'darwin' ? nut.Key.LeftSuper : nut.Key.LeftControl await nut.native.click(pasteModKey, nut.Key.V) } /** 텍스트 전체를 네이티브 호출 한 번으로 타이핑한다 (글자 사이 JS 대기 없음). */ async type(text: string): Promise { if (!text) return const nut = await this._ensureNut() await nut.native.type(text) } private async _ensureNut(): Promise { if (this._nut) return this._nut if (!this._nutLoadPromise) { this._nutLoadPromise = (async () => { try { 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)}`) throw new D3ROError( ErrorCode.TextInsertKeySimulationFailed, 'nut-js 로드 실패. 키보드 시뮬레이션을 사용할 수 없습니다.' ) } })() } await this._nutLoadPromise if (!this._nut) { throw new D3ROError(ErrorCode.TextInsertKeySimulationFailed, 'nut-js not available') } return this._nut } }