@witchcraft/spellcraft 0.0.4 → 0.2.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.
Files changed (35) hide show
  1. package/README.md +113 -14
  2. package/dist/core/createManagerOptions.js +1 -0
  3. package/dist/core/createShortcuts.d.ts +1 -1
  4. package/dist/core/setShortcutsProp.js +3 -3
  5. package/dist/defaults/defaultConditionEquals.d.ts +2 -16
  6. package/dist/defaults/defaultConditionEquals.js +1 -1
  7. package/dist/helpers/doesShortcutConflict.d.ts +1 -1
  8. package/dist/helpers/doesShortcutConflict.js +1 -1
  9. package/dist/helpers/equalsCommand.js +1 -1
  10. package/dist/helpers/equalsShortcut.d.ts +6 -4
  11. package/dist/helpers/equalsShortcut.js +5 -2
  12. package/dist/helpers/getKeyCodesFromKeyIds.d.ts +12 -0
  13. package/dist/helpers/getKeyCodesFromKeyIds.js +6 -0
  14. package/dist/helpers/getKeyCodesFromKeys.d.ts +13 -0
  15. package/dist/helpers/getKeyCodesFromKeys.js +16 -0
  16. package/dist/module.json +1 -1
  17. package/dist/module.mjs +2 -2
  18. package/dist/runtime/types.d.ts +3 -4
  19. package/dist/types/condition.d.ts +3 -1
  20. package/dist/types/manager.d.ts +12 -1
  21. package/dist/types/shortcuts.d.ts +2 -2
  22. package/package.json +27 -25
  23. package/src/core/createManagerOptions.ts +1 -0
  24. package/src/core/createShortcuts.ts +1 -1
  25. package/src/core/setShortcutsProp.ts +3 -3
  26. package/src/defaults/defaultConditionEquals.ts +4 -17
  27. package/src/helpers/doesShortcutConflict.ts +2 -2
  28. package/src/helpers/equalsCommand.ts +1 -1
  29. package/src/helpers/equalsShortcut.ts +17 -5
  30. package/src/helpers/getKeyCodesFromKeyIds.ts +20 -0
  31. package/src/helpers/getKeyCodesFromKeys.ts +28 -0
  32. package/src/runtime/types.ts +18 -4
  33. package/src/types/condition.ts +3 -1
  34. package/src/types/manager.ts +13 -1
  35. package/src/types/shortcuts.ts +2 -2
package/README.md CHANGED
@@ -123,7 +123,7 @@ You can choose in your execute function what exactly to do at that point for bot
123
123
 
124
124
  If non-modifier keys are still being held at this point, the manager will not allow triggering a shortcut until they are released (see `state.isAwaitingKeyup`). Modifiers are not affect by this. We usually want the user to be able to keep the modifier pressed and do, for example, `Ctrl+B` then `Ctrl+I` to bold and italicize text, without having to release `Ctrl`, only `B` and `I`.
125
125
 
126
- ## Errors and `{check}` Option
126
+ ## Errors and Checking if Actions can be Performed
127
127
 
128
128
  Note the use of `unwrap()`. Because many actions can throw "soft" errors, to better help deal with all the errors the library uses a Result monad in most of the return types. `unwrap` is like rust's unwrap and will throw the error if there was one, otherwise "unwrap" and return the value within.
129
129
 
@@ -278,12 +278,58 @@ export { }
278
278
  ```
279
279
 
280
280
  Additionally, when you create a condition, you can pass a function to `parse` it and add these needed properties:
281
+
282
+ You'll probably want to create a wrapping function to do this. Here's an example with the expressit library:
283
+
284
+
285
+ <details>
286
+
287
+ <summary>Click to Expand</summary>
288
+
281
289
  ```ts
