mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-03 21:02:40 +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,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]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
17
surfsense_desktop/pnpm-lock.yaml
generated
17
surfsense_desktop/pnpm-lock.yaml
generated
|
|
@ -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: {}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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<typeof setInterval> | 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() {
|
|||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">System Permissions</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
SurfSense needs two macOS permissions to provide system-wide autocomplete.
|
||||
SurfSense needs Accessibility permission to insert suggestions into the active application.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
{/* Permission card */}
|
||||
<div className="rounded-xl border bg-background dark:bg-neutral-900 flex-1 min-h-0 overflow-y-auto px-6 py-6 space-y-6">
|
||||
{STEPS.map((step, index) => {
|
||||
const status = permissions[step.field];
|
||||
const isGranted = status === "authorized";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={step.id}
|
||||
className={`rounded-lg border p-4 transition-colors ${
|
||||
isGranted
|
||||
? "border-green-200 bg-green-50/50 dark:border-green-900 dark:bg-green-950/20"
|
||||
: "border-border"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-medium text-primary">
|
||||
{isGranted ? "✓" : index + 1}
|
||||
</span>
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-medium">{step.title}</h3>
|
||||
<p className="text-xs text-muted-foreground">{step.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge status={status} />
|
||||
</div>
|
||||
{!isGranted && (
|
||||
<div className="mt-3 pl-10 space-y-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleRequest(step.action)}
|
||||
className="text-xs"
|
||||
>
|
||||
Open System Settings
|
||||
</Button>
|
||||
{status === "denied" && (
|
||||
<p className="text-xs text-amber-700 dark:text-amber-400">
|
||||
Toggle SurfSense on in System Settings to continue.
|
||||
</p>
|
||||
)}
|
||||
<div
|
||||
className={`rounded-lg border p-4 transition-colors ${
|
||||
allGranted
|
||||
? "border-green-200 bg-green-50/50 dark:border-green-900 dark:bg-green-950/20"
|
||||
: "border-border"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-medium text-primary">
|
||||
{allGranted ? "\u2713" : "1"}
|
||||
</span>
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-medium">Accessibility</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
If SurfSense doesn't appear in the list, click <strong>+</strong> and select it from Applications.
|
||||
Lets SurfSense insert suggestions seamlessly, right where you're typing.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<StatusBadge status={permissions.accessibility} />
|
||||
</div>
|
||||
{!allGranted && (
|
||||
<div className="mt-3 pl-10 space-y-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleRequest}
|
||||
className="text-xs"
|
||||
>
|
||||
Open System Settings
|
||||
</Button>
|
||||
{permissions.accessibility === "denied" && (
|
||||
<p className="text-xs text-amber-700 dark:text-amber-400">
|
||||
Toggle SurfSense on in System Settings to continue.
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
If SurfSense doesn't appear in the list, click <strong>+</strong> and select it from Applications.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
|
|
@ -198,7 +168,7 @@ export default function DesktopPermissionsPage() {
|
|||
) : (
|
||||
<>
|
||||
<Button disabled className="text-sm h-9 min-w-[180px]">
|
||||
Grant permissions to continue
|
||||
Grant permission to continue
|
||||
</Button>
|
||||
<button
|
||||
onClick={handleSkip}
|
||||
|
|
|
|||
2
surfsense_web/types/window.d.ts
vendored
2
surfsense_web/types/window.d.ts
vendored
|
|
@ -17,10 +17,8 @@ interface ElectronAPI {
|
|||
// Permissions
|
||||
getPermissionsStatus: () => Promise<{
|
||||
accessibility: 'authorized' | 'denied' | 'not determined' | 'restricted' | 'limited';
|
||||
inputMonitoring: 'authorized' | 'denied' | 'not determined' | 'restricted' | 'limited';
|
||||
}>;
|
||||
requestAccessibility: () => Promise<void>;
|
||||
requestInputMonitoring: () => Promise<void>;
|
||||
restartApp: () => Promise<void>;
|
||||
// Autocomplete
|
||||
onAutocompleteContext: (callback: (data: { text: string; cursorPosition: number; searchSpaceId?: string }) => void) => () => void;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue