Merge branch 'refactor/0712_ask' of https://github.com/pickle-com/glass into refactor/0712_ask

This commit is contained in:
sanio 2025-07-13 09:22:12 +09:00
commit c0edcfb0f9
6 changed files with 319 additions and 224 deletions

2
aec

@ -1 +1 @@
Subproject commit 9e11f4f95707714464194bdfc9db0222ec5c6163
Subproject commit f00bb1fb948053c752b916adfee19f90644a0b2f

View File

@ -110,6 +110,13 @@ const LATEST_SCHEMA = {
{ name: 'selected_stt_model', type: 'TEXT' },
{ name: 'updated_at', type: 'INTEGER' }
]
},
shortcuts: {
columns: [
{ name: 'action', type: 'TEXT PRIMARY KEY' },
{ name: 'accelerator', type: 'TEXT NOT NULL' },
{ name: 'created_at', type: 'INTEGER' }
]
}
};

View File

@ -0,0 +1 @@
module.exports = require('./sqlite.repository');

View File

@ -0,0 +1,48 @@
const sqliteClient = require('../../common/services/sqliteClient');
const crypto = require('crypto');
function getAllKeybinds() {
const db = sqliteClient.getDb();
const query = 'SELECT * FROM shortcuts';
try {
return db.prepare(query).all();
} catch (error) {
console.error(`[DB] Failed to get keybinds:`, error);
return [];
}
}
function upsertKeybinds(keybinds) {
if (!keybinds || keybinds.length === 0) return;
const db = sqliteClient.getDb();
const upsert = db.transaction((items) => {
const query = `
INSERT INTO shortcuts (action, accelerator, created_at)
VALUES (@action, @accelerator, @created_at)
ON CONFLICT(action) DO UPDATE SET
accelerator = excluded.accelerator;
`;
const insert = db.prepare(query);
for (const item of items) {
insert.run({
action: item.action,
accelerator: item.accelerator,
created_at: Math.floor(Date.now() / 1000)
});
}
});
try {
upsert(keybinds);
} catch (error) {
console.error('[DB] Failed to upsert keybinds:', error);
throw error;
}
}
module.exports = {
getAllKeybinds,
upsertKeybinds
};

View File

