Core keymap

本页涵盖 @opentui/keymap 的裸引擎,不包含任何宿主特定的适配器。

当您需要自定义宿主或想在共享注册接口之上构建插件时使用裸引擎。如果您需要现成的宿主或需要了解宿主契约,请从宿主开始。

构建 keymap

import { Keymap, type KeymapHost } from "@opentui/keymap"

const keymap = new Keymap(host as KeymapHost<object>)

裸 keymap 在安装绑定解析器和事件匹配解析器之前不会解析字符串绑定。实际上,这通常意味着使用 registerDefaultKeys() 或默认宿主辅助函数之一。

KeymapHost

KeymapHost 是内置宿主和自定义适配器实现的契约。请参阅宿主了解内置的 OpenTUI 和 HTML 适配器。

成员必需描述
metadata平台、主快捷键修饰键和修饰键能力元数据
rootTarget宿主层次结构的根目标
isDestroyed宿主生命周期标志
getFocusedTarget()返回当前聚焦的目标或 null
getParentTarget(target)用于 focus-within 匹配的父级遍历
isTargetDestroyed(target)目标存活检查
onKeyPress(listener)订阅按下事件
onKeyRelease(listener)订阅释放事件
onFocusChange(listener)订阅焦点变更
onTargetDestroy(target, fn)订阅目标释放/移除
createCommandEvent()创建 runCommand()dispatchCommand() 使用的合成事件
onDestroy(listener)可选的宿主销毁通知
onRawInput(listener)可选的原始输入钩子,在宿主按键解析之前使用。监听器在消费序列时返回 true

宿主按键事件必须满足 KeymapEvent

成员描述
name规范化的按键名称
ctrl / shift / meta修饰键状态
super / hyper可选的额外修饰键状态
preventDefault()阻止匹配的事件到达宿主目标
stopPropagation()阻止后续 keymap/宿主监听器看到该事件,并将 propagationStopped 设为 true
propagationStopped调用 stopPropagation() 后为 true

keymap.registerLayer({
  commands: [
    { name: "save-file", title: "Save File", run() {} },
    { name: "quit", run() {} },
  ],
})

keymap.registerLayer({
  target: editor,
  targetMode: "focus-within",
  priority: 10,
  bindings: [
    { key: "ctrl+s", cmd: "save-file", desc: "Save file" },
    { key: "q", cmd: "quit", preventDefault: false },
  ],
})
字段描述
target可选的本地目标。省略则为全局层
targetMode"focus-within""focus"。当 target 存在时默认为 "focus-within"
priority数值大的优先。同一优先级内,较新的层优先
bindingsBinding[]
commands与同一层一起注册的命名命令定义
额外字段已注册的层字段编译器和绑定管道插件使用的自定义字段
  • 无目标的层始终活跃。
  • 活跃层都使用相同的优先级规则:较高的 priority 优先,然后较新的层优先。
  • 命令返回 false 会拒绝该候选项,允许较低优先级的处理程序继续。
  • fallthroughpreventDefault 是独立的。fallthrough 在 keymap 内继续;preventDefault 控制事件是否逃逸到宿主。

绑定

形式示例说明
字符串击键"x""ctrl+x""return"需要已注册的绑定解析器
字符串序列"dd""<leader>s""{count}j"连接的多按键序列
对象击键{ name: "return", ctrl: true }跳过字符串解析并直接规范化
释放绑定{ key: "a", event: "release", ...}释放绑定仅支持单次击键

核心引擎只接受完整的 Binding[] 数组。如果您想要命令到按键的配置语法糖,请使用 @opentui/keymap/extras 中的 commandBindings(map) 在调用 registerLayer() 之前构建数组。

Binding 保留 keycmdeventpreventDefaultfallthrough。所有其他字段可用于绑定字段编译器。

当自定义绑定字段应出现在查询元数据中(如 ActiveBinding.attrsActiveKey.bindingAttrs)时,在绑定字段编译器中使用 attr(...)

Binding 字段默认值:

字段默认值描述
event"press"单次击键释放绑定使用 "release"
preventDefaulttrue匹配后调用 event.preventDefault()event.stopPropagation()
fallthroughfalsetrue 时,同一分发链中后续匹配的绑定在当前命令之后仍然运行

