@kehto/services 0.21.1 → 0.21.2
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/README.md +42 -6
- package/dist/index.d.ts +6 -13
- package/dist/index.js +39 -31
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -251,6 +251,8 @@ export interface HostKeysBridge {
|
|
|
251
251
|
* - invoke `callback` exactly once per matching chord event (implementations
|
|
252
252
|
* are responsible for any OS-autorepeat filtering)
|
|
253
253
|
* - invoke `callback` synchronously during the event delivery
|
|
254
|
+
* - remove only this subscription when its unsubscribe handle is called;
|
|
255
|
+
* other callbacks for the same chord must remain live
|
|
254
256
|
* - accept the string chord format documented by @napplet/nap/keys
|
|
255
257
|
* (e.g. `'Ctrl+Shift+K'`, `'Cmd+P'`)
|
|
256
258
|
*/
|
|
@@ -300,16 +302,33 @@ keys.destroy();
|
|
|
300
302
|
Custom bridge path — swap in Electron's `globalShortcut`:
|
|
301
303
|
|
|
302
304
|
```ts
|
|
303
|
-
import { createKeysService, type HostKeysBridge } from '@kehto/services';
|
|
305
|
+
import { createKeysService, type HostKeysBridge, type HostKeyEvent } from '@kehto/services';
|
|
304
306
|
import { globalShortcut } from 'electron';
|
|
305
307
|
|
|
308
|
+
const subscribers = new Map<string, Set<(event: HostKeyEvent) => void>>();
|
|
306
309
|
const electronBridge: HostKeysBridge = {
|
|
307
310
|
subscribe(chord, cb) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
let callbacks = subscribers.get(chord);
|
|
312
|
+
if (!callbacks) {
|
|
313
|
+
const group = new Set<(event: HostKeyEvent) => void>();
|
|
314
|
+
const registered = globalShortcut.register(chord, () => {
|
|
315
|
+
for (const callback of group) callback({
|
|
316
|
+
key: '', code: '',
|
|
317
|
+
ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
if (!registered) throw new Error(`Cannot register ${chord}`);
|
|
321
|
+
callbacks = group;
|
|
322
|
+
subscribers.set(chord, callbacks);
|
|
323
|
+
}
|
|
324
|
+
callbacks.add(cb);
|
|
325
|
+
return () => {
|
|
326
|
+
if (!callbacks.delete(cb)) return;
|
|
327
|
+
if (callbacks.size === 0) {
|
|
328
|
+
globalShortcut.unregister(chord);
|
|
329
|
+
subscribers.delete(chord);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
313
332
|
},
|
|
314
333
|
};
|
|
315
334
|
|
|
@@ -322,6 +341,23 @@ Plug a `HostKeysBridge` when the default document listener is insufficient: Elec
|
|
|
322
341
|
|
|
323
342
|
A napplet drives this end to end via `@napplet/sdk` — `keys.registerAction` to claim a chord and `keys.onAction` to receive dispatches against the real backend. After a successful bound registration, the service pushes a complete `keys.bindings` list for that napplet/window, using entries shaped as `{ actionId, key }`. It pushes the complete list again after `keys.unregisterAction` removes a binding, including an empty list when no bindings remain. Injected shims use that list to suppress locally-bound keydowns before forwarding.
|
|
324
343
|
|
|
344
|
+
Action IDs are local to the trusted window passed by the runtime. Two windows may
|
|
345
|
+
each register `game.a`, including with the same suggested key, without replacing
|
|
346
|
+
one another's bindings. Unregistering or destroying one window removes only its
|
|
347
|
+
actions and bridge subscriptions. Binding lists and `keys.action` messages retain
|
|
348
|
+
the original app-local action ID; callers do not need to invent window-specific IDs.
|
|
349
|
+
Re-registering an action in the same window retains the existing rebind behavior:
|
|
350
|
+
a valid available chord replaces that window's binding, a reserved chord removes
|
|
351
|
+
its previous binding, and a malformed chord or failed bridge subscription leaves
|
|
352
|
+
the previous binding intact.
|
|
353
|
+
|
|
354
|
+
This ownership scope is an explicit Kehto policy for the scope left implicit by
|
|
355
|
+
[NAP-KEYS PR #9 at `cecb642`](https://github.com/napplet/naps/blob/cecb64257e0ac29926bb746832a477c553ab307c/naps/NAP-KEYS.md).
|
|
356
|
+
The draft describes duplicate-action errors but does not mandate service-global
|
|
357
|
+
IDs or forbid same-owner updates. Window-scoped ownership preserves its per-napplet
|
|
358
|
+
binding delivery and adds no wire fields. Chord conflicts retain the existing
|
|
359
|
+
behavior: matching registered actions receive their own `keys.action` push.
|
|
360
|
+
|
|
325
361
|
### Reserved Chords
|
|
326
362
|
|
|
327
363
|
Shell-reserved chords let a host application (window manager, launcher shell, tiling WM) claim specific chords for its own dispatch regardless of what napplets subscribe to. Declare the reserved set once at service construction via the `reservedChords` option on [`KeysServiceOptions`](#keysserviceoptions):
|
package/dist/index.d.ts
CHANGED
|
@@ -507,7 +507,7 @@ declare function createCoordinatedRelay(options: CoordinatedRelayOptions): Servi
|
|
|
507
507
|
* - keys.forward -> invokes options.onForward (hotkey passthrough, fire-and-forget)
|
|
508
508
|
* - keys.registerAction -> parses action.defaultKey into a normalized chord binding,
|
|
509
509
|
* stores bound subscriptions in an in-memory registry keyed
|
|
510
|
-
* by
|
|
510
|
+
* by windowId + actionId so onWindowDestroyed
|
|
511
511
|
* can auto-unsubscribe, echoes { actionId, binding } as .result
|
|
512
512
|
* when a binding is assigned, and pushes the window's complete
|
|
513
513
|
* keys.bindings list
|
|
@@ -577,17 +577,8 @@ interface HostKeyEvent {
|
|
|
577
577
|
*
|
|
578
578
|
* @example
|
|
579
579
|
* ```ts
|
|
580
|
-
*
|
|
581
|
-
* const
|
|
582
|
-
* subscribe(chord, cb) {
|
|
583
|
-
* const handle = globalShortcut.register(chord, () => cb({ key: '', code: '', ctrlKey: false, altKey: false, shiftKey: false, metaKey: false }));
|
|
584
|
-
* return () => globalShortcut.unregister(chord);
|
|
585
|
-
* },
|
|
586
|
-
* registerGlobalHotkey: (chord) => globalShortcut.register(chord, () => {}),
|
|
587
|
-
* onGlobalHotkey: (cb) => globalHotkeyBridge.on('global-hotkey', (_, chord) => cb(chord)),
|
|
588
|
-
* };
|
|
589
|
-
*
|
|
590
|
-
* const keys = createKeysService({ hostBridge: electronBridge });
|
|
580
|
+
* declare const hostBridge: HostKeysBridge;
|
|
581
|
+
* const keys = createKeysService({ hostBridge });
|
|
591
582
|
* runtime.registerService('keys', keys);
|
|
592
583
|
* ```
|
|
593
584
|
*/
|
|
@@ -599,6 +590,8 @@ interface HostKeysBridge {
|
|
|
599
590
|
* - invoke `callback` exactly once per matching chord event (implementations
|
|
600
591
|
* are responsible for any OS-autorepeat filtering)
|
|
601
592
|
* - invoke `callback` synchronously during the event delivery
|
|
593
|
+
* - remove only this subscription when its unsubscribe handle is called;
|
|
594
|
+
* other callbacks for the same chord must remain live
|
|
602
595
|
* - accept the string chord format documented by @napplet/nap/keys
|
|
603
596
|
* (e.g. `'Ctrl+Shift+K'`, `'Cmd+P'`)
|
|
604
597
|
*/
|
|
@@ -665,7 +658,7 @@ interface KeysServiceOptions {
|
|
|
665
658
|
/**
|
|
666
659
|
* Optional pluggable backend for chord subscription. When provided, the
|
|
667
660
|
* service delegates `keys.registerAction` → `bridge.subscribe(chord, cb)`
|
|
668
|
-
* and stores the returned unsubscribe handle keyed on
|
|
661
|
+
* and stores the returned unsubscribe handle keyed on windowId + actionId. The
|
|
669
662
|
* default document-listener path is NOT attached when `hostBridge` is
|
|
670
663
|
* provided — the bridge is authoritative. See {@link HostKeysBridge}.
|
|
671
664
|
*/
|
package/dist/index.js
CHANGED
|
@@ -727,6 +727,9 @@ function createCoordinatedRelay(options) {
|
|
|
727
727
|
}
|
|
728
728
|
|
|
729
729
|
// src/keys-service-internals.ts
|
|
730
|
+
function actionRegistryKey(windowId, actionId) {
|
|
731
|
+
return JSON.stringify([windowId, actionId]);
|
|
732
|
+
}
|
|
730
733
|
var MODIFIER_ALIASES = {
|
|
731
734
|
ctrl: "ctrl",
|
|
732
735
|
control: "ctrl",
|
|
@@ -795,7 +798,7 @@ function bindingsForWindow(windowId, registry, windowIndex) {
|
|
|
795
798
|
if (!actionIds) return [];
|
|
796
799
|
const bindings = [];
|
|
797
800
|
for (const actionId of actionIds) {
|
|
798
|
-
const entry = registry.get(actionId);
|
|
801
|
+
const entry = registry.get(actionRegistryKey(windowId, actionId));
|
|
799
802
|
if (!entry || entry.windowId !== windowId) continue;
|
|
800
803
|
bindings.push({ actionId, key: entry.chordString });
|
|
801
804
|
}
|
|
@@ -808,10 +811,9 @@ function pushBindings(windowId, registry, windowIndex, send) {
|
|
|
808
811
|
};
|
|
809
812
|
send(payload);
|
|
810
813
|
}
|
|
811
|
-
function removeActionFromWindowIndex(actionId, windowIndex) {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}
|
|
814
|
+
function removeActionFromWindowIndex(windowId, actionId, windowIndex) {
|
|
815
|
+
const actions = windowIndex.get(windowId);
|
|
816
|
+
if (actions?.delete(actionId) && actions.size === 0) windowIndex.delete(windowId);
|
|
815
817
|
}
|
|
816
818
|
|
|
817
819
|
// src/keys-service.ts
|
|
@@ -821,7 +823,7 @@ function rememberActionForWindow(windowIndex, windowId, actionId) {
|
|
|
821
823
|
windowIndex.get(windowId).add(actionId);
|
|
822
824
|
}
|
|
823
825
|
function bindActionEntry(registry, windowIndex, actionId, windowId, chord, chordString) {
|
|
824
|
-
registry.set(actionId, { chord, chordString, windowId });
|
|
826
|
+
registry.set(actionRegistryKey(windowId, actionId), { actionId, chord, chordString, windowId });
|
|
825
827
|
rememberActionForWindow(windowIndex, windowId, actionId);
|
|
826
828
|
}
|
|
827
829
|
function bindActionAndMarkChanged(registry, windowIndex, changedWindowIds, actionId, windowId, chord, chordString) {
|
|
@@ -894,6 +896,7 @@ function createKeysService(options = {}) {
|
|
|
894
896
|
const bridgeSendHandles = /* @__PURE__ */ new Map();
|
|
895
897
|
const unsubscribeHandles = /* @__PURE__ */ new Map();
|
|
896
898
|
const handleBridgeRegisterAction = (windowId, m, send) => {
|
|
899
|
+
const registryKey = actionRegistryKey(windowId, m.action.id);
|
|
897
900
|
bridgeSendHandles.set(windowId, send);
|
|
898
901
|
let binding;
|
|
899
902
|
const changedWindowIds = /* @__PURE__ */ new Set();
|
|
@@ -901,8 +904,8 @@ function createKeysService(options = {}) {
|
|
|
901
904
|
try {
|
|
902
905
|
const chord = parseChord(m.action.defaultKey);
|
|
903
906
|
const normalizedChord = formatChord(chord);
|
|
904
|
-
const existingEntry = bridgeActionRegistry.get(
|
|
905
|
-
const existing = unsubscribeHandles.get(
|
|
907
|
+
const existingEntry = bridgeActionRegistry.get(registryKey);
|
|
908
|
+
const existing = unsubscribeHandles.get(registryKey);
|
|
906
909
|
let nextUnsubscribe;
|
|
907
910
|
if (!isUnavailableBinding(chord)) {
|
|
908
911
|
nextUnsubscribe = bridge.subscribe(normalizedChord, (ev) => {
|
|
@@ -928,13 +931,13 @@ function createKeysService(options = {}) {
|
|
|
928
931
|
existing();
|
|
929
932
|
} catch {
|
|
930
933
|
}
|
|
931
|
-
unsubscribeHandles.delete(
|
|
934
|
+
unsubscribeHandles.delete(registryKey);
|
|
932
935
|
}
|
|
933
936
|
if (existingEntry) changedWindowIds.add(existingEntry.windowId);
|
|
934
|
-
removeActionFromWindowIndex(m.action.id, bridgeWindowActions);
|
|
935
|
-
bridgeActionRegistry.delete(
|
|
937
|
+
removeActionFromWindowIndex(windowId, m.action.id, bridgeWindowActions);
|
|
938
|
+
bridgeActionRegistry.delete(registryKey);
|
|
936
939
|
if (nextUnsubscribe) {
|
|
937
|
-
unsubscribeHandles.set(
|
|
940
|
+
unsubscribeHandles.set(registryKey, nextUnsubscribe);
|
|
938
941
|
binding = bindActionAndMarkChanged(
|
|
939
942
|
bridgeActionRegistry,
|
|
940
943
|
bridgeWindowActions,
|
|
@@ -964,18 +967,19 @@ function createKeysService(options = {}) {
|
|
|
964
967
|
send
|
|
965
968
|
);
|
|
966
969
|
};
|
|
967
|
-
const handleBridgeUnregisterAction = (
|
|
970
|
+
const handleBridgeUnregisterAction = (windowId, m, send) => {
|
|
968
971
|
if (!m.actionId) return;
|
|
969
|
-
const
|
|
970
|
-
const
|
|
972
|
+
const registryKey = actionRegistryKey(windowId, m.actionId);
|
|
973
|
+
const unsubscribe = unsubscribeHandles.get(registryKey);
|
|
974
|
+
const entry = bridgeActionRegistry.get(registryKey);
|
|
971
975
|
if (!unsubscribe) return;
|
|
972
976
|
try {
|
|
973
977
|
unsubscribe();
|
|
974
978
|
} catch {
|
|
975
979
|
}
|
|
976
|
-
unsubscribeHandles.delete(
|
|
977
|
-
bridgeActionRegistry.delete(
|
|
978
|
-
removeActionFromWindowIndex(m.actionId, bridgeWindowActions);
|
|
980
|
+
unsubscribeHandles.delete(registryKey);
|
|
981
|
+
bridgeActionRegistry.delete(registryKey);
|
|
982
|
+
removeActionFromWindowIndex(windowId, m.actionId, bridgeWindowActions);
|
|
979
983
|
if (entry) {
|
|
980
984
|
const ownerSend = bridgeSendHandles.get(entry.windowId) ?? send;
|
|
981
985
|
pushBindings(entry.windowId, bridgeActionRegistry, bridgeWindowActions, ownerSend);
|
|
@@ -995,15 +999,16 @@ function createKeysService(options = {}) {
|
|
|
995
999
|
const actions = bridgeWindowActions.get(windowId);
|
|
996
1000
|
if (!actions) return;
|
|
997
1001
|
for (const actionId of actions) {
|
|
998
|
-
const
|
|
1002
|
+
const registryKey = actionRegistryKey(windowId, actionId);
|
|
1003
|
+
const unsubscribe = unsubscribeHandles.get(registryKey);
|
|
999
1004
|
if (unsubscribe) {
|
|
1000
1005
|
try {
|
|
1001
1006
|
unsubscribe();
|
|
1002
1007
|
} catch {
|
|
1003
1008
|
}
|
|
1004
1009
|
}
|
|
1005
|
-
unsubscribeHandles.delete(
|
|
1006
|
-
bridgeActionRegistry.delete(
|
|
1010
|
+
unsubscribeHandles.delete(registryKey);
|
|
1011
|
+
bridgeActionRegistry.delete(registryKey);
|
|
1007
1012
|
}
|
|
1008
1013
|
bridgeWindowActions.delete(windowId);
|
|
1009
1014
|
bridgeSendHandles.delete(windowId);
|
|
@@ -1056,13 +1061,13 @@ function createKeysService(options = {}) {
|
|
|
1056
1061
|
});
|
|
1057
1062
|
}
|
|
1058
1063
|
if (isReserved) return;
|
|
1059
|
-
for (const
|
|
1064
|
+
for (const entry of actionRegistry.values()) {
|
|
1060
1065
|
if (chordMatches(entry.chord, ev)) {
|
|
1061
1066
|
const send = sendHandles.get(entry.windowId);
|
|
1062
1067
|
if (send) {
|
|
1063
1068
|
const payload = {
|
|
1064
1069
|
type: "keys.action",
|
|
1065
|
-
actionId
|
|
1070
|
+
actionId: entry.actionId
|
|
1066
1071
|
};
|
|
1067
1072
|
send(payload);
|
|
1068
1073
|
}
|
|
@@ -1071,6 +1076,7 @@ function createKeysService(options = {}) {
|
|
|
1071
1076
|
};
|
|
1072
1077
|
target.addEventListener("keydown", listener);
|
|
1073
1078
|
const handleDocumentRegisterAction = (windowId, m, send) => {
|
|
1079
|
+
const registryKey = actionRegistryKey(windowId, m.action.id);
|
|
1074
1080
|
sendHandles.set(windowId, send);
|
|
1075
1081
|
let binding;
|
|
1076
1082
|
const changedWindowIds = /* @__PURE__ */ new Set();
|
|
@@ -1078,11 +1084,11 @@ function createKeysService(options = {}) {
|
|
|
1078
1084
|
try {
|
|
1079
1085
|
const chord = parseChord(m.action.defaultKey);
|
|
1080
1086
|
const normalizedChord = formatChord(chord);
|
|
1081
|
-
const existing = actionRegistry.get(
|
|
1087
|
+
const existing = actionRegistry.get(registryKey);
|
|
1082
1088
|
if (existing) {
|
|
1083
1089
|
changedWindowIds.add(existing.windowId);
|
|
1084
|
-
removeActionFromWindowIndex(m.action.id, windowActions);
|
|
1085
|
-
actionRegistry.delete(
|
|
1090
|
+
removeActionFromWindowIndex(windowId, m.action.id, windowActions);
|
|
1091
|
+
actionRegistry.delete(registryKey);
|
|
1086
1092
|
}
|
|
1087
1093
|
if (!isUnavailableBinding(chord)) {
|
|
1088
1094
|
binding = bindActionAndMarkChanged(actionRegistry, windowActions, changedWindowIds, m.action.id, windowId, chord, normalizedChord);
|
|
@@ -1100,10 +1106,12 @@ function createKeysService(options = {}) {
|
|
|
1100
1106
|
sendRegisterActionResult(send, m, binding);
|
|
1101
1107
|
pushChangedBindings(changedWindowIds, actionRegistry, windowActions, sendHandles, send);
|
|
1102
1108
|
};
|
|
1103
|
-
const handleDocumentUnregisterAction = (
|
|
1104
|
-
if (!m.actionId
|
|
1105
|
-
const
|
|
1106
|
-
actionRegistry.
|
|
1109
|
+
const handleDocumentUnregisterAction = (windowId, m, send) => {
|
|
1110
|
+
if (!m.actionId) return;
|
|
1111
|
+
const registryKey = actionRegistryKey(windowId, m.actionId);
|
|
1112
|
+
const entry = actionRegistry.get(registryKey);
|
|
1113
|
+
if (!entry) return;
|
|
1114
|
+
actionRegistry.delete(registryKey);
|
|
1107
1115
|
const set = windowActions.get(entry.windowId);
|
|
1108
1116
|
if (set) {
|
|
1109
1117
|
set.delete(m.actionId);
|
|
@@ -1123,7 +1131,7 @@ function createKeysService(options = {}) {
|
|
|
1123
1131
|
onWindowDestroyed(windowId) {
|
|
1124
1132
|
const actions = windowActions.get(windowId);
|
|
1125
1133
|
if (actions) {
|
|
1126
|
-
for (const actionId of actions) actionRegistry.delete(actionId);
|
|
1134
|
+
for (const actionId of actions) actionRegistry.delete(actionRegistryKey(windowId, actionId));
|
|
1127
1135
|
windowActions.delete(windowId);
|
|
1128
1136
|
}
|
|
1129
1137
|
sendHandles.delete(windowId);
|