@ -0,0 +1,242 @@
const { globalShortcut, screen } = require('electron');
const shortcutsRepository = require('./repositories');
class ShortcutsService {
constructor() {
this.lastVisibleWindows = new Set(['header']);
this.mouseEventsIgnored = false;
}
getDefaultKeybinds() {
const isMac = process.platform === 'darwin';
return {
moveUp: isMac ? 'Cmd+Up' : 'Ctrl+Up',
moveDown: isMac ? 'Cmd+Down' : 'Ctrl+Down',
moveLeft: isMac ? 'Cmd+Left' : 'Ctrl+Left',
moveRight: isMac ? 'Cmd+Right' : 'Ctrl+Right',
toggleVisibility: isMac ? 'Cmd+\\' : 'Ctrl+\\',
toggleClickThrough: isMac ? 'Cmd+M' : 'Ctrl+M',
nextStep: isMac ? 'Cmd+Enter' : 'Ctrl+Enter',
manualScreenshot: isMac ? 'Cmd+Shift+S' : 'Ctrl+Shift+S',
previousResponse: isMac ? 'Cmd+[' : 'Ctrl+[',
nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]',
scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up',
scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down',
};
}
async loadKeybinds() {
let keybindsArray = await shortcutsRepository.getAllKeybinds();
if (!keybindsArray || keybindsArray.length === 0) {
console.log(`[Shortcuts] No keybinds found. Loading defaults.`);
const defaults = this.getDefaultKeybinds();
await this.saveKeybinds(defaults);
return defaults;
}
const keybinds = {};
keybindsArray.forEach(k => {
keybinds[k.action] = k.accelerator;
});
const defaults = this.getDefaultKeybinds();
let needsUpdate = false;
for (const action in defaults) {
if (!keybinds[action]) {
keybinds[action] = defaults[action];
needsUpdate = true;
}
}
if (needsUpdate) {
console.log('[Shortcuts] Updating missing keybinds with defaults.');
await this.saveKeybinds(keybinds);
}
return keybinds;
}
async saveKeybinds(newKeybinds) {
const keybindsToSave = [];
for (const action in newKeybinds) {
if (Object.prototype.hasOwnProperty.call(newKeybinds, action)) {
keybindsToSave.push({
action: action,
accelerator: newKeybinds[action],
});
}
}
await shortcutsRepository.upsertKeybinds(keybindsToSave);
console.log(`[Shortcuts] Saved keybinds.`);
}
toggleAllWindowsVisibility(windowPool) {
const header = windowPool.get('header');
if (!header) return;
if (header.isVisible()) {
this.lastVisibleWindows.clear();
windowPool.forEach((win, name) => {
if (win && !win.isDestroyed() && win.isVisible()) {
this.lastVisibleWindows.add(name);
}
});
this.lastVisibleWindows.forEach(name => {
if (name === 'header') return;
const win = windowPool.get(name);
if (win && !win.isDestroyed()) win.hide();
});
header.hide();
return;
}
this.lastVisibleWindows.forEach(name => {
const win = windowPool.get(name);
if (win && !win.isDestroyed()) {
win.show();
}
});
}
async registerShortcuts(movementManager, windowPool) {
const keybinds = await this.loadKeybinds();
globalShortcut.unregisterAll();
const header = windowPool.get('header');
const mainWindow = header;
const sendToRenderer = (channel, ...args) => {
windowPool.forEach(win => {
if (win && !win.isDestroyed()) {
try {
win.webContents.send(channel, ...args);
} catch (e) {
// Ignore errors for destroyed windows
}
}
});
};
sendToRenderer('shortcuts-updated', keybinds);
// --- Hardcoded shortcuts ---
const isMac = process.platform === 'darwin';
const modifier = isMac ? 'Cmd' : 'Ctrl';
// Monitor switching
const displays = screen.getAllDisplays();
if (displays.length > 1) {
displays.forEach((display, index) => {
const key = `${modifier}+Shift+${index + 1}`;
globalShortcut.register(key, () => movementManager.moveToDisplay(display.id));
});
}
// Edge snapping
const edgeDirections = [
{ key: `${modifier}+Shift+Left`, direction: 'left' },
{ key: `${modifier}+Shift+Right`, direction: 'right' },
];
edgeDirections.forEach(({ key, direction }) => {
globalShortcut.register(key, () => {
if (header && header.isVisible()) movementManager.moveToEdge(direction);
});
});
// --- User-configurable shortcuts ---
if (header?.currentHeaderState === 'apikey') {
if (keybinds.toggleVisibility) {
globalShortcut.register(keybinds.toggleVisibility, () => this.toggleAllWindowsVisibility(windowPool));
}
console.log('[Shortcuts] ApiKeyHeader is active, only toggleVisibility shortcut is registered.');
return;
}
for (const action in keybinds) {
const accelerator = keybinds[action];
if (!accelerator) continue;
let callback;
switch(action) {
case 'toggleVisibility':
callback = () => this.toggleAllWindowsVisibility(windowPool);
break;
case 'nextStep':
// Late require to prevent circular dependency
callback = () => require('../../window/windowManager').toggleFeature('ask', {ask: { targetVisibility: 'show' }});
break;
case 'scrollUp':
callback = () => {
const askWindow = windowPool.get('ask');
if (askWindow && !askWindow.isDestroyed() && askWindow.isVisible()) {
askWindow.webContents.send('scroll-response-up');
}
};
break;
case 'scrollDown':
callback = () => {
const askWindow = windowPool.get('ask');
if (askWindow && !askWindow.isDestroyed() && askWindow.isVisible()) {
askWindow.webContents.send('scroll-response-down');
}
};
break;
case 'moveUp':
callback = () => { if (header && header.isVisible()) movementManager.moveStep('up'); };
break;
case 'moveDown':
callback = () => { if (header && header.isVisible()) movementManager.moveStep('down'); };
break;
case 'moveLeft':
callback = () => { if (header && header.isVisible()) movementManager.moveStep('left'); };
break;
case 'moveRight':
callback = () => { if (header && header.isVisible()) movementManager.moveStep('right'); };
break;
case 'toggleClickThrough':
callback = () => {
this.mouseEventsIgnored = !this.mouseEventsIgnored;
if(mainWindow && !mainWindow.isDestroyed()){
mainWindow.setIgnoreMouseEvents(this.mouseEventsIgnored, { forward: true });
mainWindow.webContents.send('click-through-toggled', this.mouseEventsIgnored);
}
};
break;
case 'manualScreenshot':
callback = () => {
if(mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.executeJavaScript('window.captureManualScreenshot && window.captureManualScreenshot();');
}
};
break;
case 'previousResponse':
callback = () => sendToRenderer('navigate-previous-response');
break;
case 'nextResponse':
callback = () => sendToRenderer('navigate-next-response');
break;
}
if (callback) {
try {
globalShortcut.register(accelerator, callback);
} catch(e) {
console.error(`[Shortcuts] Failed to register shortcut for "${action}" (${accelerator}):`, e.message);
}
}
}
console.log('[Shortcuts] All shortcuts have been registered.');
}
unregisterAll() {
globalShortcut.unregisterAll();
console.log('[Shortcuts] All shortcuts have been unregistered.');
}
}
module.exports = new ShortcutsService();