释放绑定在按键释放时分发,但它们不会出现在 getActiveKeys() 中,因为活跃按键发现基于按下/待处理序列状态。

共享的默认解析器理解单按键命名键、修饰键组合、字面标点符号、" " 表示空格、"+" 作为字面加号键、<token> 别名和 {pattern} 运行时捕获。间隔的 Emacs 风格序列如 ctrl+x ctrl+s 不是默认解析器的一部分。

令牌和模式注册名称是无语法的。默认解析器将 <leader> 映射到名为 "leader" 的令牌,将 {count} 映射到名为 "count" 的序列模式。解析器保留的显示形式保持原始语法用于 UI,而引擎存储语义名称。

未知令牌会跳过扩展绑定直到令牌注册。这避免了不安全的回退快捷方式:没有注册 leader 令牌时 <leader> 不活跃,不等于 q。如果扩展器将配置字符串拆分为替代项,只有包含未解析令牌的替代项被跳过。

序列模式可以捕获重复的运行时输入并将最终值传递给命令负载:

模式字段描述
name语义模式名称,也是默认命令负载键
display可选的解析器面向的显示覆盖,用于结构图/帮助 UI
payloadKey当命令负载字段应与 name 不同时的可选覆盖
min / max可选的捕获限制。默认至少一个输入,无实际最大值
match运行时事件谓词,返回捕获的值/显示或 undefined 表示不匹配
finalize可选的从捕获值到最终负载值的转换,例如数字到数值

具有默认无界 max 的模式必须后跟具体的继续,如 {count}j;终端无界模式会被拒绝。

状态和发现

方法描述
setData(name, value)写入运行时数据
getData(name)读取运行时数据
hasPendingSequence()当多按键前缀活跃时为 true
getPendingSequence()当前待处理序列作为 KeySequencePart[]
clearPendingSequence()清除待处理序列
popPendingSequence()移除最后一个待处理击键。当击键被移除时返回 true
getActiveKeys(options?)返回当前焦点和待处理状态下可达的按键
parseKeySequence(key)使用当前解析器/令牌/模式环境解析 KeyLike
formatKey(key, options?)使用当前环境解析并字符串化 KeyLike
createKeyMatcher(key)解析一个 KeyLike 并返回适用于任何 KeyStringifyInput 的单次击键谓词。它不匹配多按键序列
getHostMetadata()返回 registerModBindings() 等插件使用的宿主元数据

parseKeySequence() 也遵循失败关闭令牌规则:包含未知令牌的序列返回空序列并发出与绑定编译相同的未知令牌警告。在为显示解析令牌化字符串时,请先注册令牌。

getActiveKeys() 接受 ActiveKeyOptions

选项默认值描述
includeBindingsfalse包含每按键的 bindings 数组,带有完整的绑定视图
includeMetadatafalse在活跃按键上包含顶层 bindingAttrscommandAttrs

ActiveKey 形状:

字段类型描述
strokeNormalizedKeyStroke规范化的击键(名称加修饰键标志)
displaystring击键的显示字符串,包括使用令牌时保留的令牌形式
tokenNamestring?当此按键通过已注册令牌(如 leader)解析时的令牌名称
continuesboolean当至少一个绑定继续序列超过此按键时为 true
commandBindingCommand?此精确序列处的命令(如有)
bindingsActiveBinding[]?每按键绑定;仅在 includeBindings: true 时填充
bindingAttrsReadonly<Attributes>?所选绑定的编译绑定 attr;仅在 includeMetadata: true 时填充
commandAttrsReadonly<Attributes>?所选绑定的编译命令 attr;仅在 includeMetadata: true 时填充

ActiveBinding 形状:

字段类型描述
sequenceKeySequencePart[]触发绑定的完整按键序列
event"press" | "release"绑定触发的事件
preventDefaultboolean应用默认值后的解析 preventDefault
fallthroughboolean应用默认值后的解析 fallthrough
commandBindingCommand?绑定附带的命令名称或内联处理程序
attrsReadonly<Attributes>?编译的绑定字段 attr
commandAttrsReadonly<Attributes>?绑定命令的编译命令字段 attr

字段和元数据模型

