Windows Terminal `unfocusedAppearance` 深度解析:用外观配置对象实现窗格焦点状态区分
本文基于 Windows Terminal 仓库中的设计规范文档 doc/specs/#3062 - Appearance configuration object for profiles.md,完整讲解 profiles 中 unfocusedAppearance 配置对象的设计动机、允许参数、"整体继承"模型与 JSON 配置方式,并结合当前仓库的 TerminalSettingsModel、TerminalControl 源码,剖析焦点切换时外观对象如何被选中、下发到渲染层,帮助读者既会用这个配置,也理解其底层实现链路。
一、背景:为什么需要"失焦外观"配置
规范文档的出发点很直接:当多个窗格(pane)并排显示时,用户希望有一个比默认样式更强的视觉指示,来区分当前聚焦的窗格与未聚焦的窗格(该需求对应 issue #3062)。方案是让控制对象(TermControl)能够根据自身的状态——聚焦或失焦——渲染出不同的外观,而承载这种"状态化外观"的机制,就是 profile 里的 appearance configuration object,即 unfocusedAppearance。
规范摘要(Abstract)给出的核心命题是:在 profiles 中支持"配置对象",使控制对象可以根据自身状态的不同而渲染出不同外观,例如聚焦时与非聚焦时使用不同的配色、光标样式。
二、方案设计:复用 TerminalSettings 的继承树
规范"Solution Design"一节指出,该功能的实现建立在 TerminalSettings 类引入的继承机制之上——不同的 TerminalSettings 对象可以互相继承。这一机制的原始目的,是不希望 settings reload 时抹掉 TermControl 在运行期对设置做过的覆写(override):由于传给控制对象的是 TerminalSettings 的子对象(child),重载时只需更换子对象的父节点,子对象里保存的运行时覆写就得以保留。
unfocusedAppearance 的设计思路与它一致:再传一个 TerminalSettings 对象给控制对象,这个对象只是一个"已经预置了若干覆写"的子对象。当控制对象获得或失去焦点时,只需要在两个设置对象之间做切换即可。
这一设计在当前源码中可以直接印证。ControlCore 构造时就持有两份外观对象:
// src/cascadia/TerminalControl/ControlCore.cpp
_settings = settings;
_hasUnfocusedAppearance = static_cast<bool>(unfocusedAppearance);
_unfocusedAppearance = _hasUnfocusedAppearance ? unfocusedAppearance : settings;
见 ControlCore.cpp。如果没有提供失焦外观,_unfocusedAppearance 就退化为普通设置本身,从而保证未配置该功能的用户行为完全不变。
三、允许在对象中定义哪些参数
规范明确:这些状态在最初是**纯外观(entirely appearance-based)**的,因此 Profile 里并非所有参数都能出现在该对象中(例如不希望出现会导致窗口尺寸变化的参数)。规范给出的初始允许清单为:
- 一切与颜色相关的参数:
colorScheme、foreground、background、cursorColor等; - 一切与背景图片相关的参数:
path、opacity、alignment、stretchMode; cursorShape。
规范同时声明,未来可能放行更多参数(如 bellStyle),属于超出本规范范围的议题。
而当前仓库的实现已经比规范当时的清单更宽:unfocusedAppearance 在 JSON Schema 中被声明为完整引用 AppearanceConfig 定义(类型可为对象或 null),见 profiles.schema.json:
"unfocusedAppearance": {
"$ref": "#/$defs/AppearanceConfig",
"description": "Sets the appearance of the terminal when it is unfocused.",
"type": [ "object", "null" ]
}
也就是说,凡属于外观类(AppearanceConfig)的设置都可以出现在其中,包括规范点名的颜色、背景图片与光标形状,以及 opacity、useAcrylic、intenseTextStyle、adjustIndistinguishableColors 等参数。完整的可继承外观参数列表定义在 AppearanceConfig.h 与 MTSMSettings.h 中,例如:
| 参数 | JSON 键 | 默认值(源码可见部分) |
|---|---|---|
| 光标形状 | cursorShape |
Bar |
| 光标高度 | cursorHeight |
DEFAULT_CURSOR_HEIGHT |
| 背景图路径 | backgroundImage |
空(MediaResource::Empty()) |
| 背景图透明度 | backgroundImageOpacity |
1.0 |
| 背景图对齐 | backgroundImageAlignment |
水平居中 + 垂直居中 |
| 背景图拉伸模式 | backgroundImageStretchMode |
UniformToFill |
| 窗口不透明度 | opacity |
1.0 |
| 是否使用亚克力 | useAcrylic |
false |
| 高亮文本风格 | intenseTextStyle |
Bright |
| 明暗两套配色方案名 | darkColorSchemeName / lightColorSchemeName |
Campbell |
| 前景/背景/选中/光标色 | foreground / background / selectionBackground / cursorColor |
未设置(可为 null,继承) |
其中四个颜色项与 opacity 因解析方式特殊,以 INHERITABLE_NULLABLE_SETTING 宏单独声明(见 AppearanceConfig.h),未显式设置时为 null、走继承。
四、继承模型:"整体继承"(all-or-nothing)
这是规范中最值得细读的设计决策之一。unfocusedAppearance 对象被视为一个单一设置项(a single setting),而不是"一个内含多个设置、各自分别向上找父节点的对象"。规范解释选择这一模型的原因是:它比"对象内每个设置各自带一个父节点"的备选方案更干净、更易理解。
在 Profile 类的头文件注释里,作者直接画出了失焦设置的继承树(DAG),见 Profile.h:
+-------------------+
|Profile.defaults |
|DefaultAppearance |
|UnfocusedAppearance|
+-------------------+
^ ^
| |
+------------------+ +------------------+
|MyProfile | |Profile.defaults |
|DefaultAppearance | |UnfocusedAppearance|
+-------------------+ +-------------------+
^
|
+------------------+
|MyProfile |
|UnfocusedAppearance|
+-------------------+
即:profile 自身的 unfocusedAppearance 的父节点是 profile 的默认外观(DefaultAppearance),而后者再向上追溯到全局 profileDefaults 中的对应外观。规范中给出的未定义参数取值顺序与此一致:
- profile(或 globals/profileDefaults)中定义的 unfocused config;
- 终端控制对象(control)在运行期做出的覆写;
- 父 profile。
源码中 Profile::CreateUnfocusedAppearance 精确实现了"把 profile 默认外观加为最低优先级父节点"这一行为:
// src/cascadia/TerminalSettingsModel/Profile.cpp
void Profile::CreateUnfocusedAppearance()
{
if (!_UnfocusedAppearance)
{
auto unfocusedAppearance{ winrt::make_self<implementation::AppearanceConfig>(weak_ref<Model::Profile>(*this)) };
// If an unfocused appearance is defined in this profile, any undefined parameters are
// taken from this profile's default appearance, so add it as a parent
com_ptr<AppearanceConfig> parentCom;
parentCom.copy_from(winrt::get_self<implementation::AppearanceConfig>(_DefaultAppearance));
unfocusedAppearance->AddLeastImportantParent(parentCom);
_UnfocusedAppearance = *unfocusedAppearance;
}
}
见 Profile.cpp。Profile 类里该字段本身就是以可继承设置声明的:INHERITABLE_SETTING(Model::Profile, Model::IAppearanceConfig, UnfocusedAppearance, nullptr),见 Profile.h。
"整体继承"在复制 profile 的逻辑里同样体现:当 CascadiaSettings 复制一个 profile 时,注释明确写道 UnfocusedAppearance is treated as a single setting,整个对象作为一个整体被复制,并把"被复制 profile 的默认外观"重新挂为副本中 UnfocusedAppearance 的父节点,见 CascadiaSettings.cpp;Profile::CopySettings 中克隆时也调用 AddLeastImportantParent(defaultAppearance) 重建这条父子链,见 Profile.cpp。
五、配置实战:如何在 settings.json 中书写 unfocusedAppearance
规范"UI/UX Design"一节给出的用户侧配置形态如下(可直接放入 profile 中):
"unfocusedAppearance":
{
"colorScheme": "Campbell",
"cursorColor": "#888",
"cursorShape": "emptyBox",
"foreground": "#C0C0C0",
"background": "#000000"
}
效果:当该窗格失焦时,光标变为灰色空盒、文字变为浅灰、背景变为纯黑;聚焦时恢复 profile 的常规外观。由于未列出的参数(如 selectionBackground、opacity、字体等)会沿第四节描述的继承链向上取值,因此只需要写"你希望和聚焦态不同的那几个字段"。
JSON 解析入口在 Profile::FromJson / LayerJson 中,键名常量即 "unfocusedAppearance"(见 Profile.cpp),解析逻辑是:若 JSON 中存在该键,则创建一个 AppearanceConfig 并用 LayerJson 合并后赋给 _UnfocusedAppearance;序列化时再经 ToJson 写回。
一个值得注意的行为细节(规范"UI/UX Design"末尾明确说明):当某些外观设置被 OSC 转义序列在运行期修改(例如 OSC 10/11 改前景色/背景色)时,只有聚焦/常规外观会变化,失焦外观本身保持不变;但由于失焦对象继承自常规对象,只要它没有为该设置定义自己的值,该变化仍会透过继承反映到失焦渲染上。这与 ControlCore 中运行期配色覆写只作用在 _focusedColorSchemeOverride 上的处理方式相互印证:
// src/cascadia/TerminalControl/ControlCore.cpp (ApplyAppearance)
const IControlAppearance newAppearance{ focused ? _settings : _unfocusedAppearance };
_terminal->UpdateAppearance(newAppearance);
if ((focused || !_hasUnfocusedAppearance) && _focusedColorSchemeOverride)
{
_terminal->UpdateColorScheme(_focusedColorSchemeOverride);
}
六、焦点切换的运行时链路
从"焦点变化"到"重新着色"的完整调用链,在当前仓库中可以完整追溯:
-
TermControl 层(UI 线程):
UpdateControlSettings接收新的控制设置后,根据当前是否聚焦选择下发哪份外观对象:// src/cascadia/TerminalControl/TermControl.cpp void TermControl::UpdateControlSettings(IControlSettings settings, IControlAppearance unfocusedAppearance) { _core.UpdateSettings(settings, unfocusedAppearance); _UpdateSettingsFromUIThread(); _UpdateAppearanceFromUIThread(_focused ? _core.FocusedAppearance() : _core.UnfocusedAppearance()); } -
ControlCore 层:
UpdateSettings更新_hasUnfocusedAppearance与_unfocusedAppearance(若新外观为空则回退为普通设置,见 ControlCore.cpp);ApplyAppearance(focused)则完成真正的对象切换——按focused选择_settings或_unfocusedAppearance,调用_terminal->UpdateAppearance,并同步更新渲染引擎的不透明度、亚克力、像素着色器等外观相关状态,最后TriggerRedrawAll触发全量重绘(见 ControlCore.cpp)。 -
对外接口:
ControlCore通过 WinRT 接口暴露FocusedAppearance()、UnfocusedAppearance()与HasUnfocusedAppearance(),供宿主(如 TerminalApp 的 ContentManager)判断与查询,见 ControlCore.idl。
从源码结构看,焦点切换并不产生新对象,只是在两份已构造好的外观对象间做选择,这与规范"它 simply switches between the two settings objects"的描述一致,也解释了性能章节的结论:窗格间常规切换的开销是可控的,只有短时间内高频切换大量窗格时,连续的多次外观变更才可能对性能产生可感知影响。
七、能力影响面:规范原文的五项评估
规范"Capabilities"一节对该功能的副作用评估值得原样保留,这是理解功能边界的关键:
- 可访问性(Accessibility):不影响。
- 安全性(Security):不影响。
- 可靠性(Reliability):这是设置解析/加载可能失败的新位置,但任何新增设置都有此风险,规范认为这是该功能合理的代价。
- 兼容性(Compatibility):不应产生影响。
- 性能、功耗与效率(Performance, Power, and Efficiency):短时间内快速切换大量窗格、引发连续多次外观变更时可能影响性能;常规的合理窗格切换不应有明显影响。
八、潜在问题与未来演进方向
规范"Potential Issues"一节指出了一个真实工程风险:非活动(inactive)标签页在后台会按 UnfocusedRenderingParams 渲染,需要确保切换到某个非活动标签页、从而让渲染器用"常规参数"刷新时,不会导致窗口闪现或显示出突兀的渲染值变化指示。这是实现该功能时必须重点回归测试的场景。
"Future considerations"一节还保留了两条尚未落地的路线:
- 设置 UI(Settings UI)中的呈现方式:当时未定。从当前仓库看,设置编辑器侧已存在对
UnfocusedAppearance的支持代码(如 Profiles_Appearance.cpp 与 ProfileViewModel.cpp),该议题在实现中已有着落。 - 更多状态(如
elevated):规范记录了团队的讨论结论——当多个状态可能同时生效(如"失焦 + 提权")时,找不到合适的分层方案;加之状态数量不确定,决定当前只支持 unfocused 一种状态,未来若确有新增状态,可以扩展实现(extension)而非内建支持;若最终只有 unfocused 与 elevated 两种,也可以允许组合出"unfocused elevated"状态。
九、如何自行验证
结合仓库内的可验证材料,读者可以按以下路径核对本文各结论:
- 设计动机与完整决策过程:[规范原文](https://gitcode.com/GitHub_Trending/term/terminal/blob/20588130d8ef2ba40eb56bdae88e04cce7fc5b5d/doc/specs/?utm_source=gitcode_repo_files#3062 - Appearance configuration object for profiles.md)(作者 Pankaj Bhojwani,创建 2020-11-20,issue #8345);
- 设置模型:
Profile的字段声明、创建/删除方法与 JSON 序列化(Profile.h、Profile.cpp),AppearanceConfig的参数定义(AppearanceConfig.h); - 运行层切换逻辑:ControlCore.cpp 与 TermControl.cpp;
- JSON 校验:profiles.schema.json 中
unfocusedAppearance的类型为object | null,即 profile 中可以整体省略该字段; - 单元测试:
src/cascadia/UnitTests_SettingsModel/目录下的 ColorSchemeTests.cpp 与 MediaResourceTests.cpp 等测试文件中存在对UnfocusedAppearance的引用,覆盖配色方案与媒体资源在该对象上的行为,可作为回归验证的起点。
小结
unfocusedAppearance 是 Windows Terminal 将"状态化渲染"做进设置模型的代表性设计:它没有为失焦态单独发明一套机制,而是复用 TerminalSettings 的继承树,用一个"预置覆写的子对象 + 整体继承"的模型,让控制对象在聚焦/失焦间切换两份外观即可。配置侧只需在 profile 中写差异字段,未写字段沿"profile 的失焦配置 → 控制对象运行期覆写 → 父 profile 默认外观 → 全局 profileDefaults"一路继承;运行侧由 ControlCore::ApplyAppearance 在每次焦点变化时完成对象切换与重绘。理解这一条链路,既能正确配置窗格焦点视觉区分,也能在扩展或排查相关渲染问题时快速定位到实现层。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00