From 8ba571566d2e1c3c296934c52e28a4202ede1dda Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Fri, 3 Apr 2026 16:10:52 +0200 Subject: [PATCH] remove uiohook-napi and keystroke monitoring --- surfsense_desktop/electron-builder.yml | 10 +- surfsense_desktop/package.json | 3 +- surfsense_desktop/pnpm-lock.yaml | 17 -- surfsense_desktop/scripts/build-electron.mjs | 2 +- surfsense_desktop/src/ipc/channels.ts | 1 - surfsense_desktop/src/ipc/handlers.ts | 5 - .../src/modules/autocomplete/index.ts | 156 +----------------- .../modules/autocomplete/keystroke-buffer.ts | 76 --------- surfsense_desktop/src/modules/permissions.ts | 12 +- surfsense_desktop/src/preload.ts | 1 - .../app/desktop/permissions/page.tsx | 122 ++++++-------- surfsense_web/types/window.d.ts | 2 - 12 files changed, 57 insertions(+), 350 deletions(-) delete mode 100644 surfsense_desktop/src/modules/autocomplete/keystroke-buffer.ts diff --git a/surfsense_desktop/electron-builder.yml b/surfsense_desktop/electron-builder.yml index 115b69c8e..3de0f266d 100644 --- a/surfsense_desktop/electron-builder.yml +++ b/surfsense_desktop/electron-builder.yml @@ -9,10 +9,6 @@ directories: files: - dist/**/* - "!node_modules" - - node_modules/uiohook-napi/**/* - - "!node_modules/uiohook-napi/src" - - "!node_modules/uiohook-napi/libuiohook" - - "!node_modules/uiohook-napi/binding.gyp" - node_modules/node-gyp-build/**/* - node_modules/bindings/**/* - node_modules/file-uri-to-path/**/* @@ -39,7 +35,6 @@ extraResources: filter: ["**/*"] asarUnpack: - "**/*.node" - - "node_modules/uiohook-napi/**/*" - "node_modules/node-gyp-build/**/*" - "node_modules/bindings/**/*" - "node_modules/file-uri-to-path/**/*" @@ -51,9 +46,8 @@ mac: hardenedRuntime: false gatekeeperAssess: false extendInfo: - NSInputMonitoringUsageDescription: "SurfSense uses input monitoring to provide system-wide autocomplete suggestions as you type." - NSAccessibilityUsageDescription: "SurfSense uses accessibility features to read text fields and insert suggestions." - NSAppleEventsUsageDescription: "SurfSense uses Apple Events to read text from the active application and insert autocomplete suggestions." + NSAccessibilityUsageDescription: "SurfSense uses accessibility features to insert suggestions into the active application." + NSAppleEventsUsageDescription: "SurfSense uses Apple Events to interact with the active application." target: - target: dmg arch: [x64, arm64] diff --git a/surfsense_desktop/package.json b/surfsense_desktop/package.json index 01a63b265..ab4fa0b8f 100644 --- a/surfsense_desktop/package.json +++ b/surfsense_desktop/package.json @@ -32,7 +32,6 @@ "bindings": "^1.5.0", "electron-updater": "^6.8.3", "get-port-please": "^3.2.0", - "node-mac-permissions": "^2.5.0", - "uiohook-napi": "^1.5.5" + "node-mac-permissions": "^2.5.0" } } diff --git a/surfsense_desktop/pnpm-lock.yaml b/surfsense_desktop/pnpm-lock.yaml index d0b453d31..96541c579 100644 --- a/surfsense_desktop/pnpm-lock.yaml +++ b/surfsense_desktop/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: node-mac-permissions: specifier: ^2.5.0 version: 2.5.0 - uiohook-napi: - specifier: ^1.5.5 - version: 1.5.5 devDependencies: '@electron/rebuild': specifier: ^4.0.3 @@ -1128,10 +1125,6 @@ packages: node-api-version@0.2.1: resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} - node-gyp-build@4.8.4: - resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} - hasBin: true - node-gyp@11.5.0: resolution: {integrity: sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -1454,10 +1447,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - uiohook-napi@1.5.5: - resolution: {integrity: sha512-oSlTdnECw2GBfsJPTbBQBeE4v/EXP0EZmX6BJq5nzH/JgFaBE8JpFwEA/kLhiEP7HxQw28FViWiYgdIZzWuuJQ==} - engines: {node: '>= 16'} - undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -2785,8 +2774,6 @@ snapshots: dependencies: semver: 7.7.4 - node-gyp-build@4.8.4: {} - node-gyp@11.5.0: dependencies: env-paths: 2.2.1 @@ -3113,10 +3100,6 @@ snapshots: typescript@5.9.3: {} - uiohook-napi@1.5.5: - dependencies: - node-gyp-build: 4.8.4 - undici-types@7.16.0: {} undici-types@7.18.2: {} diff --git a/surfsense_desktop/scripts/build-electron.mjs b/surfsense_desktop/scripts/build-electron.mjs index c2869ec46..9f507ea37 100644 --- a/surfsense_desktop/scripts/build-electron.mjs +++ b/surfsense_desktop/scripts/build-electron.mjs @@ -104,7 +104,7 @@ async function buildElectron() { bundle: true, platform: 'node', target: 'node18', - external: ['electron', 'uiohook-napi', 'node-mac-permissions', 'bindings', 'file-uri-to-path'], + external: ['electron', 'node-mac-permissions', 'bindings', 'file-uri-to-path'], sourcemap: true, minify: false, define: { diff --git a/surfsense_desktop/src/ipc/channels.ts b/surfsense_desktop/src/ipc/channels.ts index 2965f516f..905a84bc3 100644 --- a/surfsense_desktop/src/ipc/channels.ts +++ b/surfsense_desktop/src/ipc/channels.ts @@ -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', diff --git a/surfsense_desktop/src/ipc/handlers.ts b/surfsense_desktop/src/ipc/handlers.ts index a6d82be4b..8597a39e8 100644 --- a/surfsense_desktop/src/ipc/handlers.ts +++ b/surfsense_desktop/src/ipc/handlers.ts @@ -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(); }); diff --git a/surfsense_desktop/src/modules/autocomplete/index.ts b/surfsense_desktop/src/modules/autocomplete/index.ts index 2ea37d051..3d9d67eef 100644 --- a/surfsense_desktop/src/modules/autocomplete/index.ts +++ b/surfsense_desktop/src/modules/autocomplete/index.ts @@ -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 = new Set(); - -let debounceTimer: ReturnType | 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 { 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 { 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 { 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 */ } - } } diff --git a/surfsense_desktop/src/modules/autocomplete/keystroke-buffer.ts b/surfsense_desktop/src/modules/autocomplete/keystroke-buffer.ts deleted file mode 100644 index ca232d307..000000000 --- a/surfsense_desktop/src/modules/autocomplete/keystroke-buffer.ts +++ /dev/null @@ -1,76 +0,0 @@ -const MAX_BUFFER_LENGTH = 4000; -const KEYCODE_TO_CHAR: Record = {}; - -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]; -} diff --git a/surfsense_desktop/src/modules/permissions.ts b/surfsense_desktop/src/modules/permissions.ts index 9a6159c9a..4ac671b7c 100644 --- a/surfsense_desktop/src/modules/permissions.ts +++ b/surfsense_desktop/src/modules/permissions.ts @@ -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 { - if (!isMac()) return 'authorized'; - const perms = getNodeMacPermissions(); - return perms.askForInputMonitoringAccess('listen'); -} - export function restartApp(): void { app.relaunch(); app.exit(0); diff --git a/surfsense_desktop/src/preload.ts b/surfsense_desktop/src/preload.ts index 956afcc46..157fe216b 100644 --- a/surfsense_desktop/src/preload.ts +++ b/surfsense_desktop/src/preload.ts @@ -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) => { diff --git a/surfsense_web/app/desktop/permissions/page.tsx b/surfsense_web/app/desktop/permissions/page.tsx index 8bde63357..e0d3131e0 100644 --- a/surfsense_web/app/desktop/permissions/page.tsx +++ b/surfsense_web/app/desktop/permissions/page.tsx @@ -10,26 +10,8 @@ type PermissionStatus = "authorized" | "denied" | "not determined" | "restricted interface PermissionsStatus { accessibility: PermissionStatus; - inputMonitoring: PermissionStatus; } -const STEPS = [ - { - id: "input-monitoring", - title: "Input Monitoring", - description: "Helps you write faster by enriching your text with suggestions from your knowledge base.", - action: "requestInputMonitoring", - field: "inputMonitoring" as const, - }, - { - id: "accessibility", - title: "Accessibility", - description: "Lets you accept suggestions seamlessly, right where you're typing.", - action: "requestAccessibility", - field: "accessibility" as const, - }, -]; - function StatusBadge({ status }: { status: PermissionStatus }) { if (status === "authorized") { return ( @@ -66,13 +48,11 @@ export default function DesktopPermissionsPage() { let interval: ReturnType | null = null; - const isResolved = (s: string) => s === "authorized" || s === "restricted"; - const poll = async () => { const status = await window.electronAPI!.getPermissionsStatus(); setPermissions(status); - if (isResolved(status.accessibility) && isResolved(status.inputMonitoring)) { + if (status.accessibility === "authorized" || status.accessibility === "restricted") { if (interval) clearInterval(interval); } }; @@ -98,14 +78,10 @@ export default function DesktopPermissionsPage() { ); } - const allGranted = permissions.accessibility === "authorized" && permissions.inputMonitoring === "authorized"; + const allGranted = permissions.accessibility === "authorized"; - const handleRequest = async (action: string) => { - if (action === "requestInputMonitoring") { - await window.electronAPI!.requestInputMonitoring(); - } else if (action === "requestAccessibility") { - await window.electronAPI!.requestAccessibility(); - } + const handleRequest = async () => { + await window.electronAPI!.requestAccessibility(); }; const handleContinue = () => { @@ -127,61 +103,55 @@ export default function DesktopPermissionsPage() {

System Permissions

- SurfSense needs two macOS permissions to provide system-wide autocomplete. + SurfSense needs Accessibility permission to insert suggestions into the active application.

- {/* Steps */} + {/* Permission card */}
- {STEPS.map((step, index) => { - const status = permissions[step.field]; - const isGranted = status === "authorized"; - - return ( -
-
-
- - {isGranted ? "✓" : index + 1} - -
-

{step.title}

-

{step.description}

-
-
- -
- {!isGranted && ( -
- - {status === "denied" && ( -

- Toggle SurfSense on in System Settings to continue. -

- )} +
+
+
+ + {allGranted ? "\u2713" : "1"} + +
+

Accessibility

- If SurfSense doesn't appear in the list, click + and select it from Applications. + Lets SurfSense insert suggestions seamlessly, right where you're typing.

-
- )} +
- ); - })} + +
+ {!allGranted && ( +
+ + {permissions.accessibility === "denied" && ( +

+ Toggle SurfSense on in System Settings to continue. +

+ )} +

+ If SurfSense doesn't appear in the list, click + and select it from Applications. +

+
+ )} +
{/* Footer */} @@ -198,7 +168,7 @@ export default function DesktopPermissionsPage() { ) : ( <>