自定义字段是原始配置。编译后的元数据由字段编译器通过 attr(...) 发布。

接口自定义字段元数据输出
额外层属性馈入层字段编译器和绑定管道图快照中的 GraphLayer.attrs
绑定额外绑定属性馈入绑定字段编译器ActiveBinding.attrsActiveKey.bindingAttrs
命令额外命令属性馈入命令字段编译器活跃按键和绑定上的 commandAttrs

字段编译器可以在发布 attr 之前验证和规范化值。它们还可以将仅行为的字段保留在公共元数据之外。

keymap.registerLayerFields({
  name(value, ctx) {
    ctx.attr("name", String(value).trim())
  },
})

keymap.registerCommandFields({
  enabled(value, ctx) {
    ctx.activeWhen(() => value === true)
  },
  title(value, ctx) {
    ctx.attr("title", String(value).trim())
  },
})

在该示例中,层 name 为图/调试 UI 暴露元数据。命令 enabled 控制可用性但不暴露元数据,而命令 title 为命令面板和帮助 UI 暴露规范化的元数据。

使用 stringifyKeyStroke()stringifyKeySequence() 进行显示。规范格式化将 render 渲染为 enterpreferDisplay: true 保留令牌显示如 <leader>。两者都从 @opentui/keymap 导出:

import { stringifyKeyStroke, stringifyKeySequence } from "@opentui/keymap"

命令和查询

命令仅保留 namerun。其他顶层属性是自定义命令字段。

keymap.registerLayer({
  commands: [
    {
      name: "save-file",
      namespace: "file",
      title: "Save File",
      run() {},
    },
  ],
})

// getCommands()[0] 是同一个命令对象:
// { name: "save-file", namespace: "file", title: "Save File", run }

命令字段编译器可以发布元数据、添加要求或附加运行时匹配器。未知命令字段保留在命令对象上;仅因为没有注册编译器不会发出警告。

当命令想以特定编程结果拒绝时,从 run() 返回 { ok: false, reason: "invalid-args" }

命令查询搜索和过滤顶层命令字段和编译命令元数据。namespace 快捷方式读取顶层 namespace 字段。

执行

方法描述
runCommand(cmd, options?)针对已注册的命令链加命令解析器运行命令
dispatchCommand(cmd, options?)通过当前/提供的焦点的活跃分发链运行命令

@opentui/keymap/extras 导出的纯扩展:

辅助函数描述
commandBindings(map)将命令到按键的对象映射转换为 Binding[],在发出之前修剪命令键
createBindingLookup(config)将扁平命令配置解析为命令查找和惰性缓存的聚集绑定组
formatKeySequence(parts, opts)格式化解析的按键序列,支持可选的令牌、按键名称和修饰键别名
formatCommandBindings(...)格式化命令绑定列表,复用 formatKeySequence() 并默认去重

commandBindings() 接受可选的 onWarningonError 回调。onWarning 报告修剪后的命令覆盖(如 " save-file ""save-file");无效的按键值默认被跳过,onError 允许调用者观察它们或在需要严格行为时抛出异常。

formatKeySequence()formatCommandBindings() 是用于已解析 keymap 数据的无状态显示辅助函数。它们不读取运行时状态或宿主配置。当您的应用需要特定于呈现的标签(如用 "alt" 代替 "meta" 或用 "pgup" 代替 "pageup")时,使用 tokenDisplaykeyNameAliasesmodifierAliases

示例:

import { commandBindings } from "@opentui/keymap/extras"

const bindings = commandBindings({
  "  save-file  ": "ctrl+s",
  ":write session.log": "<leader>s",
})

// => [
//      { key: "ctrl+s", cmd: "save-file" },
//      { key: "<leader>s", cmd: ":write session.log" },
//    ]

keymap.registerLayer({ bindings })
import { formatCommandBindings, formatKeySequence } from "@opentui/keymap/extras"

formatKeySequence(keymap.parseKeySequence("<leader>s"), {
  tokenDisplay: { leader: "space" },
  modifierAliases: { meta: "alt" },
  keyNameAliases: { pageup: "pgup", pagedown: "pgdn", delete: "del" },
})

formatCommandBindings(keymap.getCommandBindings({ visibility: "registered", commands: ["save-file"] }).get("save-file"))

