fix(text-insert): restore every clipboard format after paste and never wipe non-text clipboards

This commit is contained in:
Yun Chan 2026-09-28 00:53:42 +09:00
parent 190b6db284
commit 0adedf5e7b
5 changed files with 704 additions and 113 deletions

View file

@ -0,0 +1,75 @@
// src/main/services/text-insert/nut-keyboard-port.ts
// 키 입력 시뮬레이션 포트와 nut-js 어댑터.
// nut-js 는 ESM + 네이티브 모듈이므로 첫 사용 시 lazy dynamic import 한다.
import { getLogger } from '../LoggerService'
import { D3ROError, ErrorCode } from '@d3ro/core/errors'
const logger = getLogger('TextInsertService')
/** 텍스트 삽입에 필요한 키 입력만 노출하는 포트. */
export interface KeyboardPort {
/** 플랫폼 붙여넣기 단축키 (macOS ⌘+V, 그 외 Ctrl+V) */
paste(): Promise<void>
/** 글자 단위 타이핑 (클립보드 비파괴) */
type(text: string): Promise<void>
}
type NutModule = typeof import('@nut-tree-fork/nut-js')
interface NutKeyboard {
pressKey: NutModule['keyboard']['pressKey']
releaseKey: NutModule['keyboard']['releaseKey']
type: NutModule['keyboard']['type']
Key: NutModule['Key']
}
export class NutKeyboardPort implements KeyboardPort {
private _nutKeyboard: NutKeyboard | null = null
private _nutLoadPromise: Promise<void> | null = null
constructor(private readonly _platform: NodeJS.Platform = process.platform) {}
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)
}
async type(text: string): Promise<void> {
const nut = await this._ensureNut()
await nut.type(text)
}
private async _ensureNut(): Promise<NutKeyboard> {
if (this._nutKeyboard) return this._nutKeyboard
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
}
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._nutKeyboard) {
throw new D3ROError(ErrorCode.TextInsertKeySimulationFailed, 'nut-js not available')
}
return this._nutKeyboard
}
}