Windows Terminal 添加设置完全指南:从 Terminal Settings Model 到 Settings UI 的完整链路
【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal
本文为 Windows Terminal(OpenConsole 仓库)的开发者指南,系统讲解如何向 Terminal 添加一个新设置(Profile 设置、全局设置)或新 Action(含 Action 参数)。文档基于doc/cascadia/AddASetting.md的核心步骤展开,并结合当前仓库中src/cascadia/TerminalSettingsModel/项目的真实源码印证每一步的底层机制。读完本文,你将掌握:INHERITABLE_SETTING宏的继承与反序列化原理、设置如何从 JSON 流转到 XAML 控件、Action 事件的派发链路,以及设置 UI 中可绑定枚举的完整实现方式。
1. 总览:一个设置的三层实现结构
在 Windows Terminal 中,"添加一个设置"并不是改一个配置文件那么简单。设置系统被拆成三个职责清晰的项目:
- Terminal Settings Model(
Microsoft.Terminal.Settings.Model):负责设置的 (de)serialization(序列化/反序列化)与对外暴露,是整个设置系统的数据层核心; - 功能层(Setting Functionality):
TerminalApp等消费方项目读取模型中已加载的设置并驱动 UI 行为; - Settings UI(
TerminalSettingsEditor):设置编辑界面,将设置绑定为可交互控件并本地化。
添加设置的标准流程即依次完成这三层:先在 Model 中声明并支持序列化 → 再让功能代码读取生效 → 最后在设置 UI 中暴露。
2. Terminal Settings Model:声明一个设置
2.1INHERITABLE_SETTING宏与继承机制
INHERITABLE_SETTING宏用于为新设置实现"继承"能力并在模型中存储该设置。它接收三个参数:
type:设置存储所用的类型;name:存储用变量的名称;defaultValue:用户在任何地方都未定义该设置时使用的值。
从源码看,该宏的真实定义位于 IInheritable.h,它展开后会自动生成一组方法,这正是设置继承机制的全部秘密:
| 宏展开产物 | 职责 |
|---|---|
Has<name>() | 判断用户是否显式设置了该值(返回_name.has_value()) |
<name>OverrideSource() | 返回实际提供该解析值的父对象(用于 UI 展示"继承自哪里") |
Clear<name>() | 清除用户设置值,使该设置回落到继承值 |
<name>()getter | 返回解析后的值,回退顺序为:用户设置值 → 继承值 → 系统默认值 |
<name>(value)setter | 直接覆写用户设置值 |
内部存储为std::optional<type>(见 _BASE_INHERITABLE_SETTING 宏),nullopt即表示"需从父对象继承"。解析时沿_parents向量依次向上查找第一个有值的祖先,找不到才返回默认值。这套机制支撑了 Windows Terminal 设置中"默认配置 → 配置文件 → 具体 Profile(含"parent"继承链)"的多层叠加模型。
当前仓库中还有两个变体值得注意:
INHERITABLE_SETTING_WITH_LOGGING(IInheritable.h#L211):setter 时记录设置变更日志,Profile 设置目前统一经由它初始化;INHERITABLE_NULLABLE_SETTING(IInheritable.h#L236):用于"null 本身是合法值"的可选设置(如Profile.Foreground),以IReference形式对外暴露,区别于"nullopt 表示继承"的语义。
2.2 添加一个 Profile 设置(以CloseOnExit为例)
以下以教程中的示例——为 Profile 添加CloseOnExitMode CloseOnExit设置——走一遍完整步骤。
第 1 步:在Profile.h中声明/定义设置。
INHERITABLE_SETTING(CloseOnExitMode, CloseOnExit, CloseOnExitMode::Graceful)当前仓库的演进:在最新代码中,Profile 设置的注册已收敛到 X-macro 列表。MTSMSettings.h 中定义了
MTSM_PROFILE_SETTINGS(X),每个条目按(type, name, jsonKey, defaultArgs)格式声明,例如:X(CloseOnExitMode, CloseOnExit, "closeOnExit", CloseOnExitMode::Automatic)随后 Profile.h 通过
PROFILE_SETTINGS_INITIALIZE将该列表展开为INHERITABLE_SETTING_WITH_LOGGING实例。相比文档中逐行手写,这种集中式注册把"新增设置"压缩为在列表中加一行,同时 jsonKey 与默认值都显式出现在同一处。
第 2 步:在Profile.idl中通过 WinRT 暴露设置:
Boolean HasCloseOnExit(); void ClearCloseOnExit(); CloseOnExitMode CloseOnExit;第 3 步:在Profile.cpp中添加 (de)serialization 与复制逻辑:
// Top of file: // - Add the serialization key static constexpr std::string_view CloseOnExitKey{ "closeOnExit" }; // CopySettings() or Copy(): // - The setting is exposed in the Settings UI profile->_CloseOnExit = source->_CloseOnExit; // LayerJson(): // - get the value from the JSON JsonUtils::GetValueForKey(json, CloseOnExitKey, _CloseOnExit); // ToJson(): // - write the value to the JSON JsonUtils::SetValueForKey(json, CloseOnExitKey, _CloseOnExit);- 若设置不是基础类型(primitive type),还需在
TerminalSettingsSerializationHelpers.h中为它添加枚举/枚举标志的 (de)serialization 逻辑:
// For enum values... JSON_ENUM_MAPPER(::winrt::Microsoft::Terminal::Settings::Model::CloseOnExitMode) { JSON_MAPPINGS(3) = { pair_type{ "always", ValueType::Always }, pair_type{ "graceful", ValueType::Graceful }, pair_type{ "never", ValueType::Never }, }; }; // For enum flag values... JSON_FLAG_MAPPER(::winrt::Microsoft::Terminal::TerminalControl::CopyFormat) { JSON_MAPPINGS(5) = { pair_type{ "none", AllClear }, pair_type{ "html", ValueType::HTML }, pair_type{ "rtf", ValueType::RTF }, pair_type{ "all", AllSet }, }; }; // NOTE: This is also where you can add functionality for... // - overloaded type support (i.e. accept a bool and an enum) // - custom (de)serialization logic (i.e. coordinates)当前仓库的实证:TerminalSettingsSerializationHelpers.h#L169-L194 中的
CloseOnExitModemapper 正好演示了注释提到的"重载类型支持":FromJson被覆写以同时接受布尔值(true→Graceful,false→Never,兼容旧版布尔写法),并且映射表已扩展到 4 个值——新增了"automatic"。默认值也随之更新:MTSMSettings.h#L105 与 defaults.json 中均为"closeOnExit": "automatic"。
2.3 添加一个 Global 设置
步骤与 Profile 设置完全相同,只是把修改对象换成GlobalAppSettings相关文件(GlobalAppSettings.idl/.h/.cpp,位于src/cascadia/TerminalSettingsModel/)。当前仓库中全局设置同样通过 MTSMSettings.h 的 X-macro 集中注册,且区分了两类:
MTSM_GLOBAL_ONLY_SETTINGS:真正全局的设置(如language、firstWindowPreference、alwaysShowNotificationIcon),不随窗口变化;MTSM_WINDOW_SETTINGS:按窗口变化的设置(如initialRows、copyOnSelect、launchMode)。
二者并集构成MTSM_GLOBAL_SETTINGS,由GlobalAppSettings统一承载。
2.4 添加一个 Action
以下以教程中的openSettingsaction 为例。
第 1 步:在KeyMapping.idl中声明该 action:
// Add the action to ShortcutAction enum ShortcutAction { OpenSettings }第 2 步:在ActionAndArgs.cpp中添加序列化逻辑:
// Top of file: // - Add the serialization key static constexpr std::string_view OpenSettingsKey{ "openSettings" }; // ActionKeyNamesMap: // - map the new enum to the json key { OpenSettingsKey, ShortcutAction::OpenSettings },第 3 步(可选):让 Action 在命令面板(Command Palette)中出现时自动生成名称:
// In ActionAndArgs.cpp GenerateName() --> GeneratedActionNames { ShortcutAction::OpenSettings, RS_(L"OpenSettingsCommandKey") }, // In Resources.resw for Microsoft.Terminal.Settings.Model.Lib, // add the generated name // NOTE: Visual Studio presents the resw file as a table. // If you choose to edit the file with a text editor, // the code should look something like this... <data name="OpenSettingsCommandKey" xml:space="preserve"> <value>Open settings file</value> </data>第 4 步(可选):如果该 Action 支持参数
- 在
ActionArgs.idl中声明参数:
[default_interface] runtimeclass OpenSettingsArgs : IActionArgs { // this declares the "target" arg SettingsTarget Target { get; }; };- 在
ActionArgs.h中定义新的 runtime class:
struct OpenSettingsArgs : public OpenSettingsArgsT<OpenSettingsArgs> { OpenSettingsArgs() = default; // adds a getter/setter for your argument, and defines the json key WINRT_PROPERTY(SettingsTarget, Target, SettingsTarget::SettingsFile); static constexpr std::string_view TargetKey{ "target" }; public: hstring GenerateName() const; bool Equals(const IActionArgs& other) { auto otherAsUs = other.try_as<OpenSettingsArgs>(); if (otherAsUs) { return otherAsUs->_Target == _Target; } return false; }; static FromJsonResult FromJson(const Json::Value& json) { // LOAD BEARING: Not using make_self here _will_ break you in the future! auto args = winrt::make_self<OpenSettingsArgs>(); JsonUtils::GetValueForKey(json, TargetKey, args->_Target); return { *args, {} }; } IActionArgs Copy() const { auto copy{ winrt::make_self<OpenSettingsArgs>() }; copy->_Target = _Target; return *copy; } };- 在
ActionArgs.cpp中定义GenerateName()(用于命令面板中的自动命名); - 在
ActionAndArgs.cpp的ActionKeyNamesMap --> argParsers中注册参数解析器:
{ ShortcutAction::OpenSettings, OpenSettingsArgs::FromJson },注意FromJson中的强调注释:必须使用winrt::make_self创建实例,否则会在未来踩坑——WinRT 类型需要走工厂路径以正确初始化引用计数与 COM 状态。
2.5 添加一个 Action Argument
步骤同"添加 Action"的第 3 步,但修改对象是相关的ActionArgs文件(在已有 Args 类中新增WINRT_PROPERTY成员及对应 jsonKey 即可)。
3. Setting Functionality:让设置真正生效
Terminal Settings Model 更新完成后,Windows Terminal 已经能够读写设置文件。本节说明如何为新设置添加功能。
3.1 应用级(App-level)设置
应用级设置影响 Windows Terminal 的"帧"(frame),通常即全局设置。TerminalApp项目负责呈现 Terminal 的帧,其中两个关键文件:
TerminalPage:负责 Windows Terminal 外观与交互的 XAML 控件;AppLogic:负责窗口相关事务的 WinRT 类(如标题栏、Focus mode 等)。
二者都可以访问一个CascadiaSettings对象(模型侧实现见 CascadiaSettings.h),供你读取已加载的设置并相应更新 Terminal 行为。
3.2 终端级(Terminal-level)设置
终端级设置影响某个 shell 会话,通常是 Profile 设置。TerminalApp负责把 Terminal Settings Model 的设置打包进终端实例,分两类接口:
IControlSettings:影响TerminalControl(承载 shell 会话的 XAML 控件)。例如背景图定制、交互行为(如选择)、acrylic 与字体定制。TerminalControl项目通过一个保存的IControlSettings成员访问它们。ICoreSettings:影响TerminalCore(与文本缓冲区交互的底层对象)。例如初始尺寸、历史缓冲区大小、光标定制。TerminalCore项目通过保存的ICoreSettings成员访问它们。
创建新的终端实例时,TerminalApp会把这些设置打包进TerminalSettings : IControlSettings, ICoreSettings对象。为此需要提交以下改动:
- 在
IControlSettings.idl或ICoreSettings.idl(依设置归属选择)中声明该设置。若设置是枚举类型,枚举应声明在这里,而不是在TerminalSettingsModel项目中; - 在
TerminalSettings.h中声明/定义设置:
// The WINRT_PROPERTY macro declares/defines a getter setter for the setting. // Like INHERITABLE_SETTING, it takes in a type, name, and defaultValue. WINRT_PROPERTY(bool, UseAcrylic, false);- 在
TerminalSettings.cpp中:- Profile 设置更新
_ApplyProfileSettings; - 全局设置更新
_ApplyGlobalSettings; - 如需额外处理,在此完成。例如
backgroundImageAlignment在 Terminal Settings Model 中以ConvergedAlignment存储(可对照 MTSMSettings.h#L154 的 X-macro 条目),但打包进 XAML 时会被拆分为水平与垂直两个对齐枚举。
- Profile 设置更新
3.3 Actions 的功能实现
Actions 被打包为ActionAndArgs对象,然后在TerminalApp中处理。为 Action 添加功能:
- 在
ShortcutActionDispatch文件中,当 action 发生时派发事件:
// ShortcutActionDispatch.idl event Windows.Foundation.TypedEventHandler<ShortcutActionDispatch, Microsoft.Terminal.Settings.Model.ActionEventArgs> OpenSettings; // ShortcutActionDispatch.h TYPED_EVENT(OpenSettings, TerminalApp::ShortcutActionDispatch, Microsoft::Terminal::Settings::Model::ActionEventArgs); // ShortcutActionDispatch.cpp --> DoAction() // - dispatch the appropriate event case ShortcutAction::OpenSettings: { _OpenSettingsHandlers(*this, eventArgs); break; }- 在
TerminalPage文件中处理该事件:
// TerminalPage.h // - declare the handler void _HandleOpenSettings(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); // TerminalPage.cpp --> _RegisterActionCallbacks() // - register the handler _actionDispatch->OpenSettings({ this, &TerminalPage::_HandleOpenSettings }); // AppActionHandlers.cpp // - direct the function to the right place and call a helper function void TerminalPage::_HandleOpenSettings(const IInspectable& /*sender*/, const ActionEventArgs& args) { // NOTE: this if-statement can be omitted if the action does not support arguments if (const auto& realArgs = args.ActionArgs().try_as<OpenSettingsArgs>()) { _LaunchSettings(realArgs.Target()); args.Handled(true); } }AppActionHandlers因 action 而异,几个常用辅助函数:
_GetFocusedTab():获取当前聚焦的 tab;_GetActiveControl():获取处于活动状态的终端控件;_GetTerminalTabImpl():尝试把给定 tab 向下转型为TerminalTab(承载终端实例的 tab)。
4. Settings UI:在设置界面中暴露设置
4.1 暴露枚举设置(EnumMappings)
如果新设置支持枚举,需要在 Terminal Settings Model 的EnumMappings中暴露"枚举 → 值"的映射表:
// EnumMappings.idl static Windows.Foundation.Collections.IMap<String, Microsoft.Terminal.Settings.Model.CloseOnExitMode> CloseOnExitMode { get; }; // EnumMappings.h static winrt::Windows::Foundation::Collections::IMap<winrt::hstring, CloseOnExitMode> CloseOnExitMode(); // EnumMappings.cpp // - this macro leverages the json enum mapper in TerminalSettingsSerializationHelper to expose // the mapped values across project boundaries DEFINE_ENUM_MAP(Model::CloseOnExitMode, CloseOnExitMode);当前仓库的实证:EnumMappings.cpp#L16-L28 中
DEFINE_ENUM_MAP宏的实现印证了注释所说的"跨项目边界复用"——它直接遍历JsonUtils::ConversionTrait<type>::mappings(即TerminalSettingsSerializationHelpers.h中的 JSON 枚举 mapper),把同一份映射表转成IMap<hstring, type>供 Settings Editor 使用。EnumMappings.cpp#L49 正是CloseOnExitMode的注册行。这样 JSON 解析与 UI 下拉列表永远不会出现两份不一致的枚举定义。
4.2 绑定并本地化枚举设置
先在 Settings UI 中找到该设置最适合的页面。以添加LaunchMode为例:
第 1 步:在Launch.idl中暴露可绑定属性:
// Expose the current value for the setting IInspectable CurrentLaunchMode; // Expose the list of possible values Windows.Foundation.Collections.IObservableVector<Microsoft.Terminal.Settings.Editor.EnumEntry> LaunchModeList { get; };第 2 步:在Launch.h中声明可绑定枚举设置:
// the GETSET_BINDABLE_ENUM_SETTING macro accepts... // - name: the name of the setting // - enumType: the type of the setting // - settingsModelName: how to retrieve the setting (use State() to get access to the settings model) // - settingNameInModel: the name of the setting in the terminal settings model GETSET_BINDABLE_ENUM_SETTING(LaunchMode, Model::LaunchMode, State().Settings().GlobalSettings, LaunchMode);第 3 步:在Launch.cpp的构造函数中(InitializeComponent()之后)初始化:
// the INITIALIZE_BINDABLE_ENUM_SETTING macro accepts... // - name: the name of the setting // - enumMappingsName: the name from the TerminalSettingsModel's EnumMappings // - enumType: the type for the enum // - resourceSectionAndType: prefix for the localization // - resourceProperty: postfix for the localization INITIALIZE_BINDABLE_ENUM_SETTING(LaunchMode, LaunchMode, LaunchMode, L"Globals_LaunchMode", L"Content");第 4 步:在 Microsoft.Terminal.Settings.Editor 的Resources.resw中为每个枚举值添加本地化文本。键名格式为<SettingGroup>_<SettingName><EnumValue>.Content:
SettingGroup:全局设置用Globals,Profile 设置用Profile;SettingName:设置类型的 Pascal-case 形式(如"launchMode"→LaunchMode);EnumValue:该值在 JSON 中的键、首字母大写(如"focus"→Focus);- 最终键名形如
Globals_LaunchModeFocus.Content,这就是控件中实际显示的文本。
4.3 更新 XAML:枚举设置
创建 XAML 控件时遵循 UWP 设计指南,并掌握以下技巧:
- 用符合
SettingContainerStyle样式的ContentPresenter包裹控件; - 将
SelectedItem绑定到对应的Current<Setting>(如CurrentLaunchMode),必须是TwoWay绑定; - 将
ItemsSource绑定到<Setting>List(如LaunchModeList); ItemTemplate设为Enum<ControlType>Template(如单选按钮用EnumRadioButtonTemplate);- 样式取自
CommonResources.xaml中的合适样式。
<!--Launch Mode--> <ContentPresenter Style="{StaticResource SettingContainerStyle}"> <muxc:RadioButtons x:Uid="Globals_LaunchMode" SelectedItem="{x:Bind CurrentLaunchMode, Mode="TwoWay"}" ItemsSource="{x:Bind LaunchModeList}" ItemTemplate="{StaticResource EnumRadioButtonTemplate}" Style="{StaticResource RadioButtonsSettingStyle}"/> </ContentPresenter>添加本地化文本时加上x:Uid,然后通过Resources.resw访问对应属性。例如Globals_LaunchMode.Header设置该控件的标题;同理可以设置 tooltip:
Globals_DefaultProfile.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip4.4 非枚举设置的 XAML 绑定
同样参考CommonResources.xaml的样式、用类似的ContentPresenter包裹,但不经过Current<Setting>与<Setting>List,而是直接经由 state 绑定到设置本身。例如绑定altGrAliasing:
<!--AltGr Aliasing--> <ContentPresenter Style="{StaticResource SettingContainerStyle}"> <CheckBox x:Uid="Profile_AltGrAliasing" IsChecked="{x:Bind State.Profile.AltGrAliasing, Mode=TwoWay}" Style="{StaticResource CheckBoxSettingStyle}"/> </ContentPresenter>4.5 Profile 设置的"可观察化"
如果添加的是 Profile 设置,除上述步骤外还需修改Profiles相关文件,使设置变成可观察(observable)的:
// Profiles.idl --> ProfileViewModel // - this declares the setting as observable using the type and the name of the setting OBSERVABLE_PROJECTED_SETTING(Microsoft.Terminal.Settings.Model.CloseOnExitMode, CloseOnExit); // Profiles.h --> ProfileViewModel // - this defines the setting as observable off of the _profile object OBSERVABLE_PROJECTED_SETTING(_profile, CloseOnExit); // Profiles.h --> ProfileViewModel // - if the setting cannot be inherited by another profile (aka missing the Clear() function), use the following macro instead: PERMANENT_OBSERVABLE_PROJECTED_SETTING(_profile, Guid);ProfilePageNavigationState持有一个ProfileViewModel,它包装了 Terminal Settings Model 中的Profile对象,ProfileViewModel的作用就是把所有 Profile 设置变为可观察属性,供 XAML 的x:Bind使用。
4.6 Action 与设置 UI
目前 Action 尚不支持在 Settings UI 中直接编辑(按键与 Action 的映射通过 JSON 设置文件中的actions段完成)。
5. 小结:新增设置的全链路检查清单
综合上述三层,向 Windows Terminal 添加一个完整的新设置需要触碰以下文件(以 Profile 枚举设置为参照):
| 层次 | 文件 | 关键动作 |
|---|---|---|
| 模型注册 | src/cascadia/TerminalSettingsModel/MTSMSettings.h、Profile.idl | X-macro 一行注册 + WinRT 暴露 |
| 序列化 | Profile.cpp、TerminalSettingsSerializationHelpers.h | LayerJson/ToJson+JSON_ENUM_MAPPER |
| 枚举共享 | EnumMappings.idl/.h/.cpp | DEFINE_ENUM_MAP跨项目暴露映射 |
| 功能生效 | TerminalSettings.h/.cpp(TerminalApp) | WINRT_PROPERTY+_ApplyProfileSettings |
| Action(可选) | KeyMapping.idl、ActionAndArgs.cpp、ActionArgs.idl/.h/.cpp、ShortcutActionDispatch.* | 枚举、键名映射、参数类、事件派发 |
| 设置 UI | Settings Editor 的页面.idl/.h/.cpp、Resources.resw、XAML | 可绑定枚举/直连绑定 +x:Uid本地化 |
理解这一链路后,再结合 doc/cascadia/Json-Utility-API.md 中JsonUtils的 API 细节,即可独立为 Terminal 扩展任意新设置或新 Action。
【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考