createBindingLookup() 是用于扁平命令配置的有主见配置到 keymap 转换。键是命令名称并成为 binding.cmd。值可以是 false"none"、按键、完整绑定对象或按键/绑定对象的数组。false"none" 和空数组不发出绑定。

它有意将命令标识与 UI/层分组分开。组件需要哪些绑定是使用端的考虑,不是命令名称的一部分,因此组件在注册层时从普通命令名称收集命名绑定组。

当面向用户的配置名称应与内部命令名称不同时使用 commandMap。映射在查找创建或更新时发生;运行时查找使用内部命令名称。

import { createBindingLookup } from "@opentui/keymap/extras"

const bindings = createBindingLookup(
  {
    show_palette: "ctrl+p",
    exit_app: ["ctrl+c", "<leader>q"],
    debug_app: "none",
    paste_prompt: { key: "ctrl+v", preventDefault: false },
    prompt_history_previous: "up",
    prompt_history_next: "down",
  },
  {
    bindingDefaults({ binding }) {
      if (binding.group !== undefined) return
      return { group: "App" }
    },
    commandMap: {
      show_palette: "app.palette.show",
      exit_app: "app.exit",
    },
  },
)

keymap.registerLayer({ bindings: bindings.gather("app", ["app.palette.show", "app.exit"]) })
bindings.get("app.exit")
bindings.has("app.exit")
bindings.pick("app", ["app.exit"])
bindings.omit("app", ["app.palette.show"])
bindings.invalidate("app")

查找暴露以下视图:

API描述
bindings所有已启用的绑定,保持配置顺序
get(command)一个精确命令名称的绑定,或命令不存在/已禁用时返回空数组
has(command)精确的内部命令是否有已启用的绑定
gather(name, commands)首次使用时构建并缓存命名绑定组;后续使用相同名称的调用返回缓存
pick(name, commands)从现有聚集组中按调用者命令顺序获取绑定;缺失的组或命令被跳过
omit(name, commands)所有已聚集的绑定中排除匹配的字符串命令绑定,保持组顺序
invalidate(name?)清除一个聚集组,或省略 name 时清除所有聚集组
update(config?)从新或当前配置重建命令查找并清除聚集组

使用 bindingDefaults 为每个发出的绑定添加应用级字段,而不修改原始配置。默认值在解析的绑定之前展开,因此显式绑定字段优先。命令名称是精确的且不被修剪。get()gather()pick()omit() 尽可能直接返回缓存的绑定数组而不是防御性副本。

令牌如 leader 仍通过正常的 keymap 令牌 API 注册,并通过解析器语法如 <leader> 引用。

dispatchCommand() 遵循当前激活状态,当命令存在但当前不可分发时可以返回 inactivedisabledrunCommand() 直接遍历已注册的链,因此即使没有活跃绑定也可以编程执行仅命令的层。

RunCommandResult 成功时为 { ok: true, command? },失败时为 { ok: false, reason, command? }。拒绝原因:

