커맨드 선택 팝업 (Ctrl+Shift+C): 커서 위치에서 빠른 명령어 전환

- command-popup: Vanilla JS 팝업 (히스토리 팝업과 동일 패턴)
  Arrow↑↓ 선택, Enter 적용, 1-5 직접 선택, ESC 닫기
- WindowManager: showCommandPopup/hideCommandPopup/sendKeyToCommandPopup
- bootstrap: Ctrl+Shift+C 단축키 등록 + command:selected IPC
- electron.vite.config: command-popup HTML 빌드 엔트리 추가
- 선택한 명령어가 즉시 activeInstructionId로 설정 → 다음 녹음에 적용
This commit is contained in:
Yun Chan 2026-04-05 13:26:59 +09:00
parent fea923d302
commit 1361b95a4d
6 changed files with 419 additions and 4 deletions

View file

@ -17,6 +17,7 @@ let mainWindow: BrowserWindow | null = null
let recordingTipWindow: BrowserWindow | null = null
let resultPopupWindow: BrowserWindow | null = null
let historyPopupWindow: BrowserWindow | null = null
let commandPopupWindow: BrowserWindow | null = null
// ── 메인 윈도우 ───────────────────────────────────────
@ -355,12 +356,93 @@ export function isHistoryPopupVisible(): boolean {
return !!(historyPopupWindow && !historyPopupWindow.isDestroyed() && historyPopupWindow.isVisible())
}
// ── CommandPopup 팝업 ─────────────────────────────────
function createCommandPopupWindow(): BrowserWindow {
const win = new BrowserWindow({
width: 340,
height: 300,
show: false,
frame: false,
transparent: true,
resizable: false,
alwaysOnTop: true,
skipTaskbar: true,
focusable: false,
webPreferences: {
preload: join(__dirname, '../preload/popup.js'),
sandbox: false,
contextIsolation: true,
nodeIntegration: false
}
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
win.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/popups/command-popup/index.html`)
} else {
win.loadFile(join(__dirname, '../renderer/popups/command-popup/index.html'))
}
win.on('closed', () => { commandPopupWindow = null })
return win
}
export function getCommandPopupWindow(): BrowserWindow {
if (!commandPopupWindow || commandPopupWindow.isDestroyed()) {
commandPopupWindow = createCommandPopupWindow()
}
return commandPopupWindow
}
export function showCommandPopup(commands: Array<Record<string, unknown>>, activeId: string | null): void {
const win = getCommandPopupWindow()
const cursorPos = screen.getCursorScreenPoint()
const display = screen.getDisplayNearestPoint(cursorPos)
const popupWidth = 340
const popupHeight = Math.min(80 + commands.length * 48, 400)
let x = cursorPos.x - Math.round(popupWidth / 2)
let y = cursorPos.y - popupHeight - 20
x = Math.max(display.workArea.x, Math.min(x, display.workArea.x + display.workArea.width - popupWidth))
if (y < display.workArea.y) { y = cursorPos.y + 20 }
win.setBounds({ x, y, width: popupWidth, height: popupHeight })
win.webContents.send('command:showItems', { commands, activeId })
if (!win.isVisible()) { win.showInactive() }
win.webContents.send('command:show', {})
}
export function hideCommandPopup(): void {
if (commandPopupWindow && !commandPopupWindow.isDestroyed()) {
commandPopupWindow.webContents.send('command:hide', {})
setTimeout(() => {
if (commandPopupWindow && !commandPopupWindow.isDestroyed()) {
commandPopupWindow.hide()
}
}, 150)
}
}
export function sendKeyToCommandPopup(key: string): void {
if (commandPopupWindow && !commandPopupWindow.isDestroyed() && commandPopupWindow.isVisible()) {
commandPopupWindow.webContents.send('command:keyEvent', { key })
}
}
export function isCommandPopupVisible(): boolean {
return !!(commandPopupWindow && !commandPopupWindow.isDestroyed() && commandPopupWindow.isVisible())
}
// ── 프리로딩 ──────────────────────────────────────────
export function preloadPopupWindows(): void {
getRecordingTipWindow()
getResultPopupWindow()
getHistoryPopupWindow()
getCommandPopupWindow()
logger.info('Popup windows preloaded')
}