看到这个标题,估计不少人都跟我一样,第一反应是“Windows Terminal 不是微软已经做好的一个终端工具吗?还有什么可深度源码评测的?”说实话,我一开始也这么想。但当你把microsoft/terminal这个仓库真正 clone 下来,把src目录一层层翻开之后,你会发现它根本不是一个“终端应用”那么简单,更像是一个藏在开源仓库里的操作系统级控制台架构实验室。
这篇文章是从源码评测者的视角去写的,不打算变成“Windows Terminal 使用教程”。我们会一起把它当成一个 C++ 工程样本,去看微软怎么组织一套跨越多层抽象的代码,怎么设计 ConPTY、文本缓冲、渲染引擎和设置模型,再看它的工程治理怎么运转,最后落到“如果你真的想在它的基础上做二次开发,最可能的落地路径在哪里”。无论你是做终端模拟器、编辑器、IDE、命令行工具,还是只是对成熟 C++ 项目架构感兴趣的开发者,这篇文章应该都能给你一些比官方文档更接地气的东西。
1. 源码整体设计拆解:这个仓库到底装了什么
1.1 从产品身份到代码现实
很多人以为 Windows Terminal 是微软从零写的一个新终端,其实它在血缘上跟 Windows 自带的 conhost(控制台主机)是连在一起的。当年 Windows 的老控制台窗口(就是黑底白字那个cmd窗口的宿主进程)和 Windows Terminal 共享了相当一部分底层控制台基础设施,包括文本缓冲区、VT 序列解析、伪终端支持。
从工程上讲,这个仓库里通常并存着好几套代码世界:
src/下的buffer、renderer、server、terminal等目录,属于控制台和终端的核心逻辑,这部分是“不依赖 UI 框架”的纯 C++ 层,甚至有一部分逻辑可以在不开图形界面的情况下单独编译运行。src/app或旧版目录里的cascadia相关代码,属于 Windows Terminal 独有的现代壳层,包含基于 XAML 的界面、Window 管理、Tab 管理、设置界面等。src/interactivity和src/wincon这类目录,是跟 Windows 具体窗口机制交互的中间适配层。
所以“Windows Terminal 的源码”并不是单一产品终端的代码,它实际上是把经典控制台主机(OpenConsole)、伪终端(ConPTY)、现代 UI 壳层、渲染器,以及若干可复用库打包在一起的一个巨型仓库。这种结构的好处是一处底层修复可以同时惠及老控制台和新终端,坏处是如果你只想看懂某一小块,初期确实有点迷路。
1.2 读源码之前先分清“核心层”和“UI层”
要真正理解这套代码,建议先忘掉你看到的窗口和标签页,把那当成一个普通的前端客户端来看。Windows Terminal 的独特之处,在于它把“终端能力”抽出来做成了一个相当完整的服务端核心层。
这部分核心层包括:
- TextBuffer:负责保存屏幕上所有字符、颜色、光标状态的缓冲区。
- Terminal Core:负责维护伪终端和缓冲区状态机的逻辑,处理输入输出重定向。
- Renderer:把缓冲区内容绘制成像素的渲染管线,Windows Terminal 下主要是基于 DirectX 的 DX 渲染器。
- VT/ANSI 解析器:解释从 shell 程序传来的转义序列,比如
\x1b[31m这种颜色控制码。
UI 层则主要是把终端渲染结果塞进一个 Modern Windows 控件里,再给用户提供标签页、搜索框、下拉菜单、快捷键管理和设置页面。
这个“核心与界面分离”的架构思路,是 Windows Terminal 能同时在稳定性和扩展性上做得相对扎实的重要原因。它意味着大部分复杂的终端状态机逻辑可以独立于 UI 测试和复用,终端核心本身不需要知道外面是一个 XAML 窗口还是在跑自动化测试。
1.3 源码目录:一张先画后逛的“地图”
如果你打开仓库主目录,第一眼也许会被大量目录吓到,其实顶层结构并不复杂。一个比较合理的方式是先找到几个关键目录:
doc/:设计文档、规范讨论、架构决策,强烈建议二次开发之前先翻一遍,很多前人的设计取舍在里面写得明明白白。src/:正宗的核心代码所在地。tools/:辅助构建、打包、自动化脚本。samples/或类似目录:一些演示项目,适合做上手实验。
在src内部,建议先看这几个地方:
| 目录/模块 | 职责 | 二开关注度 |
|---|---|---|
buffer/ | 字符缓冲区读写、坐标管理 | 中 |
terminal/ | 终端核心状态机、设置存储 | 高 |
renderer/ | 多种渲染后端,DX/GDI/ATLAS | 高 |
server/ | 服务端连接、进程 IO | 中 |
app/ | XAML UI、窗口标签页、命令面板 | 高 |
types/ | 颜色、视图位置等公共类型 | 中 |
有了这张地图,你不会在一个名叫Foo.cpp的文件里转半天出不来。这就是第一课:读大型源码项目,永远先用“目录职责”建立索引,再去按细节追行号,不然信息就像散落在地上的钢珠,看着光亮却连不成线。
2. 底层架构深度拆解:从按键到像素到底发生了什么
2.1 一条按键消息的完整“旅程”
如果你在 Windows Terminal 里打开一个 WSL 的 bash 页面,然后敲下一个ls,代码层面会发生什么?我把这个过程简化成一条链路:
- XAML 前台窗口捕获到键盘事件,把它交给 Terminal Control 控件。
- Terminal Control 把按键翻译成输入字节流,写入 ConPTY 的输入管道。这就像你坐在一个远程终端前,把键盘信号当成数据流发给对面的 shell。
- ConPTY 把这串字节转交到真正的命令行程序(比如
bash.exe)的 stdin。 bash解析并执行命令,把结果以输出字节流的方式写回 stdout。- 输出数据经过 ConPTY 转译后进入 Windows Terminal 的 VT 解析器。
- VT 解析器把类似
\x1b[H、\x1b[42m这种控制码翻译成对 TextBuffer 的增删改操作。 - 渲染器收到“缓冲区有变化”的通知,在下一个渲染帧里把新内容画出来。
- 你看到屏幕上的
ls结果。
这八步看起来不复杂,但每一步背后都有大量细节。最容易被初学者忽略的是:终端模拟器里的“屏幕”,本质上不是一个画布,而是一个字符状态矩阵。每一个单元格要记录字符内容、前景色、背景色、加粗、斜体、下划线、超链接等属性。渲染器每次刷新,不是重画整张图,而是尽量绘制变化区域。
2.2 ConPTY 的巧妙设计:让老程序也能跑进新终端
Windows 的历史包袱很重。传统的cmd、PowerShell、甚至很多 Windows 原生控制台程序,都依赖一个真正存在的 console window 和一套 Windows 控制台 API。如果你直接禁用这个窗口,很多程序的输出会异常。
Windows Terminal 用了一个叫 ConPTY(Pseudo Console)的机制来打补丁。它的基本思路是:
- 创建一对虚拟终端设备,一头接到命令行程序的输出,一头接到 Windows Terminal。
- 命令行程序以为自己还在跟一个真正的控制台窗口交互。
- Windows Terminal 侧收到的不是原始字节流,而是经过转换的 VT/ANSI 控制序列。
- 反过来,Windows Terminal 发出的键盘指令也经过 ConPTY 转换成老式控制台输入事件,让老程序能正常识别。
这套机制的核心价值不在于“显示输出”,而在于兼容层。如果没有 ConPTY,现代终端想对接 Windows 上的老式命令行程序,几乎寸步难行。读源码时,src/wincon和src/interactivity里的实现,就是 Windows 解决这种历史兼容性问题的活教材。你会看到大量 Win32 API、环境变量、句柄传递的细节,对一个想要把自家工具嵌入 Windows 生态的开发者来说是极好的参考。
2.3 渲染器为什么那么重要:从 CPU 绘制走向 GPU 加速
老一代控制台的渲染性能放在今天已经不太行。字符多、翻屏快的时候,很容易出现闪烁和卡顿。Windows Terminal 在渲染器上花了很大力气,源码里默认渲染器通常叫AtlasEngine或者 DX 渲染引擎,核心目标是用 GPU 来排版字符。
渲染器里几个值得关注的设计点:
- 使用 DirectWrite 来做字体排版和字形分析,让它能像现代文本应用一样处理复杂文本、Emoji、连字等。
- 使用 Direct2D/Direct3D 来做最终的绘制,把字符纹理上传到 GPU,再按需重绘。
- 渲染帧不是“按键触发一次画一次”,而是由 CPU 端通知“需要重绘的区域”,再由渲染线程在合适的帧率窗口内统一重绘。
从源码评测角度看,渲染器里的改动相当频繁,因为要处理字体回退、颜色矩阵、对比度模式、背景图片、透明度等各种边界情况。二开过程中最容易被忽视的坑也在渲染层:如果你只改了缓冲区内容,而没有正确触发渲染区域的更新标记,那么界面看不出任何变化,但代码好像执行了。排查这种“状态对了但是画面没变”的问题,是所有终端类二次开发者的必修课。
2.4 设置模型的现代改造:从注册表到 JSON
老的 Windows 控制台设置分散在注册表和系统属性对话框里,对深度用户和开发者来说根本没办法做配置同步。Windows Terminal 改成了基于 JSON 的配置系统,也就是你熟悉的settings.json。
在源码里,这个设置模型被拆分得很细致:
- 启动时解析 JSON 文件。
- 解析结果映射到一个强类型的 Settings 对象。
- 全局设置、Profile 设置、键绑定设置、配色方案设置分别有各自的默认值合并逻辑。
- 当你保存
settings.json时,系统通过文件监听器感知变化,然后对 UI 里的属性做热更新。
这套模型实际解决了两类问题。第一,用户能把自己的终端配置保存到 Git 仓库里管理,跟着环境走。第二,微软自己也受益于这种结构化配置,新功能上线时只需要增加新的 JSON 字段,而不是每次改一套对话框代码。
从二开角度,设置模型是我建议非资深人员优先研究的部分,因为 80% 的需求可以靠 JSON 配置方案解决,而不需要深入改 C++ 核心。后面第 4 节我会具体讲落地路径,这里先按住不展开。
3. 工程治理审计:微软是怎么管理这么复杂的 C++ 代码的
3.1 依赖选型:为什么是老牌 C++ 阵营里的新派打法
打开代码库你会发现,Windows Terminal 虽然是一个根正苗红的微软项目,但它在语言风格和库选型上并没有停留在老式 Win32 C++ 那套“裸指针 + 宏 + 手写 GUID”的时代,而是大量使用现代 C++ 特性,配合三个关键库:
- C++/WinRT:微软推荐的标准 C++ WinRT 投影,用它来访问 XAML 和 Windows Runtime 类型。
- WIL(Windows Implementation Libraries):一套错误处理、COM 和资源管理的辅助库,里面有个很实用的 ScopeGuard 机制,能在异常分支里自动做资源清理,降低内存泄漏的概率。
- 标准模板库(STL)以及一些现代 C++ 的 RAII 习惯,贯穿整个核心层。
这种选型让我感觉微软是真心想降低维护成本。对于一套可能几百万行、长期演进的代码库,如果依赖原始 Win32 API 到处裸奔,开发效率会很低。WIL 这一类工具库其实特别适合普通 Windows C++ 项目引用,哪怕你不是在做终端模拟器,也建议单独把 WIL 拿出来学习一下,里面很多错误处理模式放在你自己的工作里也能直接用。
3.2 目录分层的“依赖规则”
我最欣赏这套代码的一点是它的模块边界很清楚。简单概括就是:
- 底层模块(如 buffer、types)不依赖上层模块。
- 上层模块(如 app、Terminal Control)可以依赖底层模块,但不能反过来。
- 渲染器是相对独立的一层,通过接口和核心层交互。
- UI 框架相关代码被控制在 app 和控件目录以内,不会渗透到 buffer 和 terminal 核心里去。
这种单向依赖规则,保证了终端核心逻辑可以脱离 UI 做单元测试。你可以想象一下,如果 TextBuffer 里不小心 include 了一个 XAML 头文件,整个构建和维护都会变成噩梦。Windows Terminal 的构建系统基本能保证这种边界在编译期就被检查出来,有越界依赖通常会导致 link 失败或者组件化检测失败,使得工程结构能长期维持整洁。
3.3 测试体系的“三层防线”
我读代码时顺手统计过,项目在多个模块下都有对应的测试目录。大体上分三类:
- 单元测试:针对 TextBuffer、VT 解析器、设置模型等核心组件的小型测试。
- 集成测试 / 功能测试:跑一些模拟的交互脚本、关注整体行为的测试。
- 手工验证列表:很多 UI 状态和 WinUI 控件行为仍然依赖人工过一遍,因为终端界面太依赖真实用户体验了。
你会发现,微软在“用户可见变化”这种区域,测试手段通常没那么自动化。这其实是很多大型 UI 项目都面临的两难:UI 自动化测试成本高、维护难,而终端模拟器里大量的渲染细节和颜色准确性,用传统的 UI 点击型自动化覆盖率不足。所以他们把更重的自动化押在了逻辑核心层,把 UI 行为的验证交给更频繁的手工回归。
这种取舍给你的启示是:不要迷信“90% 测试覆盖率”这种口号,要分析代码的稳定性取决于哪些模块,然后把测试力量集中到最好压的那些核心模块上。Windows Terminal 的文本缓冲区、VT 解析器、配色算法都是非常适合单元测试的纯逻辑模块,所以它们的测试密度明显高于按钮点击逻辑。
3.4 工程流程里几个值得偷师的细节
代码之外,这个仓库的工程流程也有不少亮点。比如:
- 每个较大的变更都会先写设计文档,描述背景、方案、备选方案。这就是
doc/目录存在的价值。它不是写给人看的摆设,而是确保重大设计决策在代码动工前就经过充分的讨论。 - Commit 信息通常描述“为什么改”,而不是单纯“改了什么”。这看起来是小事,但对长期维护极有帮助。
- PR review 很严格,尤其是涉及公共 API、设置格式和兼容性变化的部分。很多 PR 评论区里能直接看到微软内部和社区贡献者之间的技术辩论,这本身是最好的学习材料。
- 自动化构建和验证覆盖面很广,每一次 commit 都会触发多个平台的编译和测试任务,确保你改完不会顺手弄坏别人的模块。
如果说 Windows Terminal 本身作为“终端”的完成度还有可挑之处,那它作为“工程治理案例”的价值我认为是顶级的。特别是对想要提升团队 C++ 工程规范、代码评审文化、模块化设计的开发者来说,比读一堆软件工程理论书有用得多。
4. 二次开发落地指南:从源码构建到实现自己的扩展点
4.1 环境准备:本地把仓库“盘活”
如果你已经决定不只是看代码,还要动手改,那么第一步就是把代码编译起来。我自己踩过一些坑,先说结论:
准备这些东西:
- 一台 Windows 10 2004 以上或 Windows 11 的系统,最好是 x64 架构,能省很多事。
- Visual Studio 2022(版本尽量新),安装时必须勾选“使用 C++ 的桌面开发”以及“通用 Windows 平台开发”两个 workload。前者负责 C++ 编译,后者提供 XAML/WinUI 所需的 Windows SDK 工具链。
- 在系统设置里开启 Windows 开发者模式。
- 安装最新版 Windows SDK,并确保 Visual Studio 的 SDK 版本跟代码里声明的版本一致。如果 SDK 版本不匹配,编译时会提示找不到某些头文件或库。
然后是拉代码。仓库根目录有官方 README,建议按 README 来。不过我更推荐直接命令行操作:
git clone --recurse-submodules https://github.com/microsoft/terminal.git cd terminal注意--recurse-submodules这个参数不是可选项,它是必须的。仓库里有子模块,少了它们你会在构建的某个阶段突然发现缺了关键头文件,回看才发现自己 clone 漏了。之后,可以打开解决方案文件OpenConsole.sln(这个解决方案虽然名字里带 OpenConsole,实际上包含了整个 Terminal 应用),在 Visual Studio 里把启动项目设为 Terminal 对应的那个项目,然后直接 F5 编译运行。
第一次编译时间会比较长,建议保持耐心。我自己的机器上首次全量构建超过二十分钟,这跟配置有关,正常现象。为了避免反复等待,第二次开始可以只改需要改的模块项目,单独 Build 它而不是全量 Build。
4.2 不改 C++ 也能完成的需求:settings.json 扩展点清单
做过几年工程的人都知道,真正的二次开发不是上来就改底层,而是先找现成的扩展点。Windows Terminal 的插件体系不像 VS Code 那么开放,目前并不能加载任意语言编写的插件,但它给你的 JSON 配置和动作系统提供了很强的可定制能力,很多需求完全可以在不改 C++ 的前提下完成。
我把这些“轻量级二开”的方向整理出来:
- 自定义快捷键行为:动作系统里可以给几乎每个 UI 行为绑定组合键,比如新建标签、切换标签、复制粘贴、清空缓冲区、打开设置的 JSON 文件。这个通过
actions数组配置。 - 自定义配色主题和背景:每个 profile 都能指定使用哪套 color scheme,可以把自己的公司主色调、护眼底色配置成全局主题。
- 修改 profile 的启动行为:包括启动目录、启动命令行、窗口大小、字体、光标形状等。
- 自动启动命令:如果你的团队有统一的开发容器环境,可以让终端启动时自动连接到指定容器,省去每次敲命令的麻烦。
- 状态栏和提示信息:部分场景可以通过修改 XAML 资源或者配置实现自定义显示内容,让终端一开就给你想要的提示。
我在网络上看到很多真实案例,比如有人给团队里的 Windows Terminal 配了一套统一配色和常用的 SSH 连接 profile,把每个环境连接做成了下拉菜单里的独立入口。这件事完全没有碰 C++,全靠settings.json就能完成。所以如果有人在问“怎么二次开发 Windows Terminal”,第一个答案一定是:先把你自己的settings.json玩明白。
4.3 深水区:真要改 C++ 时怎么定位和下手
当轻量配置不够用的时候,才需要进入真正修改源码的阶段。我从个人经验里挑几个常见的方向,帮你形成一套定位思路。
假设我的需求是“想要一个新的终端行为,比如关闭最后一个标签时自动执行某个命令”。我先去源码里搜关键词:last tab、CloseTab、TabClosed、SettingsAction,通常在src下的 XAML 文件和控制逻辑代码里能找到相关事件处理函数。定位之后,你会看到关闭标签页的路径大致是从 UI 按钮触发到 Tab 管理器的某个方法。接下来要做的不是盲目加代码,而是先理解现有的关闭流程里哪里是“最后一个标签页”的判断分支,然后在那个位置插入你的逻辑。
另一个典型需求是“自定义渲染效果”,比如强制使用某种字体特征、滤镜、背景动画。这种需求要动的是渲染器和TerminalControl的 XAML 层。你需要知道颜色画刷从哪来、每帧重绘的时钟信号在哪触发。这块我建议先从打开一个简单的 log 点开始,跟着一次重绘流程确认代码经过了哪里,再在关键对象上打断点看调用栈。调用的堆栈能告诉你的信息,往往比读一个静态类全图还要多。
在修改过程中,有几点经验很值得分享:
- 改动尽量先放在“内部行为”,不要一上来就改对外设置格式和公共 API。一旦改动了 schema,后续升级会很痛苦,对你和整个社区都不太友好。
- 每次动手前,先去
doc/里查一下有没有相关讨论。如果你准备实现的功能正好是社区已经讨论过但没实现的东西,设计文档里通常会写上原因和障碍,避免你重复踩坑。 - 熟读 Test 目录里的测试风格。如果你加了新逻辑,顺手补上一个测试,这对提升代码通过审阅的概率很有帮助,也避免下一个人接手时不知道你的行为应该如何被维持。
4.4 编译、调试和报错排查实用工具箱
Windows Terminal 的二次开发不像写 Node.js 那样改完就热更新,它需要一套自己的“断点调试 + 日志追踪 + 配置验证”组合方法。
我实操中用到比较多的排错工具包括:
- Visual Studio 的本地调试器:断点设在你关心的 C++ 代码路径上,这是最直接的手段。
- 运行日志:项目里有很多 TraceLogging 或类似的日志调用,可以在调试模式下开着,观察程序在某个操作前后打印的信息。
- 开发者模式下的内部状态:新版 Terminal 带有一些为调试者准备的开发者模式选项,能查看当前渲染帧率、使用中的渲染器等信息。打开这些选项,便于判断“是逻辑没生效,还是画面没刷新”。
- 检查
settings.json是否合法:改配置后如果程序无法启动,多半是 JSON 语法错误。可以先单独用任意 JSON 校验工具验证,再让程序加载。
一个常见问题是:编译成功了,但运行时窗口闪现一下就退出。这种时候不要直接去看代码,而是先去 Windows 事件查看器里看应用程序错误日志,很多崩溃原因会直接写在那里。如果是 XAML 资源加载失败,日志会告诉你某个资源文件没找到。如果是因为缺了 MSIX 打包依赖,你会看到Package相关的报错。把这些错误类型做个分类,排查会快很多。
我把比较典型的几个编译/运行错误和应对思路整理成了一张表:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 编译报 XAML 类型找不到 | 对应模块没有引用或 SDK 版本不匹配 | 检查解决方案的项目引用和 Windows SDK 版本 |
| 编译时报 C++/WinRT 头文件缺失 | 子模块未拉全或工具链不完整 | 重新git submodule update --init --recursive,检查 workload |
| 运行后闪退 | 配置文件解析失败,或依赖 DLL 缺失 | 先用 JSON 校验器检查配置文件,再看事件查看器 |
| 画面更新异常但逻辑正确 | 渲染区域失效标记未触发 | 查找你修改的代码路径是否调用了渲染器“需要重绘”的接口 |
| 快捷键无响应 | 动作 ID 拼写错误或配置冲突 | 检查 actions 数组里是否有重复 ID,比对官方 schema 名称 |
4.5 二次开发的“路线图”:由浅入深三板斧
如果你是一个想系统学习这个项目的开发者,我建议不要一次性扎到renderer里,而是按三步走。
第一步,先用settings.json深度定制自己的使用环境。目标是搞明白每个配置含义,对 Profile、Action、Color Scheme、Keybinding 有直觉。这一步,你收获的是使用层的能力。
第二步,在源码里寻找你刚才使用的 JSON 配置项是如何被读取的。比如搜索settings.json、KeyMapping、Profile等字段,见证从磁盘文件到内存对象的转换过程。这一步,你收获的是“配置系统设计”的能力。
第三步,找一个你反复不满意的小痛点,动手修改源码。不要选太大,也不要选需求不明确的,最好就是“我想把右键菜单里的某个功能文本改得更清晰”这类约束明确、边界清晰的任务。当你第一次改完源码、编译、运行,看到自己的改动生效时,整个项目的底层架构基本就串起来了。
我实际测试下来,这套路线比直接啃核心代码更不容易劝退。因为终端这个项目涉及的概念实在太多,直接深入渲染器很容易被字形、像素、DPI 这些概念淹没,坚持不下去。而通过“配置 → 追源码 → 改源码”的循环,你对每一层代码的动机都会更明确。
5. 写在最后:我看完这份源码最大的收获
如果只能挑一个让我印象最深的点,我认为不是某个具体技术,而是微软在这个项目里展现出来的“工程耐心”。一个终端应用,为了让老程序继续工作,甘愿造出一个 ConPTY 兼容层;为了让现代文本渲染达到高标准,投入了一套完整的 GPU 渲染管线;为了保证后续十年还能维护,连 UI 框架和错误处理库这种“基础设施”都换成了更适合长期演进的选择。
这种耐心在商业软件里不常见,但在开源世界却是项目活得久的关键。对我们这些阅读者来说,Windows Terminal 的开源仓库像一座架在“Windows 老控制台生态”和“现代终端体验”之间的桥梁,你既能在里面学到如何处理历史包袱,也能看到如何用现代 C++ 一步一步重构一个成熟系统。
我个人建议,不要在第一次阅读时就追求把所有细节装进脑子里。项目太大,一次吃成胖子是不可能的。更有效的方式是抱着一两个真实问题反复在代码里游荡,比如“当我改主题色时,渲染器是怎么感知的?”“当我复制文本时,文本内容来自哪一层?”。带着自己的问题去读,代码会变得友好得多。
如果后续有人问我,Windows Terminal 的二次开发到底值不值得投入,我的答案会是:值得,但你要清楚你是想通过配置解决自己的日常需求,还是想学习怎么构建一款现代终端产品。前者很简单,文档和 JSON 就能帮你实现绝大部分。后者则要把这个仓库当成一个为期数月的学习项目,从终端协议到图形渲染再到工程治理,一关一关过。
其实折腾到最后的体会是,终端窗口只是冰山一角,真正精彩的东西都在代码和设计决策的裂缝里。希望这篇文章能陪你走完第一段探索的路。