@tanstack/hotkeys 0.10.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/adapter.d.ts +116 -0
- package/dist/adapter.js +134 -0
- package/dist/app-hotkeys.d.ts +239 -0
- package/dist/app-hotkeys.js +295 -0
- package/dist/conflicts.d.ts +5 -9
- package/dist/conflicts.js +6 -11
- package/dist/constants.d.ts +1 -1
- package/dist/format.d.ts +19 -24
- package/dist/format.js +19 -30
- package/dist/hint.d.ts +1 -0
- package/dist/hint.js +5 -2
- package/dist/hotkey-manager.d.ts +110 -59
- package/dist/hotkey-manager.js +254 -151
- package/dist/hotkey-recorder.d.ts +66 -41
- package/dist/hotkey-recorder.js +158 -109
- package/dist/hotkey.types.d.ts +26 -12
- package/dist/index.d.ts +10 -11
- package/dist/index.js +5 -6
- package/dist/match.d.ts +38 -5
- package/dist/match.js +72 -4
- package/dist/parse.d.ts +20 -8
- package/dist/parse.js +15 -5
- package/dist/recorder-options.d.ts +6 -3
- package/dist/validate.d.ts +10 -4
- package/dist/validate.js +26 -4
- package/package.json +3 -2
- package/dist/hotkey-sequence-recorder.d.ts +0 -83
- package/dist/hotkey-sequence-recorder.js +0 -254
- package/dist/sequence-manager.d.ts +0 -169
- package/dist/sequence-manager.js +0 -443
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { detectPlatform } from "./platform.js";
|
|
2
|
+
import { isHotkeySequence, simplifyHotkey } from "./parse.js";
|
|
3
|
+
import { validateHotkey } from "./validate.js";
|
|
4
|
+
import { areHotkeysEqual } from "./match.js";
|
|
5
|
+
import { getHotkeyManager } from "./hotkey-manager.js";
|
|
6
|
+
import { Store } from "@tanstack/store";
|
|
7
|
+
|
|
8
|
+
//#region src/app-hotkeys.ts
|
|
9
|
+
/**
|
|
10
|
+
* Declares every app hotkey up front, without registering any, so cheat
|
|
11
|
+
* sheets and settings screens can list hotkeys whose components have not
|
|
12
|
+
* mounted. Pass the result to `HotkeysProvider` as `appHotkeys`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* export const appHotkeys = defineAppHotkeys({
|
|
17
|
+
* 'doc.save': { hotkey: 'Mod+S', meta: { name: 'Save', group: 'Document' } },
|
|
18
|
+
* 'nav.home': { hotkey: ['G', 'H'], meta: { name: 'Go home' }, options: { timeout: 800 } },
|
|
19
|
+
* 'palette.open': { hotkey: null, meta: { name: 'Command palette' } },
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
function defineAppHotkeys(definitions) {
|
|
24
|
+
const result = {};
|
|
25
|
+
for (const [id, input] of Object.entries(definitions)) {
|
|
26
|
+
const hotkey = input.hotkey === null ? null : simplifyHotkey(input.hotkey);
|
|
27
|
+
result[id] = {
|
|
28
|
+
"~appHotkey": true,
|
|
29
|
+
id,
|
|
30
|
+
hotkey,
|
|
31
|
+
defaultHotkey: hotkey,
|
|
32
|
+
meta: input.meta,
|
|
33
|
+
context: input.context,
|
|
34
|
+
options: input.options
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
/** Whether a value is an app hotkey created by `defineAppHotkeys`. */
|
|
40
|
+
function isAppHotkey(value) {
|
|
41
|
+
return typeof value === "object" && value !== null && value["~appHotkey"] === true;
|
|
42
|
+
}
|
|
43
|
+
function hotkeysEqual(left, right, platform) {
|
|
44
|
+
if (left === null || right === null) return left === right;
|
|
45
|
+
return areHotkeysEqual(left, right, platform);
|
|
46
|
+
}
|
|
47
|
+
/** Whether an app hotkey's effective hotkey differs from its default. */
|
|
48
|
+
function isCustomHotkey(appHotkey, platform = detectPlatform()) {
|
|
49
|
+
return !hotkeysEqual(appHotkey.hotkey, appHotkey.defaultHotkey, platform);
|
|
50
|
+
}
|
|
51
|
+
/** Parses a saved custom hotkey. Returns `undefined` when it is not a valid hotkey. */
|
|
52
|
+
function parseCustomHotkey(value) {
|
|
53
|
+
if (value === null) return null;
|
|
54
|
+
return (typeof value === "string" || Array.isArray(value) && value.every((step) => typeof step === "string")) && validateHotkey(value).valid ? simplifyHotkey(value) : void 0;
|
|
55
|
+
}
|
|
56
|
+
/** Applies custom hotkeys to declarations. Unchanged entries keep their identity. */
|
|
57
|
+
function applyCustomHotkeys(appHotkeys, customHotkeys) {
|
|
58
|
+
const custom = customHotkeys;
|
|
59
|
+
const result = {};
|
|
60
|
+
for (const [id, appHotkey] of Object.entries(appHotkeys)) {
|
|
61
|
+
const hotkey = custom[id] === void 0 ? appHotkey.defaultHotkey : custom[id];
|
|
62
|
+
result[id] = hotkey === appHotkey.hotkey ? appHotkey : {
|
|
63
|
+
...appHotkey,
|
|
64
|
+
hotkey
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
/** Keeps only valid custom hotkeys for known ids, warning about the rest. */
|
|
70
|
+
function sanitizeCustomHotkeys(appHotkeys, saved) {
|
|
71
|
+
const result = {};
|
|
72
|
+
if (typeof saved !== "object" || saved === null) return result;
|
|
73
|
+
for (const [id, value] of Object.entries(saved)) {
|
|
74
|
+
if (value === void 0) continue;
|
|
75
|
+
if (!(id in appHotkeys)) {
|
|
76
|
+
console.warn(`Ignoring custom hotkey for unknown app hotkey '${id}'`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const hotkey = parseCustomHotkey(value);
|
|
80
|
+
if (hotkey === void 0) {
|
|
81
|
+
console.warn(`Ignoring invalid custom hotkey for app hotkey '${id}':`, value);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
result[id] = hotkey;
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
/** Whether two app hotkeys can apply at the same time. */
|
|
89
|
+
function contextsOverlap(a, b) {
|
|
90
|
+
return a.context === void 0 || b.context === void 0 || a.context === b.context;
|
|
91
|
+
}
|
|
92
|
+
/** Whether two hotkeys collide: equal chords, or one sequence starts with the other. */
|
|
93
|
+
function hotkeysOverlap(left, right, platform) {
|
|
94
|
+
const a = isHotkeySequence(left) ? left : [left];
|
|
95
|
+
const b = isHotkeySequence(right) ? right : [right];
|
|
96
|
+
const length = Math.min(a.length, b.length);
|
|
97
|
+
for (let i = 0; i < length; i++) if (!areHotkeysEqual(a[i], b[i], platform)) return false;
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Holds declared app hotkeys and the user's custom hotkeys. Framework
|
|
102
|
+
* providers create one and expose it through hooks; devtools read it from
|
|
103
|
+
* `HotkeyManager.appHotkeys`.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* const store = new AppHotkeysStore(appHotkeys, {
|
|
108
|
+
* customHotkeys: JSON.parse(localStorage.getItem('hotkeys') ?? '{}'),
|
|
109
|
+
* onCustomHotkeysChange: (custom) =>
|
|
110
|
+
* localStorage.setItem('hotkeys', JSON.stringify(custom)),
|
|
111
|
+
* })
|
|
112
|
+
* store.setCustomHotkey('doc.save', 'Mod+Shift+S')
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
var AppHotkeysStore = class {
|
|
116
|
+
store;
|
|
117
|
+
#declarations;
|
|
118
|
+
#options;
|
|
119
|
+
constructor(appHotkeys, options = {}) {
|
|
120
|
+
this.#declarations = appHotkeys;
|
|
121
|
+
this.#options = options;
|
|
122
|
+
const customHotkeys = sanitizeCustomHotkeys(appHotkeys, options.customHotkeys);
|
|
123
|
+
this.store = new Store({
|
|
124
|
+
appHotkeys: applyCustomHotkeys(appHotkeys, customHotkeys),
|
|
125
|
+
customHotkeys
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
get #platform() {
|
|
129
|
+
return this.#options.platform ?? detectPlatform();
|
|
130
|
+
}
|
|
131
|
+
/** Effective app hotkeys. */
|
|
132
|
+
get appHotkeys() {
|
|
133
|
+
return this.store.state.appHotkeys;
|
|
134
|
+
}
|
|
135
|
+
/** The user's hotkeys that differ from the defaults. */
|
|
136
|
+
get customHotkeys() {
|
|
137
|
+
return this.store.state.customHotkeys;
|
|
138
|
+
}
|
|
139
|
+
/** Updates callbacks and other options without resetting custom hotkeys. */
|
|
140
|
+
setOptions(options) {
|
|
141
|
+
this.#options = {
|
|
142
|
+
...this.#options,
|
|
143
|
+
...options
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Replaces the declarations, for example after hot module replacement.
|
|
148
|
+
* Custom hotkeys for ids that still exist are kept.
|
|
149
|
+
*/
|
|
150
|
+
setAppHotkeys(appHotkeys) {
|
|
151
|
+
if (appHotkeys === this.#declarations) return;
|
|
152
|
+
this.#declarations = appHotkeys;
|
|
153
|
+
const customHotkeys = sanitizeCustomHotkeys(appHotkeys, this.store.state.customHotkeys);
|
|
154
|
+
this.store.setState(() => ({
|
|
155
|
+
appHotkeys: applyCustomHotkeys(appHotkeys, customHotkeys),
|
|
156
|
+
customHotkeys
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
/** Effective app hotkey for an id, or `undefined` when it is not declared. */
|
|
160
|
+
get(id) {
|
|
161
|
+
return this.store.state.appHotkeys[id];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Other app hotkeys that would collide if `id` used `hotkey`. App hotkeys
|
|
165
|
+
* collide when their contexts match or either has no context, and their
|
|
166
|
+
* hotkeys are equal or one sequence starts with the other.
|
|
167
|
+
*/
|
|
168
|
+
findConflicts(id, hotkey) {
|
|
169
|
+
const self = this.store.state.appHotkeys[id];
|
|
170
|
+
if (!self) return [];
|
|
171
|
+
const candidate = simplifyHotkey(hotkey);
|
|
172
|
+
return Object.values(this.store.state.appHotkeys).filter((other) => other.id !== id && other.hotkey !== null && contextsOverlap(self, other) && hotkeysOverlap(candidate, other.hotkey, this.#platform));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Sets the user's hotkey for an app hotkey. `null` unbinds it. A hotkey
|
|
176
|
+
* equal to the default removes the customization.
|
|
177
|
+
*
|
|
178
|
+
* @returns Conflicting app hotkeys. When any exist, nothing changes.
|
|
179
|
+
*/
|
|
180
|
+
setCustomHotkey(id, hotkey) {
|
|
181
|
+
const appHotkey = this.store.state.appHotkeys[id];
|
|
182
|
+
if (!appHotkey) {
|
|
183
|
+
console.warn(`Cannot set a custom hotkey for unknown app hotkey '${id}'`);
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
const next = hotkey === null ? null : simplifyHotkey(hotkey);
|
|
187
|
+
if (next !== null) {
|
|
188
|
+
const conflicts = this.findConflicts(id, next);
|
|
189
|
+
if (conflicts.length > 0) return conflicts;
|
|
190
|
+
}
|
|
191
|
+
const { [id]: _previous, ...rest } = this.store.state.customHotkeys;
|
|
192
|
+
this.#commit(hotkeysEqual(next, appHotkey.defaultHotkey, this.#platform) ? rest : {
|
|
193
|
+
...rest,
|
|
194
|
+
[id]: next
|
|
195
|
+
});
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
/** Restores the default hotkey for an app hotkey. */
|
|
199
|
+
resetCustomHotkey(id) {
|
|
200
|
+
const { [id]: _previous, ...rest } = this.store.state.customHotkeys;
|
|
201
|
+
this.#commit(rest);
|
|
202
|
+
}
|
|
203
|
+
/** Restores every default hotkey. */
|
|
204
|
+
resetCustomHotkeys() {
|
|
205
|
+
this.#commit({});
|
|
206
|
+
}
|
|
207
|
+
#commit(customHotkeys) {
|
|
208
|
+
this.store.setState(() => ({
|
|
209
|
+
appHotkeys: applyCustomHotkeys(this.#declarations, customHotkeys),
|
|
210
|
+
customHotkeys
|
|
211
|
+
}));
|
|
212
|
+
this.#options.onCustomHotkeysChange?.(customHotkeys);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Joins app hotkeys with live registrations, in declaration order.
|
|
216
|
+
* Adapters call this with the manager's registration views.
|
|
217
|
+
*/
|
|
218
|
+
getList(registrations) {
|
|
219
|
+
const byId = /* @__PURE__ */ new Map();
|
|
220
|
+
for (const registration of registrations) {
|
|
221
|
+
const id = registration.options.appHotkeyId;
|
|
222
|
+
if (id === void 0) continue;
|
|
223
|
+
const list = byId.get(id);
|
|
224
|
+
if (list) list.push(registration);
|
|
225
|
+
else byId.set(id, [registration]);
|
|
226
|
+
}
|
|
227
|
+
return Object.values(this.store.state.appHotkeys).map((appHotkey) => {
|
|
228
|
+
const registered = byId.get(appHotkey.id) ?? [];
|
|
229
|
+
return {
|
|
230
|
+
appHotkey,
|
|
231
|
+
status: appHotkey.hotkey === null ? "unbound" : registered.length === 0 ? "inactive" : registered.some((r) => r.options.enabled !== false) ? "active" : "disabled",
|
|
232
|
+
isCustom: isCustomHotkey(appHotkey, this.#platform),
|
|
233
|
+
registrations: registered
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* Returns the provider's version of an app hotkey, so custom hotkeys apply
|
|
240
|
+
* even when a component imports the declaration directly. Falls back to the
|
|
241
|
+
* given app hotkey when no store declares it.
|
|
242
|
+
*/
|
|
243
|
+
function resolveAppHotkey(appHotkey, store) {
|
|
244
|
+
return store?.get(appHotkey.id) ?? appHotkey;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Options that adapters pass to the manager when registering an app hotkey:
|
|
248
|
+
* declared options, meta, and the app hotkey id. The manager drops options
|
|
249
|
+
* that do not apply to the effective hotkey's kind.
|
|
250
|
+
*/
|
|
251
|
+
function getAppHotkeyRegistrationOptions(appHotkey) {
|
|
252
|
+
return {
|
|
253
|
+
...appHotkey.options,
|
|
254
|
+
meta: appHotkey.meta,
|
|
255
|
+
appHotkeyId: appHotkey.id
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Publishes a provider's store on the HotkeyManager so devtools can list
|
|
260
|
+
* declared hotkeys. Returns a cleanup function that unpublishes it if it is
|
|
261
|
+
* still the published store.
|
|
262
|
+
*/
|
|
263
|
+
function publishAppHotkeysStore(store) {
|
|
264
|
+
const manager = getHotkeyManager();
|
|
265
|
+
manager.appHotkeys.setState(() => store);
|
|
266
|
+
return () => {
|
|
267
|
+
if (manager.appHotkeys.state === store) manager.appHotkeys.setState(() => null);
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Creates an app hotkeys store and publishes it on the HotkeyManager, for
|
|
272
|
+
* adapters without a provider component (such as Lit and Alpine). Hotkey
|
|
273
|
+
* APIs in those adapters resolve app hotkeys and custom hotkeys from the
|
|
274
|
+
* published store. Call it once at startup. Returns the store and a function
|
|
275
|
+
* that unpublishes it.
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* ```ts
|
|
279
|
+
* provideAppHotkeys(appHotkeys, {
|
|
280
|
+
* customHotkeys: JSON.parse(localStorage.getItem('hotkeys') ?? '{}'),
|
|
281
|
+
* onCustomHotkeysChange: (custom) =>
|
|
282
|
+
* localStorage.setItem('hotkeys', JSON.stringify(custom)),
|
|
283
|
+
* })
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
function provideAppHotkeys(appHotkeys, options = {}) {
|
|
287
|
+
const store = new AppHotkeysStore(appHotkeys, options);
|
|
288
|
+
return {
|
|
289
|
+
store,
|
|
290
|
+
dispose: publishAppHotkeysStore(store)
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
//#endregion
|
|
295
|
+
export { AppHotkeysStore, defineAppHotkeys, getAppHotkeyRegistrationOptions, isAppHotkey, isCustomHotkey, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey };
|
package/dist/conflicts.d.ts
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { RegisterableHotkey } from "./hotkey.types.js";
|
|
2
2
|
import { HotkeyRegistrationView } from "./hotkey-manager.js";
|
|
3
|
-
import { SequenceRegistrationView } from "./sequence-manager.js";
|
|
4
3
|
//#region src/conflicts.d.ts
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
/** A live registration that overlaps a candidate hotkey. */
|
|
5
|
+
export interface HotkeyConflict {
|
|
7
6
|
registration: HotkeyRegistrationView;
|
|
8
|
-
}
|
|
9
|
-
type: 'sequence';
|
|
10
|
-
registration: SequenceRegistrationView;
|
|
11
|
-
};
|
|
7
|
+
}
|
|
12
8
|
export interface HotkeyConflictOptions {
|
|
13
9
|
/** Intended registration target. Defaults to document; disjoint targets are excluded. */
|
|
14
10
|
target?: HTMLElement | Document | Window;
|
|
@@ -21,7 +17,7 @@ export interface HotkeyConflictOptions {
|
|
|
21
17
|
/** IDs of registrations being edited. */
|
|
22
18
|
excludeIds?: ReadonlyArray<string>;
|
|
23
19
|
/** Additional app-specific exclusions, such as all instances of one action. */
|
|
24
|
-
exclude?: (registration: HotkeyRegistrationView
|
|
20
|
+
exclude?: (registration: HotkeyRegistrationView) => boolean;
|
|
25
21
|
/** Platform used to resolve Mod in the candidate. Registrations retain their own platform. */
|
|
26
22
|
platform?: 'mac' | 'windows' | 'linux';
|
|
27
23
|
/** Source events allow detecting physical/logical overlap on the recorded layout. */
|
|
@@ -32,5 +28,5 @@ export interface HotkeyConflictOptions {
|
|
|
32
28
|
* With source events, also checks observed logical/physical overlap. Does not
|
|
33
29
|
* discover unmounted handlers, external listeners, or other keyboard layouts.
|
|
34
30
|
*/
|
|
35
|
-
export declare function findHotkeyConflicts(candidate: RegisterableHotkey
|
|
31
|
+
export declare function findHotkeyConflicts(candidate: RegisterableHotkey, options?: HotkeyConflictOptions): Array<HotkeyConflict>;
|
|
36
32
|
//#endregion
|
package/dist/conflicts.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { detectPlatform } from "./platform.js";
|
|
2
2
|
import { keysEqual } from "./_keyboard-event.js";
|
|
3
|
-
import { parseRegisterableHotkey } from "./parse.js";
|
|
3
|
+
import { isHotkeySequence, parseRegisterableHotkey } from "./parse.js";
|
|
4
4
|
import { matchesKeyboardEvent } from "./match.js";
|
|
5
5
|
import { getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
|
|
6
|
-
import { getSequenceManager } from "./sequence-manager.js";
|
|
7
6
|
|
|
8
7
|
//#region src/conflicts.ts
|
|
9
8
|
/** Whether two targets share a document and one contains the other (or is its window). */
|
|
@@ -24,7 +23,7 @@ function targetsOverlap(a, b) {
|
|
|
24
23
|
* discover unmounted handlers, external listeners, or other keyboard layouts.
|
|
25
24
|
*/
|
|
26
25
|
function findHotkeyConflicts(candidate, options = {}) {
|
|
27
|
-
const steps =
|
|
26
|
+
const steps = isHotkeySequence(candidate) ? candidate : [candidate];
|
|
28
27
|
if (steps.length === 0) return [];
|
|
29
28
|
const platform = options.platform ?? detectPlatform();
|
|
30
29
|
const parsed = steps.map((step) => parseRegisterableHotkey(step, platform));
|
|
@@ -39,14 +38,10 @@ function findHotkeyConflicts(candidate, options = {}) {
|
|
|
39
38
|
return same || event !== void 0 && matchesKeyboardEvent(event, a, platform) && matchesKeyboardEvent(event, b, registration.options.platform);
|
|
40
39
|
});
|
|
41
40
|
};
|
|
42
|
-
for (const registration of getHotkeyManager().registrations.state.values())
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
for (const registration of getSequenceManager().registrations.state.values()) if (matches(registration, registration.sequence)) conflicts.push({
|
|
47
|
-
type: "sequence",
|
|
48
|
-
registration
|
|
49
|
-
});
|
|
41
|
+
for (const registration of getHotkeyManager().registrations.state.values()) {
|
|
42
|
+
const view = toHotkeyRegistrationView(registration);
|
|
43
|
+
if (matches(view, view.kind === "sequence" ? view.hotkey : [view.hotkey])) conflicts.push({ registration: view });
|
|
44
|
+
}
|
|
50
45
|
return conflicts;
|
|
51
46
|
}
|
|
52
47
|
|
package/dist/constants.d.ts
CHANGED
|
@@ -118,7 +118,7 @@ export declare const PUNCTUATION_CODE_MAP: Record<string, string>;
|
|
|
118
118
|
* @see {@link EDITING_KEYS}
|
|
119
119
|
* @see {@link PUNCTUATION_KEYS}
|
|
120
120
|
*/
|
|
121
|
-
export declare const ALL_KEYS: Set<
|
|
121
|
+
export declare const ALL_KEYS: Set<"CapsLock" | "ContextMenu" | "Convert" | "KanaMode" | "NonConvert" | "Help" | "Insert" | "NumLock" | "Fn" | "FnLock" | "PrintScreen" | "ScrollLock" | "Pause" | "BrowserBack" | "BrowserFavorites" | "BrowserForward" | "BrowserHome" | "BrowserRefresh" | "BrowserSearch" | "BrowserStop" | "Eject" | "LaunchMail" | "MediaPlayPause" | "MediaStop" | "MediaTrackNext" | "MediaTrackPrevious" | "Power" | "AudioVolumeDown" | "AudioVolumeMute" | "AudioVolumeUp" | "WakeUp" | "Hyper" | "Super" | "Again" | "Copy" | "Cut" | "Find" | "Open" | "Paste" | "Props" | "Select" | "Undo" | "Hiragana" | "Katakana" | "Clear" | "Cancel" | "Standby" | "LaunchApplication1" | "LaunchApplication2" | "LaunchMediaPlayer" | "HangulMode" | "HanjaMode" | "Zenkaku" | "Hankaku" | "ZenkakuHankaku" | LetterKey | NumberKey | FunctionKey | NavigationKey | EditingKey | PunctuationKey>;
|
|
122
122
|
/**
|
|
123
123
|
* Normalizes a key name to its canonical form.
|
|
124
124
|
*
|
package/dist/format.d.ts
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ParsedHotkey, RegisterableHotkeyChord } from "./hotkey.types.js";
|
|
2
2
|
//#region src/format.d.ts
|
|
3
|
-
/**
|
|
4
|
-
* Joins stored sequence steps with spaces, preserving physical code tokens.
|
|
5
|
-
* For user-facing labels, call formatForDisplay on each step instead.
|
|
6
|
-
*
|
|
7
|
-
* @param sequence - Array of hotkey strings that form the sequence
|
|
8
|
-
* @returns A space-separated string (e.g. ['G','G'] → 'G G')
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```ts
|
|
12
|
-
* formatHotkeySequence(['G', 'G']) // 'G G'
|
|
13
|
-
* formatHotkeySequence(['D', 'I', 'W']) // 'D I W'
|
|
14
|
-
* ```
|
|
15
|
-
*/
|
|
16
|
-
export declare function formatHotkeySequence(sequence: Array<Hotkey>): string;
|
|
17
3
|
/**
|
|
18
4
|
* Converts a ParsedHotkey back to a hotkey string.
|
|
19
5
|
*
|
|
@@ -27,7 +13,10 @@ export declare function formatHotkeySequence(sequence: Array<Hotkey>): string;
|
|
|
27
13
|
* ```
|
|
28
14
|
*/
|
|
29
15
|
export declare function formatHotkey(parsed: ParsedHotkey): string;
|
|
30
|
-
|
|
16
|
+
/** A single chord to display: a string, raw object, or parsed hotkey. */
|
|
17
|
+
export type DisplayHotkeyChord = RegisterableHotkeyChord | ParsedHotkey | (string & {});
|
|
18
|
+
/** A hotkey to display: a chord or a sequence of chords. */
|
|
19
|
+
export type DisplayHotkey = DisplayHotkeyChord | ReadonlyArray<DisplayHotkeyChord>;
|
|
31
20
|
/**
|
|
32
21
|
* Formats a hotkey for display in a user interface.
|
|
33
22
|
*
|
|
@@ -39,10 +28,12 @@ export type DisplayHotkey = RegisterableHotkey | ParsedHotkey | (string & {});
|
|
|
39
28
|
* `[Digit2]` becomes `2`. These labels do not infer the active keyboard layout
|
|
40
29
|
* or change matching. Supply a resolved `layoutMap` for layout-aware labels;
|
|
41
30
|
* `keyLabels` overrides both layout entries and fallback labels.
|
|
31
|
+
* Sequence steps are formatted individually and joined with `sequenceSeparator`
|
|
32
|
+
* (a space by default).
|
|
42
33
|
*
|
|
43
|
-
* @param hotkey - A logical or physical string, raw
|
|
34
|
+
* @param hotkey - A logical or physical string, raw object, ParsedHotkey, or sequence
|
|
44
35
|
* @param options - Formatting options
|
|
45
|
-
* @returns Display text, or individual key labels when parts is true
|
|
36
|
+
* @returns Display text, or individual key labels when parts is true (one array per step for sequences)
|
|
46
37
|
*
|
|
47
38
|
* @example
|
|
48
39
|
* ```ts
|
|
@@ -54,19 +45,21 @@ export type DisplayHotkey = RegisterableHotkey | ParsedHotkey | (string & {});
|
|
|
54
45
|
*
|
|
55
46
|
* formatForDisplay('Escape')
|
|
56
47
|
* // Returns: 'Esc' (on all platforms)
|
|
48
|
+
*
|
|
49
|
+
* formatForDisplay(['G', 'H'])
|
|
50
|
+
* // Returns: 'G H'
|
|
57
51
|
* ```
|
|
58
52
|
*/
|
|
59
|
-
export declare function formatForDisplay(hotkey:
|
|
53
|
+
export declare function formatForDisplay(hotkey: ReadonlyArray<DisplayHotkeyChord>, options: FormatDisplayOptions & {
|
|
54
|
+
parts: true;
|
|
55
|
+
}): Array<Array<string>>;
|
|
56
|
+
export declare function formatForDisplay(hotkey: DisplayHotkeyChord, options: FormatDisplayOptions & {
|
|
60
57
|
parts: true;
|
|
61
58
|
}): Array<string>;
|
|
62
59
|
export declare function formatForDisplay(hotkey: DisplayHotkey, options?: FormatDisplayOptions & {
|
|
63
60
|
parts?: false;
|
|
64
61
|
}): string;
|
|
65
|
-
export declare function formatForDisplay(hotkey: DisplayHotkey, options: FormatDisplayOptions): string | Array<string
|
|
66
|
-
/**
|
|
67
|
-
* @deprecated Use {@link formatForDisplay} instead with `useSymbols: false` option.
|
|
68
|
-
*/
|
|
69
|
-
export declare function formatWithLabels(hotkey: RegisterableHotkey, options?: Omit<FormatDisplayOptions, 'useSymbols' | 'parts'>): string;
|
|
62
|
+
export declare function formatForDisplay(hotkey: DisplayHotkey, options: FormatDisplayOptions): string | Array<string> | Array<Array<string>>;
|
|
70
63
|
/**
|
|
71
64
|
* Options for formatting hotkeys for display.
|
|
72
65
|
*/
|
|
@@ -93,5 +86,7 @@ export interface FormatDisplayOptions {
|
|
|
93
86
|
keyLabels?: Readonly<Record<string, string>>;
|
|
94
87
|
/** Override the separator between display tokens. Defaults to platform-specific formatting when null/undefined. */
|
|
95
88
|
separatorToken?: string | null;
|
|
89
|
+
/** Separator between sequence steps. Default: a space. */
|
|
90
|
+
sequenceSeparator?: string;
|
|
96
91
|
}
|
|
97
92
|
//#endregion
|
package/dist/format.js
CHANGED
|
@@ -6,22 +6,6 @@ import { isModifierKey, normalizeHotkeyFromParsed, normalizeRegisterableHotkey }
|
|
|
6
6
|
|
|
7
7
|
//#region src/format.ts
|
|
8
8
|
/**
|
|
9
|
-
* Joins stored sequence steps with spaces, preserving physical code tokens.
|
|
10
|
-
* For user-facing labels, call formatForDisplay on each step instead.
|
|
11
|
-
*
|
|
12
|
-
* @param sequence - Array of hotkey strings that form the sequence
|
|
13
|
-
* @returns A space-separated string (e.g. ['G','G'] → 'G G')
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```ts
|
|
17
|
-
* formatHotkeySequence(['G', 'G']) // 'G G'
|
|
18
|
-
* formatHotkeySequence(['D', 'I', 'W']) // 'D I W'
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
|
-
function formatHotkeySequence(sequence) {
|
|
22
|
-
return sequence.join(" ");
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
9
|
* Converts a ParsedHotkey back to a hotkey string.
|
|
26
10
|
*
|
|
27
11
|
* @param parsed - The parsed hotkey object
|
|
@@ -41,11 +25,26 @@ function formatHotkey(parsed) {
|
|
|
41
25
|
return parts.join("+");
|
|
42
26
|
}
|
|
43
27
|
function formatForDisplay(hotkey, options = {}) {
|
|
28
|
+
if (Array.isArray(hotkey)) {
|
|
29
|
+
const steps = hotkey.map((step) => formatChordParts(step, options));
|
|
30
|
+
return options.parts ? steps : steps.map((parts) => parts.join(chordSeparator(options))).join(options.sequenceSeparator ?? " ");
|
|
31
|
+
}
|
|
32
|
+
const parts = formatChordParts(hotkey, options);
|
|
33
|
+
return options.parts ? parts : parts.join(chordSeparator(options));
|
|
34
|
+
}
|
|
35
|
+
function modifierSymbolsEnabled(options) {
|
|
36
|
+
const symbols = options.useSymbols ?? true;
|
|
37
|
+
return typeof symbols === "boolean" ? symbols : symbols.modifiers ?? true;
|
|
38
|
+
}
|
|
39
|
+
function chordSeparator(options) {
|
|
40
|
+
const platform = options.platform ?? detectPlatform();
|
|
41
|
+
return options.separatorToken ?? (platform === "mac" && modifierSymbolsEnabled(options) ? " " : "+");
|
|
42
|
+
}
|
|
43
|
+
function formatChordParts(hotkey, options) {
|
|
44
44
|
const platform = options.platform ?? detectPlatform();
|
|
45
45
|
const symbols = options.useSymbols ?? true;
|
|
46
|
-
const modifierSymbols =
|
|
46
|
+
const modifierSymbols = modifierSymbolsEnabled(options);
|
|
47
47
|
const keySymbols = typeof symbols === "boolean" ? symbols : symbols.keys ?? true;
|
|
48
|
-
const separator = options.separatorToken ?? (platform === "mac" && modifierSymbols ? " " : "+");
|
|
49
48
|
const normalized = typeof hotkey === "object" && "modifiers" in hotkey ? normalizeHotkeyFromParsed(hotkey, platform) : normalizeRegisterableHotkey(hotkey, platform);
|
|
50
49
|
const segments = splitHotkeyParts(normalized);
|
|
51
50
|
if (platform === "mac") {
|
|
@@ -55,7 +54,7 @@ function formatForDisplay(hotkey, options = {}) {
|
|
|
55
54
|
};
|
|
56
55
|
segments.sort((a, b) => modifierIndex(a) - modifierIndex(b));
|
|
57
56
|
}
|
|
58
|
-
|
|
57
|
+
return segments.map((segment) => {
|
|
59
58
|
if (isModifierKey(segment)) {
|
|
60
59
|
const modifier = MODIFIER_ALIASES[segment] ?? MODIFIER_ALIASES[segment.toLowerCase()];
|
|
61
60
|
return platform === "mac" ? modifierSymbols ? MAC_MODIFIER_SYMBOLS[modifier] : MAC_MODIFIER_LABELS[modifier] : platform === "windows" ? WINDOWS_MODIFIER_LABELS[modifier] : LINUX_MODIFIER_LABELS[modifier];
|
|
@@ -76,17 +75,7 @@ function formatForDisplay(hotkey, options = {}) {
|
|
|
76
75
|
const display = (keySymbols ? KEY_DISPLAY_SYMBOLS : PUNCTUATION_KEY_DISPLAY_LABELS)[displayKey] ?? displayKey;
|
|
77
76
|
return numpad ? `Numpad ${display.replace(/([a-z])([A-Z])/g, "$1 $2")}` : display;
|
|
78
77
|
});
|
|
79
|
-
return options.parts ? parts : parts.join(separator);
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* @deprecated Use {@link formatForDisplay} instead with `useSymbols: false` option.
|
|
83
|
-
*/
|
|
84
|
-
function formatWithLabels(hotkey, options = {}) {
|
|
85
|
-
return formatForDisplay(hotkey, {
|
|
86
|
-
...options,
|
|
87
|
-
useSymbols: false
|
|
88
|
-
});
|
|
89
78
|
}
|
|
90
79
|
|
|
91
80
|
//#endregion
|
|
92
|
-
export { formatForDisplay, formatHotkey
|
|
81
|
+
export { formatForDisplay, formatHotkey };
|
package/dist/hint.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface HeldModifierOptions {
|
|
|
11
11
|
* By default, any nonempty subset of the binding's modifiers qualifies; an
|
|
12
12
|
* unrelated modifier hides the hint. `exact` requires every binding modifier.
|
|
13
13
|
* Nonmodifier keys are ignored. This does not check registration or focus.
|
|
14
|
+
* A sequence reveals its hint from its first step.
|
|
14
15
|
*
|
|
15
16
|
* @example
|
|
16
17
|
* matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt']) // true
|
package/dist/hint.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MODIFIER_KEYS, normalizeKeyName } from "./constants.js";
|
|
2
|
-
import { parseRegisterableHotkey } from "./parse.js";
|
|
2
|
+
import { isHotkeySequence, parseRegisterableHotkey } from "./parse.js";
|
|
3
3
|
|
|
4
4
|
//#region src/hint.ts
|
|
5
5
|
/**
|
|
@@ -7,6 +7,7 @@ import { parseRegisterableHotkey } from "./parse.js";
|
|
|
7
7
|
* By default, any nonempty subset of the binding's modifiers qualifies; an
|
|
8
8
|
* unrelated modifier hides the hint. `exact` requires every binding modifier.
|
|
9
9
|
* Nonmodifier keys are ignored. This does not check registration or focus.
|
|
10
|
+
* A sequence reveals its hint from its first step.
|
|
10
11
|
*
|
|
11
12
|
* @example
|
|
12
13
|
* matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt']) // true
|
|
@@ -15,7 +16,9 @@ import { parseRegisterableHotkey } from "./parse.js";
|
|
|
15
16
|
function matchesHeldModifiers(hotkey, heldKeys, options = {}) {
|
|
16
17
|
if (heldKeys.includes("AltGraph")) return false;
|
|
17
18
|
const held = [...new Set(heldKeys.map(normalizeKeyName).filter((key) => MODIFIER_KEYS.has(key)))];
|
|
18
|
-
const
|
|
19
|
+
const first = isHotkeySequence(hotkey) ? hotkey[0] : hotkey;
|
|
20
|
+
if (first === void 0) return false;
|
|
21
|
+
const { modifiers } = parseRegisterableHotkey(first, options.platform);
|
|
19
22
|
return held.length > 0 && held.every((key) => modifiers.includes(key)) && (!options.exact || held.length === modifiers.length);
|
|
20
23
|
}
|
|
21
24
|
|