首页
/ Windows Terminal 新标签菜单定制(newTabMenu):从设计规格到源码实现

Windows Terminal 新标签菜单定制(newTabMenu):从设计规格到源码实现

2026-09-04 18:15:38作者:姚月梅Lane

本文以官方规格文档 #1571 - New Tab Menu Customization 为主体,完整讲解 Windows Terminal 中 "newTabMenu" 设置项的设计动机、六种菜单条目类型(profile / separator / folder / action / remainingProfiles / matchProfile)的配置语法与行为规则,并结合仓库中 TerminalSettingsModelTerminalApp 的实际源码,说明这套菜单结构是如何被反序列化、过滤、展开并渲染为 XAML 下拉菜单的。读完后你可以直接写出可落地的 settings.json 定制方案,并能理解 openNewTabDropdown 等关键绑定背后的实现逻辑。

![新标签菜单定制效果示意图](https://gitcode.com/GitHub_Trending/term/terminal/blob/20588130d8ef2ba40eb56bdae88e04cce7fc5b5d/doc/specs/?utm_source=gitcode_repo_files#1571 - New Tab Menu Customization/Menu-Customization-000.png)

一、背景:为什么需要定制新标签下拉菜单

Windows Terminal 允许用户在配置中定义大量 profiles,动态 profile 生成器(如 WSL、Visual Studio、PowerShell Core 等)还会在运行时继续追加 profile。当 profile 数量变多、且其中不少并不常用时,"新标签"下拉菜单会变得非常杂乱。用户普遍希望有一种方式对这个下拉列表进行重排和重新组织

规格文档 doc/specs/#1571 - New Tab Menu Customization/#1571 - New Tab Menu Customization.md(作者 Mike Griese,创建于 2020-5-13,最后更新于 2022-11-18)给出的方案是:新增一个名为 "newTabMenu" 的全局设置。

  • 未设置时(默认):新标签菜单按用户在配置文件中出现的顺序,填入全部 profile;
  • 已设置时:由用户完全控制下拉菜单中呈现的内容与结构。

二、核心配置语法:newTabMenu 的完整示例

规格给出的示例配置如下(原文完整保留):

{
    "profiles":{ ... },
    "newTabMenu": [
        { "type":"profile", "profile": "cmd" },
        { "type":"profile", "profile": "Windows PowerShell" },
        { "type":"separator" },
        {
            "type":"folder",
            "name": "ssh",
            "icon": "C:\\path\\to\\icon.png",
            "entries":[
                { "type":"profile", "profile": "Host 1" },
                { "type":"profile", "profile": "8.8.8.8" },
                { "type":"profile", "profile": "Host 2" }
            ]
        },
        { "type":"separator" },
        { "type":"profile", "profile": "Ubuntu-18.04" },
        { "type":"profile", "profile": "Fedora" }
    ]
}

使用该配置后,新标签下拉菜单呈现为:cmd、Windows PowerShell 两个顶层条目,一条分隔线,一个名为 "ssh" 的嵌套文件夹子菜单(含 3 个远程主机 profile),再一条分隔线,最后是 Ubuntu-18.04 与 Fedora。

六种条目类型逐一详解

newTabMenu 数组中的对象共有六种 type

1. "type":"profile" — 单个 profile 条目 点击该条目会用指定 profile 打开新标签。profile 通过 "profile" 参数标识,接受 profile 的 name 或 GUID。条目的图标取 profile 自身的 icon,显示文本取 profile 的名称。

2. "type":"separator" — 分隔线 对应 XAML 的 MenuFlyoutSeparator,用于在视觉上分隔条目组。

3. "type":"folder" — 嵌套子菜单

  • "name":显示在组上的文本;
  • "icon":图标的图片路径,可选;
  • "entries":嵌套在该条目下的子条目列表,子条目里可以再次包含 "type":"folder",即支持任意深度嵌套;
  • "inline":取值两种
    • auto:当文件夹内只有一个条目时,不再生成嵌套层,直接把该唯一条目放在文件夹本应占据的位置(对只有一个条目的动态 profile 来源很有用);
    • never(默认):即使只有一个子项也始终创建嵌套条目。
  • "allowEmpty":强制该条目即使在没有任何 profile 时也显示在菜单中,默认 false(空文件夹在生成菜单时直接忽略)。为 true 且文件夹为空时,应添加一个占位 <empty> 条目提示该文件夹中没有 profile。规格明确了四种组合的行为:
allowEmpty inline 行为
true auto 完全忽略该条目,不向父级添加占位
true never 添加嵌套条目,内含 <empty> 占位
false auto 完全忽略该条目
false never 完全忽略该条目

4. "type":"action" — 执行快捷键动作 表示一个执行特定 ShortcutAction 的菜单条目。

  • "id" 属性指定全局动作 ID(即全局动作列表,见 issue #6899 / #7175),用于标识用户选中该条目时执行的动作;ID 无效的动作会被忽略并省略
  • 条目文本取动作的 label(全局动作列表中显式提供的 "name",未提供则为生成的名称);
  • 图标同样复用动作的 icon

5. "type":"remainingProfiles" — 兜底条目 一个特殊条目,会展开为所有尚未被列入菜单的 profile 对应的 "type":"profile" 条目,让用户只需一条配置就能补上"我没有手动加进菜单的全部 profile"。

  • 该类型只能出现一次:重复添加会发出警告,且除第一个外全部被忽略;
  • 也可以放在 folder 内,实现"顶层只突出几个 profile,其余全部仍可通过某个子文件夹访问";
  • 展开出的条目名称即 profile 名称,图标即 profile 图标;
  • 不会包含通过 matchProfile 条目已经引入的 profile。

6. "type":"matchProfile" — 按条件批量匹配 展开为所有匹配给定字符串的 profile,让用户无需手工逐个添加即可把一整批 profile 收进某个文件夹。

  • "name""commandline""source" 三个属性用于按 profile 自身对应属性过滤,取值是与 profile 对应属性做完整字符串比较的字符串(规格明确:不是正则、不是部分匹配)。

规格还指出,若选择本路线,也应支持命令面板(Command Palette)所使用的 { "key": "SomeResourceString"} 资源字符串语法来指定可本地化的名称。

三、"默认菜单"的等价表达与默认值建议

规格文档认为,"默认"新标签菜单可以想象成如下 JSON:

{
    "newTabMenu": [
        { "type":"remainingProfiles" }
    ]
}

也即:默认行为 ≈ 菜单里放一个 remainingProfiles 条目

文档进一步给出了一个更结构化的候选默认(userDefaults.json 是用户新建 settings 文件时所用模板,见 userDefaults.json):

{
    "newTabMenu": [
        { "type":"profile", "profile": "cmd" },
        { "type":"profile", "profile": "Windows PowerShell" },
        { "type": "matchProfile", "source": "Microsoft.Terminal.PowerShellCore" }
        {
            "type": "folder",
            "name": "WSL",
            "entries": [ { "type": "matchProfile", "source": "Microsoft.Terminal.Wsl" } ]
        },
        {
            "type": "folder",
            "name": "Visual Studio",
            "entries": [ { "type": "matchProfile", "source": "Microsoft.Terminal.VisualStudio" } ]
        },
        // ... 其他 profile 生成器同理
        { "type": "remainingProfiles" }
    ]
}

该方案会把 cmd、PowerShell 及所有 PowerShell Core 放在根部顶端,其后是各动态 profile 生成器的嵌套条目。规格作者建议仅把这种结构写入 userDefaults.json,避免"动了用户的奶酪"——如果用户已经习惯当前列表(例如默认 profile 是某个 WSL 发行版,改造后从一次点击变成两次点击),就不应被强行改变。

四、源码实现剖析:从 JSON 到 XAML 菜单

结合仓库源码可以看到,该规格已经落地为 TerminalSettingsModel 项目中的一组 WinRT 类型,并遵循"基类 + 类型分派"的结构。

4.1 条目类型与 JSON 反序列化分派

NewTabMenuEntry.cpp 中定义了反序列化入口 FromJson:读取 "type" 键,然后按 NewTabMenuEntryType 枚举分派到六种具体实现:

  • SeparatorSeparatorEntry
  • FolderFolderEntry
  • ProfileProfileEntry
  • RemainingProfilesRemainingProfilesEntry
  • MatchProfilesMatchProfilesEntry
  • ActionActionEntry

无法识别的 type 返回 nullptr(对应"无效条目被忽略"的规格约定)。基类 NewTabMenuEntry.h 的构造函数是受保护的,消费方不能直接实例化基类,只能经过 FromJson 或某个子类构造,这与 JSON 中 type 分派的唯一入口保持一致。

newTabMenu 作为可继承的全局设置在 GlobalAppSettings.idl 中声明为:

INHERITABLE_SETTING(IVector<NewTabMenuEntry>, NewTabMenu);

即它是继承型设置(可被 profiles 之外的顶层设置覆盖),值为 NewTabMenuEntry 的向量。

4.2 folder 的空条目过滤逻辑

FolderEntry.cpp 精确实现了规格中 inline/allowEmpty 的组合行为。其 Entries() 方法只对真正要渲染到 WinRT 侧的条目做过滤,把折叠/展开逻辑集中在一处。关键规则(源码中的注释与逻辑):

  • Profile 条目:若指向的 profile 解析失败(Profile() 为空)则被过滤;
  • RemainingProfiles / MatchProfiles 集合:若展开后 Profiles().Size() == 0 则被过滤;
  • Folder 条目:若其有效条目数为 0,且(不允许为空 inline 为 Auto)则被过滤——即只有 allowEmpty:trueinline:never 的空文件夹会保留(渲染时显示 <empty> 占位,见下文 TerminalPageNewTabMenuFolderEmpty 字符串);
  • 图标路径会经 ResolveMediaResourcesWithBasePath 解析,条目内实现 IPathlessMediaResourceContainer 的子条目被递归解析。

4.3 matchProfile 的匹配实现

规格最初明确采用完整字符串匹配,把正则留作未来扩展(见"Future considerations")。但当前仓库的实现已经前进了一步:MatchProfilesEntry.cppFromJson 中读取 "name""commandline""source" 三个键后,会调用 _validateAndPopulateNameRegex 等函数,通过 til::ICU::CreateRegex 把取值编译为 ICU 正则表达式MatchesProfile 则用 uregex_matches 对 profile 的 Name()Source()Commandline() 依次做正则匹配,任一命中即算匹配。从源码结构看,当前实现实际上是"ICU 正则匹配"(完全匹配的字符串自然是合法正则的特例),这与规格"未来考虑正则"的演进方向一致。若正则在解析期无效,ValidateRegexes 返回 false,对应条目不会被使用。

4.4 菜单渲染:TerminalPage 构建 MenuFlyout 条目

在 UI 层,TerminalPage.cpp 负责把设置模型转换为实际的下拉菜单:

  • _CreateNewTabFlyoutItems(IVector<NewTabMenuEntry> entries)(约 L1155 起)对条目做 switch 分派:Separator 生成 MenuFlyoutSeparatorFolder 生成含子菜单的条目(空文件夹时插入 NewTabMenuFolderEmpty 占位文本);RemainingProfiles/MatchProfiles 按展开出的 profile 列表逐一生成条目;Profile 生成单个 profile 条目;Action 生成动作条目。规格中"profile 图标 + 快捷键 accelerator 文本"的要求也在此处落实——与现在 UI 一致,若有键绑定打开该 profile 的新标签,则作为 accelerator 文本显示在 MenuFlyoutItem 上;
  • 文件末尾固定追加 "Settings"、"Feedback"、"About" 三个条目并以 MenuFlyoutSeparator 分隔,与规格"UI/UX Design"一节"这三项始终存在、不可被该功能移除"的约定一致;
  • 约 L1439 处的处理函数对应 openNewTabDropdown 键绑定(见下节)。

单元测试位于 NewTabMenuTests.cpp,序列化层面的覆盖见 SerializationTests.cpp;设置编辑器(Settings UI)侧的菜单编辑页在 NewTabMenu.cpp

五、对键绑定的影响:index 语义与 openNewTabDropdown

这是规格中"Potential Issues"一节,也是落地时最重要的兼容性考量。

5.1 openTab / splitPane 的 index 参数语义变化

此前 openTabsplitPaneindex 参数有两种含义:

  1. "用第 N 个 profile 创建新标签/窗格";
  2. "用新标签下拉菜单中第 N 项的 profile 创建新标签/窗格"。

两者过去是同义的,因为"第 N 个 profile"总是下拉菜单中的第 N 项。引入 newTabMenu 后二者不再等价,规格明确:index 参数的含义显式改为第一种——"用第 N 个 profile 打开标签/窗格"(profile 列表的顺序,而非菜单顺序)。

5.2 openNewTabDropdown 的 index 行为

为覆盖"按下按键直达菜单中某一项"的场景,考虑给 openNewTabDropdown 动作增加 index 参数。由于菜单中第 N 项未必是 profile(可能是文件夹或动作),规格定义了如下行为("type":"separator" 在计数时忽略):

  • 第 N 个顶层条目是 "type":"profile":执行针对该 profile 的 newTabsplitPane
  • "type":"folder":聚焦其子菜单的第一个元素,用户可继续用键盘导航;
  • "type":"action":直接执行该动作。

规格给出的示例菜单:

New Tab Button ▽
├─ Folder 1
│  └─ Profile A
│  └─ Action B
├─ Separator
├─ Folder 2
│  └─ Profile C
│  └─ Profile D
├─ Action E
└─ Profile F

配合键绑定:

{
  "bindings":
  [
    { "command": { "action": "openNewTabDropdown", "index": 0 }, "keys": "ctrl+shift+1" },
    { "command": { "action": "openNewTabDropdown", "index": 1 }, "keys": "ctrl+shift+2" },
    { "command": { "action": "openNewTabDropdown", "index": 2 }, "keys": "ctrl+shift+3" },
    { "command": { "action": "openNewTabDropdown", "index": 3 }, "keys": "ctrl+shift+4" }
  ]
}

行为分别是:

  • Ctrl+Shift+1:聚焦 "Profile A",需再按 Enter/Space 才创建新标签/窗格;
  • Ctrl+Shift+2:聚焦 "Profile C",需再按 Enter/Space;
  • Ctrl+Shift+3:直接执行 Action E;
  • Ctrl+Shift+4:直接用 Profile F 创建新标签/窗格。

(注意:"type":"folder""type":"separator" 不计入顶层索引,所以 index 0/1 分别指向 Folder 1、Folder 2。)

六、关键设计决策:为什么不复用 profiles 列表结构

规格"Other considerations"一节记录了被否决的替代方案:直接给 profiles 列表本身加入结构,例如:

"profiles": {
  "defaults": {},
  "list":
  [
    { "name": "cmd" },
    { "name": "powershell" },
    { "type": "separator" },
    {
      "type": "folder" ,
      "profiles": [
        { "name": "ubuntu" }
      ]
    }
  ]
}

否决理由:

  1. 这会给 profile 列表对象的内容带来不必要的复杂性;设计上希望 profiles 列表只包含 Profile 对象,让 JSON 的其他部分去引用这些 profile;
  2. 会产生意外的耦合行为:如果有人想让某个动作"打开新标签用第 4 个 profile",而他又把该动作设成了 profile 列表的第 4 项——列表的"位置"含义就混乱了;
  3. 无法表达更多场景:比如一个条目想开一个含两个不同 profile 窗格的标签,或同一 profile 在菜单中出现两次;
  4. 重载 profiles 结构会迫使所有其他 profile 列表消费方都关心列表元素的结构,而它们本应只关心"profile 列表"本身;同时把动作混进 profile 列表也会让 profile 列表本身更复杂。

规格最终选择的方案更干净地分离了"profile 列表"与"新标签菜单内容"两个职责:两者各自独立定义、互不依赖。

matchProfile 的展开顺序

为实现 matchProfile,构建菜单时按如下顺序求值:

  1. 所有显式 profile 条目;
  2. 所有 matchProfile 条目(使用尚未被指定过的 profile);
  3. 最后用 remainingProfiles 展开上述未覆盖到的剩余 profile。

规格示例:

{
    "newTabMenu": [
        { "type": "matchProfile", "source": "Microsoft.Terminal.Wsl" }
        {
            "type": "folder",
            "name": "WSLs",
            "entries": [ { "type": "matchProfile", "source": "Microsoft.Terminal.Wsl" } ]
        },
        { "type": "remainingProfiles" }
    ]
}

对 profile 集合 { "Profile A", "Profile B (WSL)", "Profile C (WSL)" },展开结果为:

New Tab Button ▽
├─ Profile A
├─ Profile B (WSL)
├─ Profile C (WSL)
└─ WSLs
   └─ Profile B (WSL)
   └─ Profile C (WSL)

即同一 profile 可以出现在多处:顶层的 matchProfile 和文件夹内的 matchProfile 都命中 WSL 系列,而 remainingProfiles 不再重复包含它们("不会被 matchProfile 包含的 profile 之外再有")。

七、能力面:可访问性、安全与兼容性

  • 可访问性:菜单以与现行新标签 flyout 完全相同的方式加入 XAML 树,预期无显著变化;
  • 安全 / 可靠性 / 兼容性 / 性能功耗:规格均标注"预期无变化"。

八、未来方向(Future considerations)

规格列举了以下尚待考虑的扩展:

  1. 允许用户对菜单条目手工设置 "name"/"text""icon",覆盖来自 profile 或动作的默认值(完全可选);

  2. 为所有 folder / action 提供默认图标(如文件夹用 📁、动作用 ⚡),当前默认不设置,留待未来评估;

  3. "选中某生成器的全部 profile"这类更复杂的过滤语法(例如把所有 SSH* profile 收进 "SSH" 文件夹),因需要更复杂的属性筛选,被判定超出本规格范围;

  4. 类似结构可复用于定制 control 上下文菜单或标签页上下文菜单(issue #3337);那时可能需要一种方式在 JSON 中引用当前标签/控件的上下文,例如把"关闭标签"实现为 { "action": "closeTab", "index": "${selectedTab.index}" } 这类占位符语法;

  5. matchProfile 引入正则、标签等匹配方式(规格原本考虑默认用正则,但为给正则留余地而先采用完整字符串匹配;若走向正则,建议使用如下结构化写法):

    "type": "profileMatch",
    "source": { "type": "regex", "value": ".wsl." }
    
  6. 扩展 matchProfile 可匹配的更多属性(如 title);

  7. 支持捕获组,例如:

    {
      "type": "profileMatch",
      "name": { "type": "regex", "value": "^ssh: (.*)" }
    }
    

    用于匹配所有 ssh: 前缀的 profile,并用第一个捕获组填充条目名称——["ssh: foo", "ssh: bar"] 将展开为 "foo" 和 "bar" 两个条目。

九、文档更新记录与适用前提

  • 2022 年 2 月更新:基于 issue #11326 与 #7774 的讨论,明确了需要一种简单方式自动汇集一整组 profile 用于菜单排序。尽管讨论焦点是"扩展能否自行提供这种定制",仍新增了 match 语句(即本文的 matchProfile),让用户可以自行过滤 profile。该能力最初被列为"未来考虑",最终被提前纳入。
  • 适用前提:本文以当前仓库代码为准。newTabMenu 的键名、六种 type、folder 的 inline/allowEmpty 语义均可在 TerminalSettingsModel 源码中逐一对应;其中 matchProfile 的匹配在当前实现中已按 ICU 正则处理(见 MatchProfilesEntry.cpp),而规格文本描述的是"完整字符串比较"——使用时请以实际构建版本的实现行为为准,完整字符串在两种语义下均可安全匹配。
  • 配套资料:规格所引用的"Unified keybindings and commands, and synthesized action names"(命令面板附录,动作 ID 与 openNewTabDropdown 命名依据)见 规格文档;动作 ID 体系详见 doc/specs/#6899 - Action IDs/#6899 - Action IDs.md。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
903
1.82 K
docsdocs
暂无描述
Markdown
888
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.51 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341