Termina 0.17.0
dotnet add package Termina --version 0.17.0
NuGet\Install-Package Termina -Version 0.17.0
<PackageReference Include="Termina" Version="0.17.0" />
<PackageVersion Include="Termina" Version="0.17.0" />
<PackageReference Include="Termina" />
paket add Termina --version 0.17.0
#r "nuget: Termina, 0.17.0"
#:package Termina@0.17.0
#addin nuget:?package=Termina&version=0.17.0
#tool nuget:?package=Termina&version=0.17.0
Termina
![]()
Termina is a reactive terminal UI (TUI) framework for .NET with declarative layouts and surgical region-based rendering. It provides an MVVM architecture with reactive properties, ASP.NET Core-style routing, and seamless integration with Microsoft.Extensions.Hosting.
See It In Action

A guided tour of the Termina component gallery.
Documentation
Features
- Reactive MVVM Architecture - ViewModels with
ReactiveProperty<T>for observable state management - Declarative Layouts - Tree-based layout system with size constraints (Fixed, Fill, Auto, Percent)
- Surgical Rendering - Only changed regions re-render, enabling smooth streaming updates
- ASP.NET Core-Style Routing - Route templates with parameters (
/tasks/{id:int}) and type constraints - Source Generators - AOT-compatible code generation for route parameters
- Streaming Support - Native
StreamingTextNodefor real-time content like LLM output - Dependency Injection - Full integration with
Microsoft.Extensions.DependencyInjection - Hosting Integration - Works with
Microsoft.Extensions.Hostingfor clean lifecycle management - Presentation Modes - Supports the default full-screen mode and an opt-in primary-buffer mode
Installation
dotnet add package Termina
dotnet add package Microsoft.Extensions.Hosting
Upgrading to 0.7.0? This release migrates from System.Reactive to R3 with breaking API changes. See the Migration Guide for details.
Quick Start
1. Define a ViewModel
using R3;
using Termina.Input;
using Termina.Reactive;
public class CounterViewModel : ReactiveViewModel
{
public ReactiveProperty<int> Count { get; } = new(0);
public ReactiveProperty<string> Message { get; } = new("Press Up/Down to change count");
public override void OnActivated()
{
Input.OfType<IInputEvent, KeyPressed>()
.Subscribe(HandleKey)
.DisposeWith(Subscriptions);
}
private void HandleKey(KeyPressed key)
{
switch (key.KeyInfo.Key)
{
case ConsoleKey.UpArrow:
Count.Value++;
Message.Value = $"Count: {Count.Value}";
break;
case ConsoleKey.DownArrow:
Count.Value--;
Message.Value = $"Count: {Count.Value}";
break;
case ConsoleKey.Escape:
Shutdown();
break;
}
}
public override void Dispose()
{
Count.Dispose();
Message.Dispose();
base.Dispose();
}
}
ReactiveProperty<T> is both a value holder and an Observable<T> — subscribe directly in your Page for reactive UI bindings.
2. Define a Page
using R3;
using Termina.Extensions;
using Termina.Layout;
using Termina.Reactive;
using Termina.Rendering;
using Termina.Terminal;
public class CounterPage : ReactivePage<CounterViewModel>
{
public override ILayoutNode BuildLayout()
{
return Layouts.Vertical()
.WithChild(
new PanelNode()
.WithTitle("Counter Demo")
.WithBorder(BorderStyle.Rounded)
.WithBorderColor(Color.Cyan)
.WithContent(
ViewModel.Count
.Select<int, ILayoutNode>(count => new TextNode($"Count: {count}")
.WithForeground(Color.BrightCyan))
.AsLayout())
.Height(5))
.WithChild(
ViewModel.Message
.Select<string, ILayoutNode>(msg => new TextNode(msg))
.AsLayout()
.Height(1));
}
}
3. Configure and Run
using Microsoft.Extensions.Hosting;
using Termina.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTermina("/counter", termina =>
{
termina.RegisterRoute<CounterPage, CounterViewModel>("/counter");
});
await builder.Build().RunAsync();
Layout System
Termina uses a declarative tree-based layout system:
Layouts.Vertical()
.WithChild(header.Height(3)) // Fixed height
.WithChild(content.Fill()) // Take remaining space
.WithChild(sidebar.Width(20)) // Fixed width
.WithChild(footer.Height(1)); // Fixed height
Layouts.Horizontal()
.WithChild(menu.Width(30))
.WithChild(main.Fill(2)) // 2x weight
.WithChild(aside.Fill(1)); // 1x weight
Routing
ASP.NET Core-style route templates with parameter support:
builder.Services.AddTermina("/", termina =>
{
termina.RegisterRoute<HomePage, HomeViewModel>("/");
termina.RegisterRoute<TasksPage, TasksViewModel>("/tasks");
termina.RegisterRoute<TaskDetailPage, TaskDetailViewModel>("/tasks/{id:int}");
termina.RegisterRoute<UserPage, UserViewModel>("/users/{name}");
});
Route Parameter Injection
public partial class TaskDetailViewModel : ReactiveViewModel
{
[FromRoute] private int _id; // Injected from route
public override void OnActivated()
{
LoadTask(Id); // Id is already populated
}
}
Navigation
Navigate("/tasks/42");
NavigateWithParams("/tasks/{id}", new { id = 42 });
Shutdown(); // Exit the application
Streaming Content
For real-time content like LLM output, Pages own StreamingTextNode and subscribe to ViewModel observables:
// In Page
private StreamingTextNode _output = null!;
protected override void OnBound()
{
_output = StreamingTextNode.Create();
ViewModel.StreamOutput.Subscribe(chunk => _output.Append(chunk));
}
// In ViewModel
public Observable<string> StreamOutput => _streamOutput;
private readonly Subject<string> _streamOutput = new();
private async Task StreamResponse()
{
await foreach (var chunk in GetStreamingData())
{
_streamOutput.OnNext(chunk); // Character-level updates
}
}
Testing
VirtualInputSource enables automated testing:
var scriptedInput = new VirtualInputSource();
builder.Services.AddTerminaVirtualInput(scriptedInput);
scriptedInput.EnqueueKey(ConsoleKey.UpArrow);
scriptedInput.EnqueueString("Hello World");
scriptedInput.EnqueueKey(ConsoleKey.Enter);
scriptedInput.Complete();
await host.RunAsync();
Requirements
- .NET 10.0 or later
- AOT-compatible (Native AOT publishing supported)
License
Apache 2.0 - See LICENSE for details.
Contributing
Contributions are welcome! See CONTRIBUTING.md for development setup and guidelines.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.10)
- R3 (>= 1.3.1)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Termina:
| Package | Downloads |
|---|---|
|
Termina.Demo.Inline
Reactive terminal UI (TUI) framework for .NET with custom ANSI rendering, MVVM architecture, source-generated reactive properties, and ASP.NET Core-style routing. |
GitHub repositories (1)
Showing the top 1 popular GitHub repositories that depend on Termina:
| Repository | Stars |
|---|---|
|
netclaw-dev/netclaw
Simple, secure, reliable agents. Self-hosted. Open source. Built with .NET.
|
| Version | Downloads | Last Updated |
|---|---|---|
| 0.17.0 | 2,046 | 10/16/2026 |
| 0.17.0-beta.6 | 833 | 8/12/2026 |
| 0.17.0-beta.5 | 163 | 8/12/2026 |
| 0.17.0-beta.4 | 83 | 8/12/2026 |
| 0.17.0-beta.3 | 90 | 8/12/2026 |
| 0.17.0-beta.2 | 87 | 8/11/2026 |
| 0.17.0-beta.1 | 89 | 8/11/2026 |
| 0.16.4 | 1,357 | 10/14/2026 |
| 0.16.3 | 2,400 | 10/8/2026 |
| 0.16.2 | 16,751 | 8/14/2026 |
| 0.16.1 | 4,207 | 8/8/2026 |
| 0.16.0 | 128 | 8/7/2026 |
| 0.15.1 | 2,351 | 7/21/2026 |
| 0.15.0 | 7,262 | 7/1/2026 |
| 0.15.0-beta1 | 1,047 | 6/26/2026 |
| 0.14.0 | 1,057 | 6/23/2026 |
| 0.14.0-beta3 | 753 | 6/22/2026 |
| 0.14.0-beta.2 | 108 | 6/20/2026 |
| 0.14.0-beta.1 | 638 | 6/18/2026 |
| 0.13.0 | 150 | 6/18/2026 |
**Behavior Changes**
- **The event loop no longer runs queued work after it stops** ([#400](https://github.com/Aaronontheweb/termina/pull/400))
- When the loop stops, it drops all work that is still in the queue. This includes work that `Post` or `InvokeAsync` queued before the call to `Shutdown()`. Earlier versions could run that work.
- A task that `InvokeAsync` returned for dropped work ends as canceled with `TaskCanceledException`. The task ends this way even when the token of the caller is not canceled.
- `InvokeAsync` called after the loop stopped returns a canceled task. `Post` called after the loop stopped does not run its work.
- A queued `CommitAsync` call for inline output ends as canceled in the same way.
- Code that must run during shutdown must run before `Shutdown()`, not as queued work.
- **`RequestRedraw` can merge with a redraw request that is already queued** ([#400](https://github.com/Aaronontheweb/termina/pull/400))
- Change the state before you call `RequestRedraw()`. A render that runs between the call and the state change can draw the old state. The loop then skips the merged request.
- A `RedrawRequested.Instance` that an input source writes to the event channel always renders.
- `Input` subscribers still see every redraw request as `RedrawRequested`.
**New Features**
- **Added an opt-in inline presentation mode** ([#373](https://github.com/Aaronontheweb/termina/pull/373), fixes [#366](https://github.com/Aaronontheweb/termina/issues/366) and [#370](https://github.com/Aaronontheweb/termina/issues/370))
- `TerminalPresentationMode.Inline` keeps settled output in the primary buffer.
- `IInlineOutput` commits stable layouts above one bounded live region.
- `ScrollInputMode.NativeTerminal` leaves selection and scrollback under terminal control.
- `IInlineTerminalControl` adds relative cursor operations without a change to `IAnsiTerminal`. `AnsiTerminal` and `VirtualTerminal` implement the new contract.
- Full-screen mode remains the default mode.
- **Added semantic clipboard content for copyable text** ([#374](https://github.com/Aaronontheweb/termina/pull/374), fixes [#367](https://github.com/Aaronontheweb/termina/issues/367))
- `WithSemanticContent` separates complete clipboard text from compact display text.
- `TryCopy` reports clipboard failure and preserves the current selection.
- **Added a modified Enter key capability result** ([#375](https://github.com/Aaronontheweb/termina/pull/375), fixes [#240](https://github.com/Aaronontheweb/termina/issues/240))
- Termina queries active Kitty keyboard flags without a terminal-name guess.
- `TerminalInputCapabilitiesChanged` reports whether modified Enter keys remain distinct.
- `TerminaApplication.InputCapabilities` exposes the latest result.
- Windows console records report native modifier support.
- **Added prompt history cancellation** ([#376](https://github.com/Aaronontheweb/termina/pull/376), fixes [#368](https://github.com/Aaronontheweb/termina/issues/368))
- `CancelHistoryNavigation` restores the draft that existed before history navigation.
- The API resets history navigation without a change to current method signatures.
- **Added pointer-aware wheel routing** ([#376](https://github.com/Aaronontheweb/termina/pull/376), fixes [#240](https://github.com/Aaronontheweb/termina/issues/240))
- `MouseScrollEvent` preserves zero-based pointer coordinates and keyboard modifiers.
- Termina routes wheel input to the measured scroll region under the pointer.
- `ScrollableContainerNode` now implements the current `IScrollable` contract.
- **Added an optional panel background surface** ([#379](https://github.com/Aaronontheweb/termina/pull/379), fixes [#378](https://github.com/Aaronontheweb/termina/issues/378))
- `PanelNode.WithBackground(Color)` sets the surface. `PanelNode.Background` reads it.
- The panel fills its complete bounds, including the border cells.
- A child that resets its colors returns to the surface. A child that selects its own background keeps it.
- A scrollable or streaming node in a surface panel keeps its screen bounds, so the mouse wheel reaches it.
- A panel without a background keeps its current behavior.
**Performance**
- **The event loop renders once for each batch of queued events** ([#400](https://github.com/Aaronontheweb/termina/pull/400))
- The loop processes the events that are queued when a batch starts, and then renders once. Events that arrive during the batch go to the next batch.
- Before, the loop rendered the full page after every event. A slow render let the queue grow without limit.
- 20 `RequestRedraw` calls in one loop turn now cause 2 to 3 renders. Before, they caused 24 renders.
- The cursor blink alone now causes about 1.9 renders each second. Before, it caused about 4.3.
- The final frame is the same as before.
- **`TextAreaNode` measures a long line cheaply** ([#400](https://github.com/Aaronontheweb/termina/pull/400))
- `Measure` keeps the column counts of the text until the text changes. A call with unchanged text takes under 0.01 ms and allocates no memory. Before, it took 43 ms and allocated 16 MB for a line of 600,000 characters.
- `DisplayWidth.GetColumnCount` returns the length of printable ASCII text without an allocation.
- `TextAreaNode` keeps one line wrap for each width. A layout that measures and renders at different widths no longer wraps the line twice for each frame.
- One render of an unchanged 600,000-character line takes under 1 ms instead of 69 ms, and allocates 0.27 MB instead of 32 MB.
- **Ctrl+Q exits fast with a large transcript or a large paste** ([#400](https://github.com/Aaronontheweb/termina/pull/400))
- The loop waits for each batch with `WaitToReadAsync` and stops at a shutdown request. Before, the loop kept running while events kept arriving.
- An app with a 1,000,000-character line now exits in about 360 ms. Before, it did not exit within 90 s.
- **Wrapping a very long line is now linear in its length** ([#398](https://github.com/Aaronontheweb/termina/pull/398), fixes [#397](https://github.com/Aaronontheweb/termina/issues/397), first shipped in 0.16.4)
- `StyledWordWrapper`, `WordWrapper`, and `TextAreaNode` no longer re-measure the rest of a long line for every wrapped row.
- **Text writes to the diffing terminal allocate less** ([#379](https://github.com/Aaronontheweb/termina/pull/379))
- `DiffingTerminal.Write` skips the grapheme parse when every char is printable ASCII. All other text uses the old loop.
- A full-screen frame of a panel with a background allocates 40 KB instead of 400 KB. It renders in about 0.7 ms instead of 1.5 ms.
- This change applies to every app and every render, not only panels. The output is the same.
**Bug Fixes**
- **Fixed tail follow after a large content update** ([#383](https://github.com/Aaronontheweb/termina/pull/383))
- A `ScrollableContainerNode` with `TailWhenAtBottom` stopped at the old offset after the content grew by more than 2 rows. This happens often now, because the loop renders once for each batch of events.
- `Measure` now reads `IsNearBottom` before it replaces the content height.
- When the content shrinks, `Measure` clamps the offset to the new range. Before, a shrink could leave a blank pane.
- A user who scrolled up stays where the user is.
- **Restored all terminal and input state after a render failure**
- Termina now cancels input tasks before it propagates a render exception.
- Termina restores the cursor, mouse modes, keyboard modes, and input mode.
- **Corrected text-area prompt history traversal** ([#377](https://github.com/Aaronontheweb/termina/pull/377), fixes [#368](https://github.com/Aaronontheweb/termina/issues/368))
- Up recalls prompt history from the first visual line and saves the current draft.
- Down restores the saved draft from the last visual line.
- Up and Down keep their current visual movement between multiline rows.
- **Made container disposal safe when layout retirement overlaps teardown** ([#384](https://github.com/Aaronontheweb/termina/pull/384), first shipped in 0.16.2)
- `ContainerNode.Dispose()` now claims teardown atomically, so concurrent disposal no longer completes the same reactive invalidation subject twice.
- **Added a way to place the cursor in a pre-filled text input** ([#395](https://github.com/Aaronontheweb/termina/pull/395), fixes [#394](https://github.com/Aaronontheweb/termina/issues/394), first shipped in 0.16.3)
- `TextInputBaseNode.CursorPosition` gets and sets the cursor as a UTF-16 index into the editable text.
- `TextInputBaseNode.MoveCursorToEnd()` moves the cursor to the end of the text.
**Compatibility**
- Current enum values and public signatures keep their behavior. The release adds no public API change that breaks a caller, apart from the event loop behavior changes above.
- The current `AnsiTerminal(bool)` constructor keeps its behavior.
- An API approval test prevents accidental contract changes.
**Maintenance**
- Bumped `Microsoft.SourceLink.GitHub` to 10.0.303 ([#396](https://github.com/Aaronontheweb/termina/pull/396)) and pinned `System.Security.Cryptography.Pkcs` to 10.0.13 in the test project ([#399](https://github.com/Aaronontheweb/termina/pull/399)) to clear security advisory warnings.
- Updated `Microsoft.Extensions.DependencyInjection` and `Microsoft.Extensions.Hosting` to 10.0.11 ([#380](https://github.com/Aaronontheweb/termina/pull/380), [#389](https://github.com/Aaronontheweb/termina/pull/389)).
- Updated `Microsoft.CodeAnalysis.CSharp` and `Microsoft.CodeAnalysis.Analyzers` to 5.9.0 for the repository ([#387](https://github.com/Aaronontheweb/termina/pull/387)). The source generators and analyzers that ship in the package stay on 5.3.0, as in 0.16.4 ([#403](https://github.com/Aaronontheweb/termina/pull/403)). Consumers keep the same .NET SDK requirement.
- Updated the `incrementalist.cmd` tool to 1.2.2 ([#385](https://github.com/Aaronontheweb/termina/pull/385)).