remove uiohook-napi and keystroke monitoring

This commit is contained in:
CREDO23 2026-04-03 16:10:52 +02:00
parent a99d999a36
commit 8ba571566d
12 changed files with 57 additions and 350 deletions

View file

@ -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',

View file

@ -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();
});

View file

@ -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 */ }
}
}

View file

@ -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];
}

View file

@ -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);

View file

@ -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) => {