Windows Terminal 命令面板设计解析:让每个动作都可搜索、可执行、可脱离键位
【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal
本篇基于 Windows Terminal 仓库中#2046号设计规范,系统讲解命令面板(Command Palette)的设计动机、双模式架构(Action Mode 与 Commandline Mode)、commands配置模型与模糊搜索交互,并结合仓库中TerminalSettingsModel、TerminalApp与fzf的实际源码实现,说明该规范如何落地为"无需绑定快捷键即可执行任意终端动作"的完整能力。
特性定位:两种使用模式
命令面板是 Windows Terminal 提供的一个 GUI 组件:用户激活它之后,可以搜索并执行命令,而且这些命令即使没有绑定任何键位也能被调用。这个想法直接来源于 VsCode、Sublime Text 等编辑器中的 Command Palette,最早在 2019 年的年度黑客松上被原型化,最终沉淀为仓库中的 设计规范。
规范将命令面板划分为两个本质不同的工作模式:
- Action Mode(动作模式):快速查找预定义的动作(
ShortcutAction)并派发它; - Commandline Mode(命令行模式):输入一条
wt.exe风格的命令行,解析后立即应用到当前终端窗口。
这两个模式在 UI 上共享同一套界面,通过前缀字符切换(详见后文"模式切换"一节)。
Action Mode:commands配置与Command模型
Action Mode 的核心是在用户设置中引入顶层数组commands。规范定义的元素 schema 为:
{ "name": string|object, "action": string|object, "icon": string }命令名应是人类友好的动作名称,不必与动作本身严格对应——例如 action 为newTab的命令可以叫"Open New Tab"。
规范最初设想的Command类接口为:
class Command { winrt::hstring Name(); winrt::TerminalApp::ActionAndArgs ActionAndArgs(); winrt::hstring IconSource(); }对照仓库实现,这一设计已落地为 WinRT 类型 Command.h:其中的struct Command提供Name()、ActionAndArgs()、Icon(),并额外演进出了ID()(命令唯一标识)、NestedCommands()(嵌套子命令)与IterateOn(展开类型)等属性。规范中"name 可以是字符串或资源对象"的双形态在实现里由内部的CommandNameOrResource结构承载,FromJson负责解析,ExpandCommands负责按 profile 展开,LayerJson负责多来源设置的按名叠加。规范提到的把ActionAndArgs派发逻辑抽到统一派发类中、供键位绑定复用的思路,与当前从源码结构看由IActionMapView(见 ActionMap.cpp)统一供给 CommandPalette.h 中SetActionMap的做法一脉相承:面板、键位、设置编辑器共享同一张动作表。
列表绑定与派发链路
规范要求Command成为实现INotifyPropertyChanged的 WinRT 类型,以便把 XAMLListView元素绑定到命令对象。实际实现中,列表项绑定的是 FilteredCommand.cpp 包装后的命令(携带模糊匹配的高亮信息),点击/回车时由 CommandPalette.cpp 取出该列表项关联的ActionAndArgs并派发,触发的事件处理器与按下对应键位完全一致。
按 Profile 展开命令:iterateOn
规范中最有前瞻性的部分,是解决 [#3879] 提出的"直接在命令面板中启动某个 profile"的需求。若为每个 profile 手工维护一条命令,用户每加一个 profile 就要改一次配置。规范的解法是一种命令展开机制:用户写一条模板命令,系统为每个 profile 复制出一份:
"commands": [ { "expandOn": "profiles", "icon": "${profile.icon}", "name": "New Tab with ${profile.name}", "command": { "action": "newTab", "profile": "${profile.name}" } }, { "expandOn": "profiles", "icon": "${profile.icon}", "name": "New Vertical Split with ${profile.name}", "command": { "action": "splitPane", "split":"vertical", "profile": "${profile.name}" } } ],其中:
"expandOn": "profiles"表示该命令要对列表中每个 profile 重复一次;${profile.name}这类格式串在展开时被替换为对应 profile 的name,从而让每条展开命令的文本与图标都个性化。
规范对实现顺序给出了明确要求:展开必须放在所有设置解析完成之后(即Validate阶段),否则来自不同来源的 profile 尚未汇聚完毕,会产生不完整的命令列表。具体做法是:初次解析时保留${...}占位符不展开,同时暂存命令的原始 JSON;到验证阶段遇到expandOn命令时,将其作为模板,对每个 profile 做字符串查找替换生成新的 JSON,再解析为一条新命令加入列表。
在仓库中,这一机制的最终形态是 defaults.json 中的iterateOn键(键名常量见 Command.h 的IterateOnKey,展开逻辑在 Command.cpp 的ExpandCommands/_expandCommand)。当前默认设置里就有三个实例,包括对 color scheme 展开的"Select color scheme..."、对 profile 展开的"New tab...",以及两层嵌套的"Split pane..."(先按 profile 展开,再给出 auto/up/down/left/right 四种分割方式),完整覆盖了规范设想的 profile 展开场景。
Commandline Mode:把wt命令行派发给当前窗口
规范指出的用户痛点是:想在自己正使用的 WT 窗口内直接执行wt.exe命令行(如切换 profile、开新 tab),但目前没有可靠办法判断wt是从另一个 WT 实例调用来并透传参数。命令面板因此成为这一长期方案落地前的自然过渡:
用户在命令行模式下直接输入一条wt.exe风格命令行(无需以wt/wtd开头——既然已在 WT 里,不必重复),回车后系统解析该命令行并派发到当前窗口。例如输入new-tab就在当前窗口开新 tab;也可以像 shell 中wt一样串联多个命令:
split-pane -p "Windows PowerShell" ; split-pane -H wsl.exe这会执行两次SplitPane动作:一个用 "Windows PowerShell" profile,另一个用默认 profile 运行wsl。
交互细节上:回车后若解析成功,则关闭面板并派发;若有错误,则在文本输入框下方显示错误信息,面板保持打开、保留用户输入以便修改重试——错误提示在用户开始编辑命令几秒后(理想情况下带动画)自动隐藏。
UI/UX 设计:全局覆盖层、列表交互与模糊搜索
规范要求面板打开时作为覆盖所有 pane 的单一 overlay出现:水平居中、从顶部(tab 行)向下展开。这一决策明确避开了"把面板挂在单个 pane 上"的两个问题:小 pane 尺寸下 UI 容易变得拥挤,以及编辑器中常见的"找 overlay"问题。当输入命令时,隐含的派发目标是当前聚焦的 terminal pane。
面板由两个主要 UI 元素构成:命令输入/搜索文本框,以及 Action Mode 下的命令列表。列表交互规则:
- 默认填充全部命令,每项形如
MenuFlyoutItem(左侧图标 + 名称); - 打开时自动高亮第一项;
- 方向键导航,Enter关闭面板并执行高亮项,Escape关闭面板并把焦点交还终端;
- 无论以何种原因(执行命令、Escape、
toggleCommandPalette键位)关闭,都清空搜索文本,便于下次全新开始。
模糊搜索是规范着力强调的能力。它要求比简单字符串比较更强:用户输入搜索串后返回所有"模糊匹配"的命令,从而无需打全名即可定位。规范给出的示例命令列表:
"commands": [ { "icon": null, "name": "New Tab", "action": "newTab" }, { "icon": null, "name": "Close Tab", "action": "closeTab" }, { "icon": null, "name": "Close Pane", "action": "closePane" }, { "icon": null, "name": "[-] Split Horizontal", "action": { "action": "splitPane", "split": "horizontal" } }, { "icon": null, "name": "[ | ] Split Vertical", "action": { "action": "splitPane", "split": "vertical" } }, { "icon": null, "name": "Next Tab", "action": "nextTab" }, { "icon": null, "name": "Prev Tab", "action": "prevTab" }, { "icon": null, "name": "Open Settings", "action": "openSettings" }, { "icon": null, "name": "Open Media Controls", "action": "openTestPane" } ],匹配行为示例:
"open"命中 "OpenSettings" 与 "OpenMedia Controls";"Tab"命中 "NewTab"、"CloseTab"、"NextTab"、"PrevTab";"P"命中 "ClosePane"、"[-] Split Horizontal"、"[ | ] Split Vertical"、"Prev Tab"、"Open Settings"、"Open Media Controls";- 更强的是
"sv"命中 "[ | ] Split Vertical"(先匹配 "Split" 中的S,再匹配 "Vertical" 中的V)——这是"极少按键即可执行命令"的典型案例。
输入过程中,每个匹配字符应在命令名中加粗,让输入与结果的对应关系直观可见。列表还应展示该命令绑定的键位(如果有的话)。
从源码结构看,模糊搜索由内置的 fzf 匹配器驱动:matcher::ParsePattern解析模式、Match返回带Score与Runs(命中片段区间,用于加粗高亮)的MatchResult。规范在"潜在问题"一节担心非英文语言下"一个字符可能占多个 char"会破坏模糊匹配——实现上Pattern/UChar32(配合 ICU 头文件icu.h)表明匹配在 32 位码点层面进行,正面回应了这一顾虑。匹配行为有专门的单测覆盖,见 FilteredCommandTests.cpp。
模式切换:前缀字符的规范讨论与落地形态
规范中"两种模式如何区分"当时标记为TODO(待讨论)。作者借鉴了 VsCode 的前缀式切换:@切到符号导航、:切到行号跳转、>切到编辑器命令模式;删除前缀字符则切回默认模式。两种候选方案:
- 默认 Action Mode,输入
:进入 Commandline Mode(类似 tmux 的<prefix>-:命令提示); - 对齐 VsCode:默认 Commandline Mode,输入
>进入 Action Mode(作者认为>与 cmd 默认%PROMPT%结尾相同,语义上略"反直觉")。
规范还要求:输入为空(除前缀外)时显示占位提示,如 Action Mode 的"Enter a command name..."、Commandline Mode 的"Type a wt commandline..."。
仓库实现印证了这一设计的最终走向:CommandPalette.h 暴露可观察属性PrefixCharacter与SearchBoxPlaceholderText,私有方法_evaluatePrefix负责根据输入前缀切换模式;且CommandPaletteMode枚举已扩展为四个值:ActionMode、TabSearchMode、TabSwitchMode、CommandlineMode——规范"未来考虑"一节中借@前缀做"Navigate Mode"(在 tab/pane 之间导航)的设想,已具象为 Tab Search/Tab Switch 两种模式。
叠加与"解绑":修改和移除默认命令
由于会随终端内置一组默认命令,用户必然需要修改或删除其中一些。规范的规则是:命令按name属性求值后的值叠加(layer)。默认命令全部使用"name": { "key": "KeyName" }的本地化资源形式,因此用户可以用该资源的本地化字符串去覆盖对应命令。例如,若NewTabCommandName求值为 "Open New Tab",则:
{ "icon": null, "name": { "key": "NewTabCommandName" }, "action": "newTab" },可以被以下命令覆盖:
{ "icon": null, "name": "Open New Tab", "action": "splitPane" },若想把某条命令从面板中移除,将其 action 设为null即可:
{ "icon": null, "name": "Open New Tab", "action": null },实现侧对应 Command.cpp 的LayerJson(按名分桶叠加,用户来源命令覆盖默认来源命令)。
能力评估:可靠性、安全、本地化
可靠性:无效命令应被忽略而非崩溃。无效的定义是:name为 null 或空字符串,或action为 null、或不是真实的ShortcutAction。规范明确这类问题不值得弹错误对话框打扰用户。
安全:不引入新的安全面——依赖 jsoncpp 解析 JSON 的既有安全性,新增的 settings 键同样走 jsoncpp 的安全解析路径。
本地化:内置命令列表必须可本地化。方案是在 JSON 中支持"资源对象"语法:name若是字符串则作为字面文本;若是对象,则用其key属性到ResourceDictionary中查找本地化字符串。这样内置命令全部可出多语言,同时用户仍可轻松添加自己的命令。规范还记录了评审时的其他备选方案(每个 locale 一份defaults.json、按 locale 独立构建)均不可行,最终采纳了能复用平台既有本地化支持的资源 key 方案。
规范给出的本地化默认命令示例:
"commands": [ { "icon": null, "name": { "key": "NewTabCommandName" }, "action": "newTab" }, { "icon": null, "name": { "key": "CloseTabCommandKey" }, "action": "closeTab" }, { "icon": null, "name": { "key": "ClosePaneCommandKey" }, "action": "closePane" }, { "icon": null, "name": { "key": "SplitHorizontalCommandKey" }, "action": { "action": "splitPane", "split": "horizontal" } }, { "icon": null, "name": { "key": "SplitVerticalCommandKey" }, "action": { "action": "splitPane", "split": "vertical" } }, { "icon": null, "name": { "key": "NextTabCommandKey" }, "action": "nextTab" }, { "icon": null, "name": { "key": "PrevTabCommandKey" }, "action": "prevTab" }, { "icon": null, "name": { "key": "OpenSettingsCommandKey" }, "action": "openSettings" } ],默认命令集
规范建议的默认命令基本就是"默认有键位绑定的那些动作":
"commands": [ { "icon": null, "name": { "key": "NewTabCommandKey" }, "action": "newTab" }, { "icon": null, "name": { "key": "DuplicateTabCommandKey" }, "action": "duplicateTab" }, { "icon": null, "name": { "key": "DuplicatePaneCommandKey" }, "action": { "action": "splitPane", "split":"auto", "splitMode": "duplicate" } }, { "icon": null, "name": { "key": "SplitHorizontalCommandKey" }, "action": { "action": "splitPane", "split": "horizontal" } }, { "icon": null, "name": { "key": "SplitVerticalCommandKey" }, "action": { "action": "splitPane", "split": "vertical" } }, { "icon": null, "name": { "key": "CloseWindowCommandKey" }, "action": "closeWindow" }, { "icon": null, "name": { "key": "ClosePaneCommandKey" }, "action": "closePane" }, { "icon": null, "name": { "key": "OpenNewTabDropdownCommandKey" }, "action": "openNewTabDropdown" }, { "icon": null, "name": { "key": "OpenSettingsCommandKey" }, "action": "openSettings" }, { "icon": null, "name": { "key": "FindCommandKey" }, "action": "find" }, { "icon": null, "name": { "key": "NextTabCommandKey" }, "action": "nextTab" }, { "icon": null, "name": { "key": "PrevTabCommandKey" }, "action": "prevTab" }, { "icon": null, "name": { "key": "ToggleFullscreenCommandKey" }, "action": "toggleFullscreen" }, { "icon": null, "name": { "key": "CopyTextCommandKey" }, "action": { "action": "copy", "singleLine": false } }, { "icon": null, "name": { "key": "PasteCommandKey" }, "action": "paste" }, { "icon": null, "name": { "key": "IncreaseFontSizeCommandKey" }, "action": { "action": "adjustFontSize", "delta": 1 } }, { "icon": null, "name": { "key": "DecreaseFontSizeCommandKey" }, "action": { "action": "adjustFontSize", "delta": -1 } }, { "icon": null, "name": { "key": "ResetFontSizeCommandKey" }, "action": "resetFontSize" }, { "icon": null, "name": { "key": "ScrollDownCommandKey" }, "action": "scrollDown" }, { "icon": null, "name": { "key": "ScrollDownPageCommandKey" }, "action": "scrollDownPage" }, { "icon": null, "name": { "key": "ScrollUpCommandKey" }, "action": "scrollUp" }, { "icon": null, "name": { "key": "ScrollUpPageCommandKey" }, "action": "scrollUpPage" } ]当前仓库的 defaults.json 中这份默认集已显著扩充,并普遍携带稳定的id字段(如Terminal.ScrollDown、Terminal.IncreaseFontSize)以便按 id 引用;同时把"Select color scheme..."(iterateOn: "schemes")、"New tab..." 与"Split pane..."(iterateOn: "profiles"两层嵌套)作为嵌套命令父项提供,即规范"未来考虑"中的嵌套命令已在默认配置中生效。
无障碍与已知依赖
- 无障碍:面板整体是原生 XAML 元素,自动接入 UIA 树,屏幕阅读器可自然发现它;打开面板时自动获得焦点;面板打开期间 terminal pane 不可交互,从而保持面板打开时 UIA 树的简洁。
- 潜在依赖:规范要求先完成 [#1205](把"活动终端"的判定与"当前 XAML 聚焦元素"解耦)——否则面板打开时焦点移出 terminal control 会引起难以调试的崩溃。此外需保证模糊搜索算法对非英文语言健壮(实现上如前所述由 fzf 在码点层面匹配)。
- 性能:面板打开时增加若干 XAML 元素会抬升运行时内存占用;多出的 JSON 解析量对加载时间影响可忽略。
未来方向:规范中的延伸设想与仓库中的落地
规范末尾"未来考虑"一节列出的多个方向,不少已能在当前源码中找到对应实现,可视为该规范持续演进的路线图:
- 嵌套命令(Nested Commands):一个命令下挂多个子命令,顶层列表只显示父项、按需进入下一层,从而让顶层列表更简洁、相关命令更聚合。规范给出了完整 JSON 构想(如 "Open New Tab..." 下按 profile 嵌套、"Connect to ssh..." 下挂多条
newTab+commandline的 ssh 快捷项),并强调面板不是树形展示——一次只显示一层的命令,进入父项后 UI 切换为其子命令列表。这一机制如今正是 defaults.json 中 "New tab..."/"Split pane..." 的形态,Command的HasNestedCommands/NestedCommands与面板中的_nestedActionStack/_updateCurrentNestedCommands提供了支撑。 inputCommand动作:向 shell 输入一条命令并(可选地)附带换行执行,参数为commandline与可选的suppressNewline(默认 false)。规范同时提醒:这在 shell 提示符下效果好,在 vim 等应用中则只会把文本写进该应用的缓冲区。- Commandline Mode 历史:保存用户输入过的命令行以便重输。实现中可见
CommandPalette维护CommandLineHistoryLength = 20条近期命令,并有_loadRecentCommands/_updateRecentCommands静态方法,规范中"跨启动持久化暂无存储手段"的问题也有了演进方向。 - Tab 导航复用面板 UI:规范借 VsCode 的 Advanced Tab Switcher 提出 "Navigate Mode"(
@前缀),可复用面板 UI 按控件名在 tab/pane 间切换。当前CommandPaletteMode枚举中的TabSearchMode/TabSwitchMode及其SetTabs/EnableTabSwitcherMode/EnableTabSearchMode接口即该方向的落地。 - 模糊搜索排序优化:给"连续匹配字符更多"或"命中文首字母"的命令更高权重,例如
ot应对 "Open Tab" 高于 "Open Settings"。fzf 的Score字段正是这类排序的基础。 - 可发现性:在 New Tab 下拉菜单或 UI 上增加"显示命令面板"入口(可被全局设置隐藏),因为面板主要靠键位触达、发现成本较高。
- MRU 行为:未来可加设置让用户选择把最近常用命令排在搜索结果最前,以及把上次输入预填入搜索框——规范倾向把这两者设计为两个独立设置。
- 前缀可配置化:规范给出了设想的 settings 形态并指出校验难点(两个前缀都为
null或都非空时如何取舍):
{ "commandPaletteActionModePrefix": "", // or null, for no prefix "commandPaletteCommandlineModePrefix": ">" }- 扩展生态:命令面板是扩展把自己的动作加入 UI 的便捷入口,不强制用户为扩展动作绑定键位。
后续规范与延伸阅读
本规范有一条后续规范,在其基础上引入了统一键位与命令、以及合成动作名称等变更,原文档明确指引参阅,仓库中即 Unified keybindings and commands, and synthesized action names。
继续阅读建议:
- 规范原文:Command Palette 规范
- 命令模型实现:Command.h、Command.cpp
- 面板 UI 与交互:CommandPalette.h、CommandPalette.cpp
- 模糊匹配:fzf.h,单测 FilteredCommandTests.cpp
- 默认命令与嵌套命令实例:defaults.json
- 用户侧 JSON 设置用法:UsingJsonSettings.md
需要注意的是:本仓库当前处于规范与实现的持续演进状态,expandOn等早期键名在最终实现中定名为iterateOn,默认命令集也已超出规范初稿范围;阅读具体行为时,以defaults.json与TerminalSettingsModel源码为准。
【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考