keyvoid 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ macOS Keyboard Suppressor using pynput (preferred method).
4
+
5
+ Uses pynput's suppress=True which blocks ALL keys at the system level,
6
+ including function keys, media keys (volume, brightness), and modifiers.
7
+
8
+ Output protocol (stdout):
9
+ STARTED — helper is active and blocking keys
10
+ K:<keycode>:<1|0> — key event (1=down, 0=up)
11
+ FAILSAFE — Esc+Space held for 5 seconds
12
+
13
+ Requires: pip3 install pynput
14
+ """
15
+
16
+ import sys
17
+ import time
18
+ import signal
19
+ import threading
20
+
21
+ try:
22
+ from pynput import keyboard
23
+ except ImportError:
24
+ print("ERROR: pynput not installed. Run: pip3 install pynput", file=sys.stderr, flush=True)
25
+ sys.exit(2) # Exit code 2 = missing dependency
26
+
27
+ # ── State for failsafe detection ──
28
+ esc_pressed = False
29
+ space_pressed = False
30
+ failsafe_start = None
31
+ FAILSAFE_SECONDS = 5.0
32
+ lock = threading.Lock()
33
+
34
+
35
+ def get_keycode(key):
36
+ """Extract a numeric keycode from a pynput key object."""
37
+ # Try to get the virtual key code
38
+ if hasattr(key, 'vk') and key.vk is not None:
39
+ return key.vk
40
+ # Try scan code
41
+ if hasattr(key, 'value') and hasattr(key.value, 'vk'):
42
+ return key.value.vk
43
+ # Known special keys → macOS keycodes
44
+ key_map = {
45
+ keyboard.Key.esc: 53,
46
+ keyboard.Key.space: 49,
47
+ keyboard.Key.tab: 48,
48
+ keyboard.Key.caps_lock: 57,
49
+ keyboard.Key.shift: 56,
50
+ keyboard.Key.shift_r: 60,
51
+ keyboard.Key.ctrl: 59,
52
+ keyboard.Key.ctrl_r: 62,
53
+ keyboard.Key.alt: 58,
54
+ keyboard.Key.alt_r: 61,
55
+ keyboard.Key.cmd: 55,
56
+ keyboard.Key.cmd_r: 54,
57
+ keyboard.Key.enter: 36,
58
+ keyboard.Key.backspace: 51,
59
+ keyboard.Key.delete: 117,
60
+ keyboard.Key.up: 126,
61
+ keyboard.Key.down: 125,
62
+ keyboard.Key.left: 123,
63
+ keyboard.Key.right: 124,
64
+ keyboard.Key.home: 115,
65
+ keyboard.Key.end: 119,
66
+ keyboard.Key.page_up: 116,
67
+ keyboard.Key.page_down: 121,
68
+ keyboard.Key.f1: 122,
69
+ keyboard.Key.f2: 120,
70
+ keyboard.Key.f3: 99,
71
+ keyboard.Key.f4: 118,
72
+ keyboard.Key.f5: 96,
73
+ keyboard.Key.f6: 97,
74
+ keyboard.Key.f7: 98,
75
+ keyboard.Key.f8: 100,
76
+ keyboard.Key.f9: 101,
77
+ keyboard.Key.f10: 109,
78
+ keyboard.Key.f11: 103,
79
+ keyboard.Key.f12: 111,
80
+ keyboard.Key.media_play_pause: 1000,
81
+ keyboard.Key.media_next: 1001,
82
+ keyboard.Key.media_previous: 1002,
83
+ keyboard.Key.media_volume_up: 1003,
84
+ keyboard.Key.media_volume_down: 1004,
85
+ keyboard.Key.media_volume_mute: 1005,
86
+ }
87
+ return key_map.get(key, 0)
88
+
89
+
90
+ def is_esc(key):
91
+ """Check if the key is Escape."""
92
+ return key == keyboard.Key.esc
93
+
94
+
95
+ def is_space(key):
96
+ """Check if the key is Space."""
97
+ return key == keyboard.Key.space
98
+
99
+
100
+ def check_failsafe():
101
+ """Background thread that monitors the failsafe combo timing."""
102
+ global failsafe_start
103
+ while True:
104
+ time.sleep(0.1)
105
+ with lock:
106
+ if esc_pressed and space_pressed:
107
+ if failsafe_start is None:
108
+ failsafe_start = time.time()
109
+ elif time.time() - failsafe_start >= FAILSAFE_SECONDS:
110
+ print("FAILSAFE", flush=True)
111
+ # Give Node.js a moment to read the output
112
+ time.sleep(0.1)
113
+ import os
114
+ os._exit(0)
115
+ else:
116
+ failsafe_start = None
117
+
118
+
119
+ def on_press(key):
120
+ """Called on every key press. Returns nothing to suppress the key."""
121
+ global esc_pressed, space_pressed
122
+
123
+ keycode = get_keycode(key)
124
+ print(f"K:{keycode}:1", flush=True)
125
+
126
+ with lock:
127
+ if is_esc(key):
128
+ esc_pressed = True
129
+ if is_space(key):
130
+ space_pressed = True
131
+
132
+ # Returning None with suppress=True blocks the key
133
+ return None
134
+
135
+
136
+ def on_release(key):
137
+ """Called on every key release."""
138
+ global esc_pressed, space_pressed, failsafe_start
139
+
140
+ keycode = get_keycode(key)
141
+ print(f"K:{keycode}:0", flush=True)
142
+
143
+ with lock:
144
+ if is_esc(key):
145
+ esc_pressed = False
146
+ if is_space(key):
147
+ space_pressed = False
148
+
149
+ return None
150
+
151
+
152
+ def cleanup(sig=None, frame=None):
153
+ """Clean exit on signal."""
154
+ import os
155
+ os._exit(0)
156
+
157
+
158
+ # ── Signal handlers ──
159
+ signal.signal(signal.SIGTERM, cleanup)
160
+ signal.signal(signal.SIGINT, cleanup)
161
+
162
+ # ── Start failsafe monitor thread ──
163
+ failsafe_thread = threading.Thread(target=check_failsafe, daemon=True)
164
+ failsafe_thread.start()
165
+
166
+ # ── Start the listener ──
167
+ # suppress=True is the key: it blocks ALL keyboard input from reaching
168
+ # other applications, including function keys, media keys, and modifiers.
169
+ try:
170
+ with keyboard.Listener(
171
+ on_press=on_press,
172
+ on_release=on_release,
173
+ suppress=True
174
+ ) as listener:
175
+ print("STARTED", flush=True)
176
+ listener.join()
177
+ except Exception as e:
178
+ print(f"ERROR: {e}", file=sys.stderr, flush=True)
179
+ sys.exit(1)
@@ -0,0 +1,144 @@
1
+ # ─── Windows Keyboard Suppressor ──────────────────────────────────────
2
+ # Uses SetWindowsHookEx with WH_KEYBOARD_LL to intercept and block
3
+ # all keyboard input at the system level.
4
+ #
5
+ # Output protocol (stdout):
6
+ # STARTED — helper is active and blocking keys
7
+ # K:<keycode>:<1|0> — key event (1=down, 0=up)
8
+ # FAILSAFE — Esc+Space held for 5 seconds
9
+
10
+ Add-Type -TypeDefinition @"
11
+ using System;
12
+ using System.Diagnostics;
13
+ using System.Runtime.InteropServices;
14
+ using System.Threading;
15
+
16
+ public class KeyVoidBlocker {
17
+ private const int WH_KEYBOARD_LL = 13;
18
+ private const int WM_KEYDOWN = 0x0100;
19
+ private const int WM_KEYUP = 0x0101;
20
+ private const int WM_SYSKEYDOWN = 0x0104;
21
+ private const int WM_SYSKEYUP = 0x0105;
22
+ private const int VK_ESCAPE = 0x1B;
23
+ private const int VK_SPACE = 0x20;
24
+
25
+ private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
26
+
27
+ [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
28
+ private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
29
+
30
+ [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
31
+ [return: MarshalAs(UnmanagedType.Bool)]
32
+ private static extern bool UnhookWindowsHookEx(IntPtr hhk);
33
+
34
+ [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
35
+ private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
36
+
37
+ [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
38
+ private static extern IntPtr GetModuleHandle(string lpModuleName);
39
+
40
+ [DllImport("user32.dll")]
41
+ private static extern bool GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
42
+
43
+ [DllImport("user32.dll")]
44
+ private static extern bool TranslateMessage(ref MSG lpMsg);
45
+
46
+ [DllImport("user32.dll")]
47
+ private static extern IntPtr DispatchMessage(ref MSG lpMsg);
48
+
49
+ [StructLayout(LayoutKind.Sequential)]
50
+ private struct MSG {
51
+ public IntPtr hwnd;
52
+ public uint message;
53
+ public IntPtr wParam;
54
+ public IntPtr lParam;
55
+ public uint time;
56
+ public POINT pt;
57
+ }
58
+
59
+ [StructLayout(LayoutKind.Sequential)]
60
+ private struct POINT {
61
+ public int x;
62
+ public int y;
63
+ }
64
+
65
+ [StructLayout(LayoutKind.Sequential)]
66
+ private struct KBDLLHOOKSTRUCT {
67
+ public int vkCode;
68
+ public int scanCode;
69
+ public int flags;
70
+ public int time;
71
+ public IntPtr dwExtraInfo;
72
+ }
73
+
74
+ private static IntPtr hookId = IntPtr.Zero;
75
+ private static bool escPressed = false;
76
+ private static bool spacePressed = false;
77
+ private static DateTime? failsafeStart = null;
78
+
79
+ private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
80
+ if (nCode >= 0) {
81
+ KBDLLHOOKSTRUCT hookStruct = Marshal.PtrToStructure<KBDLLHOOKSTRUCT>(lParam);
82
+ int vkCode = hookStruct.vkCode;
83
+ bool isDown = (wParam == (IntPtr)WM_KEYDOWN || wParam == (IntPtr)WM_SYSKEYDOWN);
84
+
85
+ Console.WriteLine("K:" + vkCode + ":" + (isDown ? "1" : "0"));
86
+ Console.Out.Flush();
87
+
88
+ // Failsafe tracking
89
+ if (isDown) {
90
+ if (vkCode == VK_ESCAPE) escPressed = true;
91
+ if (vkCode == VK_SPACE) spacePressed = true;
92
+ } else {
93
+ if (vkCode == VK_ESCAPE) escPressed = false;
94
+ if (vkCode == VK_SPACE) spacePressed = false;
95
+ }
96
+
97
+ if (escPressed && spacePressed) {
98
+ if (!failsafeStart.HasValue) {
99
+ failsafeStart = DateTime.Now;
100
+ } else if ((DateTime.Now - failsafeStart.Value).TotalSeconds >= 5) {
101
+ Console.WriteLine("FAILSAFE");
102
+ Console.Out.Flush();
103
+ UnhookWindowsHookEx(hookId);
104
+ Environment.Exit(0);
105
+ }
106
+ } else {
107
+ failsafeStart = null;
108
+ }
109
+
110
+ // Block the key by returning 1
111
+ return (IntPtr)1;
112
+ }
113
+ return CallNextHookEx(hookId, nCode, wParam, lParam);
114
+ }
115
+
116
+ public static void Run() {
117
+ LowLevelKeyboardProc proc = HookCallback;
118
+ using (Process curProcess = Process.GetCurrentProcess())
119
+ using (ProcessModule curModule = curProcess.MainModule) {
120
+ hookId = SetWindowsHookEx(WH_KEYBOARD_LL, proc,
121
+ GetModuleHandle(curModule.ModuleName), 0);
122
+ }
123
+
124
+ if (hookId == IntPtr.Zero) {
125
+ Console.Error.WriteLine("ERROR: Failed to set keyboard hook");
126
+ Environment.Exit(1);
127
+ }
128
+
129
+ Console.WriteLine("STARTED");
130
+ Console.Out.Flush();
131
+
132
+ // Message loop (required for the hook to work)
133
+ MSG msg;
134
+ while (GetMessage(out msg, IntPtr.Zero, 0, 0)) {
135
+ TranslateMessage(ref msg);
136
+ DispatchMessage(ref msg);
137
+ }
138
+
139
+ UnhookWindowsHookEx(hookId);
140
+ }
141
+ }
142
+ "@ -ReferencedAssemblies System.Runtime.InteropServices
143
+
144
+ [KeyVoidBlocker]::Run()
@@ -0,0 +1,306 @@
1
+ // ─── Keyboard Suppressor ─────────────────────────────────────────────
2
+ // Manages platform-specific native helpers that actually block keyboard
3
+ // input at the system level. Communicates via child process stdout.
4
+
5
+ import { spawn, execSync } from 'child_process';
6
+ import { fileURLToPath } from 'url';
7
+ import { dirname, join } from 'path';
8
+ import { existsSync } from 'fs';
9
+ import { EventEmitter } from 'events';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+ const HELPERS_DIR = join(__dirname, 'helpers');
14
+
15
+ export class KeyboardSuppressor extends EventEmitter {
16
+ constructor() {
17
+ super();
18
+ this.process = null;
19
+ this.running = false;
20
+ this.keyCount = 0;
21
+ this.escPressed = false;
22
+ this.spacePressed = false;
23
+ this.failsafeStart = null;
24
+ this.failsafeTimeout = 5000; // 5 seconds
25
+ this.failsafeTimer = null;
26
+
27
+ // Guarantee the blocker process is killed even on hard crashes
28
+ process.on('exit', () => {
29
+ if (this.process) {
30
+ try { this.process.kill('SIGKILL'); } catch (e) {}
31
+ }
32
+ });
33
+ }
34
+
35
+ async start() {
36
+ const platform = process.platform;
37
+
38
+ try {
39
+ switch (platform) {
40
+ case 'darwin':
41
+ await this._startMacOS();
42
+ break;
43
+ case 'win32':
44
+ await this._startWindows();
45
+ break;
46
+ case 'linux':
47
+ await this._startLinux();
48
+ break;
49
+ default:
50
+ throw new Error(`Unsupported platform: ${platform}`);
51
+ }
52
+ } catch (err) {
53
+ this.emit('error', err);
54
+ return false;
55
+ }
56
+
57
+ return true;
58
+ }
59
+
60
+ stop() {
61
+ if (this.failsafeTimer) {
62
+ clearInterval(this.failsafeTimer);
63
+ this.failsafeTimer = null;
64
+ }
65
+
66
+ if (this.process) {
67
+ const p = this.process;
68
+ try {
69
+ // Use SIGKILL synchronously. If this runs inside a process.on('exit')
70
+ // event, setTimeouts won't execute. We must ensure immediate death
71
+ // to release the OS level keyboard hooks.
72
+ p.kill('SIGKILL');
73
+ } catch {
74
+ // Process may have already exited
75
+ }
76
+ this.process = null;
77
+ }
78
+ this.running = false;
79
+
80
+ // Platform-specific cleanup
81
+ if (process.platform === 'linux') {
82
+ this._cleanupLinux();
83
+ }
84
+ }
85
+
86
+ async _startMacOS() {
87
+ // Strategy: try pynput (blocks ALL keys including media/fn),
88
+ // then fall back to Swift CGEventTap helper.
89
+
90
+ // 1. Try pynput-based helper (preferred — proven to block function/media keys)
91
+ const pynputHelper = join(HELPERS_DIR, 'macos-pynput-helper.py');
92
+ if (existsSync(pynputHelper)) {
93
+ const pythonExe = await this._getPythonWithPynput();
94
+ if (pythonExe) {
95
+ this.process = spawn(pythonExe, [pynputHelper], {
96
+ stdio: ['pipe', 'pipe', 'pipe'],
97
+ });
98
+ this._bindProcess();
99
+ return;
100
+ }
101
+ }
102
+
103
+ // 2. Fall back to Swift CGEventTap helper
104
+ const helperSrc = join(HELPERS_DIR, 'macos-helper.swift');
105
+ if (!existsSync(helperSrc)) {
106
+ throw new Error('No macOS helper found. Install pynput (python3 -m venv .venv && source .venv/bin/activate && pip install pynput) or ensure Swift is available.');
107
+ }
108
+
109
+ // Compile the Swift helper to a cached binary for fast subsequent runs
110
+ const cacheDir = join(process.env.HOME || '/tmp', '.keyvoid-cache');
111
+ const helperBin = join(cacheDir, 'keyvoid-macos-helper');
112
+
113
+ // Invalidate cache if source is newer than binary
114
+ if (existsSync(helperBin)) {
115
+ try {
116
+ const { statSync } = await import('fs');
117
+ const srcStat = statSync(helperSrc);
118
+ const binStat = statSync(helperBin);
119
+ if (srcStat.mtimeMs > binStat.mtimeMs) {
120
+ execSync(`rm -f "${helperBin}"`, { stdio: 'pipe' });
121
+ }
122
+ } catch {
123
+ // Ignore stat errors
124
+ }
125
+ }
126
+
127
+ if (!existsSync(helperBin)) {
128
+ try {
129
+ execSync(`mkdir -p ${cacheDir}`, { stdio: 'pipe' });
130
+ execSync(`swiftc -O "${helperSrc}" -o "${helperBin}"`, {
131
+ stdio: 'pipe',
132
+ timeout: 30000, // 30s compile timeout
133
+ });
134
+ } catch {
135
+ // Fallback: run Swift as a script (slower startup)
136
+ this.process = spawn('swift', [helperSrc], { stdio: ['pipe', 'pipe', 'pipe'] });
137
+ this._bindProcess();
138
+ return;
139
+ }
140
+ }
141
+
142
+ this.process = spawn(helperBin, [], { stdio: ['pipe', 'pipe', 'pipe'] });
143
+ this._bindProcess();
144
+ }
145
+
146
+ // Find python executable with pynput installed
147
+ async _getPythonWithPynput() {
148
+ // Check bundled venv first
149
+ const venvPython = join(__dirname, '../../../.venv/bin/python3');
150
+ if (existsSync(venvPython)) {
151
+ try {
152
+ execSync(`"${venvPython}" -c "from pynput import keyboard"`, { stdio: 'pipe' });
153
+ return venvPython;
154
+ } catch (e) {}
155
+ }
156
+
157
+ // Check global python3
158
+ try {
159
+ execSync('python3 -c "from pynput import keyboard"', {
160
+ stdio: 'pipe',
161
+ timeout: 5000,
162
+ });
163
+ return 'python3';
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ async _startWindows() {
170
+ const helperScript = join(HELPERS_DIR, 'windows-helper.ps1');
171
+
172
+ if (!existsSync(helperScript)) {
173
+ throw new Error('Windows helper script not found');
174
+ }
175
+
176
+ this.process = spawn('powershell', [
177
+ '-ExecutionPolicy', 'Bypass',
178
+ '-File', helperScript,
179
+ ], { stdio: ['pipe', 'pipe', 'pipe'] });
180
+
181
+ this._bindProcess();
182
+ }
183
+
184
+ async _startLinux() {
185
+ const helperScript = join(HELPERS_DIR, 'linux-helper.py');
186
+
187
+ if (!existsSync(helperScript)) {
188
+ throw new Error('Linux helper script not found');
189
+ }
190
+
191
+ this.process = spawn('python3', [helperScript], {
192
+ stdio: ['pipe', 'pipe', 'pipe'],
193
+ });
194
+
195
+ this._bindProcess();
196
+ }
197
+
198
+ _bindProcess() {
199
+ const proc = this.process;
200
+
201
+ proc.stdout.on('data', (data) => {
202
+ const lines = data.toString().split('\n').filter(l => l.trim());
203
+
204
+ for (const line of lines) {
205
+ if (line === 'STARTED') {
206
+ this.running = true;
207
+ this.emit('started');
208
+ continue;
209
+ }
210
+
211
+ if (line === 'FAILSAFE') {
212
+ this.emit('failsafe');
213
+ continue;
214
+ }
215
+
216
+ if (line.startsWith('K:')) {
217
+ // Format: K:<keycode>:<down|up>
218
+ const parts = line.split(':');
219
+ const keycode = parseInt(parts[1], 10);
220
+ const isDown = parts[2] === '1';
221
+
222
+ this.keyCount++;
223
+ this.emit('key', { keycode, isDown, count: this.keyCount });
224
+
225
+ // Track failsafe combo (Esc + Space held for 5s)
226
+ this._trackFailsafe(keycode, isDown);
227
+ }
228
+ }
229
+ });
230
+
231
+ proc.stderr.on('data', (data) => {
232
+ const msg = data.toString().trim();
233
+ if (msg.includes('Accessibility') || msg.includes('ERROR')) {
234
+ this.emit('permission-error', msg);
235
+ }
236
+ });
237
+
238
+ proc.on('close', (code) => {
239
+ this.running = false;
240
+ this.emit('close', code);
241
+ });
242
+
243
+ proc.on('error', (err) => {
244
+ this.running = false;
245
+ this.emit('error', err);
246
+ });
247
+ }
248
+
249
+ _trackFailsafe(keycode, isDown) {
250
+ // macOS keycodes: Esc = 53, Space = 49
251
+ // Windows virtual keycodes: Esc = 27, Space = 32
252
+ // We handle both sets
253
+ const isEsc = keycode === 53 || keycode === 27 || keycode === 1;
254
+ const isSpace = keycode === 49 || keycode === 32 || keycode === 57;
255
+
256
+ if (isDown) {
257
+ if (isEsc) this.escPressed = true;
258
+ if (isSpace) this.spacePressed = true;
259
+ } else {
260
+ if (isEsc) this.escPressed = false;
261
+ if (isSpace) this.spacePressed = false;
262
+ }
263
+
264
+ if (this.escPressed && this.spacePressed) {
265
+ if (!this.failsafeStart) {
266
+ this.failsafeStart = Date.now();
267
+ this.emit('failsafe-progress', 0);
268
+ this.failsafeTimer = setInterval(() => {
269
+ if (!this.escPressed || !this.spacePressed) {
270
+ this.failsafeStart = null;
271
+ clearInterval(this.failsafeTimer);
272
+ this.failsafeTimer = null;
273
+ this.emit('failsafe-cancel');
274
+ return;
275
+ }
276
+ const elapsed = Date.now() - this.failsafeStart;
277
+ const progress = Math.min(1, elapsed / this.failsafeTimeout);
278
+ this.emit('failsafe-progress', progress);
279
+ if (progress >= 1) {
280
+ this.emit('failsafe');
281
+ clearInterval(this.failsafeTimer);
282
+ this.failsafeTimer = null;
283
+ }
284
+ }, 100);
285
+ }
286
+ } else {
287
+ if (this.failsafeStart) {
288
+ this.failsafeStart = null;
289
+ if (this.failsafeTimer) {
290
+ clearInterval(this.failsafeTimer);
291
+ this.failsafeTimer = null;
292
+ }
293
+ this.emit('failsafe-cancel');
294
+ }
295
+ }
296
+ }
297
+
298
+ _cleanupLinux() {
299
+ // Re-enable any disabled keyboard devices
300
+ try {
301
+ execSync('python3 -c "import subprocess; subprocess.run([\'xinput\', \'--list\', \'--id-only\'])"', { stdio: 'pipe' });
302
+ } catch {
303
+ // Best effort cleanup
304
+ }
305
+ }
306
+ }
@@ -0,0 +1,33 @@
1
+ import { execSync } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ const rootDir = join(__dirname, '../../');
9
+
10
+ if (process.platform === 'darwin') {
11
+ const venvPath = join(rootDir, '.venv');
12
+ if (!existsSync(venvPath)) {
13
+ console.log('[KeyVoid] 🍏 Setting up isolated Python environment for macOS (pynput)...');
14
+ try {
15
+ // Create virtual environment
16
+ execSync('python3 -m venv .venv', { cwd: rootDir, stdio: 'inherit' });
17
+
18
+ // Install pynput inside venv
19
+ // Using bash to source the activate script
20
+ execSync('source .venv/bin/activate && pip install pynput', {
21
+ cwd: rootDir,
22
+ stdio: 'inherit',
23
+ shell: '/bin/bash'
24
+ });
25
+
26
+ console.log('✅ macOS Python dependencies installed (pynput). Media keys will be suppressed.');
27
+ } catch (err) {
28
+ console.error('⚠️ Failed to install pynput via venv. KeyVoid will gracefully fall back to the Swift CGEventTap helper (some media keys may not be suppressed).');
29
+ }
30
+ } else {
31
+ console.log('[KeyVoid] 🍏 macOS Python environment (.venv) already exists.');
32
+ }
33
+ }