View File

@ -7,6 +7,7 @@ const os = require('os');
const util = require('util');
const execFile = util.promisify(require('child_process').execFile);
const listenService = require('../features/listen/listenService');
const shortcutsService = require('../features/shortcuts/shortcutsService');
// Try to load sharp, but don't fail if it's not available
let sharp;
@ -20,13 +21,6 @@ try {
}
const authService = require('../features/common/services/authService');
const systemSettingsRepository = require('../features/common/repositories/systemSettings');
const Store = require('electron-store');
const shortCutStore = new Store({
name: 'user-preferences',
defaults: {
customKeybinds: {}
}
});
/* ────────────────[ GLASS BYPASS ]─────────────── */
let liquidGlass;
@ -263,13 +257,11 @@ function createFeatureWindows(header, namesToCreate) {
restoreClicks();
windowPool.delete('shortcut-settings');
console.log('[Shortcuts] Re-enabled after editing.');
loadAndRegisterShortcuts(movementManager);
shortcutsService.registerShortcuts(movementManager, windowPool);
});
shortcutEditor.webContents.once('dom-ready', async () => {
const savedKeybinds = shortCutStore.get('customKeybinds', {});
const defaultKeybinds = getDefaultKeybinds();
const keybinds = { ...defaultKeybinds, ...savedKeybinds };
const keybinds = await shortcutsService.loadKeybinds();
shortcutEditor.webContents.send('load-shortcuts', keybinds);
});
@ -418,7 +410,7 @@ function createWindows() {
layoutManager = new WindowLayoutManager(windowPool);
header.webContents.once('dom-ready', () => {
loadAndRegisterShortcuts(movementManager);
shortcutsService.registerShortcuts(movementManager, windowPool);
});
setupIpcHandlers(movementManager);
@ -475,32 +467,6 @@ function createWindows() {
return windowPool;
}
function loadAndRegisterShortcuts(movementManager) {
if (windowPool.has('shortcut-settings')) {
console.log('[Shortcuts] Editing in progress, skipping registration.');
return;
}
const defaultKeybinds = getDefaultKeybinds();
const savedKeybinds = shortCutStore.get('customKeybinds', {});
const keybinds = { ...defaultKeybinds, ...savedKeybinds };
const sendToRenderer = (channel, ...args) => {
windowPool.forEach(win => {
if (win && !win.isDestroyed()) {
try {
win.webContents.send(channel, ...args);
} catch (e) {
// 창이 이미 닫혔을 수 있으므로 오류를 무시합니다.
}
}
});
};
updateGlobalShortcuts(keybinds, windowPool.get('header'), sendToRenderer, movementManager);
}
function setupIpcHandlers(movementManager) {
setupApiKeyIPC();
@ -535,50 +501,37 @@ function setupIpcHandlers(movementManager) {
} else { // 'apikey' | 'permission'
destroyFeatureWindows();
}
loadAndRegisterShortcuts(movementManager);
shortcutsService.registerShortcuts(movementManager, windowPool);
});
ipcMain.on('update-keybinds', (event, newKeybinds) => {
updateGlobalShortcuts(newKeybinds);
ipcMain.handle('get-current-shortcuts', async () => {
return await shortcutsService.loadKeybinds();
});
ipcMain.handle('get-current-shortcuts', () => {
const defaultKeybinds = getDefaultKeybinds();
const savedKeybinds = shortCutStore.get('customKeybinds', {});
return { ...defaultKeybinds, ...savedKeybinds };
});
// open-shortcut-editor handler moved to windowBridge.js to avoid duplication
ipcMain.handle('get-default-shortcuts', () => {
shortCutStore.set('customKeybinds', {});
return getDefaultKeybinds();
ipcMain.handle('get-default-shortcuts', async () => {
const defaults = shortcutsService.getDefaultKeybinds();
await shortcutsService.saveKeybinds(defaults);
// Reregister shortcuts with new defaults
await shortcutsService.registerShortcuts(movementManager, windowPool);
return defaults;
});
ipcMain.handle('save-shortcuts', async (event, newKeybinds) => {
try {
const defaultKeybinds = getDefaultKeybinds();
const customKeybinds = {};
for (const key in newKeybinds) {
if (newKeybinds[key] && newKeybinds[key] !== defaultKeybinds[key]) {
customKeybinds[key] = newKeybinds[key];
}
}
await shortcutsService.saveKeybinds(newKeybinds);
shortCutStore.set('customKeybinds', customKeybinds);
console.log('[Shortcuts] Custom keybinds saved to store:', customKeybinds);
const editor = windowPool.get('shortcut-settings');
if (editor && !editor.isDestroyed()) {
editor.close();
editor.close(); // This will trigger re-registration on 'closed' event
} else {
loadAndRegisterShortcuts(movementManager);
// If editor wasn't open, re-register immediately
await shortcutsService.registerShortcuts(movementManager, windowPool);
}
return { success: true };
} catch (error) {
console.error("Failed to save shortcuts:", error);
loadAndRegisterShortcuts(movementManager);
// On failure, re-register old shortcuts to be safe
await shortcutsService.registerShortcuts(movementManager, windowPool);
return { success: false, error: error.message };
}
});
@ -1027,163 +980,6 @@ function setupApiKeyIPC() {
//////// after_modelStateService ////////
function getDefaultKeybinds() {
const isMac = process.platform === 'darwin';
return {
moveUp: isMac ? 'Cmd+Up' : 'Ctrl+Up',
moveDown: isMac ? 'Cmd+Down' : 'Ctrl+Down',
moveLeft: isMac ? 'Cmd+Left' : 'Ctrl+Left',
moveRight: isMac ? 'Cmd+Right' : 'Ctrl+Right',
toggleVisibility: isMac ? 'Cmd+\\' : 'Ctrl+\\',
toggleClickThrough: isMac ? 'Cmd+M' : 'Ctrl+M',
nextStep: isMac ? 'Cmd+Enter' : 'Ctrl+Enter',
manualScreenshot: isMac ? 'Cmd+Shift+S' : 'Ctrl+Shift+S',
previousResponse: isMac ? 'Cmd+[' : 'Ctrl+[',
nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]',
scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up',
scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down',
};
}
function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementManager) {
globalShortcut.unregisterAll();
if (sendToRenderer) {
sendToRenderer('shortcuts-updated', keybinds);
console.log('[Shortcuts] Broadcasted updated shortcuts to all windows.');
}
// 하드코딩된 단축키 등록을 위해 변수 유지
const isMac = process.platform === 'darwin';
const modifier = isMac ? 'Cmd' : 'Ctrl';
const header = windowPool.get('header');
const state = header?.currentHeaderState || currentHeaderState;
// 기능 1: 사용자가 설정할 수 없는 '모니터 이동' 단축키 (기존 로직 유지)
const displays = screen.getAllDisplays();
if (displays.length > 1) {
displays.forEach((display, index) => {
const key = `${modifier}+Shift+${index + 1}`;
try {
globalShortcut.register(key, () => movementManager.moveToDisplay(display.id));
console.log(`Registered display switch shortcut: ${key} -> Display ${index + 1}`);
} catch (error) {
console.error(`Failed to register display switch ${key}:`, error);
}
});
}
// API 키 입력 상태에서는 필수 단축키(toggleVisibility) 외에는 아무것도 등록하지 않음
if (state === 'apikey') {
if (keybinds.toggleVisibility) {
try {
globalShortcut.register(keybinds.toggleVisibility, () => toggleAllWindowsVisibility());
} catch (error) {
console.error(`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`, error);
}
}
console.log('ApiKeyHeader is active, skipping conditional shortcuts');
return;
}
// 기능 2: 사용자가 설정할 수 없는 '화면 가장자리 이동' 단축키 (기존 로직 유지)
const edgeDirections = [
{ key: `${modifier}+Shift+Left`, direction: 'left' },
{ key: `${modifier}+Shift+Right`, direction: 'right' },
// { key: `${modifier}+Shift+Up`, direction: 'up' },
// { key: `${modifier}+Shift+Down`, direction: 'down' },
];
edgeDirections.forEach(({ key, direction }) => {
try {
globalShortcut.register(key, () => {
if (header && header.isVisible()) movementManager.moveToEdge(direction);
});
} catch (error) {
console.error(`Failed to register edge move for ${key}:`, error);
}
});
// 기능 3: 사용자가 설정 가능한 모든 단축키를 동적으로 등록 (새로운 방식 적용)
for (const action in keybinds) {
const accelerator = keybinds[action];
if (!accelerator) continue;
try {
let callback;
switch(action) {
case 'toggleVisibility':
callback = () => toggleAllWindowsVisibility();
break;
case 'nextStep':
callback = () => toggleFeature('ask', {ask: { targetVisibility: 'show' }});
break;
case 'scrollUp':
callback = () => {
// 'ask' 창을 명시적으로 가져옵니다.
const askWindow = windowPool.get('ask');
// 'ask' 창이 존재하고, 파괴되지 않았으며, 보이는 경우에만 이벤트를 전송합니다.
if (askWindow && !askWindow.isDestroyed() && askWindow.isVisible()) {
askWindow.webContents.send('scroll-response-up');
}
};
break;
case 'scrollDown':
callback = () => {
// 'ask' 창을 명시적으로 가져옵니다.
const askWindow = windowPool.get('ask');
// 'ask' 창이 존재하고, 파괴되지 않았으며, 보이는 경우에만 이벤트를 전송합니다.
if (askWindow && !askWindow.isDestroyed() && askWindow.isVisible()) {
askWindow.webContents.send('scroll-response-down');
}
};
break;
case 'moveUp':
callback = () => { if (header && header.isVisible()) movementManager.moveStep('up'); };
break;
case 'moveDown':
callback = () => { if (header && header.isVisible()) movementManager.moveStep('down'); };
break;
case 'moveLeft':
callback = () => { if (header && header.isVisible()) movementManager.moveStep('left'); };
break;
case 'moveRight':
callback = () => { if (header && header.isVisible()) movementManager.moveStep('right'); };
break;
case 'toggleClickThrough':
callback = () => {
mouseEventsIgnored = !mouseEventsIgnored;
if(mainWindow && !mainWindow.isDestroyed()){
mainWindow.setIgnoreMouseEvents(mouseEventsIgnored, { forward: true });
mainWindow.webContents.send('click-through-toggled', mouseEventsIgnored);
}
};
break;
case 'manualScreenshot':
callback = () => {
if(mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.executeJavaScript('window.captureManualScreenshot && window.captureManualScreenshot();');
}
};
break;
case 'previousResponse':
callback = () => sendToRenderer('navigate-previous-response');
break;
case 'nextResponse':
callback = () => sendToRenderer('navigate-next-response');
break;
}
if (callback) {
globalShortcut.register(accelerator, callback);
}
} catch(e) {
console.error(`Failed to register shortcut for "${action}" (${accelerator}):`, e.message);
}
}
}
async function captureScreenshot(options = {}) {
if (process.platform === 'darwin') {
try {
@ -1278,4 +1074,5 @@ module.exports = {
getStoredProvider,
getCurrentModelInfo,
captureScreenshot,
toggleFeature, // Export toggleFeature so shortcutsService can use it
};