@opentui/keymap 0.2.0 → 0.2.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 CHANGED
@@ -43,6 +43,16 @@ They do not compile into public attrs.
43
43
  Binding and command fields can compile metadata into attrs that later appear on
44
44
  active bindings, active keys, and command query results.
45
45
 
46
+ ## Formatting Keys
47
+
48
+ Use `keymap.formatKey` when formatting raw binding config. It parses string
49
+ bindings through the keymap's registered parsers and tokens before stringifying.
50
+
51
+ ```ts
52
+ keymap.formatKey("<leader>s", { separator: " " }) // "space s"
53
+ keymap.formatKey("<leader>s", { preferDisplay: true }) // "<leader>s"
54
+ ```
55
+
46
56
  ## Re-entry
47
57
 
48
58
  Runtime/data-style re-entry is supported during dispatch. For example, command
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@opentui/keymap",
3
- "module": "index.js",
4
- "main": "index.js",
3
+ "module": "src/index.js",
4
+ "main": "src/index.js",
5
5
  "types": "src/index.d.ts",
6
6
  "type": "module",
7
- "version": "0.2.0",
7
+ "version": "0.2.2",
8
8
  "description": "Standalone keymap package for OpenTUI",
9
9
  "license": "MIT",
10
10
  "repository": {
@@ -15,42 +15,52 @@
15
15
  "exports": {
16
16
  ".": {
17
17
  "types": "./src/index.d.ts",
18
- "import": "./index.js",
19
- "require": "./index.js"
18
+ "import": "./src/index.js",
19
+ "require": "./src/index.js"
20
+ },
21
+ "./extras": {
22
+ "types": "./src/extras/index.d.ts",
23
+ "import": "./src/extras/index.js",
24
+ "require": "./src/extras/index.js"
20
25
  },
21
26
  "./addons": {
22
27
  "types": "./src/addons/index.d.ts",
23
- "import": "./addons/index.js",
24
- "require": "./addons/index.js"
28
+ "import": "./src/addons/index.js",
29
+ "require": "./src/addons/index.js"
25
30
  },
26
31
  "./addons/opentui": {
27
32
  "types": "./src/addons/opentui/index.d.ts",
28
- "import": "./addons/opentui/index.js",
29
- "require": "./addons/opentui/index.js"
33
+ "import": "./src/addons/opentui/index.js",
34
+ "require": "./src/addons/opentui/index.js"
30
35
  },
31
36
  "./html": {
32
37
  "types": "./src/html.d.ts",
33
- "import": "./html.js",
34
- "require": "./html.js"
38
+ "import": "./src/html.js",
39
+ "require": "./src/html.js"
35
40
  },
36
41
  "./opentui": {
37
42
  "types": "./src/opentui.d.ts",
38
- "import": "./opentui.js",
39
- "require": "./opentui.js"
43
+ "import": "./src/opentui.js",
44
+ "require": "./src/opentui.js"
40
45
  },
41
46
  "./react": {
42
47
  "types": "./src/react/index.d.ts",
43
- "import": "./react/index.js",
44
- "require": "./react/index.js"
48
+ "import": "./src/react/index.js",
49
+ "require": "./src/react/index.js"
45
50
  },
46
51
  "./solid": {
47
52
  "types": "./src/solid/index.d.ts",
48
- "import": "./solid/index.js",
49
- "require": "./solid/index.js"
53
+ "import": "./src/solid/index.js",
54
+ "require": "./src/solid/index.js"
55
+ },
56
+ "./runtime-modules": {
57
+ "types": "./src/runtime-modules.d.ts",
58
+ "import": "./src/runtime-modules.js",
59
+ "require": "./src/runtime-modules.js"
50
60
  }
51
61
  },
52
62
  "dependencies": {
53
- "@opentui/core": "0.2.0"
63
+ "@opentui/core": "0.2.2"
54
64
  },
55
65
  "devDependencies": {
56
66
  "@opentui/react": "workspace:*",
@@ -63,8 +73,8 @@
63
73
  "typescript": "^5"
64
74
  },
65
75
  "peerDependencies": {
66
- "@opentui/react": "0.2.0",
67
- "@opentui/solid": "0.2.0",
76
+ "@opentui/react": "0.2.2",
77
+ "@opentui/solid": "0.2.2",
68
78
  "react": ">=19.0.0",
69
79
  "solid-js": "1.9.12"
70
80
  },
@@ -144,7 +144,7 @@ function stringifyKeyStroke(input, options) {
144
144
  return stringifyCanonicalStroke(normalizeKeyStroke(input));
145
145
  }
146
146
  function stringifyKeySequence(input, options) {
147
- return input.map((part) => stringifyKeyStroke(part, options)).join("");
147
+ return input.map((part) => stringifyKeyStroke(part, options)).join(options?.separator ?? "");
148
148
  }
149
149
  function stringifyCanonicalStroke(stroke) {
150
150
  const parts = [];
@@ -729,6 +729,28 @@ var RESERVED_COMMAND_FIELDS = new Set(["name", "run"]);
729
729
  var RESERVED_BINDING_FIELDS = new Set(["key", "cmd", "event", "preventDefault", "fallthrough"]);
730
730
  var RESERVED_LAYER_FIELDS = new Set(["target", "targetMode", "priority", "bindings", "commands"]);
731
731
 
732
+ // src/services/primitives/command-normalization.ts
733
+ function normalizeBindingCommand(command) {
734
+ if (command === undefined || typeof command === "function") {
735
+ return command;
736
+ }
737
+ const trimmed = command.trim();
738
+ if (!trimmed) {
739
+ throw new Error("Invalid keymap command: command cannot be empty");
740
+ }
741
+ return trimmed;
742
+ }
743
+ function normalizeCommandName(name) {
744
+ const trimmed = name.trim();
745
+ if (!trimmed) {
746
+ throw new Error("Invalid keymap command name: name cannot be empty");
747
+ }
748
+ if (/\s/.test(trimmed)) {
749
+ throw new Error(`Invalid keymap command name "${name}": command names cannot contain whitespace`);
750
+ }
751
+ return trimmed;
752
+ }
753
+
732
754
  // src/services/primitives/field-invariants.ts
733
755
  function mergeRequirement(target, name, value, source) {
734
756
  if (Object.prototype.hasOwnProperty.call(target, name) && !Object.is(target[name], value)) {
@@ -806,26 +828,6 @@ function createCommandChainCacheState() {
806
828
  fallbackWithRecordErrors: new Set
807
829
  };
808
830
  }
809
- function normalizeBindingCommand(command) {
810
- if (command === undefined || typeof command === "function") {
811
- return command;
812
- }
813
- const trimmed = command.trim();
814
- if (!trimmed) {
815
- throw new Error("Invalid keymap command: command cannot be empty");
816
- }
817
- return trimmed;
818
- }
819
- function normalizeCommandName(name) {
820
- const trimmed = name.trim();
821
- if (!trimmed) {
822
- throw new Error("Invalid keymap command name: name cannot be empty");
823
- }
824
- if (/\s/.test(trimmed)) {
825
- throw new Error(`Invalid keymap command name "${name}": command names cannot contain whitespace`);
826
- }
827
- return trimmed;
828
- }
829
831
 
830
832
  class CommandCatalogService {
831
833
  state;
@@ -898,6 +900,19 @@ class CommandCatalogService {
898
900
  bindings: item.bindings
899
901
  }));
900
902
  }
903
+ getCommandBindings(query) {
904
+ const bindingsByCommand = new Map;
905
+ for (const command of query.commands) {
906
+ if (!bindingsByCommand.has(command)) {
907
+ bindingsByCommand.set(command, []);
908
+ }
909
+ }
910
+ if (bindingsByCommand.size === 0) {
911
+ return bindingsByCommand;
912
+ }
913
+ this.collectCommandBindings(bindingsByCommand, this.getCommandQueryContext(query));
914
+ return bindingsByCommand;
915
+ }
901
916
  getResolvedCommandChain(command, focused, includeRecord) {
902
917
  const view = this.getActiveCommandView(focused);
903
918
  const entries = this.getResolvedCommandChainFromView(view, command, focused, includeRecord, "active", view.chainsByName.get(command));
@@ -926,14 +941,26 @@ class CommandCatalogService {
926
941
  cache.set(command, resolved);
927
942
  return resolved;
928
943
  }
929
- getRegisteredResolverFallback(command, includeRecord) {
930
- const view = this.getRegisteredCommandView();
931
- const fallback = this.getFallbackResolvedCommand(view, command, null, includeRecord, "registered");
932
- const hadError = (includeRecord ? view.fallbackWithRecordErrors : view.fallbackWithoutRecordErrors).has(command);
933
- return {
934
- resolved: fallback?.resolved,
935
- hadError
936
- };
944
+ getActiveRegisteredResolvedEntries(command, focused, includeRecord) {
945
+ const view = this.getActiveCommandView(focused);
946
+ const chain = view.chainsByName.get(command);
947
+ if (!chain || chain.length === 0) {
948
+ return;
949
+ }
950
+ const resolved = [];
951
+ for (const entry of chain) {
952
+ resolved.push({
953
+ target: entry.layer.target,
954
+ resolved: resolveRegisteredCommand(entry.command, { includeRecord })
955
+ });
956
+ }
957
+ return resolved;
958
+ }
959
+ resolveRegisteredResolverFallback(command, includeRecord) {
960
+ return this.resolveCommandWithResolvers(command, null, { includeRecord, mode: "registered" });
961
+ }
962
+ resolveActiveResolverFallback(command, focused, includeRecord) {
963
+ return this.resolveCommandWithResolvers(command, focused, { includeRecord, mode: "active" });
937
964
  }
938
965
  getCommandAttrs(command, focused) {
939
966
  const top = this.getTopResolvedCommand(command, focused, false);
@@ -1240,6 +1267,31 @@ class CommandCatalogService {
1240
1267
  }
1241
1268
  }
1242
1269
  }
1270
+ collectCommandBindings(bindingsByCommand, context) {
1271
+ if (context.visibility === "registered") {
1272
+ for (const layer of this.state.layers.layers) {
1273
+ for (const binding of layer.compiledBindings) {
1274
+ this.collectBindingForCommandBindings(bindingsByCommand, binding, context);
1275
+ }
1276
+ }
1277
+ return;
1278
+ }
1279
+ const activeView = context.activeView;
1280
+ if (!activeView) {
1281
+ return;
1282
+ }
1283
+ for (const layer of getActiveLayersForFocused(this.state.layers, this.host, context.focused)) {
1284
+ if (layer.compiledBindings.length === 0 || !this.conditions.layerMatchesRuntimeState(layer)) {
1285
+ continue;
1286
+ }
1287
+ for (const binding of layer.compiledBindings) {
1288
+ if (!this.conditions.matchesConditions(binding) || !this.isBindingVisible(binding, context.focused, activeView)) {
1289
+ continue;
1290
+ }
1291
+ this.collectBindingForCommandBindings(bindingsByCommand, binding, context);
1292
+ }
1293
+ }
1294
+ }
1243
1295
  collectBindingForCommandEntries(grouped, indexesByName, binding) {
1244
1296
  if (typeof binding.command !== "string") {
1245
1297
  return;
@@ -1253,16 +1305,42 @@ class CommandCatalogService {
1253
1305
  if (!item) {
1254
1306
  continue;
1255
1307
  }
1256
- item.bindings.push({
1257
- sequence: binding.sequence,
1258
- command: binding.command,
1259
- commandAttrs: item.command.attrs,
1260
- attrs: binding.attrs,
1261
- event: binding.event,
1262
- preventDefault: binding.preventDefault,
1263
- fallthrough: binding.fallthrough
1264
- });
1308
+ item.bindings.push(this.createActiveBinding(binding, item.command.attrs));
1309
+ }
1310
+ }
1311
+ collectBindingForCommandBindings(bindingsByCommand, binding, context) {
1312
+ if (typeof binding.command !== "string") {
1313
+ return;
1314
+ }
1315
+ const bindings = bindingsByCommand.get(binding.command);
1316
+ if (!bindings) {
1317
+ return;
1318
+ }
1319
+ bindings.push(this.createActiveBinding(binding, this.getCommandBindingAttrs(binding, context)));
1320
+ }
1321
+ createActiveBinding(binding, commandAttrs) {
1322
+ return {
1323
+ sequence: binding.sequence,
1324
+ command: binding.command,
1325
+ commandAttrs,
1326
+ attrs: binding.attrs,
1327
+ event: binding.event,
1328
+ preventDefault: binding.preventDefault,
1329
+ fallthrough: binding.fallthrough
1330
+ };
1331
+ }
1332
+ getCommandBindingAttrs(binding, context) {
1333
+ if (typeof binding.command !== "string") {
1334
+ return;
1335
+ }
1336
+ if (context.visibility === "registered") {
1337
+ return this.getTopRegisteredCommand(binding.command)?.command.attrs;
1265
1338
  }
1339
+ const activeView = context.activeView;
1340
+ if (!activeView) {
1341
+ return;
1342
+ }
1343
+ return this.getBindingCommandAttrs(binding, context.focused, activeView);
1266
1344
  }
1267
1345
  resolveCommandWithResolvers(command, focused, options) {
1268
1346
  const includeRecord = options?.includeRecord === true;
@@ -1457,12 +1535,53 @@ function queryLayerCommandEntries(options) {
1457
1535
  const filter = options.query?.filter;
1458
1536
  let filterEntries;
1459
1537
  let filterPredicate;
1538
+ let exactNameFilter;
1460
1539
  if (typeof filter === "function") {
1461
1540
  filterPredicate = filter;
1462
1541
  } else if (filter) {
1463
- filterEntries = Object.entries(filter);
1542
+ const entries = Object.entries(filter);
1543
+ const remainingEntries = [];
1544
+ for (const [key, matcher] of entries) {
1545
+ if (key === "name") {
1546
+ if (typeof matcher === "string") {
1547
+ exactNameFilter = new Set([matcher]);
1548
+ continue;
1549
+ }
1550
+ if (Array.isArray(matcher)) {
1551
+ const names = new Set;
1552
+ for (const value of matcher) {
1553
+ if (typeof value === "string") {
1554
+ names.add(value);
1555
+ }
1556
+ }
1557
+ exactNameFilter = names;
1558
+ continue;
1559
+ }
1560
+ }
1561
+ remainingEntries.push([key, matcher]);
1562
+ }
1563
+ filterEntries = remainingEntries.length > 0 ? remainingEntries : undefined;
1464
1564
  }
1465
1565
  const results = [];
1566
+ if (exactNameFilter) {
1567
+ for (const entry of options.entries) {
1568
+ const command = entry.command;
1569
+ if (!commandMatchesNamespace(command, namespace)) {
1570
+ continue;
1571
+ }
1572
+ if (!commandMatchesSearch(command, normalizedSearch, searchKeys)) {
1573
+ continue;
1574
+ }
1575
+ if (!exactNameFilter.has(command.name)) {
1576
+ continue;
1577
+ }
1578
+ if (!commandMatchesFilters(command, filterEntries, options)) {
1579
+ continue;
1580
+ }
1581
+ results.push(entry);
1582
+ }
1583
+ return results;
1584
+ }
1466
1585
  for (const entry of options.entries) {
1467
1586
  const command = entry.command;
1468
1587
  if (!commandMatchesNamespace(command, namespace)) {
@@ -1702,7 +1821,7 @@ class CommandExecutorService {
1702
1821
  rejectedResult = execution.result;
1703
1822
  }
1704
1823
  }
1705
- const fallback = this.catalog.getRegisteredResolverFallback(normalized, includeRecord);
1824
+ const fallback = this.catalog.resolveRegisteredResolverFallback(normalized, includeRecord);
1706
1825
  if (fallback.resolved) {
1707
1826
  const execution = this.executeResolvedCommand(normalized, fallback.resolved, {
1708
1827
  keymap: this.options.keymap,
@@ -1735,8 +1854,7 @@ class CommandExecutorService {
1735
1854
  const focused = options?.focused ?? this.activation.getFocusedTargetIfAvailable();
1736
1855
  const event = options?.event ?? this.options.createCommandEvent();
1737
1856
  const data = this.runtime.getReadonlyData();
1738
- const chainLookup = this.catalog.getResolvedCommandChain(normalized, focused, includeRecord);
1739
- const chain = chainLookup.entries;
1857
+ const chain = this.catalog.getActiveRegisteredResolvedEntries(normalized, focused, includeRecord);
1740
1858
  let rejectedResult;
1741
1859
  if (chain?.length === 1) {
1742
1860
  const [entry] = chain;
@@ -1769,7 +1887,21 @@ class CommandExecutorService {
1769
1887
  rejectedResult = execution.result;
1770
1888
  }
1771
1889
  }
1772
- if (chainLookup.hadError) {
1890
+ const fallback = this.catalog.resolveActiveResolverFallback(normalized, focused, includeRecord);
1891
+ if (fallback.resolved) {
1892
+ const execution = this.executeResolvedCommand(normalized, fallback.resolved, {
1893
+ keymap: this.options.keymap,
1894
+ event,
1895
+ focused,
1896
+ target: options?.target ?? null,
1897
+ data
1898
+ });
1899
+ if (execution.status === "handled" || execution.status === "error") {
1900
+ return execution.result;
1901
+ }
1902
+ rejectedResult = execution.result;
1903
+ }
1904
+ if (fallback.hadError) {
1773
1905
  return { ok: false, reason: "error" };
1774
1906
  }
1775
1907
  const unavailable = this.catalog.getDispatchUnavailableCommandState(normalized, focused, includeRecord);
@@ -1881,21 +2013,32 @@ function applyBindingEventEffects(binding, event) {
1881
2013
  }
1882
2014
 
1883
2015
  // src/services/primitives/binding-inputs.ts
1884
- function normalizeBindingInputs(bindings) {
1885
- if (Array.isArray(bindings)) {
1886
- return bindings;
2016
+ function isKeyLike(value) {
2017
+ return typeof value === "string" || !!value && typeof value === "object" && !Array.isArray(value);
2018
+ }
2019
+ function validateBindingInputs(bindings) {
2020
+ if (!Array.isArray(bindings)) {
2021
+ return { ok: false, reason: "Invalid keymap bindings: expected an array of binding objects" };
1887
2022
  }
1888
- const normalized = [];
1889
- for (const [key, cmd] of Object.entries(bindings)) {
1890
- if (typeof cmd !== "string" && typeof cmd !== "function") {
1891
- throw new Error(`Invalid keymap binding for "${key}": shorthand bindings must map to string or function commands`);
2023
+ for (const [index, binding] of bindings.entries()) {
2024
+ if (!binding || typeof binding !== "object" || Array.isArray(binding)) {
2025
+ return { ok: false, reason: `Invalid keymap binding at index ${index}: expected a binding object` };
2026
+ }
2027
+ if (!isKeyLike(binding.key)) {
2028
+ return {
2029
+ ok: false,
2030
+ reason: `Invalid keymap binding at index ${index}: expected "key" to be a string or keystroke object`
2031
+ };
1892
2032
  }
1893
- normalized.push({ key, cmd });
1894
2033
  }
1895
- return normalized;
2034
+ return { ok: true };
1896
2035
  }
1897
2036
  function snapshotBindingInputs(bindings) {
1898
- return normalizeBindingInputs(bindings).map((binding) => ({
2037
+ const validation = validateBindingInputs(bindings);
2038
+ if (!validation.ok) {
2039
+ throw new Error(validation.reason);
2040
+ }
2041
+ return bindings.map((binding) => ({
1899
2042
  ...binding,
1900
2043
  key: typeof binding.key === "string" ? binding.key : { ...binding.key }
1901
2044
  }));
@@ -1948,6 +2091,23 @@ class CompilerService {
1948
2091
  parseObjectKey: (value, options) => this.parseObjectKeyPart(value, options)
1949
2092
  });
1950
2093
  }
2094
+ parseKeySequence(key) {
2095
+ if (typeof key !== "string") {
2096
+ return [this.parseObjectKeyPart(key)];
2097
+ }
2098
+ const parsed = parseBindingSequenceWithParsers(key, this.state.environment.bindingParsers.values(), {
2099
+ tokens: this.state.environment.tokens,
2100
+ layer: EMPTY_COMPILE_FIELDS,
2101
+ parseObjectKey: (value, options) => this.parseObjectKeyPart(value, options)
2102
+ });
2103
+ for (const tokenName of parsed.unknownTokens) {
2104
+ this.options.warnUnknownToken(tokenName, key);
2105
+ }
2106
+ return parsed.parts;
2107
+ }
2108
+ formatKey(key, options) {
2109
+ return stringifyKeySequence(this.parseKeySequence(key), options);
2110
+ }
1951
2111
  compileBindings(bindings, tokens, sourceTarget, sourceLayerOrder, compileFields) {
1952
2112
  const root = createSequenceNode(null, null, null);
1953
2113
  const compiledBindings = [];
@@ -3219,6 +3379,15 @@ class EnvironmentService {
3219
3379
  prependBindingTransformer(transformer) {
3220
3380
  return this.state.environment.bindingTransformers.prepend(transformer);
3221
3381
  }
3382
+ prependLayerBindingsTransformer(transformer) {
3383
+ return this.state.environment.layerBindingsTransformers.prepend(transformer);
3384
+ }
3385
+ appendLayerBindingsTransformer(transformer) {
3386
+ return this.state.environment.layerBindingsTransformers.append(transformer);
3387
+ }
3388
+ clearLayerBindingsTransformers() {
3389
+ this.state.environment.layerBindingsTransformers.clear();
3390
+ }
3222
3391
  appendBindingTransformer(transformer) {
3223
3392
  return this.state.environment.bindingTransformers.append(transformer);
3224
3393
  }
@@ -3417,7 +3586,7 @@ class LayerService {
3417
3586
  let targetMode;
3418
3587
  try {
3419
3588
  targetMode = this.normalizeTargetMode(layer);
3420
- bindingInputs = snapshotBindingInputs(layer.bindings ?? []);
3589
+ bindingInputs = this.applyLayerBindingsTransformers(snapshotBindingInputs(layer.bindings ?? []), layer);
3421
3590
  commands = !layer.commands || layer.commands.length === 0 ? [] : this.options.commands.normalizeCommands(layer.commands);
3422
3591
  commandLookup = createCommandLookup(commands);
3423
3592
  ({ requires, matchers, conditionKeys, hasUnkeyedMatchers, compileFields } = this.compileLayerRuntimeState(layer));
@@ -3570,6 +3739,24 @@ class LayerService {
3570
3739
  }
3571
3740
  return layer.target ? "focus-within" : undefined;
3572
3741
  }
3742
+ applyLayerBindingsTransformers(bindings, layer) {
3743
+ const transformers = this.state.environment.layerBindingsTransformers.values();
3744
+ if (transformers.length === 0) {
3745
+ return bindings;
3746
+ }
3747
+ let current = bindings;
3748
+ for (const transformer of transformers) {
3749
+ const next = transformer(current, {
3750
+ layer,
3751
+ validateBindings: (bindings2) => validateBindingInputs(bindings2)
3752
+ });
3753
+ if (!next) {
3754
+ continue;
3755
+ }
3756
+ current = snapshotBindingInputs(next);
3757
+ }
3758
+ return current;
3759
+ }
3573
3760
  runLayerAnalyzers(options) {
3574
3761
  const analyzers = this.state.layers.layerAnalyzers.values();
3575
3762
  if (analyzers.length === 0) {
@@ -4043,6 +4230,7 @@ function createKeymapState() {
4043
4230
  environment: {
4044
4231
  tokens: new Map,
4045
4232
  layerFields: new Map,
4233
+ layerBindingsTransformers: new OrderedRegistry,
4046
4234
  bindingExpanders: new OrderedRegistry,
4047
4235
  bindingParsers: new OrderedRegistry,
4048
4236
  bindingTransformers: new OrderedRegistry,
@@ -4239,6 +4427,12 @@ class Keymap {
4239
4427
  return getKeyMatchKey(input) === match;
4240
4428
  };
4241
4429
  }
4430
+ parseKeySequence(key) {
4431
+ return this.compiler.parseKeySequence(key);
4432
+ }
4433
+ formatKey(key, options) {
4434
+ return this.compiler.formatKey(key, options);
4435
+ }
4242
4436
  clearPendingSequence() {
4243
4437
  this.activation.setPendingSequence(null);
4244
4438
  }
@@ -4254,11 +4448,8 @@ class Keymap {
4254
4448
  getCommandEntries(query) {
4255
4449
  return this.catalog.getCommandEntries(query);
4256
4450
  }
4257
- normalizeCommandName(name) {
4258
- return normalizeCommandName(name);
4259
- }
4260
- normalizeBindings(bindings) {
4261
- return normalizeBindingInputs(bindings);
4451
+ getCommandBindings(query) {
4452
+ return this.catalog.getCommandBindings(query);
4262
4453
  }
4263
4454
  acquireResource(key, setup) {
4264
4455
  if (this.cleanedUp || this.host.isDestroyed) {
@@ -4305,6 +4496,15 @@ class Keymap {
4305
4496
  registerLayerFields(fields) {
4306
4497
  return this.environment.registerLayerFields(fields);
4307
4498
  }
4499
+ prependLayerBindingsTransformer(transformer) {
4500
+ return this.environment.prependLayerBindingsTransformer(transformer);
4501
+ }
4502
+ appendLayerBindingsTransformer(transformer) {
4503
+ return this.environment.appendLayerBindingsTransformer(transformer);
4504
+ }
4505
+ clearLayerBindingsTransformers() {
4506
+ this.environment.clearLayerBindingsTransformers();
4507
+ }
4308
4508
  prependBindingTransformer(transformer) {
4309
4509
  return this.environment.prependBindingTransformer(transformer);
4310
4510
  }
@@ -4695,6 +4895,48 @@ function registerDefaultKeys(keymap) {
4695
4895
  offParser();
4696
4896
  };
4697
4897
  }
4898
+ // src/addons/universal/binding-overrides.ts
4899
+ function normalizeBindingOverrides(value) {
4900
+ if (!Array.isArray(value)) {
4901
+ throw new Error('Keymap layer field "bindingOverrides" must be an array of binding objects');
4902
+ }
4903
+ return value;
4904
+ }
4905
+ function getBindingOverrides(layer) {
4906
+ const overrides = layer.bindingOverrides;
4907
+ if (!overrides || !Array.isArray(overrides)) {
4908
+ return;
4909
+ }
4910
+ return normalizeBindingOverrides(overrides);
4911
+ }
4912
+ function registerBindingOverrides(keymap) {
4913
+ const offLayerField = keymap.registerLayerFields({
4914
+ bindingOverrides(value) {
4915
+ normalizeBindingOverrides(value);
4916
+ }
4917
+ });
4918
+ const offTransformer = keymap.appendLayerBindingsTransformer((bindings, ctx) => {
4919
+ const overrides = getBindingOverrides(ctx.layer);
4920
+ if (!overrides) {
4921
+ return;
4922
+ }
4923
+ const validation = ctx.validateBindings(overrides);
4924
+ if (!validation.ok) {
4925
+ throw new Error(validation.reason);
4926
+ }
4927
+ const overrideCommands = new Set(overrides.flatMap((binding) => typeof binding.cmd === "string" ? [binding.cmd.trim()] : []));
4928
+ return [
4929
+ ...overrides,
4930
+ ...bindings.filter((binding) => {
4931
+ return typeof binding.cmd !== "string" || !overrideCommands.has(binding.cmd.trim());
4932
+ })
4933
+ ];
4934
+ });
4935
+ return () => {
4936
+ offTransformer();
4937
+ offLayerField();
4938
+ };
4939
+ }
4698
4940
  // src/addons/universal/aliases.ts
4699
4941
  function normalizeAliases(value) {
4700
4942
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -4964,8 +5206,14 @@ function registerEmacsBindings(keymap) {
4964
5206
  }
4965
5207
  // src/addons/universal/ex-commands.ts
4966
5208
  var EMPTY_FIELDS = Object.freeze({});
4967
- function normalizeExCommandName(keymap, name) {
4968
- const normalized = keymap.normalizeCommandName(name);
5209
+ function normalizeExCommandName(_keymap, name) {
5210
+ const normalized = name.trim();
5211
+ if (!normalized) {
5212
+ throw new Error("Invalid keymap command name: name cannot be empty");
5213
+ }
5214
+ if (/\s/.test(normalized)) {
5215
+ throw new Error(`Invalid keymap command name "${name}": command names cannot contain whitespace`);
5216
+ }
4969
5217
  if (normalized.startsWith(":")) {
4970
5218
  return normalized;
4971
5219
  }
@@ -5233,6 +5481,7 @@ export {
5233
5481
  registerDefaultBindingParser,
5234
5482
  registerDeadBindingWarnings,
5235
5483
  registerCommaBindings,
5484
+ registerBindingOverrides,
5236
5485
  registerBackspacePopsPendingSequence,
5237
5486
  registerAliasesField,
5238
5487
  defaultEventMatchResolver,