SageTUI.Html
0.1.0-preview
dotnet add package SageTUI.Html --version 0.1.0-preview
NuGet\Install-Package SageTUI.Html -Version 0.1.0-preview
<PackageReference Include="SageTUI.Html" Version="0.1.0-preview" />
<PackageVersion Include="SageTUI.Html" Version="0.1.0-preview" />
<PackageReference Include="SageTUI.Html" />
paket add SageTUI.Html --version 0.1.0-preview
#r "nuget: SageTUI.Html, 0.1.0-preview"
#:package SageTUI.Html@0.1.0-preview
#addin nuget:?package=SageTUI.Html&version=0.1.0-preview&prerelease
#tool nuget:?package=SageTUI.Html&version=0.1.0-preview&prerelease
SageTUI
Build beautiful terminal UIs in F# with zero ceremony.
Elm Architecture • SIMD rendering • 2,797 tests • < 32 B/frame on idle • Core package has zero external dependencies
📦 NuGet • 📖 Getting Started • 🖼️ Widget Gallery • 🔄 Migration Guide
See It In Action
<table> <tr> <td align="center" width="50%">
System Monitor — tabs, live metrics, scrolling process list
</td> <td align="center" width="50%">
Kanban Board — keyboard card navigation and multi-column moves
</td> </tr> <tr> <td align="center" width="50%">
Interactive Form — text input, dropdown selection, validation
</td> <td align="center" width="50%">
Hello World — the simplest possible TEA app
</td> </tr> </table>
Run any sample yourself:
dotnet run --project samples/09-SystemMonitor
Install
Core library
dotnet add package SageTUI
Project template
dotnet new install SageTUI.Templates
dotnet new sagetui -n MyApp
cd MyApp && dotnet run
Optional HTML bridge
SageTUI.Html is a separate package/project for HTML parsing and rendering. It is not part of the core SageTUI package.
60-Second Quickstart
If you want the fastest path, use the template:
dotnet new install SageTUI.Templates
dotnet new sagetui -n MyApp
cd MyApp
dotnet run
If you want to start from an empty F# console app, paste this complete Program.fs:
open SageTUI
type Msg =
| Increment
| Decrement
| Quit
let init () = 0, Cmd.none
let update msg count =
match msg with
| Increment -> count + 1, Cmd.none
| Decrement -> max 0 (count - 1), Cmd.none
| Quit -> count, Cmd.quit
let view count =
El.column [
El.text "Hello, SageTUI!"
|> El.bold
|> El.fg (Color.Rgb(255uy, 200uy, 50uy))
El.text ""
El.text (sprintf "Count: %d" count) |> El.bold
El.text ""
El.text "[j] increment [k] decrement [q] quit" |> El.dim
]
|> El.padAll 1
|> El.bordered Rounded
let keyBindings =
Keys.bind [
Key.Char 'j', Increment
Key.Char 'k', Decrement
Key.Char 'q', Quit
Key.Escape, Quit
]
let program : Program<int, Msg> =
{ Init = init
Update = update
View = view
Subscribe = fun _ -> [ keyBindings ] }
[<EntryPoint>]
let main _ = App.run program; 0
Press q or Esc to quit.
Smallest Possible Static View
For one-off screens and demos:
open SageTUI
App.display (fun () ->
El.text "Hello from SageTUI!"
|> El.bold
|> El.fg (Color.Named(Cyan, Bright))
|> El.bordered Rounded
|> El.padAll 1)
App.display is the smallest API surface; App.run is the full Elm Architecture entry point.
Interactive App (Elm Architecture)
open SageTUI
type Msg = Increment | Quit
let init () = 0, Cmd.none
let update msg count =
match msg with
| Increment -> count + 1, Cmd.none
| Quit -> count, Cmd.quit
let view count =
El.column [
El.text (sprintf "Count: %d" count) |> El.bold
El.text "[j] increment [q] quit" |> El.dim
] |> El.padAll 1 |> El.bordered Rounded
let keyBindings =
Keys.bind [
Key.Char 'j', Increment
Key.Char 'q', Quit
Key.Escape, Quit
]
let program : Program<int, Msg> =
{ Init = init
Update = update
View = view
Subscribe = fun _ -> [ keyBindings ] }
[<EntryPoint>]
let main _ = App.run program; 0
App.run auto-detects your terminal capabilities and handles setup, rendering, and cleanup.
Features
| Category | What You Get |
|---|---|
| Architecture | Elm Architecture (init/update/view/subscribe), pure state management |
| Layout | Row, Column, Fill, Percentage, Min/Max, padding, borders, alignment, gap, flex-shrink |
| Elements | Text, Row, Column, Overlay, Constrained, Bordered, Padded, Keyed, Canvas, Scroll (viewport clipping) |
| Borders | 6 border styles (Rounded, Light, Heavy, Double, ASCII, None) with optional titled borders (El.borderedWithTitle) |
| Rendering | Arena-allocated zero-GC frame loop, SIMD-accelerated diff, 24-bit TrueColor |
| Widgets | TextInput, Select, Table, Tabs, Modal, TreeView, ProgressBar, Checkbox, Toggle, RadioGroup, SpinnerWidget, Toast, Form, FuzzyFinder, TextEditor, SplitPane, VirtualList, VirtualTable |
| Focus | FocusRing<'F> with next/prev/isFocusedAt — tab cycling with no allocations, works with any type including [<NoEquality>] via index |
| Scrolling | ScrollState, VirtualList with scroll indicators, El.scroll element for viewport clipping |
| Canvas | HalfBlock (▀/▄) and Braille (⠿) pixel modes |
| Transitions | Runtime support for Fade, Wipe, Dissolve, and ColorMorph, with additional transition shapes modeled in the API |
| Themes | 5 built-in themes (dark, light, nord, dracula, catppuccin) |
| HTML Bridge | Optional SageTUI.Html package for parsing HTML fragments into Element trees |
| Mouse | Click subscriptions, hit-testing with Z-order, focus cycling |
| Safety | Restores the terminal on unhandled managed exceptions |
Why SageTUI
- F#-native TEA — immutable model, explicit messages, pure view functions
- Terminal-native layout — rows, columns, fill, percentages, borders, alignment, gap
- Fast rendering path — packed cells, arena lowering, and diffing that avoids repainting unchanged output
- Testable by design — integration and snapshot tests can render real programs without a live terminal
- Small mental model — toolkit, not framework ceremony
Layout Engine
Terminal-native flexbox with CSS vocabulary:
// Rows and columns
El.row [ El.text "Left" |> El.fill; El.text "Right" |> El.width 20 ]
// Box model
El.text "Content" |> El.padAll 1 |> El.bordered Rounded
// Alignment
El.text "Centered" |> El.center
// Gap between children
El.column [ El.text "A"; El.text "B"; El.text "C" ] |> El.gap 1
// Percentage sizing
El.row [
El.text "Sidebar" |> El.percentage 30
El.text "Main" |> El.fill
]
Widgets
TextInput.view focused model.Input
ProgressBar.view { ProgressBar.defaults with Percent = 0.75; Width = 40 }
Tabs.view {
Items = ["Home"; "Settings"; "Help"]
ActiveIndex = activeTab
ToString = id
ActiveColor = Some (Color.Named(Cyan, Bright))
InactiveColor = None
}
Table.view columns rows (Some selectedRow)
Modal.view { Modal.defaults with BorderStyle = Rounded; MaxWidth = Some 40 } content
TreeView.view id focused nodes treeState
Form.view fields focusedKey model
These are low-level building blocks. Most real apps compose them inside view functions rather than relying on a heavyweight retained widget tree.
Themes
let themed = Theme.dark // or: light, nord, dracula, catppuccin
Theme.heading themed "Styled heading"
Theme.panel themed "My Panel" (El.column [...])
.NET Interop
Call any .NET library from your update function:
let update msg model =
match msg with
| FetchData ->
model, Cmd.ofTask
(fun () -> httpClient.GetStringAsync("https://api.example.com/data"))
(fun result -> DataReceived result)
| DataReceived data -> { model with Data = data }, Cmd.none
HTML Rendering
SageTUI.Html is an optional companion package/project for rendering HTML into SageTUI elements.
open SageTUI.Html
let html = """<div>
<h1 style="color: cyan">Dashboard</h1>
<table>
<tr><th>Service</th><th>Status</th></tr>
<tr><td>API</td><td style="color: green">● Online</td></tr>
</table>
</div>"""
let element = HtmlString.parseFragment html
Performance
Benchmarked with BenchmarkDotNet on .NET 10.0, i7-11800H:
| Benchmark | Mean | Allocated |
|---|---|---|
| Buffer.diff identical (80×24) | 596 ns | 32 B |
| Buffer.diff 10% changed (80×24) | 2.85 μs | 2.2 KB |
| Arena render dashboard (80×24) | 15.3 μs | 3.4 KB |
| Ref render dashboard (80×24) | 14.1 μs | 6.6 KB |
| Arena 50-item column (80×50) | 29.9 μs | 22.5 KB |
| Arena nested 3-level (80×100) | 48.2 μs | 11.4 KB |
Headline: The arena render path allocates < 32 bytes per frame on idle screens — a 97% reduction from the original 3,640 B/frame reference implementation. SIMD-accelerated diff skips 16-cell chunks with a single Span.SequenceEqual, so unchanged frames cost < 600 ns.
Run benchmarks yourself: dotnet run -c Release --project SageTUI.Benchmarks
Architecture
┌─────────────────────────────────────────┐
│ Your Application │
│ init → update → view → subscribe │
├─────────────────────────────────────────┤
│ Element Tree │
│ Text · Row · Column · Styled · ... │
├─────────────────────────────────────────┤
│ Arena Rendering (zero-GC) │
│ Pre-allocated nodes, single pass │
├─────────────────────────────────────────┤
│ SIMD Diff (hardware accel) │
│ Only changed cells hit the terminal │
├─────────────────────────────────────────┤
│ Terminal Backend │
│ Auto-detect, TrueColor, mouse, raw │
└─────────────────────────────────────────┘
For a deep-dive into the render pipeline, element cases, arena design, and SIMD diff, see ARCHITECTURE.md.
Samples
The sample suite is tiered so the strongest experiences lead the front door:
| Tier | Sample | What It Shows |
|---|---|---|
| Flagship | 09 SystemMonitor | Dense operator console: tabs, scrolling, live updates, themes, telemetry |
| Showcase | 06 Kanban | Keyboard-driven board navigation and multi-column state |
| Showcase | 08 Sparklines | Canvas-based telemetry and compact data visualization |
| Showcase | 04 InteractiveForm | Keyboard-first form flow, TextInput, Select, validation |
| Supporting | 01 HelloWorld | Smallest possible TEA app with borders and key bindings |
| Supporting | 05 ColorPalette | Theme semantics, color modes, text styles |
| Supporting | 07 Transitions | Keyed transitions and animated state changes |
| Supporting | 02 Dashboard | Secondary overview sample; useful for layout ideas, not the hero |
| Experimental | 03 HtmlRenderer | HTML→Element bridge and document rendering lab |
If you only run one sample, start here:
dotnet run --project samples/09-SystemMonitor
Concepts
SageTUI uses the Elm Architecture (TEA):
- Model — your app's state (an immutable F# type)
- Msg — a discriminated union of everything that can happen
- init — returns
(model, Cmd)— your starting state - update —
msg → model → (model, Cmd)— handle events, return new state - view —
model → Element— render state as a terminal UI tree - subscribe —
model → Sub list— declare ongoing subscriptions (keyboard, timers, resize) - Cmd — side effects (async tasks, quit signal).
Cmd.nonemeans "no side effect" - Sub — ongoing event sources.
Keys.bindfor keyboard,TimerSubfor polling
Testing
SageTUI ships a first-class testing module (Testing.fs) so you can unit-test your programs without a real terminal.
Simulate keystrokes and inspect model/render
open SageTUI.Testing
let app =
TestHarness.init myProgram 80 24
|> TestHarness.pressKey Key.Enter
|> TestHarness.pressKey (Key.Char 'j')
|> TestHarness.typeText "hello"
// Assert on model
app |> TuiExpect.modelSatisfies "item selected" (fun m -> m.Selected = 1)
// Assert on rendered output
app |> TuiExpect.viewContains "label visible" "hello"
app |> TuiExpect.viewNotContains "error gone" "Error"
Framed failure output
TuiExpect prints a framed box on failure for immediate readability:
╔══════════════════════════════════════╗
║ viewContains: label visible ║
║ Expected substring: "hello" ║
║ Rendered output (80×24): ║
║ > Hello, World! ║
╚══════════════════════════════════════╝
Direct element rendering (no program needed)
let lines = TestHarness.renderElement 40 3 (El.text "Hello" |> El.bordered Rounded)
lines.[0] |> Expect.stringContains "top border" "╭"
Virtual time — no Thread.Sleep
TestHarness.advanceTime fully drains causal delay chains:
let app =
TestHarness.init timerProgram 80 24
|> TestHarness.advanceTime 1000 // fires all Cmd.Delay(≤1000ms, ...) chains
app |> TuiExpect.viewContains "tick fired" "Tick: 1"
Type text, focus, send messages
let app =
TestHarness.init formProgram 80 24
|> TestHarness.typeText "Alice"
|> TestHarness.focusNext
|> TestHarness.typeText "30"
|> TestHarness.sendMsg Submit
app |> TuiExpect.modelSatisfies "name captured" (fun m -> m.Name = "Alice")
Click hit-testing via arena hit map
let app =
TestHarness.init counterProgram 80 24
|> TestHarness.clickAt 5 3 // col 5, row 3 — resolves via arena hit map
app |> TuiExpect.modelSatisfies "button clicked" (fun m -> m.Count = 1)
Support & Stability
- Package version: 0.9.0
- Target framework: .NET 10.0
- API status: pre-1.0; expect iterative changes while the surface area settles
- Core package:
SageTUI - Template package:
SageTUI.Templates - Optional companion project:
SageTUI.Html - Experimental areas: the HTML bridge and some sample/demo surfaces are still evolving
Advanced: Custom Backend
For testing or custom terminal implementations:
let profile = Detect.fromEnvironment readEnv getTerminalSize
let backend = Backend.create profile
App.runWithBackend backend program
Where readEnv is a string -> string option function and getTerminalSize returns the current terminal size.
Requirements
- .NET 10.0+
- A terminal with ANSI escape sequence support (all modern terminals)
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. 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
- AngleSharp (>= 1.4.0)
- Falco.Markup (>= 1.2.0)
- FSharp.Core (>= 11.0.100)
- SageTUI (>= 0.9.2)
-
net8.0
- AngleSharp (>= 1.4.0)
- Falco.Markup (>= 1.2.0)
- FSharp.Core (>= 11.0.100)
- SageTUI (>= 0.9.2)
-
net9.0
- AngleSharp (>= 1.4.0)
- Falco.Markup (>= 1.2.0)
- FSharp.Core (>= 11.0.100)
- SageTUI (>= 0.9.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0-preview | 53 | 3/11/2026 |
0.1.0-preview: Initial preview. HTML/CSS layout parsing via AngleSharp, rendered into SageTUI Element trees. API is not yet stable.