If you have ever tried to build anything beyond a progress bar in a .NET terminal application, you know the pain. You start with Console.SetCursorPosition, graduate to ANSI escape sequences, and eventually end up managing your own rendering loop, input handling, and layout calculations. The terminal is a grid of characters, not a UI platform, and the tooling has historically reflected that.
XenoAtom.Terminal.UI challenges that assumption. Created by Alexandre Mutel (the author of Markdig, Scriban, and other well-known .NET libraries), it is a reactive, retained-mode terminal UI framework targeting .NET 10. Rather than treating the terminal as a stream of text you write to, it treats it as a composable surface where controls have layout, state, styling, and data binding, much like a desktop UI framework. Version 2.0 landed in early April 2026, and with over 60 built-in controls and a JetBrains OSS Power-Ups webinar scheduled for it, the project is gaining traction quickly.
Retained-mode vs immediate-mode
Most terminal UI approaches in .NET are immediate-mode. You write characters to a buffer, flush, and repeat. If something changes, you redraw everything. It works, but it forces you to manage state, diffing, and invalidation yourself.
XenoAtom.Terminal.UI uses a retained-mode model. You declare a tree of controls, and the framework handles layout, rendering, and incremental updates. When a bound value changes, only the affected visuals are invalidated and re-rendered. The framework diffs its internal cell buffer against the previous frame and emits only the terminal sequences needed to update the changed cells.
This is the same architectural pattern that made React, SwiftUI, and WPF productive. Applied to the terminal, it means you stop thinking about cursor positions and start thinking about composition.
Getting started
The package targets net10.0 and requires the .NET 10 SDK with C# 14:
dotnet add package XenoAtom.Terminal.UI
The framework supports three hosting models. The simplest is inline rendering, which writes a widget to the terminal and returns:
using XenoAtom.Terminal.UI;
Terminal.Write(new Group("System Status")
.Content(new VStack("API: Healthy", "Database: Connected", "Cache: Active").Spacing(1))
);
For live-updating displays such as progress bars or monitoring dashboards, use Terminal.Live():
var downloadTask = new ProgressTask("Downloading packages");
Terminal.Live(
new ProgressTaskGroup().Tasks([downloadTask]),
onUpdate: () =>
{
downloadTask.Value = Math.Min(1, downloadTask.Value + 0.01);
return downloadTask.Value < 1
? TerminalLoopResult.Continue
: TerminalLoopResult.StopAndKeepVisual;
});
For full-screen interactive applications, Terminal.Run() switches to the alternate screen buffer and runs an input loop:
State<bool> exit = new(false);
State<string?> name = new("World");
Terminal.Run(
new VStack(
new TextBox(name),
new TextBlock(() => $"Hello, {name.Value}!"),
new Button("Quit").Click(() => exit.Value = true)
),
onUpdate: () => exit.Value
? TerminalLoopResult.StopAndKeepVisual
: TerminalLoopResult.Continue
);
Notice there is no manual rendering, no cursor management, and no ANSI codes. You declare what the UI looks like, and the framework does the rest.
Reactive state with State<T>
The binding system is the heart of the framework. State<T> is a reactive container that automatically tracks which controls depend on its value. When you update a State<T>, only the controls that read it during their last render pass are invalidated.
State<int> requestCount = new(0);
State<double> errorRate = new(0.0);
var dashboard = new VStack(
new TextBlock(() => $"Requests: {requestCount.Value:N0}"),
new TextBlock(() => $"Error rate: {errorRate.Value:P1}"),
new BarChart()
.AddItem("Requests", () => requestCount.Value, Colors.Green)
.AddItem("Errors", () => (int)(requestCount.Value * errorRate.Value), Colors.Red)
).Spacing(1);
This is not polling or timer-based refresh. The framework tracks dependencies during the update/layout/render pipeline. Changing requestCount.Value invalidates only the controls that reference it, not the entire visual tree. If you have worked with computed properties in frameworks like MobX or signals in Solid.js, this will feel familiar.
Layout system
XenoAtom.Terminal.UI provides a consistent layout pipeline across all controls. The building blocks will be recognisable to anyone who has used a modern UI framework:
- VStack / HStack for vertical and horizontal stacking with configurable spacing
- Grid for row/column layouts with proportional and fixed sizing
- DockLayout for docking controls to edges (top, bottom, left, right, fill)
- Splitters for resizable panes
- Border, Group, Padder for decoration and spacing
var layout = new DockLayout(
new CommandBar("F1: Help F5: Refresh Esc: Quit")
.Dock(Dock.Bottom),
new VStack(
new TextBlock("Sidebar").Style(TextBlockStyle.Default with { Bold = true }),
new ListBox<string>(["Logs", "Metrics", "Traces"])
).Width(25).Dock(Dock.Left),
new LogControl().Dock(Dock.Fill)
);
Controls size themselves based on their content by default, but you can set explicit widths, heights, and proportional sizes on any element. The layout pipeline runs before rendering, so controls know their available space and can wrap, truncate, or scroll accordingly.
The controls library
With over 60 controls, the library covers significantly more ground than you might expect from a terminal framework. Here are some highlights:
Data controls are where the framework really shines. DataGridControl is a virtualised data grid with sorting, filtering, inline search, column resizing, and in-place cell editing. Table is a simpler read-only table. TreeView handles hierarchical data with expand/collapse.
Text editing is built on a shared subsystem that powers both TextBox (single-line) and TextArea (multi-line). Both support selection, clipboard integration, undo/redo with Ctrl+Z/Ctrl+R, and find/replace.
Visualisation controls include BarChart, LineChart, Sparkline, BreakdownChart, and Canvas for freeform drawing. These are genuine chart controls that render in the terminal using block characters and colour.
Overlays include Popup, Dialog (resizable and draggable), TooltipHost, and Toast notifications with a host system for stacking and auto-dismiss.
CommandPalette provides a fuzzy-search command launcher similar to VS Code's Ctrl+Shift+P, which can be wired to your application's command system.
Styling and theming
Controls support per-instance styling through record-based style objects. Because styles are records, you use with expressions to override specific properties without replacing the entire style:
var headerStyle = TextBlockStyle.Default with
{
Bold = true,
ForegroundColor = Colors.CornflowerBlue
};
var header = new TextBlock("Service Dashboard").Style(headerStyle);
The framework goes further than basic foreground/background colours. It supports RGBA colours with alpha blending, enabling layered visual effects that are unusual in terminal UIs. Brush gradients can be applied to text controls:
var gradient = Brush.LinearGradient(
new GradientPoint(0f, 0f),
new GradientPoint(1f, 0f),
[
new GradientStop(0f, Colors.DeepSkyBlue),
new GradientStop(1f, Colors.MediumPurple)
]);
var title = new TextBlock("The Runtime")
.Style(TextBlockStyle.Default with { ForegroundBrush = gradient });
Theming is built on ColorScheme palettes. The framework ships with several built-in schemes, and you can generate custom palettes from a base colour using the built-in colour scheme generator.
NativeAOT and performance
The framework is designed for NativeAOT compatibility from the ground up. There is no reflection-based binding or dynamic code generation. The State<T> tracking is compile-time safe and works under trimming and AOT compilation without additional configuration.
The rendering pipeline uses a cell-buffer diffing strategy. Each frame, the framework compares the new cell buffer against the previous one and emits only the ANSI sequences needed to update changed cells. It also supports DEC synchronised output (mode 2026), which batches terminal updates to eliminate flicker on terminals that support it.
A built-in performance overlay, toggled with F12 in fullscreen applications, displays frame timings, invalidation counts, and diff statistics. This is invaluable when optimising complex dashboards or data-heavy views.
Extension packages
Two official extension packages extend the core framework:
XenoAtom.Terminal.UI.Extensions.Markdown provides a MarkdownControl that renders Markdown directly in the terminal. It also includes MarkdownMarkupConverter for converting Markdown to the framework's markup format.
XenoAtom.Terminal.UI.Extensions.Screenshot uses SkiaSharp to export terminal UI snapshots as PNG, JPEG, or WebP images. This is particularly useful for documentation, automated testing, or sharing terminal UI designs.
How it compares
The .NET terminal UI space has a few established players. Spectre.Console is widely used for rich console output, tables, and progress displays, but it is primarily immediate-mode and does not offer interactive controls or reactive binding. Terminal.Gui (gui.cs) provides interactive widgets with an event-driven model, but uses a more traditional approach without reactive state management.
XenoAtom.Terminal.UI occupies a different position. Its reactive binding model, retained-mode rendering, and NativeAOT support put it closer to a desktop UI framework than a console helper library. The trade-off is that it targets .NET 10 exclusively, so it is not an option for projects on older runtimes.
Common pitfalls
Forgetting that State<T> tracks reads, not subscriptions. If a control reads state.Value inside a lambda passed to its constructor, it will be tracked. If you read the value outside the render path and pass the result as a plain string, the control will not update when the state changes. Always use the lambda-based constructors when you need reactivity.
Mixing Console.Write with the framework. The framework owns the terminal buffer in fullscreen mode. Writing directly to Console will corrupt the display. Use the framework's controls for all output.
Overusing Terminal.Run() for simple tasks. If you just need to display a table or progress bar, Terminal.Write() or Terminal.Live() is simpler and does not take over the terminal. Reserve Terminal.Run() for genuinely interactive applications.
Ignoring the performance overlay. If your fullscreen application feels sluggish, press F12 before reaching for a profiler. The overlay shows exactly which controls are invalidating and how much time each render pass takes.
Summary
- XenoAtom.Terminal.UI is a reactive, retained-mode terminal UI framework for .NET 10 with 60+ built-in controls
State<T>provides automatic dependency tracking and granular invalidation, similar to signals in modern frontend frameworks- Three hosting models (inline, live-updating, and fullscreen) cover everything from one-off output to interactive applications
- The layout system (VStack, HStack, Grid, DockLayout) works like a desktop UI framework
- NativeAOT support is baked in with no reflection or dynamic code generation
- Extension packages add Markdown rendering and image export
- The framework is BSD-2-Clause licensed and authored by Alexandre Mutel, known for Markdig, Scriban, and other widely-used .NET libraries