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.
- package/CHANGELOG.md +15 -0
- package/CONTRIBUTING.md +42 -0
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/bin/keyvoid.js +121 -0
- package/package.json +77 -0
- package/src/app.js +294 -0
- package/src/engine/permissions.js +107 -0
- package/src/engine/suppressor/helpers/linux-helper.py +192 -0
- package/src/engine/suppressor/helpers/macos-helper.swift +108 -0
- package/src/engine/suppressor/helpers/macos-pynput-helper.py +179 -0
- package/src/engine/suppressor/helpers/windows-helper.ps1 +144 -0
- package/src/engine/suppressor/index.js +306 -0
- package/src/scripts/postinstall.js +33 -0
- package/src/ui/components/counter.js +64 -0
- package/src/ui/components/header.js +102 -0
- package/src/ui/components/status-bar.js +71 -0
- package/src/ui/components/unvoid-button.js +78 -0
- package/src/ui/mouse.js +97 -0
- package/src/ui/renderer.js +113 -0
- package/src/ui/skins/arcade.js +530 -0
- package/src/ui/skins/cat.js +223 -0
- package/src/ui/skins/clean.js +155 -0
- package/src/ui/skins/hacker.js +194 -0
- package/src/ui/skins/prank.js +195 -0
- package/src/ui/skins/toddler.js +131 -0
- package/src/ui/skins/zen.js +169 -0
- package/src/ui/unlock-sequence.js +105 -0
- package/src/utils/big-digits.js +130 -0
- package/src/utils/colors.js +114 -0
- package/src/utils/terminal.js +119 -0
|
@@ -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
|
+
}
|