282
- const condition = createCondition("a || b ", (_) => {
283
- _.ast = parse(_.text)
290
+ import { ShortcutContextParser } from "@witchcraft/expressit/examples/ShortcutContextParser"
291
+ import { createCondition as _createCondition } from "@witchcraft/spellcraft"
292
+
293
+ const dummyContext = { a: { }, b: { c: {} } }
294
+ const shortcutParser = new ShortcutContextParser(dummyContext) // see docs for details
295
+ function parser(text: string, _: Condition) {
296
+ const res = shortcutsParser.parse(text)
297
+ // === undefined is to narrow out the ErrorToken type
298
+ if (!res.valid || res.type === undefined) {
299
+ throw new Error(`Shortcut context parser failed to parse condition ${_.text}`)
300
+ }
301
+
302
+ _.ast = res
284
303
  return _
304
+ }
305
+
306
+ /** Creates a condition with the ast saved in it. */
307
+ function createCondition(text: string) {
308
+ return _createCondition(text, parser)
309
+ }
310
+ const condition = createCondition("a || b")
311
+ condition.ast // should exist
312
+
313
+ // tell the manager how to evaluare the condition:
314
+ const manager = createmanager({
315
+ context: createcontext<context<map<string, boolean>>>(new map()),
316
+ options: {
317
+ evaluatecondition(condition, context) {
318
+ if (condition.ast === undefined) throw new Error("condition ast is undefined")
319
+
320
+ const res = condition.ast.valid
321
+ ? conditionParser.evaluate(
322
+ conditionParser.normalize(condition.ast),
323
+ context.value.isActive
324
+ )
325
+ : false
326
+ return res
327
+ },
328
+ }
285
329
  })
330
+
286
331
  ```
332
+ </details>
287
333
 
288
334
  ## Contexts
289
335
 
@@ -292,10 +338,10 @@ Similarly with contexts, you can use any sort of object or type that you like.
292
338
  You can tell the manager it's type when you create it. For example, say we wanted to use a map:
293
339
 
294
340
  ```ts
295
- const manager = createManager({
296
- context: createContext<Context<Map<string, boolean>>>(new Map()),
341
+ const manager = createmanager({
342
+ context: createcontext<context<map<string, boolean>>>(new map()),
297
343
  options: {
298
- evaluateCondition(condition, context) {
344
+ evaluatecondition(condition, context) {
299
345
  // context is now correctly typed
300
346
  return context.value.has(condition.text)
301
347
  },
@@ -308,10 +354,10 @@ const manager = createManager({
308
354
  Creating a shortcut requires a the key/commands we created and the manager options to create a valid shortcut.
309
355
 
310
356
  ```ts
311
- const shortcut =createShortcut({
357
+ const shortcut = createShortcut({
312
358
  command: "test",
313
359
  chain: [["a"]],
314
- condition: createCondition("a || b", true),
360
+ condition: createCondition("a || b"),
315
361
  enabled: true,
316
362
  }, {options, keys, commands}).unwrap()
317
363
 
@@ -379,6 +425,10 @@ Note that while the built in errors are property specific, custom errors are not
379
425
 
380
426
  A helper class `ShortcutManagerManager` is provided to help manage multiple managers.
381
427
 
428
+ <details>
429
+
430
+ <summary>Click to Expand</summary>
431
+
382
432
  ```ts
383
433
  import { ShortcutManagerManager } from "@witchcraft/spellcraft"
384
434
 
@@ -435,6 +485,9 @@ managerManager.duplicateManager("myManagerName", "myDuplicateManagerName")
435
485
  managerManager.deleteManager("myManagerName")
436
486
  managerManager.renameManager("myManagerName", "myNewManagerName")
437
487
  ```
488
+
489
+ </details>
490
+
438
491
  You can then use the active manager's `onSet*Prop` hooks to call debouncedSave. You can wrap only the active manager to intercept all it's `onSet*Prop` calls. See the `useMultipleManagers` composable in the demo.
439
492
 
440
493
  ## Other Helpers and Utilities
@@ -453,6 +506,7 @@ There are many helpers provided to simplify common use cases under `/helpers`. S
453
506
  There's also some smaller utility functions in `/utils`:
454
507
  - `equals/dedupe/clone/*Key` These are particularly important for manipulating chords. This is because keys which are variants of eachother (see `Key.variants`) do not have matching ids and we usually want to be able to dedupe by the variants as well.
455
508
  - `isAny/Trigger/Wheel/MouseKey`.
509
+ - `getKeyCodesFromKeys` for getting the raw key codes from a list of keys for use with third-party libraries.
456
510
 
457
511
 
458
512
  There's also a few other functions that in the future might be moved from the demo were I created them and into the library. See [demo/src/common](https://github.com/AlansCodeLog/spellcraft/tree/master/demo/src/common).
@@ -490,18 +544,28 @@ Under gnome at least, if a key (usually Ctrl) is set to locate the cursor, it wi
490
544
 
491
545
  # FAQ
492
546
 
493
- ## Browser shortcuts interfere with certain shortcuts, how can this be avoided?
547
+
548
+ <details>
549
+
550
+ <summary>Browser shortcuts interfere with certain shortcuts, how can this be avoided?</summary>
494
551
 
495
552
  You can use a listener on the manager to e.preventDefault() some of these, but this doesn't work for all of them.
496
553
 
497
554
  If available you can also try using the [Keyboard API's](https://developer.mozilla.org/en-US/docs/Web/API/Keyboard_API) lock method (see [Keyboard Locking](https://developer.mozilla.org/en-US/docs/Web/API/Keyboard_API#keyboard_locking) ).
498
555
 
499
- ## How to label keys with their local names?
556
+ </details>
557
+
558
+ <details>
559
+
560
+ <summary>How to label keys with their local names?</summary>
500
561
 
501
562
  If the [Keyboard API](https://developer.mozilla.org/en-US/docs/Web/API/Keyboard_API) is available, you can use it's [navigator.keyboard.getLayoutMap method.](https://developer.mozilla.org/en-US/docs/Web/API/Keyboard/getLayoutMap). Helpers (getKeyboardLayoutMap and labelWithNavigator) are provided for this purpose, see them for details.
502
563
 
564
+ </details>
503
565
 
504
- ## How to set multiple manager properties safely (i.e. batch replace shortcuts/commands/keys)?
566
+ <details>
567
+
568
+ <summary>How to set multiple manager properties safely (i.e. batch replace shortcuts/commands/keys)?</summary>
505
569
 
506
570
  This can be an issue because there isn't a way to tell the manager you want to replace *multiple* properties and it might be impossible to, for example, replace commands with a smaller subset but not have it error even if you're planning to replace the shortcuts so they don't contain missing commands.
507
571
 
@@ -519,7 +583,11 @@ if (isValidManager(manager)) {
519
583
  }
520
584
 
521
585
  ```
522
- ## How to create `modifier-only` shortcuts? (e.g. a shortcut `Ctrl` that changes the some state like enabling multiple selection).
586
+ </details>
587
+
588
+ <details>
589
+
590
+ <summary>How to create `modifier-only` shortcuts? (e.g. a shortcut `Ctrl` that changes the some state like enabling multiple selection).</summary>
523
591
 
524
592
  To do this, instead of clearing the manager's chain, you just set the state directly.
525
593
 
@@ -563,17 +631,48 @@ function addToSelected(item) {
563
631
  }
564
632
  </script>
565
633
  ```
566
- # How to type the context?
634
+ </details>
635
+
636
+ <details>
637
+
638
+ <summary>How to show pretty shortcut strings to the user?</summary>
639
+
640
+ To show a pretty stringified version of a shortcut, use `manager.options.stringifier.stringify(shortcut.chain, manager)`.
641
+
642
+ Here's an advanced example in vue that shows a hint only when needed:
643
+
644
+ ```ts
645
+ const usageInstructions = computed(() => {
646
+ const manager = shortcuts.activeManager!.value!
647
+ const s = manager.options.stringifier
648
+ const context = shortcuts.context!
649
+ const isDragging = context.value.layout.drag.isDragging
650
+ const splitChain = manager.shortcuts.entries.find(_ => _.command === LAYOUT_COMMANDS.dragModifierSplit)?.chain
651
+
652
+ return isDragging && splitChain
653
+ `Hold ${s.stringify(splitChain, manager)} to Split`
654
+ : undefined,
655
+ })
656
+ ```
657
+
658
+ </details>
659
+
660
+ <details>
661
+
662
+ <summary>How to type the context?</summary>
663
+
664
+ Types can be extended multiple times from different files so long as the `NAME` part below is unique for each extension.
567
665
 
568
666
  ```ts [global.ts]
569
667
  declare module "@witchcraft/spellcraft/types" {
570
668
  // or for nuxt
571
669
  // declare module "#witchcraft/spellcraft/types.js" {
572
670
  export interface Register {
573
- ExtendedContextInfo: {
671
+ ShortcutManagerContextNAME: { //replace NAME with something
574
672
  // extend types here
575
673
  }
576
674
  }
577
675
  }
578
676
  ```
677
+ </details>
579
678
 
@@ -4,6 +4,7 @@ import { defaultSorter } from "../defaults/KeysSorter.js";
4
4
  import { defaultStringifier } from "../defaults/Stringifier.js";
5
5
  export function createManagerOptions(rawOpts) {
6
6
  const options = {
7
+ shortcutEqualityStrategy: "ignoreCommand",
7
8
  sorter: defaultSorter,
8
9
  stringifier: defaultStringifier,
9
10
  conditionEquals: defaultConditionEquals,
@@ -6,7 +6,7 @@ import type { CanHookErrors, Manager, MultipleErrors, PickManager, Shortcut, Sho
6
6
  * Creates a set of shortcuts.
7
7
  *
8
8
  */
9
- export declare function createShortcuts<THooks extends Manager["hooks"], TRawShortcuts extends Shortcut[], TCheck extends boolean | "only" = true>(shortcutsList: TRawShortcuts, manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", "evaluateCondition" | "conditionEquals" | "stringifier" | "sorter"> & {
9
+ export declare function createShortcuts<THooks extends Manager["hooks"], TRawShortcuts extends Shortcut[], TCheck extends boolean | "only" = true>(shortcutsList: TRawShortcuts, manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", "evaluateCondition" | "conditionEquals" | "stringifier" | "sorter" | "shortcutEqualityStrategy"> & {
10
10
  hooks?: THooks;
11
11
  }, opts?: Partial<Pick<Shortcuts, "ignoreModifierConflicts" | "ignoreChainConflicts">>, { check }?: {
12
12
  check?: boolean | "only";
@@ -20,7 +20,7 @@ export function setShortcutsProp(prop, val, manager, {
20
20
  castType(manager);
21
21
  const shortcut = val;
22
22
  const existing = shortcuts.entries.find(
23
- (_) => equalsShortcut(shortcut, _, manager, { ignoreCommand: true }) || doesShortcutConflict(_, shortcut, manager)
23
+ (_) => equalsShortcut(shortcut, _, manager) || doesShortcutConflict(_, shortcut, manager)
24
24
  );
25
25
  if (existing) {
26
26
  return Err(new KnownError(
@@ -41,7 +41,7 @@ export function setShortcutsProp(prop, val, manager, {
41
41
  case "entries@remove": {
42
42
  const shortcut = val;
43
43
  const existing = shortcuts.entries.find(
44
- (_) => _ === shortcut || equalsShortcut(shortcut, _, manager, { ignoreCommand: false })
44
+ (_) => _ === shortcut || equalsShortcut(shortcut, _, manager, { shortcutEqualityStrategy: "all" })
45
45
  );
46
46
  if (existing === void 0) {
47
47
  return Err(new KnownError(
@@ -77,7 +77,7 @@ export function setShortcutsProp(prop, val, manager, {
77
77
  }
78
78
  case "entries@remove": {
79
79
  const shortcut = val;
80
- const i = shortcuts.entries.findIndex((_) => _ === shortcut || equalsShortcut(shortcut, _, manager, { ignoreCommand: false }));
80
+ const i = shortcuts.entries.findIndex((_) => _ === shortcut || equalsShortcut(shortcut, _, manager, { shortcutEqualityStrategy: "all" }));
81
81
  if (i < 0) {
82
82
  throw new Error("If used correctly, shortcut should exist at this point, but it does not.");
83
83
  }
@@ -1,19 +1,5 @@
1
1
  import type { Condition } from "../types/index.js";
2
2
  /**
3
- * Returns whether the condition passed is equal to this one.
4
- *
5
- * The default method does a simplistic object check, and otherwise a check of the `text` property for equality. If both conditions are undefined, note this will return true.
6
- *
7
- * If you override the default conditionEquals option you will likely want it to just always return false.
8
- *
9
- * Why? Because unless you're using simple single variable conditions that you can presort to make them uniquely identifiable (i.e. not boolean expressions, e.g. `!a b !c`), this will return A LOT of false negatives.
10
- *
11
- * Why the false negatives? Because two conditions might be functionally equal but have differing representations (e.g: `a && b`, `b && a`). You might think, lets normalize them all, but normalizing boolean expressions (converting them to CNF) can be dangerous with very long expressions because it can take exponential time.
12
- *
13
- * Now the main reason for checking the equality of two conditions is to check if two shortcuts might conflict. If we're using boolean expressions it just can't be done safely.
14
- *
15
- * This is a personal preference, but if we have a method that gives false negatives it can be confusing that some shortcuts immediately error when added because their conditions are simple, while others don't until triggered. The simpler, more consistent alternative is to only have them error on triggering. Aditionally conflicting conditions can be shown on the keyboard layout when then user picks contexts to check against.
16
- *
17
- * Why use the default implementation at all then? Well, shortcuts aren't the only ones that have conditions, commands can too, but unlike shortcuts, usually it's developers who are in charge of assigning a command's condition, and since they are usually simple, it's more possible to make sure the conditions are unique (e.g. tests could enforce they're unique by converting them all to CNF and pre-checking them for equality).
3
+ * IMPORTANT: Please read {@link ConditionComparer} before using.
18
4
  */
19
- export declare function defaultConditionEquals<TCondition extends Condition>(conditionA?: TCondition, conditionB?: Condition): conditionB is TCondition;
5
+ export declare function defaultConditionEquals<TCondition extends Condition>(conditionA: TCondition | undefined, conditionB: Condition | undefined, _type: "shortcut" | "command"): conditionB is TCondition;
@@ -1,4 +1,4 @@
1
- export function defaultConditionEquals(conditionA, conditionB) {
1
+ export function defaultConditionEquals(conditionA, conditionB, _type) {
2
2
  if (conditionA === conditionB) return true;
3
3
  if (!conditionA || !conditionB) return false;
4
4
  return conditionA.text === conditionB.text;
@@ -15,4 +15,4 @@ import type { Manager, PickManager, Shortcut } from "../types/index.js";
15
15
  */
16
16
  export declare function doesShortcutConflict<TShortcut extends Shortcut>(shortcutA: TShortcut, shortcutB: Shortcut, manager: Pick<Manager, "keys" | "commands" | "shortcuts"> & {
17
17
  context?: Manager["context"];
18
- } & PickManager<"options", "evaluateCondition" | "conditionEquals">): boolean;
18
+ } & PickManager<"options", "evaluateCondition" | "conditionEquals" | "shortcutEqualityStrategy">): boolean;
@@ -24,7 +24,7 @@ export function doesShortcutConflict(shortcutA, shortcutB, manager) {
24
24
  if (!shortcutCondition) return false;
25
25
  }
26
26
  } else {
27
- if (!conditionEquals(shortcutA.condition, shortcutB.condition)) return false;
27
+ if (!conditionEquals(shortcutA.condition, shortcutB.condition, "shortcut")) return false;
28
28
  }
29
29
  const { keys } = manager;
30
30
  if (shortcutA.chain.length === 0 || shortcutB.chain.length === 0) {
@@ -1,4 +1,4 @@
1
1
  export function equalsCommand(commandA, commandB, manager) {
2
2
  if (commandA === commandB) return true;
3
- return commandA.name === commandB.name && commandA.execute === commandB.execute && manager.options.conditionEquals(commandA.condition, commandB.condition) && commandA.description === commandB.description;
3
+ return commandA.name === commandB.name && commandA.execute === commandB.execute && manager.options.conditionEquals(commandA.condition, commandB.condition, "command") && commandA.description === commandB.description;
4
4
  }
@@ -2,8 +2,10 @@ import type { Manager, PickManager, Shortcut } from "../types/index.js";
2
2
  /**
3
3
  * Returns whether the shortcut passed is equal to shortcutA one.
4
4
  *
5
- * To return true, their keys and command must be equal (unless ignoreCommand is passed), their condition must be equal according to shortcutA shortcut's condition.
5
+ * To return true, their keys, conditions, and commands (depends on the configured `manager.options.shortcutEqualityStrategy`) must be equal.
6
+ *
7
+ * See {@link Manager.options.shortcutEqualityStrategy} for details.
8
+ *
9
+ * You can temporarily override the strategy witht the third parameter.
6
10
  */
7
- export declare function equalsShortcut<TShortcut extends Shortcut>(shortcutA: TShortcut, shortcutB: Shortcut, manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", "evaluateCondition" | "conditionEquals">, { ignoreCommand }?: {
8
- ignoreCommand?: boolean;
9
- }): shortcutB is TShortcut;
11
+ export declare function equalsShortcut<TShortcut extends Shortcut>(shortcutA: TShortcut, shortcutB: Shortcut, manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", "evaluateCondition" | "conditionEquals" | "shortcutEqualityStrategy">, overrides?: PickManager<"options", "shortcutEqualityStrategy">["options"]): shortcutB is TShortcut;
@@ -1,7 +1,10 @@
1
1
  import { equalsCommand } from "./equalsCommand.js";
2
2
  import { equalsKeys } from "../utils/equalsKeys.js";
3
- export function equalsShortcut(shortcutA, shortcutB, manager, { ignoreCommand = false } = {}) {
3
+ export function equalsShortcut(shortcutA, shortcutB, manager, overrides) {
4
4
  if (shortcutA.forceUnequal || shortcutB.forceUnequal) return false;
5
5
  if (shortcutA === shortcutB) return true;
6
- return equalsKeys(shortcutA.chain, shortcutB.chain, manager.keys, void 0, { allowVariants: true }) && manager.options.conditionEquals(shortcutA.condition, shortcutB.condition) && (ignoreCommand || shortcutA.command === shortcutB.command && shortcutA.command === void 0 || shortcutA.command !== void 0 && shortcutB.command !== void 0 && equalsCommand(manager.commands.entries[shortcutA.command], manager.commands.entries[shortcutB.command], manager));
6
+ const commandA = shortcutA.command ? manager.commands.entries[shortcutA.command] : void 0;
7
+ const commandB = shortcutB.command ? manager.commands.entries[shortcutB.command] : void 0;
8
+ const shortcutEqualityStrategy = overrides?.shortcutEqualityStrategy ?? manager.options.shortcutEqualityStrategy;
9
+ return equalsKeys(shortcutA.chain, shortcutB.chain, manager.keys, void 0, { allowVariants: true }) && manager.options.conditionEquals(shortcutA.condition, shortcutB.condition, "shortcut") && (shortcutEqualityStrategy === "ignoreCommand" || shortcutEqualityStrategy === "ignoreCommandWithDifferentCondition" && commandA?.condition && commandB?.condition && manager.options.conditionEquals(commandA.condition, commandB.condition, "command") || shortcutA.command === shortcutB.command && shortcutA.command === void 0 || shortcutA.command !== void 0 && shortcutB.command !== void 0 && equalsCommand(manager.commands.entries[shortcutA.command], manager.commands.entries[shortcutB.command], manager));
7
10
  }
@@ -0,0 +1,12 @@
1
+ import { getKeyCodesFromKeys } from "./getKeyCodesFromKeys.js";
2
+ import type { Keys } from "../types/keys.js";
3
+ /**
4
+ * Given an list of key ids, returns an array of deduped key codes. If you have a list of `Key`s instead use {@link getKeyCodesFromKeys}.
5
+ *
6
+ * Useful for when the codes must be passed to a third-party library (for example, a dragging library that takes a list modifiers).
7
+ *
8
+ * This properly takes a look at the key and all it's variants.
9
+ *
10
+ * `skipIdIfHasVariants` is true by default and will skip adding the key's id if it has variants as it's assumed the id is not a valid name.
11
+ */
12
+ export declare function getKeyCodesFromKeyIds(keyIds: string[], keys: Keys, opts: Parameters<typeof getKeyCodesFromKeys>[1]): string[];
@@ -0,0 +1,6 @@
1
+ import { getKeyCodesFromKeys } from "./getKeyCodesFromKeys.js";
2
+ import { getKeyFromIdOrVariant } from "./getKeyFromIdOrVariant.js";
3
+ export function getKeyCodesFromKeyIds(keyIds, keys, opts) {
4
+ const keysList = keyIds.flatMap((id) => getKeyFromIdOrVariant(id, keys).unwrap());
5
+ return getKeyCodesFromKeys(keysList, opts);
6
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Given an list of keys, returns an array of deduped key codes.
3
+ *
4
+ * Useful for when the codes must be passed to a third-party library (for example, a dragging library that takes a list modifiers).
5
+ *
6
+ * This properly takes a look at the key and all it's variants.
7
+ *
8
+ * `skipIdIfHasVariants` is true by default and will skip adding the key's id if it has variants as it's assumed the id is not a valid name.
9
+ */
10
+ import type { Key } from "../types/keys.js";
11
+ export declare function getKeyCodesFromKeys(keys: Key[], { skipIdIfHasVariants }?: {
12
+ skipIdIfHasVariants?: boolean;
13
+ }): string[];
@@ -0,0 +1,16 @@
1
+ export function getKeyCodesFromKeys(keys, { skipIdIfHasVariants = true } = {}) {
2
+ const res = /* @__PURE__ */ new Set();
3
+ for (const key of keys) {
4
+ if (key.variants && key.variants.length > 0) {
5
+ if (!skipIdIfHasVariants) {
6
+ res.add(key.id);
7
+ }
8
+ for (const variant of key.variants) {
9
+ res.add(variant);
10
+ }
11
+ } else {
12
+ res.add(key.id);
13
+ }
14
+ }
15
+ return Array.from(res);
16
+ }
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "witchcraftSpellcraft",
3
3
  "configKey": "witchcraftSpellcraft",
4
- "version": "0.0.4",
4
+ "version": "0.2.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createResolver, defineNuxtModule, addTypeTemplate, addImportsDir } from '@nuxt/kit';
2
2
 
3
3
  const { resolve } = createResolver(import.meta.url);
4
- const module = defineNuxtModule({
4
+ const module$1 = defineNuxtModule({
5
5
  meta: {
6
6
  name: "witchcraftSpellcraft",
7
7
  configKey: "witchcraftSpellcraft"
@@ -32,4 +32,4 @@ const module = defineNuxtModule({
32
32
  }
33
33
  });
34
34
 
35
- export { module as default };
35
+ export { module$1 as default };
@@ -1,9 +1,8 @@
1
+ import type { OrToAnd } from "@alanscodelog/utils/types";
1
2
  export interface Register {
2
3
  }
3
- type ExtendedContextInfo = Register extends {
4
- ExtendedContextInfo: infer T;
5
- } ? T : unknown;
6
- export type ContextInfo = ExtendedContextInfo & {
4
+ export type ExtendedShortcutManagerInfo = keyof Register extends `ShortcutManagerContext${infer T}` ? Register extends Record<`ShortcutManagerContext${T}`, infer U> ? U : {} : {};
5
+ export type ContextInfo = OrToAnd<ExtendedShortcutManagerInfo> & {
7
6
  count: Record<string, number>;
8
7
  isActive: Record<string, boolean>;
9
8
  };
@@ -24,6 +24,8 @@ export interface Condition {
24
24
  * This is a personal preference, but if we have a method that gives false negatives it can be confusing that some shortcuts immediately error when added because their conditions are simple, while others don't until triggered. The simpler, more consistent alternative is to only have them error on triggering. Aditionally conflicting conditions can be shown on the keyboard layout when then user picks contexts to check against.
25
25
  *
26
26
  * Why use the default implementation at all then? Well, shortcuts aren't the only ones that have conditions, commands can too, but unlike shortcuts, usually it's developers who are in charge of assigning a command's condition, and since they are usually simple, it's more possible to make sure the conditions are unique (e.g. tests could enforce they're unique by converting them all to CNF and pre-checking them for equality).
27
+ *
28
+ * This is why the manager now has the option `shortcutEqualityStrategy` which can be se to `ignoreCommandWithDifferentCondition`. You can implement a comparer that only checks when a command is being checked.
27
29
  */
28
- export type ConditionComparer = (conditionA?: Condition, conditionB?: Condition) => boolean;
30
+ export type ConditionComparer = (conditionA: Condition | undefined, conditionB: Condition | undefined, type: "shortcut" | "command") => boolean;
29
31
  export type ConditionEvaluator<TContext extends Context<any>> = (condition: Condition, context: TContext) => boolean;
@@ -114,9 +114,20 @@ export type Manager<THooks extends Partial<Hooks> = Partial<Hooks>, TKeys extend
114
114
  /**
115
115
  * Determines if two conditions are equal.
116
116
  *
117
- * This is actually not a good idea to implement if you use boolean conditions. See {@link ConditionComparer} for why.
117
+ * This is actually not a good idea to implement for shortcuts if you use boolean conditions. See {@link ConditionComparer} for why.
118
+ *
119
+ * See also {@link Manager.options.shortcutEqualityStrategy}.
118
120
  */
119
121
  conditionEquals: ConditionComparer;
122
+ /**
123
+ * Determines how shortcuts are compared.
124
+ *
125
+ * - `ignoreCommand` (default) will ignore both shortcuts' commands. If everything else is equal, they are considered equal.
126
+ * - `ignoreCommandWithDifferentCondition` will ignore both commands' names and only compare their conditions. If everything else is equal, they are considered equal. Use it if you allow command conditions to equal eachother. See {@link ConditionComparer} for details.
127
+ * - `all` will compare everything.
128
+ *
129
+ */
130
+ shortcutEqualityStrategy: "ignoreCommand" | "ignoreCommandWithDifferentCondition" | "all";
120
131
  /** Determines how conditions are evaluated. */
121
132
  evaluateCondition: ConditionEvaluator<TContext>;
122
133
  /** Enable/disable triggering of shortcuts. The manager otherwise works as normal. */
@@ -126,12 +126,12 @@ export type CanHookShortcutProps = Exclude<OnHookShortcutProps, "forceUnequal">;
126
126
  export type SyntheticOnHookShortcutsProps = "entries@add" | "entries@remove";
127
127
  export type CanHookShortcutsProps = SyntheticOnHookShortcutsProps;
128
128
  export type OnHookShortcutsProps = SyntheticOnHookShortcutsProps;
129
- type BaseShortcutsManager = Pick<Manager, "keys" | "commands" | "shortcuts"> & PickManager<"options", "evaluateCondition" | "conditionEquals" | "stringifier"> & Record<any, any>;
129
+ type BaseShortcutsManager = Pick<Manager, "keys" | "commands" | "shortcuts"> & PickManager<"options", "evaluateCondition" | "conditionEquals" | "stringifier" | "shortcutEqualityStrategy"> & Record<any, any>;
130
130
  export type ShortcutsSetEntries = {
131
131
  "entries@add": {
132
132
  val: Shortcut;
133
133
  hooks: GetShortcutHooks<`entries@add`>;
134
- manager: BaseShortcutsManager & PickManager<"options", "sorter">;
134
+ manager: BaseShortcutsManager & PickManager<"options", "sorter" | "shortcutEqualityStrategy">;
135
135
  error: typeof SHORTCUT_ERROR.DUPLICATE_SHORTCUT | typeof SHORTCUT_ERROR.UNKNOWN_COMMAND | ChainError;
136
136
  };
137
137
  "entries@remove": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@witchcraft/spellcraft",
3
3
  "description": "A shortcut manager library for handling ALL the shortcut needs of an application.",
4
- "version": "0.0.4",
4
+ "version": "0.2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/core/index.js",
7
7
  "sideEffects": false,
@@ -39,8 +39,8 @@
39
39
  }
40
40
  },
41
41
  "peerDependencies": {
42
- "@witchcraft/expressit": "^0.4.1",
43
- "@witchcraft/ui": "^0.3.7"
42
+ "@witchcraft/expressit": "^0.4.2",
43
+ "@witchcraft/ui": "^0.3.20"
44
44
  },
45
45
  "peerDependenciesMeta": {
46
46
  "@witchcraft/ui": {
@@ -52,45 +52,46 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@alanscodelog/utils": "^6.0.2",
55
- "@witchcraft/expressit": "^0.4.1",
56
- "@witchcraft/ui": "^0.3.7",
57
- "vue": "^3.5.21"
55
+ "@witchcraft/expressit": "^0.4.2",
56
+ "@witchcraft/ui": "^0.3.20",
57
+ "reka-ui": "^2.8.0",
58
+ "vue": "^3.5.27"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@alanscodelog/commitlint-config": "^3.1.2",
61
62
  "@alanscodelog/eslint-config": "^6.3.1",
62
63
  "@alanscodelog/semantic-release-config": "^6.0.0",
63
64
  "@alanscodelog/tsconfigs": "^6.2.0",
64
- "@alanscodelog/vite-config": "^0.0.6",
65
- "@commitlint/cli": "^19.8.1",
66
- "@iconify/json": "^2.2.385",
67
- "@nuxt/eslint-config": "^1.9.0",
68
- "@nuxt/kit": "^4.1.2",
65
+ "@alanscodelog/vite-config": "^0.0.7",
66
+ "@commitlint/cli": "^20.4.1",
67
+ "@iconify/json": "^2.2.436",
68
+ "@nuxt/eslint-config": "^1.13.0",
69
+ "@nuxt/kit": "^4.3.0",
69
70
  "@nuxt/module-builder": "^1.0.2",
70
- "@nuxt/schema": "^4.1.2",
71
+ "@nuxt/schema": "^4.3.0",
71
72
  "@nuxt/types": "^2.18.1",
72
- "@types/node": "^24.5.2",
73
- "@vitejs/plugin-vue": "^6.0.1",
73
+ "@types/node": "^25.2.1",
74
+ "@vitejs/plugin-vue": "^6.0.4",
74
75
  "@vitest/coverage-c8": "^0.33.0",
75
76
  "concurrently": "^9.2.1",
76
- "cross-env": "^10.0.0",
77
+ "cross-env": "^10.1.0",
77
78
  "defu": "^6.1.4",
78
79
  "fast-glob": "^3.3.3",
79
80
  "http-server": "^14.1.1",
80
81
  "husky": "^9.1.7",
81
82
  "indexit": "2.1.0-beta.3",
82
83
  "madge": "^8.0.0",
83
- "nuxt": "^4.1.2",
84
+ "nuxt": "^4.3.0",
84
85
  "onchange": "^7.1.0",
85
- "semantic-release": "^24.2.8",
86
- "tailwindcss": "^4.1.13",
87
- "typedoc": "0.28.13",
88
- "typescript": "^5.9.2",
86
+ "semantic-release": "^25.0.3",
87
+ "tailwindcss": "^4.1.18",
88
+ "typedoc": "0.28.16",
89
+ "typescript": "^5.9.3",
89
90
  "unbuild": "^3.6.1",
90
- "unplugin-icons": "^22.3.0",
91
- "vite-tsconfig-paths": "^5.1.4",
92
- "vitest": "^3.2.4",
93
- "vue-tsc": "^3.0.7"
91
+ "unplugin-icons": "^23.0.1",
92
+ "vite-tsconfig-paths": "^6.0.5",
93
+ "vitest": "^4.0.18",
94
+ "vue-tsc": "^3.2.4"
94
95
  },
95
96
  "author": "Alan <alanscodelog@gmail.com>",
96
97
  "repository": "https://github.com/witchcraftjs/spellcraft",
@@ -126,7 +127,8 @@
126
127
  "node": ">=20.0.0"
127
128
  },
128
129
  "publishConfig": {
129
- "access": "public"
130
+ "access": "public",
131
+ "provenance": true
130
132
  },
131
133
  "scripts": {
132
134
  "build": "nuxt-module-build prepare && nuxt-module-build build && nuxi generate playground",
@@ -16,6 +16,7 @@ export function createManagerOptions(
16
16
  rawOpts: Parameters<typeof createManager>[0]["options"]
17
17
  ): Manager["options"] {
18
18
  const options: Manager["options"] = {
19
+ shortcutEqualityStrategy: "ignoreCommand",
19
20
  sorter: defaultSorter,
20
21
  stringifier: defaultStringifier,
21
22
  conditionEquals: defaultConditionEquals,
@@ -17,7 +17,7 @@ export function createShortcuts<
17
17
  >(
18
18
  shortcutsList: TRawShortcuts,
19
19
  manager: Pick<Manager, "keys" | "commands">
20
- & PickManager<"options", | "evaluateCondition" | "conditionEquals" | "stringifier" | "sorter">
20
+ & PickManager<"options", | "evaluateCondition" | "conditionEquals" | "stringifier" | "sorter" | "shortcutEqualityStrategy">
21
21
  & { hooks?: THooks },
22
22
  opts?: Partial<Pick<Shortcuts, "ignoreModifierConflicts" | "ignoreChainConflicts">>,
23
23
  {
@@ -43,7 +43,7 @@ export function setShortcutsProp<
43
43
 
44
44
  const shortcut = val as any as Shortcut
45
45
  const existing = (shortcuts.entries).find(_ =>
46
- equalsShortcut(shortcut, _, manager, { ignoreCommand: true })
46
+ equalsShortcut(shortcut, _, manager)
47
47
  || doesShortcutConflict(_, shortcut, manager)
48
48
  )
49
49
 
@@ -67,7 +67,7 @@ export function setShortcutsProp<
67
67
  const shortcut = val as any as Shortcut
68
68
  // note we don't ignore the command here, we want to find an exact match
69
69
  const existing = (shortcuts.entries).find(_ =>
70
- _ === shortcut || equalsShortcut(shortcut, _, manager, { ignoreCommand: false })
70
+ _ === shortcut || equalsShortcut(shortcut, _, manager, { shortcutEqualityStrategy: "all" })
71
71
  )
72
72
  if (existing === undefined) {
73
73
  return Err(new KnownError(
@@ -105,7 +105,7 @@ export function setShortcutsProp<
105
105
  }
106
106
  case "entries@remove": {
107
107
  const shortcut = val
108
- const i = shortcuts.entries.findIndex(_ => _ === shortcut || equalsShortcut(shortcut, _, manager, { ignoreCommand: false }))
108
+ const i = shortcuts.entries.findIndex(_ => _ === shortcut || equalsShortcut(shortcut, _, manager, { shortcutEqualityStrategy: "all" }))
109
109
  if (i < 0) {
110
110
  throw new Error("If used correctly, shortcut should exist at this point, but it does not.")
111
111
  }
@@ -1,25 +1,12 @@
1
1
  import type { Condition } from "../types/index.js"
2
2
 
3
3
  /**
4
- * Returns whether the condition passed is equal to this one.
5
- *
6
- * The default method does a simplistic object check, and otherwise a check of the `text` property for equality. If both conditions are undefined, note this will return true.
7
- *
8
- * If you override the default conditionEquals option you will likely want it to just always return false.
9
- *
10
- * Why? Because unless you're using simple single variable conditions that you can presort to make them uniquely identifiable (i.e. not boolean expressions, e.g. `!a b !c`), this will return A LOT of false negatives.
11
- *
12
- * Why the false negatives? Because two conditions might be functionally equal but have differing representations (e.g: `a && b`, `b && a`). You might think, lets normalize them all, but normalizing boolean expressions (converting them to CNF) can be dangerous with very long expressions because it can take exponential time.
13
- *
14
- * Now the main reason for checking the equality of two conditions is to check if two shortcuts might conflict. If we're using boolean expressions it just can't be done safely.
15
- *
16
- * This is a personal preference, but if we have a method that gives false negatives it can be confusing that some shortcuts immediately error when added because their conditions are simple, while others don't until triggered. The simpler, more consistent alternative is to only have them error on triggering. Aditionally conflicting conditions can be shown on the keyboard layout when then user picks contexts to check against.
17
- *
18
- * Why use the default implementation at all then? Well, shortcuts aren't the only ones that have conditions, commands can too, but unlike shortcuts, usually it's developers who are in charge of assigning a command's condition, and since they are usually simple, it's more possible to make sure the conditions are unique (e.g. tests could enforce they're unique by converting them all to CNF and pre-checking them for equality).
4
+ * IMPORTANT: Please read {@link ConditionComparer} before using.
19
5
  */
20
6
  export function defaultConditionEquals<TCondition extends Condition>(
21
- conditionA?: TCondition,
22
- conditionB?: Condition
7
+ conditionA: TCondition | undefined,
8
+ conditionB: Condition | undefined,
9
+ _type: "shortcut" | "command"
23
10
  ): conditionB is TCondition {
24
11
  // both are the same object or both undefined
25
12
  if (conditionA === conditionB) return true
@@ -23,7 +23,7 @@ export function doesShortcutConflict<TShortcut extends Shortcut>(
23
23
  shortcutA: TShortcut,
24
24
  shortcutB: Shortcut,
25
25
  manager: Pick<Manager, "keys" | "commands" | "shortcuts"> & { context?: Manager["context"] }
26
- & PickManager<"options", "evaluateCondition" | "conditionEquals">
26
+ & PickManager<"options", "evaluateCondition" | "conditionEquals" | "shortcutEqualityStrategy">
27
27
  ): boolean {
28
28
  const context = manager.context
29
29
  if (!context && manager.shortcuts.useContextInConflictCheck) {
@@ -47,7 +47,7 @@ export function doesShortcutConflict<TShortcut extends Shortcut>(
47
47
  if (!shortcutCondition) return false
48
48
  }
49
49
  } else {
50
- if (!conditionEquals(shortcutA.condition, shortcutB.condition)) return false
50
+ if (!conditionEquals(shortcutA.condition, shortcutB.condition, "shortcut")) return false
51
51
  }
52
52
  const { keys } = manager
53
53
  // an empty chain is always in conflict ?
@@ -13,6 +13,6 @@ export function equalsCommand<TCommand extends Command>(
13
13
  if (commandA === commandB) return true
14
14
  return commandA.name === commandB.name
15
15
  && commandA.execute === commandB.execute
16
- && manager.options.conditionEquals(commandA.condition, commandB.condition)
16
+ && manager.options.conditionEquals(commandA.condition, commandB.condition, "command")
17
17
  && commandA.description === commandB.description
18
18
  }
@@ -6,20 +6,32 @@ import { equalsKeys } from "../utils/equalsKeys.js"
6
6
  /**
7
7
  * Returns whether the shortcut passed is equal to shortcutA one.
8
8
  *
9
- * To return true, their keys and command must be equal (unless ignoreCommand is passed), their condition must be equal according to shortcutA shortcut's condition.
9
+ * To return true, their keys, conditions, and commands (depends on the configured `manager.options.shortcutEqualityStrategy`) must be equal.
10
+ *
11
+ * See {@link Manager.options.shortcutEqualityStrategy} for details.
12
+ *
13
+ * You can temporarily override the strategy witht the third parameter.
10
14
  */
11
15
  export function equalsShortcut<TShortcut extends Shortcut>(
12
16
  shortcutA: TShortcut,
13
17
  shortcutB: Shortcut,
14
- manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", | "evaluateCondition" | "conditionEquals">,
15
- { ignoreCommand = false }: { ignoreCommand?: boolean } = {}
18
+ manager: Pick<Manager, "keys" | "commands"> & PickManager<"options", | "evaluateCondition" | "conditionEquals" | "shortcutEqualityStrategy">,
19
+ overrides?: PickManager<"options", "shortcutEqualityStrategy">["options"]
16
20
  ): shortcutB is TShortcut {
17
21
  if (shortcutA.forceUnequal || shortcutB.forceUnequal) return false
18
22
  if (shortcutA === shortcutB) return true
23
+ const commandA = shortcutA.command ? manager.commands.entries[shortcutA.command] : undefined
24
+ const commandB = shortcutB.command ? manager.commands.entries[shortcutB.command] : undefined
25
+ const shortcutEqualityStrategy = overrides?.shortcutEqualityStrategy ?? manager.options.shortcutEqualityStrategy
19
26
  return (
20
27
  equalsKeys(shortcutA.chain, shortcutB.chain, manager.keys, undefined, { allowVariants: true })
21
- && manager.options.conditionEquals(shortcutA.condition, shortcutB.condition)
22
- && (ignoreCommand
28
+ && manager.options.conditionEquals(shortcutA.condition, shortcutB.condition, "shortcut")
29
+ && (shortcutEqualityStrategy === "ignoreCommand"
30
+ || (
31
+ shortcutEqualityStrategy === "ignoreCommandWithDifferentCondition"
32
+ && commandA?.condition && commandB?.condition
33
+ && manager.options.conditionEquals(commandA.condition, commandB.condition, "command")
34
+ )
23
35
  || (
24
36
  shortcutA.command === shortcutB.command
25
37
  && shortcutA.command === undefined
@@ -0,0 +1,20 @@
1
+ import { getKeyCodesFromKeys } from "./getKeyCodesFromKeys.js"
2
+ import { getKeyFromIdOrVariant } from "./getKeyFromIdOrVariant.js"
3
+
4
+ import type { Keys } from "../types/keys.js"
5
+
6
+ /**
7
+ * Given an list of key ids, returns an array of deduped key codes. If you have a list of `Key`s instead use {@link getKeyCodesFromKeys}.
8
+ *
9
+ * Useful for when the codes must be passed to a third-party library (for example, a dragging library that takes a list modifiers).
10
+ *
11
+ * This properly takes a look at the key and all it's variants.
12
+ *
13
+ * `skipIdIfHasVariants` is true by default and will skip adding the key's id if it has variants as it's assumed the id is not a valid name.
14
+ */
15
+
16
+
17
+ export function getKeyCodesFromKeyIds(keyIds: string[], keys: Keys, opts: Parameters<typeof getKeyCodesFromKeys>[1]): string[] {
18
+ const keysList = keyIds.flatMap(id => getKeyFromIdOrVariant(id, keys).unwrap())
19
+ return getKeyCodesFromKeys(keysList, opts)
20
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Given an list of keys, returns an array of deduped key codes.
3
+ *
4
+ * Useful for when the codes must be passed to a third-party library (for example, a dragging library that takes a list modifiers).
5
+ *
6
+ * This properly takes a look at the key and all it's variants.
7
+ *
8
+ * `skipIdIfHasVariants` is true by default and will skip adding the key's id if it has variants as it's assumed the id is not a valid name.
9
+ */
10
+
11
+ import type { Key } from "../types/keys.js"
12
+
13
+ export function getKeyCodesFromKeys(keys: Key[], { skipIdIfHasVariants = true}: { skipIdIfHasVariants?: boolean } = {}): string[] {
14
+ const res = new Set<string>()
15
+ for (const key of keys) {
16
+ if (key.variants && key.variants.length > 0) {
17
+ if (!skipIdIfHasVariants) {
18
+ res.add(key.id)
19
+ }
20
+ for (const variant of key.variants) {
21
+ res.add(variant)
22
+ }
23
+ } else {
24
+ res.add(key.id)
25
+ }
26
+ }
27
+ return Array.from(res)
28
+ }
@@ -1,9 +1,23 @@
1
- export interface Register { }
1
+ import type { OrToAnd } from "@alanscodelog/utils/types"
2
+ // allows users to register multiple contexts
3
+ // @eslint-disable-next-line @typescript-eslint/no-empty-object-type
4
+ export interface Register {}
2
5
 
3
- // eslint-disable-next-line @typescript-eslint/naming-convention
4
- type ExtendedContextInfo = Register extends { ExtendedContextInfo: infer T } ? T : unknown
6
+ export type ExtendedShortcutManagerInfo = keyof Register extends `ShortcutManagerContext${infer T}`
7
+ ? Register extends Record<`ShortcutManagerContext${T}`, infer U>
8
+ ? U
9
+ // we can further constrain the type users can register if needed
10
+ // ? U extends { }
11
+ // ? U
12
+ // : {}
13
+ // : {}
14
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
15
+ : {}
16
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
17
+ : {}
5
18
 
6
- export type ContextInfo = ExtendedContextInfo & {
19
+
20
+ export type ContextInfo = OrToAnd<ExtendedShortcutManagerInfo> & {
7
21
  count: Record<string, number>
8
22
  isActive: Record<string, boolean>
9
23
  }
@@ -28,8 +28,10 @@ export interface Condition {
28
28
  * This is a personal preference, but if we have a method that gives false negatives it can be confusing that some shortcuts immediately error when added because their conditions are simple, while others don't until triggered. The simpler, more consistent alternative is to only have them error on triggering. Aditionally conflicting conditions can be shown on the keyboard layout when then user picks contexts to check against.
29
29
  *
30
30
  * Why use the default implementation at all then? Well, shortcuts aren't the only ones that have conditions, commands can too, but unlike shortcuts, usually it's developers who are in charge of assigning a command's condition, and since they are usually simple, it's more possible to make sure the conditions are unique (e.g. tests could enforce they're unique by converting them all to CNF and pre-checking them for equality).
31
+ *
32
+ * This is why the manager now has the option `shortcutEqualityStrategy` which can be se to `ignoreCommandWithDifferentCondition`. You can implement a comparer that only checks when a command is being checked.
31
33
  */
32
- export type ConditionComparer = (conditionA?: Condition, conditionB?: Condition) => boolean
34
+ export type ConditionComparer = (conditionA: Condition | undefined, conditionB: Condition | undefined, type: "shortcut" | "command") => boolean
33
35
 
34
36
  export type ConditionEvaluator<TContext extends Context<any>> = (condition: Condition, context: TContext) => boolean
35
37
 
@@ -212,9 +212,21 @@ export type Manager<
212
212
  /**
213
213
  * Determines if two conditions are equal.
214
214
  *
215
- * This is actually not a good idea to implement if you use boolean conditions. See {@link ConditionComparer} for why.
215
+ * This is actually not a good idea to implement for shortcuts if you use boolean conditions. See {@link ConditionComparer} for why.
216
+ *
217
+ * See also {@link Manager.options.shortcutEqualityStrategy}.
216
218
  */
217
219
  conditionEquals: ConditionComparer
220
+ /**
221
+ * Determines how shortcuts are compared.
222
+ *
223
+ * - `ignoreCommand` (default) will ignore both shortcuts' commands. If everything else is equal, they are considered equal.
224
+ * - `ignoreCommandWithDifferentCondition` will ignore both commands' names and only compare their conditions. If everything else is equal, they are considered equal. Use it if you allow command conditions to equal eachother. See {@link ConditionComparer} for details.
225
+ * - `all` will compare everything.
226
+ *
227
+ */
228
+ shortcutEqualityStrategy: "ignoreCommand" | "ignoreCommandWithDifferentCondition" | "all"
229
+
218
230
  /** Determines how conditions are evaluated. */
219
231
  evaluateCondition: ConditionEvaluator<TContext>
220
232
  /** Enable/disable triggering of shortcuts. The manager otherwise works as normal. */
@@ -178,7 +178,7 @@ export type OnHookShortcutsProps = SyntheticOnHookShortcutsProps
178
178
 
179
179
  type BaseShortcutsManager
180
180
  = & Pick<Manager, "keys" | "commands" | "shortcuts">
181
- & PickManager<"options", | "evaluateCondition" | "conditionEquals" | "stringifier">
181
+ & PickManager<"options", | "evaluateCondition" | "conditionEquals" | "stringifier" | "shortcutEqualityStrategy">
182
182
  & Record<any, any>
183
183
 
184
184
  export type ShortcutsSetEntries = {
@@ -187,7 +187,7 @@ export type ShortcutsSetEntries = {
187
187
  val: Shortcut
188
188
  hooks: GetShortcutHooks<`entries@add`>
189
189
  manager: BaseShortcutsManager
190
- & PickManager<"options", "sorter">
190
+ & PickManager<"options", "sorter" | "shortcutEqualityStrategy">
191
191
 
192
192
  error:
193
193
  | typeof SHORTCUT_ERROR.DUPLICATE_SHORTCUT