qdesk 1.0.10 → 1.0.12
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/dist/bank/key-bindings.js +64 -0
- package/dist/index.js +44 -71
- package/dist/koffi/chord-listener.js +94 -0
- package/dist/koffi/chord-utils.js +148 -0
- package/dist/{koffi-utils.js → koffi/koffi-utils.js} +12 -2
- package/dist/{screen-hue.js → koffi/screen-hue.js} +1 -10
- package/dist/{window-utils.js → koffi/window-utils.js} +17 -7
- package/dist/log/log.js +13 -0
- package/dist/process/cli.js +45 -0
- package/dist/process/get-process-creation-time.js +45 -0
- package/dist/process/process-deduplication.js +26 -0
- package/package.json +1 -1
- package/dist/chord-utils.js +0 -244
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
const STORAGE_FILE = path.join(os.tmpdir(), "qdesk.bindings");
|
|
5
|
+
export class KeyBindings {
|
|
6
|
+
bindings = new Map();
|
|
7
|
+
constructor() {
|
|
8
|
+
const loaded = load();
|
|
9
|
+
for (const { chord, info } of loaded) {
|
|
10
|
+
this.bindings.set(chord, info);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
get(chord) {
|
|
14
|
+
return this.bindings.get(chord);
|
|
15
|
+
}
|
|
16
|
+
set(chord, info) {
|
|
17
|
+
this.bindings.set(chord, info);
|
|
18
|
+
console.log(`[add] Bound ${chord} -> "${info.name}"`);
|
|
19
|
+
save(this.bindings);
|
|
20
|
+
}
|
|
21
|
+
delete(chord) {
|
|
22
|
+
const output = this.bindings.delete(chord);
|
|
23
|
+
if (output) {
|
|
24
|
+
console.log(`[remove] Unbound ${chord}`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.log(`[remove] No binding exists for ${chord}`);
|
|
28
|
+
}
|
|
29
|
+
save(this.bindings);
|
|
30
|
+
return output;
|
|
31
|
+
}
|
|
32
|
+
get size() {
|
|
33
|
+
return this.bindings.size;
|
|
34
|
+
}
|
|
35
|
+
entries() {
|
|
36
|
+
return this.bindings.entries();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function load() {
|
|
40
|
+
try {
|
|
41
|
+
const raw = fs.readFileSync(STORAGE_FILE, "utf8");
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
return parsed.map(({ chord, info }) => ({
|
|
44
|
+
chord,
|
|
45
|
+
info: {
|
|
46
|
+
handle: BigInt(info.handle),
|
|
47
|
+
name: info.name,
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function save(bindings) {
|
|
56
|
+
const data = Array.from(bindings.entries()).map(([chord, info]) => ({
|
|
57
|
+
chord,
|
|
58
|
+
info: {
|
|
59
|
+
handle: info.handle.toString(),
|
|
60
|
+
name: info.name,
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
fs.writeFileSync(STORAGE_FILE, JSON.stringify(data), "utf8");
|
|
64
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,72 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return process.argv.slice(2).includes(flag);
|
|
12
|
-
}
|
|
13
|
-
function getPackageVersion() {
|
|
14
|
-
try {
|
|
15
|
-
const packageJson = readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
16
|
-
const parsed = JSON.parse(packageJson);
|
|
17
|
-
return parsed.version ?? "unknown";
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
return "unknown";
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
function setTerminalTitle(title) {
|
|
24
|
-
process.title = title;
|
|
25
|
-
if (process.stdout.isTTY) {
|
|
26
|
-
process.stdout.write(`\u001b]0;${title}\u0007`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (hasCliFlag("--version") || hasCliFlag("-v")) {
|
|
30
|
-
console.log(getPackageVersion());
|
|
31
|
-
process.exit(0);
|
|
32
|
-
}
|
|
33
|
-
setTerminalTitle("qdesk");
|
|
34
|
-
function normalizeChordInput(chord) {
|
|
35
|
-
return normalizeChord(chord.trim().toLowerCase());
|
|
36
|
-
}
|
|
37
|
-
function getCliArgValue(flag) {
|
|
38
|
-
const args = process.argv.slice(2);
|
|
39
|
-
const exact = args.find((arg) => arg.startsWith(`${flag}=`));
|
|
40
|
-
if (exact) {
|
|
41
|
-
return exact.slice(flag.length + 1);
|
|
42
|
-
}
|
|
43
|
-
const idx = args.indexOf(flag);
|
|
44
|
-
if (idx >= 0 && idx + 1 < args.length) {
|
|
45
|
-
return args[idx + 1];
|
|
46
|
-
}
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
const ADD_CHORD = normalizeChordInput(getCliArgValue("-a") ?? DEFAULT_ADD_CHORD);
|
|
50
|
-
const DROP_CHORD = normalizeChordInput(getCliArgValue("-d") ?? DEFAULT_DROP_CHORD);
|
|
2
|
+
import { startListening } from "./koffi/chord-listener.js";
|
|
3
|
+
import { setScreenHue } from "./koffi/screen-hue.js";
|
|
4
|
+
import { activateWindowByHandle, checkIfWindowExists, getActiveWindowHandleAndName, } from "./koffi/window-utils.js";
|
|
5
|
+
import { ensureSingleProcess } from "./process/process-deduplication.js";
|
|
6
|
+
import { getCliArgStuff } from "./process/cli.js";
|
|
7
|
+
import { KeyBindings } from "./bank/key-bindings.js";
|
|
8
|
+
import { logInfo, logSwitch, logWarn } from "./log/log.js";
|
|
9
|
+
ensureSingleProcess();
|
|
10
|
+
const { ADD_CHORD, DROP_CHORD } = getCliArgStuff();
|
|
51
11
|
if (ADD_CHORD === DROP_CHORD) {
|
|
52
|
-
|
|
12
|
+
logWarn(`add and drop chords are identical (${ADD_CHORD}); behavior may conflict.`);
|
|
53
13
|
}
|
|
54
|
-
const bindings = new Map();
|
|
55
14
|
let activeRecording;
|
|
56
15
|
let awaitingChordRemoval = false;
|
|
57
16
|
const toolWindowAtStartup = getActiveWindowHandleAndName() ?? undefined;
|
|
58
|
-
|
|
59
|
-
const
|
|
17
|
+
logInfo(`Tool window at startup: ${toolWindowAtStartup?.name ?? "(unknown)"}`);
|
|
18
|
+
const bindings = new KeyBindings();
|
|
19
|
+
startListening({
|
|
60
20
|
onEscape: (stopPropagating) => {
|
|
61
21
|
if (!activeRecording && !awaitingChordRemoval) {
|
|
62
22
|
return;
|
|
63
23
|
}
|
|
64
24
|
if (activeRecording) {
|
|
65
|
-
|
|
25
|
+
logInfo("Canceled recording mode via Escape.");
|
|
66
26
|
activeRecording = undefined;
|
|
67
27
|
}
|
|
68
28
|
if (awaitingChordRemoval) {
|
|
69
|
-
|
|
29
|
+
logInfo("Canceled removal mode via Escape.");
|
|
70
30
|
awaitingChordRemoval = false;
|
|
71
31
|
}
|
|
72
32
|
setScreenHue("off");
|
|
@@ -77,11 +37,10 @@ const listener = startListening({
|
|
|
77
37
|
// recording
|
|
78
38
|
if (activeRecording) {
|
|
79
39
|
if ([ADD_CHORD, DROP_CHORD].includes(chord)) {
|
|
80
|
-
|
|
40
|
+
logWarn(`Ignoring ${chord} chord during active recording to avoid conflicts.`);
|
|
81
41
|
return stopPropagating();
|
|
82
42
|
}
|
|
83
43
|
bindings.set(chord, activeRecording);
|
|
84
|
-
console.log(`[add] Bound ${chord} -> "${activeRecording.name}"`);
|
|
85
44
|
activeRecording = undefined;
|
|
86
45
|
setScreenHue("off");
|
|
87
46
|
printBindings();
|
|
@@ -91,12 +50,7 @@ const listener = startListening({
|
|
|
91
50
|
if (awaitingChordRemoval) {
|
|
92
51
|
awaitingChordRemoval = false;
|
|
93
52
|
setScreenHue("off");
|
|
94
|
-
|
|
95
|
-
console.log(`[drop] Removed binding for ${chord}`);
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
console.log(`[drop] No binding exists for ${chord}`);
|
|
99
|
-
}
|
|
53
|
+
bindings.delete(chord);
|
|
100
54
|
printBindings();
|
|
101
55
|
return stopPropagating();
|
|
102
56
|
}
|
|
@@ -106,10 +60,10 @@ const listener = startListening({
|
|
|
106
60
|
setScreenHue("off");
|
|
107
61
|
activeRecording = getActiveWindowHandleAndName();
|
|
108
62
|
if (!activeRecording) {
|
|
109
|
-
|
|
63
|
+
logWarn("No foreground window found to bind.");
|
|
110
64
|
}
|
|
111
65
|
else {
|
|
112
|
-
|
|
66
|
+
logInfo(`Armed for "${activeRecording.name}". Waiting for next chord... (Esc to cancel)`);
|
|
113
67
|
setScreenHue("record");
|
|
114
68
|
}
|
|
115
69
|
return stopPropagating();
|
|
@@ -124,7 +78,7 @@ const listener = startListening({
|
|
|
124
78
|
if (toolWindowAtStartup) {
|
|
125
79
|
activateWindowByHandle(toolWindowAtStartup.handle);
|
|
126
80
|
}
|
|
127
|
-
|
|
81
|
+
logInfo("Armed for removal. Press the chord you want to remove. (Esc to cancel)");
|
|
128
82
|
printBindings();
|
|
129
83
|
setScreenHue("remove");
|
|
130
84
|
return stopPropagating();
|
|
@@ -134,14 +88,26 @@ const listener = startListening({
|
|
|
134
88
|
if (binding) {
|
|
135
89
|
const result = activateWindowByHandle(binding.handle);
|
|
136
90
|
if (result === "missing") {
|
|
137
|
-
|
|
91
|
+
logSwitch({
|
|
92
|
+
chord,
|
|
93
|
+
name: binding.name,
|
|
94
|
+
type: "missing",
|
|
95
|
+
});
|
|
138
96
|
bindings.delete(chord);
|
|
139
97
|
}
|
|
140
98
|
else if (result === "activated") {
|
|
141
|
-
|
|
99
|
+
logSwitch({
|
|
100
|
+
chord,
|
|
101
|
+
name: binding.name,
|
|
102
|
+
type: "success",
|
|
103
|
+
});
|
|
142
104
|
}
|
|
143
105
|
else {
|
|
144
|
-
|
|
106
|
+
logSwitch({
|
|
107
|
+
chord,
|
|
108
|
+
name: binding.name,
|
|
109
|
+
type: "failed",
|
|
110
|
+
});
|
|
145
111
|
}
|
|
146
112
|
return stopPropagating();
|
|
147
113
|
}
|
|
@@ -153,7 +119,7 @@ function printBindings() {
|
|
|
153
119
|
console.log(" (none)");
|
|
154
120
|
}
|
|
155
121
|
else {
|
|
156
|
-
for (const [chord, window] of bindings) {
|
|
122
|
+
for (const [chord, window] of bindings.entries()) {
|
|
157
123
|
console.log(` ${chord} -> "${window.name}"`);
|
|
158
124
|
}
|
|
159
125
|
}
|
|
@@ -163,4 +129,11 @@ console.log("[qdesk] Starting...");
|
|
|
163
129
|
console.log(` Adding a chord: ${ADD_CHORD}`);
|
|
164
130
|
console.log(` Dropping a chord: ${DROP_CHORD}`);
|
|
165
131
|
console.log(" CLI overrides: -a <combo>, -d <combo>");
|
|
166
|
-
|
|
132
|
+
for (const [chord, window] of bindings.entries()) {
|
|
133
|
+
if (!checkIfWindowExists(window.handle)) {
|
|
134
|
+
bindings.delete(chord);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (bindings.size > 0) {
|
|
138
|
+
printBindings();
|
|
139
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { HC_ACTION, PM_REMOVE, VK_ESCAPE, WH_KEYBOARD_LL, WM_KEYDOWN, WM_SYSKEYDOWN, buildCurrentChord, normalizeChord, } from "./chord-utils.js";
|
|
2
|
+
import { registerKeyboardHookCallback, CallNextHookEx, decodeKeyboardHookLParam, GetModuleHandleW, SetWindowsHookExW, UnhookWindowsHookEx, unregisterCallback, PeekMessageW, TranslateMessage, DispatchMessageW, } from "./koffi-utils.js";
|
|
3
|
+
export function startListening(options) {
|
|
4
|
+
const activationNormalized = options.activationChord
|
|
5
|
+
? normalizeChord(options.activationChord)
|
|
6
|
+
: null;
|
|
7
|
+
if (options.activationChord && !activationNormalized) {
|
|
8
|
+
console.error(`[error] Invalid activation chord: ${options.activationChord}`);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
let keyboardHook = null;
|
|
12
|
+
let keyboardHookProcPtr = null;
|
|
13
|
+
let messagePump;
|
|
14
|
+
let isShuttingDown = false;
|
|
15
|
+
const callback = (nCode, wParam, lParam) => {
|
|
16
|
+
const passThrough = () => BigInt(Number(CallNextHookEx(keyboardHook, nCode, wParam, lParam)));
|
|
17
|
+
try {
|
|
18
|
+
if (nCode !== HC_ACTION) {
|
|
19
|
+
return passThrough();
|
|
20
|
+
}
|
|
21
|
+
const message = Number(wParam);
|
|
22
|
+
if (message !== WM_KEYDOWN && message !== WM_SYSKEYDOWN) {
|
|
23
|
+
return passThrough();
|
|
24
|
+
}
|
|
25
|
+
const kb = decodeKeyboardHookLParam(lParam);
|
|
26
|
+
const vkCode = Number(kb.vkCode);
|
|
27
|
+
let shouldStopPropagation = false;
|
|
28
|
+
if (vkCode === VK_ESCAPE) {
|
|
29
|
+
options.onEscape?.(() => {
|
|
30
|
+
shouldStopPropagation = true;
|
|
31
|
+
});
|
|
32
|
+
if (shouldStopPropagation) {
|
|
33
|
+
return 1n;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const combo = buildCurrentChord(vkCode);
|
|
37
|
+
if (!combo) {
|
|
38
|
+
return passThrough();
|
|
39
|
+
}
|
|
40
|
+
if (activationNormalized && combo === activationNormalized) {
|
|
41
|
+
options.onActivation?.(() => {
|
|
42
|
+
shouldStopPropagation = true;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
options.onChord(combo, () => {
|
|
46
|
+
shouldStopPropagation = true;
|
|
47
|
+
});
|
|
48
|
+
if (shouldStopPropagation) {
|
|
49
|
+
return 1n;
|
|
50
|
+
}
|
|
51
|
+
return passThrough();
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.error("[error] Keyboard hook callback failed:", error);
|
|
55
|
+
return passThrough();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
keyboardHookProcPtr = registerKeyboardHookCallback(callback);
|
|
59
|
+
const moduleHandle = GetModuleHandleW(null);
|
|
60
|
+
keyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, keyboardHookProcPtr, moduleHandle, 0);
|
|
61
|
+
if (!keyboardHook) {
|
|
62
|
+
console.error("[error] SetWindowsHookExW failed.");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
function shutdown() {
|
|
66
|
+
if (isShuttingDown) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
isShuttingDown = true;
|
|
70
|
+
console.log("\n[qdesk] Shutting down...");
|
|
71
|
+
if (messagePump) {
|
|
72
|
+
clearInterval(messagePump);
|
|
73
|
+
messagePump = undefined;
|
|
74
|
+
}
|
|
75
|
+
if (keyboardHook) {
|
|
76
|
+
UnhookWindowsHookEx(keyboardHook);
|
|
77
|
+
keyboardHook = null;
|
|
78
|
+
}
|
|
79
|
+
if (keyboardHookProcPtr) {
|
|
80
|
+
unregisterCallback(keyboardHookProcPtr);
|
|
81
|
+
keyboardHookProcPtr = null;
|
|
82
|
+
}
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
process.on("SIGINT", shutdown);
|
|
86
|
+
process.on("SIGTERM", shutdown);
|
|
87
|
+
const msg = {};
|
|
88
|
+
messagePump = setInterval(() => {
|
|
89
|
+
while (Number(PeekMessageW(msg, null, 0, 0, PM_REMOVE)) !== 0) {
|
|
90
|
+
TranslateMessage(msg);
|
|
91
|
+
DispatchMessageW(msg);
|
|
92
|
+
}
|
|
93
|
+
}, 8);
|
|
94
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { GetAsyncKeyState } from "./koffi-utils.js";
|
|
2
|
+
const MOD_ALT = 0x0001;
|
|
3
|
+
const MOD_CONTROL = 0x0002;
|
|
4
|
+
const MOD_SHIFT = 0x0004;
|
|
5
|
+
const MOD_WIN = 0x0008;
|
|
6
|
+
const VK_SHIFT = 0x10;
|
|
7
|
+
const VK_CONTROL = 0x11;
|
|
8
|
+
const VK_MENU = 0x12;
|
|
9
|
+
export const VK_ESCAPE = 0x1b;
|
|
10
|
+
const VK_LWIN = 0x5b;
|
|
11
|
+
const VK_RWIN = 0x5c;
|
|
12
|
+
export const HC_ACTION = 0;
|
|
13
|
+
export const WH_KEYBOARD_LL = 13;
|
|
14
|
+
export const WM_KEYDOWN = 0x0100;
|
|
15
|
+
export const WM_SYSKEYDOWN = 0x0104;
|
|
16
|
+
export const PM_REMOVE = 0x0001;
|
|
17
|
+
export function normalizeChord(chord) {
|
|
18
|
+
chord = chord.trim().toLowerCase();
|
|
19
|
+
const parts = chord
|
|
20
|
+
.split("+")
|
|
21
|
+
.map((part) => part.trim().toLowerCase())
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
if (parts.length < 2) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const key = parts[parts.length - 1];
|
|
27
|
+
if (!/^[0-9a-z]$/.test(key) && !/^f([1-9]|1[0-9]|2[0-4])$/.test(key)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const modifiers = new Set(parts.slice(0, -1));
|
|
31
|
+
for (const mod of modifiers) {
|
|
32
|
+
if (!["ctrl", "control", "alt", "shift", "win", "meta"].includes(mod)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const normalizedMods = [];
|
|
37
|
+
if (modifiers.has("ctrl") || modifiers.has("control")) {
|
|
38
|
+
normalizedMods.push("ctrl");
|
|
39
|
+
}
|
|
40
|
+
if (modifiers.has("alt")) {
|
|
41
|
+
normalizedMods.push("alt");
|
|
42
|
+
}
|
|
43
|
+
if (modifiers.has("shift")) {
|
|
44
|
+
normalizedMods.push("shift");
|
|
45
|
+
}
|
|
46
|
+
if (modifiers.has("win") || modifiers.has("meta")) {
|
|
47
|
+
normalizedMods.push("win");
|
|
48
|
+
}
|
|
49
|
+
if (normalizedMods.length === 0) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return `${normalizedMods.join("+")}+${key}`;
|
|
53
|
+
}
|
|
54
|
+
export function parseHotkeyCombo(combo) {
|
|
55
|
+
const parts = combo
|
|
56
|
+
.split("+")
|
|
57
|
+
.map((part) => part.trim().toLowerCase())
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
if (parts.length < 2) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const key = parts[parts.length - 1];
|
|
63
|
+
const modifiers = parts.slice(0, -1);
|
|
64
|
+
let modMask = 0;
|
|
65
|
+
const normalizedModifiers = [];
|
|
66
|
+
for (const mod of modifiers) {
|
|
67
|
+
if (mod === "alt") {
|
|
68
|
+
modMask |= MOD_ALT;
|
|
69
|
+
normalizedModifiers.push("alt");
|
|
70
|
+
}
|
|
71
|
+
else if (mod === "ctrl" || mod === "control") {
|
|
72
|
+
modMask |= MOD_CONTROL;
|
|
73
|
+
normalizedModifiers.push("ctrl");
|
|
74
|
+
}
|
|
75
|
+
else if (mod === "shift") {
|
|
76
|
+
modMask |= MOD_SHIFT;
|
|
77
|
+
normalizedModifiers.push("shift");
|
|
78
|
+
}
|
|
79
|
+
else if (mod === "win" || mod === "meta") {
|
|
80
|
+
modMask |= MOD_WIN;
|
|
81
|
+
normalizedModifiers.push("win");
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
let vk = -1;
|
|
88
|
+
if (/^[0-9]$/.test(key)) {
|
|
89
|
+
vk = key.charCodeAt(0);
|
|
90
|
+
}
|
|
91
|
+
else if (/^[a-z]$/.test(key)) {
|
|
92
|
+
vk = key.toUpperCase().charCodeAt(0);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const functionMatch = /^f([1-9]|1[0-9]|2[0-4])$/.exec(key);
|
|
96
|
+
if (functionMatch) {
|
|
97
|
+
const fn = Number(functionMatch[1]);
|
|
98
|
+
vk = 0x70 + (fn - 1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (vk < 0 || modMask === 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const canonicalModifiers = ["ctrl", "alt", "shift", "win"].filter((mod) => normalizedModifiers.includes(mod));
|
|
105
|
+
return {
|
|
106
|
+
modifiers: modMask,
|
|
107
|
+
vk,
|
|
108
|
+
normalized: `${canonicalModifiers.join("+")}+${key}`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function isPressed(vk) {
|
|
112
|
+
return (Number(GetAsyncKeyState(vk)) & 0x8000) !== 0;
|
|
113
|
+
}
|
|
114
|
+
function vkToKeyToken(vkCode) {
|
|
115
|
+
if (vkCode >= 0x30 && vkCode <= 0x39) {
|
|
116
|
+
return String.fromCharCode(vkCode);
|
|
117
|
+
}
|
|
118
|
+
if (vkCode >= 0x41 && vkCode <= 0x5a) {
|
|
119
|
+
return String.fromCharCode(vkCode).toLowerCase();
|
|
120
|
+
}
|
|
121
|
+
if (vkCode >= 0x70 && vkCode <= 0x87) {
|
|
122
|
+
return `f${vkCode - 0x6f}`;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
export function buildCurrentChord(vkCode) {
|
|
127
|
+
const key = vkToKeyToken(vkCode);
|
|
128
|
+
if (!key) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const parts = [];
|
|
132
|
+
if (isPressed(VK_CONTROL)) {
|
|
133
|
+
parts.push("ctrl");
|
|
134
|
+
}
|
|
135
|
+
if (isPressed(VK_MENU)) {
|
|
136
|
+
parts.push("alt");
|
|
137
|
+
}
|
|
138
|
+
if (isPressed(VK_SHIFT)) {
|
|
139
|
+
parts.push("shift");
|
|
140
|
+
}
|
|
141
|
+
if (isPressed(VK_LWIN) || isPressed(VK_RWIN)) {
|
|
142
|
+
parts.push("win");
|
|
143
|
+
}
|
|
144
|
+
if (parts.length === 0) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return `${parts.join("+")}+${key}`;
|
|
148
|
+
}
|
|
@@ -22,14 +22,24 @@ const KBDLLHOOKSTRUCT = koffi.struct("KBDLLHOOKSTRUCT", {
|
|
|
22
22
|
dwExtraInfo: "uint64",
|
|
23
23
|
});
|
|
24
24
|
const KeyboardHookProc = koffi.proto("int64 KeyboardHookProc(int nCode, uint64 wParam, void *lParam)");
|
|
25
|
-
const user32 = koffi.load("user32.dll");
|
|
26
|
-
const kernel32 = koffi.load("kernel32.dll");
|
|
25
|
+
export const user32 = koffi.load("user32.dll");
|
|
26
|
+
export const kernel32 = koffi.load("kernel32.dll");
|
|
27
|
+
export const gdi32 = koffi.load("gdi32.dll");
|
|
28
|
+
export const magnification = (() => {
|
|
29
|
+
try {
|
|
30
|
+
return koffi.load("Magnification.dll");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
27
36
|
export const GetForegroundWindow = user32.func("GetForegroundWindow", HWND, []);
|
|
28
37
|
export const SetForegroundWindow = user32.func("SetForegroundWindow", "int", [
|
|
29
38
|
HWND,
|
|
30
39
|
]);
|
|
31
40
|
export const IsWindow = user32.func("IsWindow", "int", [HWND]);
|
|
32
41
|
export const IsIconic = user32.func("IsIconic", "int", [HWND]);
|
|
42
|
+
export const IsZoomed = user32.func("IsZoomed", "int", [HWND]);
|
|
33
43
|
export const ShowWindow = user32.func("ShowWindow", "int", [HWND, "int"]);
|
|
34
44
|
export const BringWindowToTop = user32.func("BringWindowToTop", "int", [HWND]);
|
|
35
45
|
export const SetActiveWindow = user32.func("SetActiveWindow", HWND, [HWND]);
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import koffi from "koffi";
|
|
2
|
-
import { HWND } from "./koffi-utils.js";
|
|
2
|
+
import { gdi32, HWND, magnification, user32 } from "./koffi-utils.js";
|
|
3
3
|
const HDC = koffi.pointer("HDC", koffi.opaque());
|
|
4
|
-
const user32 = koffi.load("user32.dll");
|
|
5
|
-
const gdi32 = koffi.load("gdi32.dll");
|
|
6
|
-
let magnification = null;
|
|
7
|
-
try {
|
|
8
|
-
magnification = koffi.load("Magnification.dll");
|
|
9
|
-
}
|
|
10
|
-
catch {
|
|
11
|
-
magnification = null;
|
|
12
|
-
}
|
|
13
4
|
const GetDC = user32.func("GetDC", HDC, [HWND]);
|
|
14
5
|
const ReleaseDC = user32.func("ReleaseDC", "int", [HWND, HDC]);
|
|
15
6
|
const SetDeviceGammaRamp = gdi32.func("SetDeviceGammaRamp", "int", [
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
-
import { AttachThreadInput, BringWindowToTop, GetForegroundWindow, GetCurrentThreadId, GetWindowThreadProcessId, GetWindowTextW, IsIconic, IsWindow, SetActiveWindow, SetForegroundWindow, ShowWindow, } from "./koffi-utils.js";
|
|
2
|
+
import { AttachThreadInput, BringWindowToTop, GetForegroundWindow, GetCurrentThreadId, GetWindowThreadProcessId, GetWindowTextW, IsIconic, IsZoomed, IsWindow, SetActiveWindow, SetForegroundWindow, ShowWindow, } from "./koffi-utils.js";
|
|
3
3
|
const SW_RESTORE = 9;
|
|
4
4
|
const SW_SHOW = 5;
|
|
5
|
+
const SW_SHOWMAXIMIZED = 3;
|
|
5
6
|
function getWindowTitle(hwnd) {
|
|
6
7
|
const buf = Buffer.alloc(512);
|
|
7
8
|
const len = Number(GetWindowTextW(hwnd, buf, 256));
|
|
@@ -21,12 +22,18 @@ export function activateWindowByHandle(handle) {
|
|
|
21
22
|
if (!Number(IsWindow(handle))) {
|
|
22
23
|
return "missing";
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
const showTargetWindow = () => {
|
|
26
|
+
if (Number(IsIconic(handle))) {
|
|
27
|
+
ShowWindow(handle, SW_RESTORE);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (Number(IsZoomed(handle))) {
|
|
31
|
+
ShowWindow(handle, SW_SHOWMAXIMIZED);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
28
34
|
ShowWindow(handle, SW_SHOW);
|
|
29
|
-
}
|
|
35
|
+
};
|
|
36
|
+
showTargetWindow();
|
|
30
37
|
// Fast path for windows that can be foregrounded immediately.
|
|
31
38
|
if (Number(SetForegroundWindow(handle)) !== 0) {
|
|
32
39
|
return "activated";
|
|
@@ -51,7 +58,7 @@ export function activateWindowByHandle(handle) {
|
|
|
51
58
|
tryAttach(targetThreadId);
|
|
52
59
|
BringWindowToTop(handle);
|
|
53
60
|
SetActiveWindow(handle);
|
|
54
|
-
|
|
61
|
+
showTargetWindow();
|
|
55
62
|
if (Number(SetForegroundWindow(handle)) !== 0) {
|
|
56
63
|
return "activated";
|
|
57
64
|
}
|
|
@@ -63,3 +70,6 @@ export function activateWindowByHandle(handle) {
|
|
|
63
70
|
}
|
|
64
71
|
return "failed";
|
|
65
72
|
}
|
|
73
|
+
export function checkIfWindowExists(handle) {
|
|
74
|
+
return Boolean(IsWindow(handle));
|
|
75
|
+
}
|
package/dist/log/log.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function logSwitch({ chord, name, type, }) {
|
|
2
|
+
const status = type === "success" ? "✅" : type === "failed" ? "⚠️" : "❓";
|
|
3
|
+
console.log(`[switch ${chord}] ${status} "${name}"`);
|
|
4
|
+
}
|
|
5
|
+
export function logInfo(message) {
|
|
6
|
+
console.log(`[info] ${message}`);
|
|
7
|
+
}
|
|
8
|
+
export function logError(message) {
|
|
9
|
+
console.error(`[error] ${message}`);
|
|
10
|
+
}
|
|
11
|
+
export function logWarn(message) {
|
|
12
|
+
console.warn(`[warn] ${message}`);
|
|
13
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { normalizeChord } from "../koffi/chord-utils.js";
|
|
3
|
+
const DEFAULT_ADD_CHORD = "win+ctrl+a";
|
|
4
|
+
const DEFAULT_DROP_CHORD = "win+ctrl+d";
|
|
5
|
+
export function getCliArgStuff() {
|
|
6
|
+
if (hasCliFlag("--version") || hasCliFlag("-v")) {
|
|
7
|
+
console.log(getPackageVersion());
|
|
8
|
+
process.exit(0);
|
|
9
|
+
}
|
|
10
|
+
setTerminalTitle("qdesk");
|
|
11
|
+
const ADD_CHORD = normalizeChord(getCliArgValue("-a") ?? DEFAULT_ADD_CHORD);
|
|
12
|
+
const DROP_CHORD = normalizeChord(getCliArgValue("-d") ?? DEFAULT_DROP_CHORD);
|
|
13
|
+
return { ADD_CHORD, DROP_CHORD };
|
|
14
|
+
}
|
|
15
|
+
function hasCliFlag(flag) {
|
|
16
|
+
return process.argv.slice(2).includes(flag);
|
|
17
|
+
}
|
|
18
|
+
function getPackageVersion() {
|
|
19
|
+
try {
|
|
20
|
+
const packageJson = readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
21
|
+
const parsed = JSON.parse(packageJson);
|
|
22
|
+
return parsed.version ?? "unknown";
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return "unknown";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function setTerminalTitle(title) {
|
|
29
|
+
process.title = title;
|
|
30
|
+
if (process.stdout.isTTY) {
|
|
31
|
+
process.stdout.write(`\u001b]0;${title}\u0007`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function getCliArgValue(flag) {
|
|
35
|
+
const args = process.argv.slice(2);
|
|
36
|
+
const exact = args.find((arg) => arg.startsWith(`${flag}=`));
|
|
37
|
+
if (exact) {
|
|
38
|
+
return exact.slice(flag.length + 1);
|
|
39
|
+
}
|
|
40
|
+
const idx = args.indexOf(flag);
|
|
41
|
+
if (idx >= 0 && idx + 1 < args.length) {
|
|
42
|
+
return args[idx + 1];
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import koffi from "koffi";
|
|
2
|
+
import { kernel32 } from "../koffi/koffi-utils.js";
|
|
3
|
+
const HANDLE = koffi.pointer("HANDLE", koffi.opaque());
|
|
4
|
+
const OpenProcess = kernel32.func("OpenProcess", HANDLE, [
|
|
5
|
+
"uint32",
|
|
6
|
+
"int",
|
|
7
|
+
"uint32",
|
|
8
|
+
]);
|
|
9
|
+
const GetProcessTimes = kernel32.func("GetProcessTimes", "int", [
|
|
10
|
+
HANDLE,
|
|
11
|
+
"void *",
|
|
12
|
+
"void *",
|
|
13
|
+
"void *",
|
|
14
|
+
"void *",
|
|
15
|
+
]);
|
|
16
|
+
const CloseHandle = kernel32.func("CloseHandle", "int", [HANDLE]);
|
|
17
|
+
const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
|
|
18
|
+
function readFileTime(buffer) {
|
|
19
|
+
const low = BigInt(buffer.readUInt32LE(0));
|
|
20
|
+
const high = BigInt(buffer.readUInt32LE(4));
|
|
21
|
+
return ((high << 32n) | low).toString();
|
|
22
|
+
}
|
|
23
|
+
export function getProcessCreationTime(pid) {
|
|
24
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const processHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
|
|
28
|
+
if (!processHandle) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const creationTime = Buffer.alloc(8);
|
|
32
|
+
const exitTime = Buffer.alloc(8);
|
|
33
|
+
const kernelTime = Buffer.alloc(8);
|
|
34
|
+
const userTime = Buffer.alloc(8);
|
|
35
|
+
try {
|
|
36
|
+
const ok = Number(GetProcessTimes(processHandle, creationTime, exitTime, kernelTime, userTime));
|
|
37
|
+
if (!ok) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return readFileTime(creationTime);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
CloseHandle(processHandle);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { getProcessCreationTime } from "./get-process-creation-time.js";
|
|
5
|
+
const LOCK_FILE = path.join(os.tmpdir(), "qdesk.lock");
|
|
6
|
+
export function isAlreadyRunning() {
|
|
7
|
+
try {
|
|
8
|
+
const lockInfo = JSON.parse(fs.readFileSync(LOCK_FILE, "utf8").trim());
|
|
9
|
+
process.kill(lockInfo.pid, 0);
|
|
10
|
+
return getProcessCreationTime(lockInfo.pid) === lockInfo.creationTime;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// stale lock, fall through
|
|
14
|
+
}
|
|
15
|
+
fs.writeFileSync(LOCK_FILE, JSON.stringify({
|
|
16
|
+
pid: process.pid,
|
|
17
|
+
creationTime: getProcessCreationTime(process.pid),
|
|
18
|
+
}));
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
export function ensureSingleProcess() {
|
|
22
|
+
if (isAlreadyRunning()) {
|
|
23
|
+
console.error("Another instance is already running. Exiting.");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
package/package.json
CHANGED
package/dist/chord-utils.js
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { CallNextHookEx, decodeKeyboardHookLParam, DispatchMessageW, GetAsyncKeyState, GetModuleHandleW, PeekMessageW, registerKeyboardHookCallback, SetWindowsHookExW, TranslateMessage, UnhookWindowsHookEx, unregisterCallback, } from "./koffi-utils.js";
|
|
2
|
-
const process = globalThis.process;
|
|
3
|
-
const MOD_ALT = 0x0001;
|
|
4
|
-
const MOD_CONTROL = 0x0002;
|
|
5
|
-
const MOD_SHIFT = 0x0004;
|
|
6
|
-
const MOD_WIN = 0x0008;
|
|
7
|
-
const VK_SHIFT = 0x10;
|
|
8
|
-
const VK_CONTROL = 0x11;
|
|
9
|
-
const VK_MENU = 0x12;
|
|
10
|
-
const VK_ESCAPE = 0x1b;
|
|
11
|
-
const VK_LWIN = 0x5b;
|
|
12
|
-
const VK_RWIN = 0x5c;
|
|
13
|
-
const HC_ACTION = 0;
|
|
14
|
-
const WH_KEYBOARD_LL = 13;
|
|
15
|
-
const WM_KEYDOWN = 0x0100;
|
|
16
|
-
const WM_SYSKEYDOWN = 0x0104;
|
|
17
|
-
const PM_REMOVE = 0x0001;
|
|
18
|
-
export function normalizeChord(chord) {
|
|
19
|
-
const parts = chord
|
|
20
|
-
.split("+")
|
|
21
|
-
.map((part) => part.trim().toLowerCase())
|
|
22
|
-
.filter(Boolean);
|
|
23
|
-
if (parts.length < 2) {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
const key = parts[parts.length - 1];
|
|
27
|
-
if (!/^[0-9a-z]$/.test(key) && !/^f([1-9]|1[0-9]|2[0-4])$/.test(key)) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
const modifiers = new Set(parts.slice(0, -1));
|
|
31
|
-
for (const mod of modifiers) {
|
|
32
|
-
if (!["ctrl", "control", "alt", "shift", "win", "meta"].includes(mod)) {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
const normalizedMods = [];
|
|
37
|
-
if (modifiers.has("ctrl") || modifiers.has("control")) {
|
|
38
|
-
normalizedMods.push("ctrl");
|
|
39
|
-
}
|
|
40
|
-
if (modifiers.has("alt")) {
|
|
41
|
-
normalizedMods.push("alt");
|
|
42
|
-
}
|
|
43
|
-
if (modifiers.has("shift")) {
|
|
44
|
-
normalizedMods.push("shift");
|
|
45
|
-
}
|
|
46
|
-
if (modifiers.has("win") || modifiers.has("meta")) {
|
|
47
|
-
normalizedMods.push("win");
|
|
48
|
-
}
|
|
49
|
-
if (normalizedMods.length === 0) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
return `${normalizedMods.join("+")}+${key}`;
|
|
53
|
-
}
|
|
54
|
-
function parseHotkeyCombo(combo) {
|
|
55
|
-
const parts = combo
|
|
56
|
-
.split("+")
|
|
57
|
-
.map((part) => part.trim().toLowerCase())
|
|
58
|
-
.filter(Boolean);
|
|
59
|
-
if (parts.length < 2) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
const key = parts[parts.length - 1];
|
|
63
|
-
const modifiers = parts.slice(0, -1);
|
|
64
|
-
let modMask = 0;
|
|
65
|
-
const normalizedModifiers = [];
|
|
66
|
-
for (const mod of modifiers) {
|
|
67
|
-
if (mod === "alt") {
|
|
68
|
-
modMask |= MOD_ALT;
|
|
69
|
-
normalizedModifiers.push("alt");
|
|
70
|
-
}
|
|
71
|
-
else if (mod === "ctrl" || mod === "control") {
|
|
72
|
-
modMask |= MOD_CONTROL;
|
|
73
|
-
normalizedModifiers.push("ctrl");
|
|
74
|
-
}
|
|
75
|
-
else if (mod === "shift") {
|
|
76
|
-
modMask |= MOD_SHIFT;
|
|
77
|
-
normalizedModifiers.push("shift");
|
|
78
|
-
}
|
|
79
|
-
else if (mod === "win" || mod === "meta") {
|
|
80
|
-
modMask |= MOD_WIN;
|
|
81
|
-
normalizedModifiers.push("win");
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
let vk = -1;
|
|
88
|
-
if (/^[0-9]$/.test(key)) {
|
|
89
|
-
vk = key.charCodeAt(0);
|
|
90
|
-
}
|
|
91
|
-
else if (/^[a-z]$/.test(key)) {
|
|
92
|
-
vk = key.toUpperCase().charCodeAt(0);
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
const functionMatch = /^f([1-9]|1[0-9]|2[0-4])$/.exec(key);
|
|
96
|
-
if (functionMatch) {
|
|
97
|
-
const fn = Number(functionMatch[1]);
|
|
98
|
-
vk = 0x70 + (fn - 1);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (vk < 0 || modMask === 0) {
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
const canonicalModifiers = ["ctrl", "alt", "shift", "win"].filter((mod) => normalizedModifiers.includes(mod));
|
|
105
|
-
return {
|
|
106
|
-
modifiers: modMask,
|
|
107
|
-
vk,
|
|
108
|
-
normalized: `${canonicalModifiers.join("+")}+${key}`,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
function isPressed(vk) {
|
|
112
|
-
return (Number(GetAsyncKeyState(vk)) & 0x8000) !== 0;
|
|
113
|
-
}
|
|
114
|
-
function vkToKeyToken(vkCode) {
|
|
115
|
-
if (vkCode >= 0x30 && vkCode <= 0x39) {
|
|
116
|
-
return String.fromCharCode(vkCode);
|
|
117
|
-
}
|
|
118
|
-
if (vkCode >= 0x41 && vkCode <= 0x5a) {
|
|
119
|
-
return String.fromCharCode(vkCode).toLowerCase();
|
|
120
|
-
}
|
|
121
|
-
if (vkCode >= 0x70 && vkCode <= 0x87) {
|
|
122
|
-
return `f${vkCode - 0x6f}`;
|
|
123
|
-
}
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
function buildCurrentChord(vkCode) {
|
|
127
|
-
const key = vkToKeyToken(vkCode);
|
|
128
|
-
if (!key) {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
const parts = [];
|
|
132
|
-
if (isPressed(VK_CONTROL)) {
|
|
133
|
-
parts.push("ctrl");
|
|
134
|
-
}
|
|
135
|
-
if (isPressed(VK_MENU)) {
|
|
136
|
-
parts.push("alt");
|
|
137
|
-
}
|
|
138
|
-
if (isPressed(VK_SHIFT)) {
|
|
139
|
-
parts.push("shift");
|
|
140
|
-
}
|
|
141
|
-
if (isPressed(VK_LWIN) || isPressed(VK_RWIN)) {
|
|
142
|
-
parts.push("win");
|
|
143
|
-
}
|
|
144
|
-
if (parts.length === 0) {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
return `${parts.join("+")}+${key}`;
|
|
148
|
-
}
|
|
149
|
-
export function startListening(options) {
|
|
150
|
-
const activationNormalized = options.activationChord
|
|
151
|
-
? normalizeChord(options.activationChord)
|
|
152
|
-
: null;
|
|
153
|
-
if (options.activationChord && !activationNormalized) {
|
|
154
|
-
console.error(`[error] Invalid activation chord: ${options.activationChord}`);
|
|
155
|
-
process.exit(1);
|
|
156
|
-
}
|
|
157
|
-
let keyboardHook = null;
|
|
158
|
-
let keyboardHookProcPtr = null;
|
|
159
|
-
let messagePump;
|
|
160
|
-
let isShuttingDown = false;
|
|
161
|
-
const callback = (nCode, wParam, lParam) => {
|
|
162
|
-
const passThrough = () => BigInt(Number(CallNextHookEx(keyboardHook, nCode, wParam, lParam)));
|
|
163
|
-
try {
|
|
164
|
-
if (nCode !== HC_ACTION) {
|
|
165
|
-
return passThrough();
|
|
166
|
-
}
|
|
167
|
-
const message = Number(wParam);
|
|
168
|
-
if (message !== WM_KEYDOWN && message !== WM_SYSKEYDOWN) {
|
|
169
|
-
return passThrough();
|
|
170
|
-
}
|
|
171
|
-
const kb = decodeKeyboardHookLParam(lParam);
|
|
172
|
-
const vkCode = Number(kb.vkCode);
|
|
173
|
-
let shouldStopPropagation = false;
|
|
174
|
-
if (vkCode === VK_ESCAPE) {
|
|
175
|
-
options.onEscape?.(() => {
|
|
176
|
-
shouldStopPropagation = true;
|
|
177
|
-
});
|
|
178
|
-
if (shouldStopPropagation) {
|
|
179
|
-
return 1n;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
const combo = buildCurrentChord(vkCode);
|
|
183
|
-
if (!combo) {
|
|
184
|
-
return passThrough();
|
|
185
|
-
}
|
|
186
|
-
if (activationNormalized && combo === activationNormalized) {
|
|
187
|
-
options.onActivation?.(() => {
|
|
188
|
-
shouldStopPropagation = true;
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
options.onChord(combo, () => {
|
|
192
|
-
shouldStopPropagation = true;
|
|
193
|
-
});
|
|
194
|
-
if (shouldStopPropagation) {
|
|
195
|
-
return 1n;
|
|
196
|
-
}
|
|
197
|
-
return passThrough();
|
|
198
|
-
}
|
|
199
|
-
catch (error) {
|
|
200
|
-
console.error("[error] Keyboard hook callback failed:", error);
|
|
201
|
-
return passThrough();
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
keyboardHookProcPtr = registerKeyboardHookCallback(callback);
|
|
205
|
-
const moduleHandle = GetModuleHandleW(null);
|
|
206
|
-
keyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, keyboardHookProcPtr, moduleHandle, 0);
|
|
207
|
-
if (!keyboardHook) {
|
|
208
|
-
console.error("[error] SetWindowsHookExW failed.");
|
|
209
|
-
process.exit(1);
|
|
210
|
-
}
|
|
211
|
-
function shutdown() {
|
|
212
|
-
if (isShuttingDown) {
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
isShuttingDown = true;
|
|
216
|
-
console.log("\n[qdesk] Shutting down...");
|
|
217
|
-
if (messagePump) {
|
|
218
|
-
clearInterval(messagePump);
|
|
219
|
-
messagePump = undefined;
|
|
220
|
-
}
|
|
221
|
-
if (keyboardHook) {
|
|
222
|
-
UnhookWindowsHookEx(keyboardHook);
|
|
223
|
-
keyboardHook = null;
|
|
224
|
-
}
|
|
225
|
-
if (keyboardHookProcPtr) {
|
|
226
|
-
unregisterCallback(keyboardHookProcPtr);
|
|
227
|
-
keyboardHookProcPtr = null;
|
|
228
|
-
}
|
|
229
|
-
process.exit(0);
|
|
230
|
-
}
|
|
231
|
-
process.on("SIGINT", shutdown);
|
|
232
|
-
process.on("SIGTERM", shutdown);
|
|
233
|
-
return {
|
|
234
|
-
runMessageLoop: () => {
|
|
235
|
-
const msg = {};
|
|
236
|
-
messagePump = setInterval(() => {
|
|
237
|
-
while (Number(PeekMessageW(msg, null, 0, 0, PM_REMOVE)) !== 0) {
|
|
238
|
-
TranslateMessage(msg);
|
|
239
|
-
DispatchMessageW(msg);
|
|
240
|
-
}
|
|
241
|
-
}, 8);
|
|
242
|
-
},
|
|
243
|
-
};
|
|
244
|
-
}
|