Skip to content

Instantly share code, notes, and snippets.

@artydev
Created August 7, 2026 10:41
Show Gist options
  • Select an option

  • Save artydev/43950a51fe2373e04b79396a83eacd53 to your computer and use it in GitHub Desktop.

Select an option

Save artydev/43950a51fe2373e04b79396a83eacd53 to your computer and use it in GitHub Desktop.
Termina llm.txt
# Termina: Reactive Terminal UI Framework for .NET
## Overview
Termina is a reactive terminal UI (TUI) framework for .NET that enables developers to build beautiful, interactive terminal applications with declarative layouts and reactive state management. It provides an MVVM architecture with automatic UI updates, ASP.NET Core-style routing, and seamless integration with Microsoft.Extensions.DependencyInjection and Microsoft.Extensions.Hosting.
**Version:** 0.15.1 (at documentation time)
**License:** Apache 2.0
**Repository:** https://github.com/Aaronontheweb/Termina
**Documentation:** https://aaronstannard.com/termina/
**NuGet Package:** https://www.nuget.org/packages/Termina
## Key Features
### Reactive MVVM Architecture
- ViewModels use `ReactiveProperty<T>` for observable state management
- Source-generated reactive properties with no boilerplate required
- `ReactiveProperty<T>` is both a value holder and an `Observable<T>` for automatic UI updates
- Full integration with R3 observables library (migrated from System.Reactive in v0.7.0)
### Declarative Layouts
- Tree-based layout system for composing complex UIs
- Fluent API with `.WithChild()` for clean composition
- Support for Vertical, Horizontal, Grid, and Stack layouts
- Size constraints: Fixed, Fill, Auto, and Percent
- Nesting layouts for complex UI composition
- Responsive design with terminal resize support
### Surgical Region-Based Rendering
- Only changed regions re-render, not the entire screen
- Enables smooth streaming updates for real-time content
- Direct ANSI rendering with zero external rendering dependencies
- Surgical updates for efficient terminal performance
### ASP.NET Core-Style Routing
- Type-safe route parameters with templates like `/tasks/{id:int}`
- Route parameter injection via `[FromRoute]` attribute
- Navigation via `Navigate()` and `NavigateWithParams()`
- Full type constraint support for route parameters
### Source Generators
- AOT (Ahead-of-Time) compatible code generation
- Native AOT publishing support with single-file executables
- Zero reflection approach for performance
### Streaming Support
- Native `StreamingTextNode` for token-by-token content rendering
- Ideal for LLM output, real-time logs, and live data streams
- Character-level subscription updates for smooth rendering
### Dependency Injection
- Full integration with `Microsoft.Extensions.DependencyInjection`
- Works seamlessly with `Microsoft.Extensions.Hosting`
- Allows clean lifecycle management and service composition
### Testing Support
- `VirtualInputSource` for automated testing with scripted input
- Test mode detection for CI/CD integration
- Deterministic input replay for regression testing
## Installation
### Requirements
- .NET 10.0 SDK or later
- Terminal emulator with ANSI support (Windows Terminal, iTerm2, etc.)
### Package Installation
```bash
dotnet add package Termina
dotnet add package Microsoft.Extensions.Hosting
```
## Core Architecture
### The Three-Part Pattern
Termina applications follow an MVVM pattern with three key components:
#### 1. ViewModel
- Inherits from `ReactiveViewModel`
- Manages application state using `ReactiveProperty<T>`
- Handles keyboard input via `Input.OfType<KeyPressed>()`
- Provides navigation and shutdown actions
- Must dispose all `ReactiveProperty<T>` instances in `Dispose()` method
- Implements `OnActivated()` for initialization logic
#### 2. Page
- Inherits from `ReactivePage<TViewModel>`
- Builds UI layout in `BuildLayout()` method
- Implements reactive bindings via `.Select(...).AsLayout()` pattern
- Manages focus for modals and interactive controls
- Optionally implements `OnBound()` for additional initialization
#### 3. Host Configuration
- Uses `Microsoft.Extensions.Hosting` for lifecycle management
- Registers routes via `AddTermina()` and `RegisterRoute<TPage, TViewModel>()`
- Supports virtual input for testing
### Reactive Property Pattern
`ReactiveProperty<T>` is the foundation of Termina's reactivity:
- Holds a value accessible via `.Value` property
- Emits observables when value changes
- Built-in `DistinctUntilChanged` behavior (only emits on actual value change)
- Used in layout bindings: `ViewModel.Count.Select(...).AsLayout()`
- Must be disposed properly to prevent memory leaks
## Layout System
### How Layout Works
The layout system processes node trees in two phases:
**1. Measure Phase:** Starting from root, each node calculates how much space it needs given available space. Container nodes recursively measure children.
**2. Render Phase:** Once measurements are complete, each node receives final bounds and renders to the terminal.
### Layout Nodes vs. Container Nodes
**Layout Nodes (render content):**
- `TextNode` - Styled text with word wrapping
- `PanelNode` - Bordered container with title
- `SpinnerNode` - Animated loading indicator
- `StreamingTextNode` - Streaming content with scrolling
- `GraphNode` - Live scrolling graph with gradient coloring
- `ProgressBarNode` - Progress bar with gradient fill and label
- `TextInputNode` - Single-line text input with cursor
- `TextAreaNode` - Multi-line text input with word wrap
- `CopyableTextNode` - Read-only text with clipboard support
- `SelectionListNode` - Interactive list selection
- `FilePickerNode` - File/folder picker
**Container Nodes (arrange children):**
- `VerticalLayout` - Stack children top-to-bottom
- `HorizontalLayout` - Stack children left-to-right
- `GridNode` - 2D grid with consistent column/row sizing
- `StackLayout` - Overlay children (z-stack)
- `ScrollableContainer` - Scrollable content area
- `ModalNode` - Modal overlay with backdrop
### Size Constraints
Four constraint types control sizing:
| Constraint | Description | Example |
|-----------|------------|---------|
| `Fixed(n)` | Exactly n rows/columns | `.Height(3)` |
| `Fill(weight)` | Take remaining space | `.Fill()` or `.Fill(2)` |
| `Auto` | Size to content | `.HeightAuto()` |
| `Percent(n)` | n% of available space | `.Height(SizeConstraint.Percent(50))` |
### Common Layout Patterns
**Vertical stacking with header, content, footer:**
```csharp
Layouts.Vertical()
.WithChild(header.Height(3)) // Fixed height
.WithChild(content.Fill()) // Take remaining space
.WithChild(footer.Height(1));
```
**Horizontal layout with weighted fills:**
```csharp
Layouts.Horizontal()
.WithChild(menu.Width(30))
.WithChild(main.Fill(2)) // 2x weight
.WithChild(aside.Fill(1)); // 1x weight
```
**Nested layouts:**
```csharp
Layouts.Vertical()
.WithChild(
Layouts.Horizontal()
.WithChild(sidebar.Width(20))
.WithChild(content.Fill())
)
.WithChild(footer.Height(1));
```
## Component Library
### Display Components
**TextNode**
- Renders styled text with word wrapping
- Supports foreground/background colors
- Methods: `.Bold()`, `.Dim()`, `.Italic()`, `.Underline()`, `.Strike()`, `.WithForeground()`, `.NoWrap()`
**PanelNode**
- Bordered container with optional title
- Border styles: `BorderStyle.Single`, `BorderStyle.Double`, `BorderStyle.Rounded`
- Methods: `.WithTitle()`, `.WithBorder()`, `.WithBorderColor()`, `.WithTitleColor()`, `.WithContent()`
**SpinnerNode**
- Animated loading indicator
- Various spinner styles available
**StreamingTextNode**
- For real-time, token-by-token content rendering
- Created via `StreamingTextNode.Create()`
- Add content: `.Append(chunk)`
- Subscription pattern for ViewModel observables
**GraphNode**
- Live scrolling graph with gradient coloring
**ProgressBarNode**
- Progress bar with gradient fill and label
### Input Components
**TextInputNode**
- Single-line text input with cursor
- Keyboard input handling
**TextAreaNode**
- Multi-line text input
- Word wrap and vertical scrolling
**SelectionListNode**
- Interactive list selection
- Keyboard navigation
**FilePickerNode**
- File/folder picker with directory navigation
**CopyableTextNode**
- Read-only text with keyboard selection and clipboard support
### Container Components
**ScrollableContainer**
- Vertical scrolling container for overflow content
**StackLayout**
- Overlapping children (z-stack) for layering
**ModalNode**
- Modal overlay with backdrop
### Reactive Components
**ReactiveLayoutNode**
- Updates content from observables
**ConditionalNode**
- Show/hide content based on condition
**DynamicLayoutNode**
- Re-evaluates factory on invalidation
**KeyedDynamicLayoutNode**
- Key-based content switching with caching
**WizardNode**
- Multi-step wizard with progress, navigation, and focus
### Utility Components
**EmptyNode**
- Placeholder that renders nothing
**DeferredNode**
- Delegates to node without owning it
## Input Handling
### Keyboard Input
Input is accessed via `Input.OfType<IInputEvent, KeyPressed>()` in ViewModels:
```csharp
public override void OnActivated()
{
Input.OfType<IInputEvent, KeyPressed>()
.Subscribe(HandleKeyPress)
.DisposeWith(Subscriptions);
}
private void HandleKeyPress(KeyPressed key)
{
switch (key.KeyInfo.Key)
{
case ConsoleKey.UpArrow:
// Handle up arrow
break;
case ConsoleKey.Enter:
// Handle enter
break;
case ConsoleKey.Escape:
Shutdown();
break;
default:
// Handle printable characters
if (key.KeyInfo.KeyChar >= 32 && key.KeyInfo.KeyChar < 127)
{
// Character input
}
break;
}
}
```
### Printable Character Handling
Access the character via `key.KeyInfo.KeyChar` property.
### Shutdown
Call `Shutdown()` to exit the application.
## Routing
### Route Registration
Routes are registered during host configuration:
```csharp
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 Parameters
Routes support type-safe parameters with type constraints:
```csharp
// Route template with parameter
"/tasks/{id:int}"
// ViewModel with 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 between routes programmatically:
```csharp
// Simple navigation
Navigate("/tasks/42");
// With parameters
NavigateWithParams("/tasks/{id}", new { id = 42 });
// Exit application
Shutdown();
```
## Styling and Colors
### Color System
Termina supports ANSI colors:
- Basic colors: `Color.Black`, `Color.Red`, `Color.Green`, `Color.Yellow`, `Color.Blue`, `Color.Magenta`, `Color.Cyan`, `Color.White`, `Color.Gray`
- Bright variants: `Color.BrightRed`, `Color.BrightGreen`, etc.
### Text Styling
Apply styling via fluent API:
- `.WithForeground(Color.Cyan)` - Text color
- `.WithBackground(Color.Black)` - Background color
- `.Bold()` - Bold text
- `.Dim()` - Dimmed text
- `.Italic()` - Italic text
- `.Underline()` - Underlined text
- `.Strike()` - Strikethrough text
### Word Wrapping
Control text wrapping:
- `.NoWrap()` - Disable word wrapping, truncate at edge
## Reactive Bindings
### Observable to Layout Pattern
Convert observables to layout nodes:
```csharp
ViewModel.Count
.Select<int, ILayoutNode>(count => new TextNode($"Count: {count}")
.WithForeground(Color.BrightCyan))
.AsLayout()
```
### Multiple Observable Bindings
Combine multiple observables:
```csharp
Observable.CombineLatest(
ViewModel.Title,
ViewModel.Message
)
.Select(x => new TextNode($"{x[0]}: {x[1]}").WithForeground(Color.Cyan))
.AsLayout()
```
### Streaming Content Pattern
For real-time content like LLM output:
```csharp
// 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
Use `VirtualInputSource` for automated testing with scripted input:
```csharp
var scriptedInput = new VirtualInputSource();
builder.Services.AddTerminaVirtualInput(scriptedInput);
// Queue input
scriptedInput.EnqueueKey(ConsoleKey.UpArrow);
scriptedInput.EnqueueString("Hello World");
scriptedInput.EnqueueKey(ConsoleKey.Enter);
scriptedInput.Complete();
await host.RunAsync();
```
### Test Mode Detection
Detect test mode with command-line flag:
```csharp
var testMode = args.Contains("--test");
if (testMode)
{
var scriptedInput = new VirtualInputSource();
builder.Services.AddTerminaVirtualInput(scriptedInput);
// Queue test inputs
scriptedInput.EnqueueKey(ConsoleKey.UpArrow);
// ... more inputs
scriptedInput.Complete();
}
```
## Complete Application Template
### Step 1: Define ViewModel
```csharp
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");
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();
}
}
```
### Step 2: Define Page
```csharp
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));
}
}
```
### Step 3: Configure Host
```csharp
using Microsoft.Extensions.Hosting;
using Termina.Hosting;
var builder = Host.CreateApplicationBuilder(args);
// Optional: suppress host logging
builder.Logging.SetMinimumLevel(LogLevel.Warning);
// Register Termina
builder.Services.AddTermina("/counter", termina =>
{
termina.RegisterRoute<CounterPage, CounterViewModel>("/counter");
});
var host = builder.Build();
await host.RunAsync();
```
## Common Patterns
### List Display
```csharp
ViewModel.Items
.Select<List<string>, ILayoutNode>(items => new TextNode(
string.Join("\n", items.Take(10)) // Show first 10 items
))
.AsLayout()
```
### Status Display with Reactive Updates
```csharp
new PanelNode()
.WithTitle("Status")
.WithContent(
Observable.CombineLatest(
ViewModel.Status,
ViewModel.Progress
)
.Select(x => new TextNode($"{x[0]} - {x[1]}%"))
.AsLayout()
)
```
### Modal Dialog
```csharp
new ModalNode()
.WithTitle("Confirm")
.WithContent(new TextNode("Are you sure?"))
.WithBackdrop(true)
```
### Scrollable Content
```csharp
new ScrollableContainer()
.WithChild(contentNode)
.Height(20)
```
### Loading Indicator
```csharp
new SpinnerNode()
.WithLabel("Loading...")
```
## Performance Considerations
1. **Surgical Rendering:** Only changed regions re-render automatically
2. **ReactiveProperty Disposal:** Always dispose in ViewModel.Dispose()
3. **Subscription Management:** Use `.DisposeWith(Subscriptions)` in OnActivated()
4. **String Concatenation:** For frequently updated content, consider StringBuilder
5. **Observable Subscriptions:** Subscribe only to properties that change in BuildLayout()
## Breaking Changes
### Version 0.7.0
- Migrated from System.Reactive to R3
- R3 observable syntax may differ slightly from System.Reactive
- See migration guide at https://aaronstannard.com/termina/guide/migration-0.7.html
### Version 0.11.0
- Changes to render loop threading
- See upgrade guide at https://aaronstannard.com/termina/guide/upgrade-0.11.html
## Resources
- **Official Documentation:** https://aaronstannard.com/termina/
- **GitHub Repository:** https://github.com/Aaronontheweb/Termina
- **NuGet Package:** https://www.nuget.org/packages/Termina
- **Author:** Aaron Stannard (https://aaronstannard.com/)
- **License:** Apache 2.0
## Akka.NET Integration Potential
While Termina doesn't have a hard dependency on Akka.NET, the framework was created by Aaron Stannard, who also founded Akka.NET. This creates significant architectural synergy opportunities:
### Observable-to-Observable Bridge
Termina's R3 observables can integrate with Akka.NET's streaming infrastructure:
```csharp
// Termina observable to Akka.Streams
public ReactiveProperty<string> MessageStream { get; } = new("");
// Bridge to Akka.Streams
Source.FromObservable(ViewModel.MessageStream)
.RunForEach(msg => {
// Process Akka.NET actor messages into Termina UI
});
```
### Actor-Based Backend with Termina UI
Use Akka.NET actors for backend logic and Termina for the TUI frontend:
```csharp
// Host configuration with both Akka.NET and Termina
var builder = Host.CreateApplicationBuilder(args);
// Register Akka.NET
builder.Services.AddAkka("termina-app", configBuilder => {
configBuilder.AddActorSystem();
});
// Register Termina
builder.Services.AddTermina("/dashboard", termina => {
termina.RegisterRoute<DashboardPage, DashboardViewModel>("/dashboard");
});
```
### Real-Time Dashboard Pattern
Common use case: Use Akka.NET actors to process distributed data and Termina to display it:
- **Akka.NET:** Handles concurrent data aggregation, event sourcing, clustering
- **Termina:** Displays real-time metrics with surgical rendering and responsive UI
- **Integration:** R3 observables bridge actor state to UI bindings
### Streaming Architecture
Akka.Streams integrates with observables (since version 1.3.2):
```csharp
// Expose Akka.Streams as observable
IObservable<T> observable = akkaStream.RunWith(Sink.AsObservable<T>(), materializer);
// Use in Termina UI
observable
.Select(item => new TextNode($"{item}"))
.AsLayout()
```
### Backpressure and Flow Control
Akka.Streams offers backpressure awareness that R3 observables can leverage:
- Prevents UI from being overwhelmed by rapid state changes
- Buffers with configurable overflow strategies
- Maintains responsiveness under high-volume actor message streams
### Testing Distributed Systems
Combine Akka.TestKit with Termina's VirtualInputSource for end-to-end testing:
```csharp
// Test Akka actors and their UI representation
var probe = CreateTestProbe();
var actorRef = ActorOf<MyActor>();
// Verify UI behavior with scripted input
var scriptedInput = new VirtualInputSource();
builder.Services.AddTerminaVirtualInput(scriptedInput);
// Send messages to actor, verify UI updates
actorRef.Tell(new MyMessage());
scriptedInput.EnqueueKey(ConsoleKey.Enter);
```
### Deployment Scenarios
**Single-Node Monitor:**
- Termina TUI for local terminal-based dashboards
- Akka.NET local actors for data collection
**Distributed Monitoring:**
- Akka.Cluster processes data on multiple nodes
- Termina TUI on each node for local monitoring
- Akka.Remote for inter-node communication
**CLI Tools with Actor Backend:**
- Termina provides interactive CLI interface
- Akka.NET handles long-running background tasks
- Observable streams bridge progress/status updates
## Related Concepts
- **MVVM Pattern:** Model-View-ViewModel architectural pattern
- **Reactive Programming:** Using observables for state management
- **Terminal UI (TUI):** Text-based user interfaces in terminal/console
- **ANSI Escape Codes:** Terminal formatting and color codes
- **Dependency Injection:** Microsoft.Extensions.DependencyInjection
- **Async/Await:** Asynchronous programming model for .NET
- **R3 Observables:** Modern reactive extensions library for .NET
- **Akka.NET:** Actor model framework for distributed/concurrent systems
- **Akka.Streams:** Stream processing with backpressure on top of actors
- **Microsoft.Extensions.Hosting:** Host lifecycle management
## Summary
Termina is a production-ready, modern terminal UI framework that brings ASP.NET Core patterns to terminal applications. Its reactive architecture, surgical rendering, and declarative layout system make it ideal for building interactive CLI tools, dashboards, monitoring applications, and any text-based UI that needs real-time updates. The framework's AOT compatibility ensures you can deploy single-file executables for any platform.
## Complementary Technologies
Aaron Stannard (Termina's creator) is also the founder of Akka.NET, which creates natural architectural synergy:
- **Termina:** Excellent for responsive, declarative TUI layer
- **Akka.NET:** Ideal for concurrent, distributed, fault-tolerant backend
- **Observable Bridge:** R3 observables seamlessly connect actor state to UI updates
For systems requiring both sophisticated backend logic (actor model, distributed processing, event sourcing) and interactive terminal interfaces (dashboards,
monitoring, real-time displays), combining Akka.NET and Termina provides a cohesive, modern development experience across the full stack.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment