mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-28 18:36:23 +02:00
remove uiohook-napi and keystroke monitoring
This commit is contained in:
parent
a99d999a36
commit
8ba571566d
12 changed files with 57 additions and 350 deletions
|
|
@ -9,7 +9,6 @@ export const IPC_CHANNELS = {
|
|||
// Permissions
|
||||
GET_PERMISSIONS_STATUS: 'get-permissions-status',
|
||||
REQUEST_ACCESSIBILITY: 'request-accessibility',
|
||||
REQUEST_INPUT_MONITORING: 'request-input-monitoring',
|
||||
RESTART_APP: 'restart-app',
|
||||
// Autocomplete
|
||||
AUTOCOMPLETE_CONTEXT: 'autocomplete-context',
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { IPC_CHANNELS } from './channels';
|
|||
import {
|
||||
getPermissionsStatus,
|
||||
requestAccessibility,
|
||||
requestInputMonitoring,
|
||||
restartApp,
|
||||
} from '../modules/permissions';
|
||||
|
||||
|
|
@ -31,10 +30,6 @@ export function registerIpcHandlers(): void {
|
|||
requestAccessibility();
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.REQUEST_INPUT_MONITORING, async () => {
|
||||
return await requestInputMonitoring();
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.RESTART_APP, () => {
|
||||
restartApp();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,152 +1,23 @@
|
|||
import { clipboard, ipcMain, screen } from 'electron';
|
||||
import { clipboard, globalShortcut, ipcMain, screen } from 'electron';
|
||||
import { IPC_CHANNELS } from '../../ipc/channels';
|
||||
import { getFrontmostApp, hasAccessibilityPermission, simulatePaste } from '../platform';
|
||||
import { getMainWindow } from '../window';
|
||||
import {
|
||||
appendToBuffer, buildKeycodeMap, getBuffer, getBufferTrimmed,
|
||||
getLastTrackedApp, removeLastChar, resetBuffer, resolveChar, setLastTrackedApp,
|
||||
} from './keystroke-buffer';
|
||||
import { createSuggestionWindow, destroySuggestion, getSuggestionWindow } from './suggestion-window';
|
||||
|
||||
const DEBOUNCE_MS = 600;
|
||||
|
||||
let uIOhook: any = null;
|
||||
let UiohookKey: any = {};
|
||||
let IGNORED_KEYCODES: Set<number> = new Set();
|
||||
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let hookStarted = false;
|
||||
let autocompleteEnabled = true;
|
||||
let savedClipboard = '';
|
||||
let sourceApp = '';
|
||||
let pendingSuggestionText = '';
|
||||
|
||||
function loadUiohook(): boolean {
|
||||
if (uIOhook) return true;
|
||||
try {
|
||||
const mod = require('uiohook-napi');
|
||||
uIOhook = mod.uIOhook;
|
||||
UiohookKey = mod.UiohookKey;
|
||||
IGNORED_KEYCODES = new Set([
|
||||
UiohookKey.Shift, UiohookKey.ShiftRight,
|
||||
UiohookKey.Ctrl, UiohookKey.CtrlRight,
|
||||
UiohookKey.Alt, UiohookKey.AltRight,
|
||||
UiohookKey.Meta, UiohookKey.MetaRight,
|
||||
UiohookKey.CapsLock, UiohookKey.NumLock, UiohookKey.ScrollLock,
|
||||
UiohookKey.F1, UiohookKey.F2, UiohookKey.F3, UiohookKey.F4,
|
||||
UiohookKey.F5, UiohookKey.F6, UiohookKey.F7, UiohookKey.F8,
|
||||
UiohookKey.F9, UiohookKey.F10, UiohookKey.F11, UiohookKey.F12,
|
||||
UiohookKey.PrintScreen,
|
||||
]);
|
||||
buildKeycodeMap();
|
||||
console.log('[autocomplete] uiohook-napi loaded');
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('[autocomplete] Failed to load uiohook-napi:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearDebounce(): void {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function isSurfSenseWindow(): boolean {
|
||||
const app = getFrontmostApp();
|
||||
return app === 'Electron' || app === 'SurfSense' || app === 'surfsense-desktop';
|
||||
}
|
||||
|
||||
function onKeyDown(event: {
|
||||
keycode: number;
|
||||
shiftKey?: boolean;
|
||||
ctrlKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
altKey?: boolean;
|
||||
}): void {
|
||||
if (!autocompleteEnabled) return;
|
||||
|
||||
const currentApp = getFrontmostApp();
|
||||
if (currentApp !== getLastTrackedApp()) {
|
||||
resetBuffer();
|
||||
setLastTrackedApp(currentApp);
|
||||
}
|
||||
|
||||
const win = getSuggestionWindow();
|
||||
|
||||
if (event.keycode === UiohookKey.Tab && win && !win.isDestroyed()) {
|
||||
if (pendingSuggestionText) {
|
||||
acceptAndInject(pendingSuggestionText);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.keycode === UiohookKey.Escape) {
|
||||
if (win && !win.isDestroyed()) {
|
||||
destroySuggestion();
|
||||
pendingSuggestionText = '';
|
||||
}
|
||||
clearDebounce();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentApp === 'Electron' || currentApp === 'SurfSense' || currentApp === 'surfsense-desktop') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.ctrlKey || event.metaKey || event.altKey) {
|
||||
resetBuffer();
|
||||
clearDebounce();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.keycode === UiohookKey.Backspace) {
|
||||
removeLastChar();
|
||||
} else if (event.keycode === UiohookKey.Delete) {
|
||||
// forward delete doesn't affect our trailing buffer
|
||||
} else if (event.keycode === UiohookKey.Enter) {
|
||||
appendToBuffer('\n');
|
||||
} else if (event.keycode === UiohookKey.Space) {
|
||||
appendToBuffer(' ');
|
||||
} else if (
|
||||
event.keycode === UiohookKey.ArrowLeft || event.keycode === UiohookKey.ArrowRight ||
|
||||
event.keycode === UiohookKey.ArrowUp || event.keycode === UiohookKey.ArrowDown ||
|
||||
event.keycode === UiohookKey.Home || event.keycode === UiohookKey.End ||
|
||||
event.keycode === UiohookKey.PageUp || event.keycode === UiohookKey.PageDown
|
||||
) {
|
||||
resetBuffer();
|
||||
clearDebounce();
|
||||
return;
|
||||
} else if (IGNORED_KEYCODES.has(event.keycode)) {
|
||||
return;
|
||||
} else {
|
||||
const ch = resolveChar(event.keycode, !!event.shiftKey);
|
||||
if (ch) appendToBuffer(ch);
|
||||
}
|
||||
|
||||
if (win && !win.isDestroyed()) {
|
||||
destroySuggestion();
|
||||
}
|
||||
|
||||
clearDebounce();
|
||||
debounceTimer = setTimeout(() => {
|
||||
triggerAutocomplete();
|
||||
}, DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
function onMouseClick(): void {
|
||||
resetBuffer();
|
||||
}
|
||||
|
||||
async function triggerAutocomplete(): Promise<void> {
|
||||
if (!hasAccessibilityPermission()) return;
|
||||
if (isSurfSenseWindow()) return;
|
||||
|
||||
const text = getBufferTrimmed();
|
||||
if (!text || text.length < 5) return;
|
||||
|
||||
sourceApp = getFrontmostApp();
|
||||
savedClipboard = clipboard.readText();
|
||||
|
||||
|
|
@ -168,8 +39,8 @@ async function triggerAutocomplete(): Promise<void> {
|
|||
setTimeout(() => {
|
||||
if (sw && !sw.isDestroyed()) {
|
||||
sw.webContents.send(IPC_CHANNELS.AUTOCOMPLETE_CONTEXT, {
|
||||
text: getBuffer(),
|
||||
cursorPosition: getBuffer().length,
|
||||
text: '',
|
||||
cursorPosition: 0,
|
||||
searchSpaceId,
|
||||
});
|
||||
}
|
||||
|
|
@ -190,7 +61,6 @@ async function acceptAndInject(text: string): Promise<void> {
|
|||
simulatePaste();
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
clipboard.writeText(savedClipboard);
|
||||
appendToBuffer(text);
|
||||
} catch {
|
||||
clipboard.writeText(savedClipboard);
|
||||
}
|
||||
|
|
@ -210,7 +80,6 @@ function registerIpcHandlers(): void {
|
|||
ipcMain.handle(IPC_CHANNELS.SET_AUTOCOMPLETE_ENABLED, (_event, enabled: boolean) => {
|
||||
autocompleteEnabled = enabled;
|
||||
if (!enabled) {
|
||||
clearDebounce();
|
||||
destroySuggestion();
|
||||
}
|
||||
});
|
||||
|
|
@ -220,25 +89,10 @@ function registerIpcHandlers(): void {
|
|||
export function registerAutocomplete(): void {
|
||||
registerIpcHandlers();
|
||||
|
||||
if (!loadUiohook()) {
|
||||
console.error('[autocomplete] Cannot start: uiohook-napi failed to load');
|
||||
return;
|
||||
}
|
||||
|
||||
uIOhook.on('keydown', onKeyDown);
|
||||
uIOhook.on('click', onMouseClick);
|
||||
try {
|
||||
uIOhook.start();
|
||||
hookStarted = true;
|
||||
} catch (err) {
|
||||
console.error('[autocomplete] uIOhook.start() failed:', err);
|
||||
}
|
||||
// TODO: Phase 2 — replace with vision-based trigger (desktopCapturer + globalShortcut)
|
||||
console.log('[autocomplete] IPC handlers registered');
|
||||
}
|
||||
|
||||
export function unregisterAutocomplete(): void {
|
||||
clearDebounce();
|
||||
destroySuggestion();
|
||||
if (uIOhook && hookStarted) {
|
||||
try { uIOhook.stop(); } catch { /* already stopped */ }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
const MAX_BUFFER_LENGTH = 4000;
|
||||
const KEYCODE_TO_CHAR: Record<number, [string, string]> = {};
|
||||
|
||||
let keystrokeBuffer = '';
|
||||
let lastTrackedApp = '';
|
||||
|
||||
export function buildKeycodeMap(): void {
|
||||
const letters: [string, number][] = [
|
||||
['q', 16], ['w', 17], ['e', 18], ['r', 19], ['t', 20],
|
||||
['y', 21], ['u', 22], ['i', 23], ['o', 24], ['p', 25],
|
||||
['a', 30], ['s', 31], ['d', 32], ['f', 33], ['g', 34],
|
||||
['h', 35], ['j', 36], ['k', 37], ['l', 38],
|
||||
['z', 44], ['x', 45], ['c', 46], ['v', 47],
|
||||
['b', 48], ['n', 49], ['m', 50],
|
||||
];
|
||||
for (const [ch, code] of letters) {
|
||||
KEYCODE_TO_CHAR[code] = [ch, ch.toUpperCase()];
|
||||
}
|
||||
|
||||
const digits: [string, string, number][] = [
|
||||
['1', '!', 2], ['2', '@', 3], ['3', '#', 4], ['4', '$', 5],
|
||||
['5', '%', 6], ['6', '^', 7], ['7', '&', 8], ['8', '*', 9],
|
||||
['9', '(', 10], ['0', ')', 11],
|
||||
];
|
||||
for (const [norm, shifted, code] of digits) {
|
||||
KEYCODE_TO_CHAR[code] = [norm, shifted];
|
||||
}
|
||||
|
||||
const punctuation: [string, string, number][] = [
|
||||
[';', ':', 39], ['=', '+', 13], [',', '<', 51], ['-', '_', 12],
|
||||
['.', '>', 52], ['/', '?', 53], ['`', '~', 41], ['[', '{', 26],
|
||||
['\\', '|', 43], [']', '}', 27], ["'", '"', 40],
|
||||
];
|
||||
for (const [norm, shifted, code] of punctuation) {
|
||||
KEYCODE_TO_CHAR[code] = [norm, shifted];
|
||||
}
|
||||
}
|
||||
|
||||
export function resetBuffer(): void {
|
||||
keystrokeBuffer = '';
|
||||
}
|
||||
|
||||
export function appendToBuffer(char: string): void {
|
||||
keystrokeBuffer += char;
|
||||
if (keystrokeBuffer.length > MAX_BUFFER_LENGTH) {
|
||||
keystrokeBuffer = keystrokeBuffer.slice(-MAX_BUFFER_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeLastChar(): void {
|
||||
if (keystrokeBuffer.length > 0) {
|
||||
keystrokeBuffer = keystrokeBuffer.slice(0, -1);
|
||||
}
|
||||
}
|
||||
|
||||
export function getBuffer(): string {
|
||||
return keystrokeBuffer;
|
||||
}
|
||||
|
||||
export function getBufferTrimmed(): string {
|
||||
return keystrokeBuffer.trim();
|
||||
}
|
||||
|
||||
export function getLastTrackedApp(): string {
|
||||
return lastTrackedApp;
|
||||
}
|
||||
|
||||
export function setLastTrackedApp(app: string): void {
|
||||
lastTrackedApp = app;
|
||||
}
|
||||
|
||||
export function resolveChar(keycode: number, shift: boolean): string | null {
|
||||
const mapping = KEYCODE_TO_CHAR[keycode];
|
||||
if (!mapping) return null;
|
||||
return shift ? mapping[1] : mapping[0];
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ type PermissionStatus = 'authorized' | 'denied' | 'not determined' | 'restricted
|
|||
|
||||
export interface PermissionsStatus {
|
||||
accessibility: PermissionStatus;
|
||||
inputMonitoring: PermissionStatus;
|
||||
}
|
||||
|
||||
function isMac(): boolean {
|
||||
|
|
@ -17,19 +16,18 @@ function getNodeMacPermissions() {
|
|||
|
||||
export function getPermissionsStatus(): PermissionsStatus {
|
||||
if (!isMac()) {
|
||||
return { accessibility: 'authorized', inputMonitoring: 'authorized' };
|
||||
return { accessibility: 'authorized' };
|
||||
}
|
||||
|
||||
const perms = getNodeMacPermissions();
|
||||
return {
|
||||
accessibility: perms.getAuthStatus('accessibility'),
|
||||
inputMonitoring: perms.getAuthStatus('input-monitoring'),
|
||||
};
|
||||
}
|
||||
|
||||
export function allPermissionsGranted(): boolean {
|
||||
const status = getPermissionsStatus();
|
||||
return status.accessibility === 'authorized' && status.inputMonitoring === 'authorized';
|
||||
return status.accessibility === 'authorized';
|
||||
}
|
||||
|
||||
export function requestAccessibility(): void {
|
||||
|
|
@ -38,12 +36,6 @@ export function requestAccessibility(): void {
|
|||
perms.askForAccessibilityAccess();
|
||||
}
|
||||
|
||||
export async function requestInputMonitoring(): Promise<string> {
|
||||
if (!isMac()) return 'authorized';
|
||||
const perms = getNodeMacPermissions();
|
||||
return perms.askForInputMonitoringAccess('listen');
|
||||
}
|
||||
|
||||
export function restartApp(): void {
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
// Permissions
|
||||
getPermissionsStatus: () => ipcRenderer.invoke(IPC_CHANNELS.GET_PERMISSIONS_STATUS),
|
||||
requestAccessibility: () => ipcRenderer.invoke(IPC_CHANNELS.REQUEST_ACCESSIBILITY),
|
||||
requestInputMonitoring: () => ipcRenderer.invoke(IPC_CHANNELS.REQUEST_INPUT_MONITORING),
|
||||
restartApp: () => ipcRenderer.invoke(IPC_CHANNELS.RESTART_APP),
|
||||
// Autocomplete
|
||||
onAutocompleteContext: (callback: (data: { text: string; cursorPosition: number; searchSpaceId?: string }) => void) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue