Building Windows Terminal with WinUI
WinUI and Windows Terminal have a strong relationship that goes back to the origins of Windows Terminal. This blog post goes into the history and architecture of how these two technologies came together.
The history of WinUI and Windows Terminal goes back to December 2017. This was around the time the first prototyping of the application that would become Windows Terminal began. We, as the Windows Terminal team, had just started getting conpty into a place that was good enough to be useful as a translation layer between the console and a terminal application. We knew that we wanted to have conpty be able to power a new terminal application, allowing us to build a new user experience for the command line on Windows.
When we were in the earliest planning stages for Terminal, we knew right off the bat that we wanted to be able to build a modern application that used the best features the Windows platform had to offer. We wanted to make sure that it was visually consistent with other inbox applications and followed Fluent Design principles.
We made the choice to build Windows Terminal as a UWP application. That would immediately grant us access to UWP XAML and the whole WinUI 2 library, which has native controls, styles, and capabilities built specifically for Windows apps. At the time, “UWP applications” had special restrictions placed on them by the OS. These security features ensured that UWP apps delivered by the Microsoft Store were unable to access restricted locations within the OS, or leave artifacts laying around on disk, to simplify the uninstall process. These UWP apps are all launched inside a “low integrity app container”, which basically means they have effectively no permissions on the OS. They’re not even allowed to change their working directory. They’re allowed to launch external processes, but these external processes are additionally bound by the same restrictions the app is bound to.
Therefore, if we wanted to build Terminal as a UWP app in 2018, then any of the shells that we would spawn (like cmd.exe, powershell.exe or bash) would be unable to do anything to the system. Could you imagine using your shell without being able to change directories, read file contents, or launch any other processes that you could interact with? Obviously, this was a non-starter for us.
We spent a good portion of that year discussing our options with a number of teams across the Windows organization. We considered options like a special restricted capability for launching full-trust processes from a UWP. We tried pushing on allowing UWP apps to run outside the app container. However, most of the routes we pursued ended up either as a gaping security hole, or as something that would require years of effort across the organization. (In fact, many of these discussions helped lead to the effort now known as “Project Reunion“, as well as the WinUI 3 effort that extends the modern UI framework to Desktop apps).
Fortunately for us, the solution was actually closer than we thought. Just down the hall from us was the WinUI team, and little did we know, they had been building XAML Islands for the better part of the previous year. XAML Islands would enable a traditional Win32-style application to host UWP XAML content within its window without the traditional restrictions of a UWP application. As it turns out, this was exactly what we needed.
What we ended up building was an application that was composed of two parts:
- A thin Win32 window that is mostly just responsible for drawing a window to the screen and starting up the XAML Hosting infrastructure
- The bulk of the application which is built as a UWP XAML Application that uses WinUI 2. This layer doesn’t even know that it’s being hosted by a Win32 window. For all it knows, it’s just a normal C++/WinRT UWP application.
This model allows Terminal to get the best of both worlds. As a Win32 application, we’re able to use all of the legacy Win32 APIs that a terminal app might need to launch processes and query the filesystem. We’ve also been able to use the power of UWP XAML and WinUI 2 to help build the UI of Terminal quickly and beautifully.
For a more detailed breakdown of Windows Terminal’s architecture, let’s refer to the following diagram:
In this figure, the top, orange box is our Win32 layer. This layer builds our final executable, and contains all the logic for manipulating our Win32 window, as well as hosting a XAML island for our XAML content.
The lower two blue boxes are both components within the Windows Terminal application. Both of these two components are essentially just UWP XAML components, which could be hosted in a traditional UWP application. They just so happen to be hosted in a Win32 app for Windows Terminal, but there’s nothing stopping them from being reused in a pure UWP context. In fact, the Terminal repo even builds a pure UWP application that hosts the TerminalApp, to prove that it’s possible to do so.
The TerminalApp project represents all of the logic for the app itself – things like tabs, panes, settings, most of our UI elements; they’re all a part of the TerminalApp project. As a part of the terminal application, it can host multiple terminal instances simultaneously, either in other tabs, or even side-by-side (in panes). To help us break the solution up logically, the TerminalControl is its own project: the TerminalControl project. Some day, we hope to spin out a nuget package for just the TerminalControl, so that other application developers can also make use of it inside their own applications.
We knew that we wanted to build the absolute fastest terminal possible on Windows. XAML text elements are quick and convenient, but the WinUI team pointed out that we could achieve even faster performance by rendering our text buffer with a custom DirectX text renderer, so we went that route.
We fortunately had already begun work on a DirectX renderer for the vintage console to replace the GDI renderer. It was relatively trivial then to be able to hook up the renderer we had already to XAML’s SwapChainPanel class. This allowed us to not only render text incredibly quickly on the GPU, but also to reuse a large portion of the console codebase in the new Terminal. This means that improvements to Terminal also improve the inbox console.
That’s not to say that the journey to building a hybrid application was totally pain-free. We’ve hit a few major roadblocks since we started working on Terminal which we didn’t expect going into this process:
- It has proven to be fairly difficult to get our UI into the title bar area. While Terminal now does iconically have its tab strip in the title bar, that was actually a decent amount of heavy lifting in the Win32 part of the application. Fortunately, we had the help of community member @greg904. Without him, the Terminal title bar would not be nearly as polished as it is today.
- Packaging a hybrid application has had its own difficulties. The application packaging project has proven finnicky to get our dependencies placed into exactly the right folders within our project, meaning we’ve had to write a lot of msbuild targets manually. This is likely because the wapproj project type wasn’t really designed for these.
- By constructing our own TerminalControl, instead of using existing XAML elements, we did need to build out UIA and accessibility support for the control ourselves. This ended up being a not entirely trivial process and definitely cost us more dev hours than we expected. However, it was absolutely imperative to ensure that all developers would be able to use Terminal effectively.
One of the key features that Terminal needed was tabs – users expect to have multiple terminals open with tasks running in parallel. Not only that, the tab control needed to look and feel good, just like the tab experience within a browser. When we first started looking at building a tab experience, there was no tab control within WinUI 2, though there was an initial implementation in the Windows Community Toolkit. While this initial C# implementation helped set the API direction for the control, we needed a high-performance, high-polish control that could only be found in WinUI. We worked closely with the WinUI team (who in turn worked closely with various teams within Microsoft such as the WCT folks and Edge team) to define the look and feel of the TabView control built into WinUI 2.
From our point of view, this partnership has been worthwhile because a lot of the functional requirements for TabView such as reordering and closing tabs are built by the WinUI team – and from the WinUI team’s point of view, this partnership has been worthwhile because Terminal has helped shape the requirements and design of a general purpose TabView that can be used within other apps. Plus, the WinUI TabView is flexible – for example, Terminal includes a custom dropdown tab button that isn’t found in the “stock” TabView but is well supported within the control.
We worked with the WinUI team as well as many designers to define what TabView would look like. We went from having basic sketches to high fidelity mockups that in turn were developed as a working control. Below is a mockup we created while we were still defining what we wanted Terminal to look like.
After working with multiple designers and iterating over many mockups, TabView became what it is today and it is a staple feature inside Windows Terminal.
As Windows Terminal gains more features, we are able to continually work with WinUI to add more controls as these become available. The next big item on the list is the settings UI, which will use many WinUI 2 controls that are available today. This partnership has been fantastic and we can’t wait to continue to improve both Windows Terminal and WinUI with the creation of new features and controls.