Termina 0.17.0

dotnet add package Termina --version 0.17.0
                    
NuGet\Install-Package Termina -Version 0.17.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Termina" Version="0.17.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Termina" Version="0.17.0" />
                    
Directory.Packages.props
<PackageReference Include="Termina" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Termina --version 0.17.0
                    
#r "nuget: Termina, 0.17.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Termina@0.17.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Termina&version=0.17.0
                    
Install as a Cake Addin
#tool nuget:?package=Termina&version=0.17.0
                    
Install as a Cake Tool

Termina

Termina Logo

NuGet Downloads GitHub License GitHub Actions Workflow Status GitHub Release

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

Termina component gallery: keyboard-driven menus, live spinners, and selection lists

A guided tour of the Termina component gallery.

Documentation

Full 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 StreamingTextNode for real-time content like LLM output
  • Dependency Injection - Full integration with Microsoft.Extensions.DependencyInjection
  • Hosting Integration - Works with Microsoft.Extensions.Hosting for 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
    }
}
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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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,030 10/16/2026
0.17.0-beta.6 818 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,342 10/14/2026
0.16.3 2,400 10/8/2026
0.16.2 16,744 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,261 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
Loading failed

**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)).