qdesk 1.0.11 → 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 -76
- 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
- package/dist/process-utils.js +0 -18
|
@@ -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,77 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
// Configure this chord at the top of file.
|
|
13
|
-
const DEFAULT_ADD_CHORD = "win+ctrl+a";
|
|
14
|
-
const DEFAULT_DROP_CHORD = "win+ctrl+d";
|
|
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
|
-
if (hasCliFlag("--version") || hasCliFlag("-v")) {
|
|
35
|
-
console.log(getPackageVersion());
|
|
36
|
-
process.exit(0);
|
|
37
|
-
}
|
|
38
|
-
setTerminalTitle("qdesk");
|
|
39
|
-
function normalizeChordInput(chord) {
|
|
40
|
-
return normalizeChord(chord.trim().toLowerCase());
|
|
41
|
-
}
|
|
42
|
-
function getCliArgValue(flag) {
|
|
43
|
-
const args = process.argv.slice(2);
|
|
44
|
-
const exact = args.find((arg) => arg.startsWith(`${flag}=`));
|
|
45
|
-
if (exact) {
|
|
46
|
-
return exact.slice(flag.length + 1);
|
|
47
|
-
}
|
|
48
|
-
const idx = args.indexOf(flag);
|
|
49
|
-
if (idx >= 0 && idx + 1 < args.length) {
|
|
50
|
-
return args[idx + 1];
|
|
51
|
-
}
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
const ADD_CHORD = normalizeChordInput(getCliArgValue("-a") ?? DEFAULT_ADD_CHORD);
|
|
55
|
-
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();
|
|
56
11
|
if (ADD_CHORD === DROP_CHORD) {
|
|
57
|
-
|
|
12
|
+
logWarn(`add and drop chords are identical (${ADD_CHORD}); behavior may conflict.`);
|
|
58
13
|
}
|
|
59
|
-
const bindings = new Map();
|
|
60
14
|
let activeRecording;
|
|
61
15
|
let awaitingChordRemoval = false;
|
|
62
16
|
const toolWindowAtStartup = getActiveWindowHandleAndName() ?? undefined;
|
|
63
|
-
|
|
64
|
-
const
|
|
17
|
+
logInfo(`Tool window at startup: ${toolWindowAtStartup?.name ?? "(unknown)"}`);
|
|
18
|
+
const bindings = new KeyBindings();
|
|
19
|
+
startListening({
|
|
65
20
|
onEscape: (stopPropagating) => {
|
|
66
21
|
if (!activeRecording && !awaitingChordRemoval) {
|
|
67
22
|
return;
|
|
68
23
|
}
|
|
69
24
|
if (activeRecording) {
|
|
70
|
-
|
|
25
|
+
logInfo("Canceled recording mode via Escape.");
|
|
71
26
|
activeRecording = undefined;
|
|
72
27
|
}
|
|
73
28
|
if (awaitingChordRemoval) {
|
|
74
|
-
|
|
29
|
+
logInfo("Canceled removal mode via Escape.");
|
|
75
30
|
awaitingChordRemoval = false;
|
|
76
31
|
}
|
|
77
32
|
setScreenHue("off");
|
|
@@ -82,11 +37,10 @@ const listener = startListening({
|
|
|
82
37
|
// recording
|
|
83
38
|
if (activeRecording) {
|
|
84
39
|
if ([ADD_CHORD, DROP_CHORD].includes(chord)) {
|
|
85
|
-
|
|
40
|
+
logWarn(`Ignoring ${chord} chord during active recording to avoid conflicts.`);
|
|
86
41
|
return stopPropagating();
|
|
87
42
|
}
|
|
88
43
|
bindings.set(chord, activeRecording);
|
|
89
|
-
console.log(`[add] Bound ${chord} -> "${activeRecording.name}"`);
|
|
90
44
|
activeRecording = undefined;
|
|
91
45
|
setScreenHue("off");
|
|
92
46
|
printBindings();
|
|
@@ -96,12 +50,7 @@ const listener = startListening({
|
|
|
96
50
|
if (awaitingChordRemoval) {
|
|
97
51
|
awaitingChordRemoval = false;
|
|
98
52
|
setScreenHue("off");
|
|
99
|
-
|
|
100
|
-
console.log(`[drop] Removed binding for ${chord}`);
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
console.log(`[drop] No binding exists for ${chord}`);
|
|
104
|
-
}
|
|
53
|
+
bindings.delete(chord);
|
|
105
54
|
printBindings();
|
|
106
55
|
return stopPropagating();
|
|
107
56
|
}
|
|
@@ -111,10 +60,10 @@ const listener = startListening({
|
|
|
111
60
|
setScreenHue("off");
|
|
112
61
|
activeRecording = getActiveWindowHandleAndName();
|
|
113
62
|
if (!activeRecording) {
|
|
114
|
-
|
|
63
|
+
logWarn("No foreground window found to bind.");
|
|
115
64
|
}
|
|
116
65
|
else {
|
|
117
|
-
|
|
66
|
+
logInfo(`Armed for "${activeRecording.name}". Waiting for next chord... (Esc to cancel)`);
|
|
118
67
|
setScreenHue("record");
|
|
119
68
|
}
|
|
120
69
|
return stopPropagating();
|
|
@@ -129,7 +78,7 @@ const listener = startListening({
|
|
|
129
78
|
if (toolWindowAtStartup) {
|
|
130
79
|
activateWindowByHandle(toolWindowAtStartup.handle);
|
|
131
80
|
}
|
|
132
|
-
|
|
81
|
+
logInfo("Armed for removal. Press the chord you want to remove. (Esc to cancel)");
|
|
133
82
|
printBindings();
|
|
134
83
|
setScreenHue("remove");
|
|
135
84
|
return stopPropagating();
|
|
@@ -139,14 +88,26 @@ const listener = startListening({
|
|
|
139
88
|
if (binding) {
|
|
140
89
|
const result = activateWindowByHandle(binding.handle);
|
|
141
90
|
if (result === "missing") {
|
|
142
|
-
|
|
91
|
+
logSwitch({
|
|
92
|
+
chord,
|
|
93
|
+
name: binding.name,
|
|
94
|
+
type: "missing",
|
|
95
|
+
});
|
|
143
96
|
bindings.delete(chord);
|
|
144
97
|
}
|
|
145
98
|
else if (result === "activated") {
|
|
146
|
-
|
|
99
|
+
logSwitch({
|
|
100
|
+
chord,
|
|
101
|
+
name: binding.name,
|
|
102
|
+
type: "success",
|
|
103
|
+
});
|
|
147
104
|
}
|
|
148
105
|
else {
|
|
149
|
-
|
|
106
|
+
logSwitch({
|
|
107
|
+
chord,
|
|
108
|
+
name: binding.name,
|
|
109
|
+
type: "failed",
|
|
110
|
+
});
|
|
150
111
|
}
|
|
151
112
|
return stopPropagating();
|
|
152
113
|
}
|
|
@@ -158,7 +119,7 @@ function printBindings() {
|
|
|
158
119
|
console.log(" (none)");
|
|
159
120
|
}
|
|
160
121
|
else {
|
|
161
|
-
for (const [chord, window] of bindings) {
|
|
122
|
+
for (const [chord, window] of bindings.entries()) {
|
|
162
123
|
console.log(` ${chord} -> "${window.name}"`);
|
|
163
124
|
}
|
|
164
125
|
}
|
|
@@ -168,4 +129,11 @@ console.log("[qdesk] Starting...");
|
|
|
168
129
|
console.log(` Adding a chord: ${ADD_CHORD}`);
|
|
169
130
|
console.log(` Dropping a chord: ${DROP_CHORD}`);
|
|
170
131
|
console.log(" CLI overrides: -a <combo>, -d <combo>");
|
|
171
|
-
|
|
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
|
-
}
|
package/dist/process-utils.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import os from "os";
|
|
3
|
-
import path from "path";
|
|
4
|
-
const LOCK_FILE = path.join(os.tmpdir(), "qdesk.lock");
|
|
5
|
-
export function isAlreadyRunning() {
|
|
6
|
-
if (fs.existsSync(LOCK_FILE)) {
|
|
7
|
-
const pid = parseInt(fs.readFileSync(LOCK_FILE, "utf8").trim());
|
|
8
|
-
try {
|
|
9
|
-
process.kill(pid, 0);
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
// stale lock, fall through
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
fs.writeFileSync(LOCK_FILE, String(process.pid));
|
|
17
|
-
return false;
|
|
18
|
-
}
|