原因含义
not-found没有已注册的命令、解析器或链条目匹配提供的名称
inactive命令存在但没有候选项对提供的/当前焦点活跃(仅 dispatchCommand
disabled所有活跃候选项被 enabled 或其他命令字段匹配器关闭
invalid-args解析器提供的拒绝,如 Ex 命令参数验证
rejected每个候选项的处理程序返回同步 false
error解析器、运行时匹配器或处理程序同步抛出异常

当设置 includeCommand: true 且 keymap 可以为该名称解析命令时,command 包含在拒绝结果中。

命令处理程序可以是同步或异步的。同步 false 拒绝候选项并允许较低优先级的处理程序继续。处理程序也可以返回显式的 RunCommandResult,如 { ok: false, reason: "invalid-args" }。返回的 promise 被视为立即处理;异步拒绝通过 error 诊断事件报告。

RunCommandOptions

选项描述
event传递给命令的事件对象。默认为 host.createCommandEvent()
focused用于活跃层解析的焦点上下文
target显式 ctx.target 覆盖
includeCommandtrue 时,成功和拒绝的结果包含解析的命令对象
payload每次调用的负载,作为 ctx.payload 暴露

查询

方法描述
getCommands(query?)返回命令对象
getCommandEntries(query?)返回命令对象加绑定,用于命令发现 UI
getCommandBindings(query)按请求的命令名称分组返回绑定,用于已知命令标签 UI

CommandQuery 字段:

字段描述
visibility"reachable"(默认)、"active""registered"
focused覆盖查询使用的焦点上下文
namespace按一个或多个命名空间值过滤
search默认按命令名称进行文本搜索
searchIn要搜索的额外命令字段或编译元数据
filter对命令字段和元数据的对象或谓词过滤
  • reachable 使用当前分发获胜者按命令名称去重。
  • active 按优先级顺序保留每个活跃候选项,包括同名重复项。
  • registered 忽略焦点并返回所有已注册的命令。

使用 getCommandEntries() 用于命令面板、可搜索帮助屏幕和其他需要命令加绑定的发现界面。它运行完整的 CommandQuery 并附加匹配的绑定,因此是较重的查询。

当 UI 已经知道要标记的命令只需要其绑定时,使用 getCommandBindings()

const bindingsByCommand = keymap.getCommandBindings({
  visibility: "registered",
  commands: ["file.save", "app.quit"],
})

const saveBindings = bindingsByCommand.get("file.save") ?? []

返回的映射保持请求的命令顺序,当不存在匹配绑定时每个请求的命令包含 []。格式化由应用控制;每个返回的 ActiveBinding 包含其解析的 sequence

CommandBindingsQuery 字段:

字段描述
commands要包含为映射键的命令名称
visibility"reachable"(默认)、"active""registered"
focused覆盖查询使用的焦点上下文

图快照

来自 @opentui/keymap/extras/graphgetGraphSnapshot(keymap, options?) 返回用于图可视化工具和调试 UI 的诊断投影。它包括层、命令、绑定、序列节点、活跃键和当前待处理序列。

层自定义字段作为原始 GraphLayer.fields 仍然可用;通过 ctx.attr(...) 发布的编译层元数据作为 GraphLayer.attrs 出现。使用 attr 作为稳定的可视化元数据,如层显示名称。

import { getGraphSnapshot } from "@opentui/keymap/extras/graph"

keymap.registerLayerFields({
  name(value, ctx) {
    ctx.attr("name", String(value).trim())
  },
})

keymap.registerLayer({
  name: "Global",
  bindings: [{ key: "?", cmd: "toggle-help" }],
})

const snapshot = getGraphSnapshot(keymap)
snapshot.layers[0]?.attrs?.name // "Global"

事件和拦截

keymap.on(...)

事件名称载荷描述
statevoid批量的”派生状态可能已变更”信号
pendingSequenceKeySequencePart[]同步待处理序列更新,包括清除
dispatchDispatchEvent序列和绑定执行跟踪事件
warningWarningEvent验证或分析器警告
errorErrorEvent注册、查询、解析器或监听器错误

DispatchEvent 暴露 phaseeventfocusedsequence 和可选的 layerbindingcommand。阶段为 sequence-startsequence-advancesequence-clearbinding-executebinding-reject

诊断事件不通过 state 批量处理;它们在警告或错误被报告时立即发出。

WarningEventErrorEvent 载荷:

载荷类型字段
WarningEventcodemessagewarning
ErrorEventcodemessageerror

诊断行为:

  • 如果没有注册 warning 监听器,keymap 回退到 console.warn(...) 输出警告。
  • 如果没有注册 error 监听器,keymap 回退到 console.error(...) 输出错误。
  • 控制台回退按事件类型区分。注册 warning 监听器会抑制警告控制台输出但不影响 error,反之亦然。
  • 控制台回退在消息前添加 [code] 前缀。如果底层原因是 Error,它作为第二个控制台参数传递。
  • 抛出异常的 warningerror 监听器不会阻止其余诊断监听器运行,也不会递归地发出另一个 keymap 错误事件。
  • warning 通常涵盖分析器和验证警告,如未知字段、令牌或序列模式。
  • error 不仅涵盖注册失败:它还报告解析器失败、命令查询失败、运行时匹配器失败以及抛出异常的 statependingSequencedispatch 监听器。

keymap.intercept(...)

形式描述
intercept("key", fn)在绑定分发之前运行。上下文暴露 eventsetDatagetDataconsume()
intercept("key:after", fn)在绑定分发之后运行。上下文暴露 handledreasonsequenceeventsetDatagetDataconsume()
intercept("raw", fn)在按键解析之前的原始宿主输入上运行。上下文暴露 sequencestop()

拦截选项:

选项适用于描述
priority全部数值大的先运行
release按键形式监听释放事件而不是按下

key:after 监听器在到达 keymap 监听器的每个按键事件时投递一次,包括无匹配、拒绝绑定、待处理序列和预拦截消费的结果。投递不受 preventDefault()stopPropagation() 控制;这些方法仅影响宿主传播/默认行为。仅当注册了匹配的 key:after 监听器时才构建 after 上下文。

扩展点

API用途
registerToken(token)定义命名的单按键别名,如 leader
registerSequencePattern(pattern)定义命名的运行时序列捕获,如 count
registerLayerFields(fields)添加带有 require(...)attr(...)activeWhen(...) 的自定义层字段
registerBindingFields(fields)添加带有 require(...)attr(...)activeWhen(...) 的自定义绑定字段
registerCommandFields(fields)添加带有 require(...)attr(...)activeWhen(...) 的自定义命令字段
prepend/appendBindingParser(parser)添加新的按键字符串语法
prepend/appendBindingExpander(expander)将一个按键字符串展开为扩展对象,可选地保留显示
prepend/appendBindingTransformer(fn)重写已解析的绑定或添加派生绑定
prepend/appendCommandTransformer(fn)在命令字段之前重写已注册的命令或添加派生命令
prepend/appendCommandResolver(resolver)动态解析字符串命令
prepend/appendLayerAnalyzer(analyzer)层编译时发出警告或错误
prepend/appendEventMatchResolver(fn)添加替代的事件到击键匹配
prepend/appendDisambiguationResolver(fn)为歧义序列选择精确与前缀行为
acquireResource(symbol, setup)在多个插件注册之间共享引用计数的设置/拆卸

上述每个注册方法都返回一个 disposer。对应的 clear*() 方法移除该阶段的所有注册。

消歧义

当同一序列既是精确命令又是更长绑定的前缀时会发生歧义,例如 ggg

同层精确/前缀歧义仅在注册至少一个消歧义解析器时允许。没有解析器时,在同一层注册 ggg 会发出编译错误并保留有效的较早绑定。

keymap.appendDisambiguationResolver((ctx) => {
  if (ctx.sequence.length === 1) {
    return ctx.continueSequence()
  }

  return ctx.runExact()
})

解析器上下文暴露:

成员描述
event不带变异方法的当前按键事件
focused当前聚焦的目标
sequence / stroke当前待处理序列和最新击键
exact当前序列处的精确绑定
continuations可达的继续按键
getData / setData运行时数据访问
runExact()立即分发精确绑定
continueSequence()保持前缀待处理
clear()清除待处理序列并消费按键
defer(handler)启动可取消的异步消歧义工作

消歧义解析器必须同步返回。对于异步行为,使用 ctx.defer(...)。如果没有解析器做出决定,引擎回退到前缀处理并发出警告。

defer(handler) 使用 KeyDeferredDisambiguationContext 调用 handler

成员描述
signalAbortSignal,当新按键、焦点变更或 clearPendingSequence() 取消延迟工作时中止
sequence调用 defer 时捕获的待处理序列
focused调用 defer 时捕获的聚焦目标
sleep(ms)如果超时已过则解析为 true,如果 signal 先中止则为 false
runExact()决定:分发捕获的精确绑定
continueSequence()决定:保持前缀待处理
clear()决定:清除待处理序列

处理程序可以返回决定、Promise<decision> 或不返回任何内容(空操作)。在 signal 中止后不返回内容是规范的取消路径。

约束

  • 运行时/数据重入在分发期间受支持。命令处理程序、拦截器和待处理序列监听器可以读写运行时数据。
  • 结构重入在分发期间不受支持。在分发进行中时不要注册或注销层、解析器、解析器、令牌或类似的环境塑造状态。
  • 宿主被销毁后,getActiveKeys() 等宿主支持的读取不可用。getCommands() 等元数据读取和运行时数据访问仍然可用。