nutui 1.0.4

dotnet add package nutui --version 1.0.4
                    
NuGet\Install-Package nutui -Version 1.0.4
                    
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="nutui" Version="1.0.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="nutui" Version="1.0.4" />
                    
Directory.Packages.props
<PackageReference Include="nutui" />
                    
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 nutui --version 1.0.4
                    
#r "nuget: nutui, 1.0.4"
                    
#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 nutui@1.0.4
                    
#: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=nutui&version=1.0.4
                    
Install as a Cake Addin
#tool nuget:?package=nutui&version=1.0.4
                    
Install as a Cake Tool

nutui

NuGet NuGet downloads License: MIT .NET

nutui is a pure C# / .NET terminal‑UI (TUI) framework for building rich, keyboard‑ and mouse‑driven console applications — file managers, dashboards, database browsers, editors, wizards.

It renders 24‑bit truecolor and degrades gracefully to 256‑color, 16‑color‑less monochrome, and ASCII box‑drawing depending on what the terminal (or the SSH session on the other end) can do. There are no native dependencies and no P/Invoke on the render path — just managed code that talks ANSI — so it runs the same on Windows, Linux, and macOS, is safe over SSH, and is NativeAOT‑compatible.

┌ nutui ───────────────────────────────────┐
│                                          │
│   Hello, terminal!  Press Esc to quit.   │
│                                          │
└──────────────────────────────────────────┘
File manager in the DosBlue theme Query Postgres browser in the Dusk theme Dashboard system monitor in the Bombshell theme
Commander · DosBlue Query · Dusk Dashboard · Bombshell

Table of contents


Features

  • Truecolor rendering with automatic degradation to 256‑color / monochrome and Unicode → ASCII box glyphs.
  • Diff‑based repaint — only changed cell runs are written each frame; steady state is close to zero allocations.
  • Two‑level theming — every widget renders against semantic tokens, so swapping one palette reskins the whole UI. Three built‑in themes; roll your own with a record with.
  • A widget toolbox — lists, data grids, dialogs, dropdowns, menus, text/hex viewers and editors, gauges, sparklines, scrollbars, hotkey bars.
  • Deterministic layout — a constraint solver (Length, Percentage, Ratio, Fill, Min) whose splits always sum exactly to the area.
  • Focus model — nestable focus scopes with modal focus traps; the framework provides the mechanism, your app chooses the policy.
  • Keyboard & mouse — a portable VT input parser (SGR mouse, function keys, modifiers) with a safe fallback when no real TTY is present.
  • Async‑friendly — a UI dispatcher lets background threads feed widgets without locks.
  • Cross‑platform & SSH‑safe, NativeAOT‑ready, no reflection, no native dependencies.

Installation

dotnet add package nutui

nutui targets .NET 10. Add the namespaces you need:

using Nutui;            // Color, Style, Rect, Cell
using Nutui.App;        // NutuiApp, IAppView, IUiDispatcher
using Nutui.Backend;    // ConsoleBackend, TerminalCapabilities
using Nutui.Rendering;  // ScreenBuffer, ColorDepth
using Nutui.Theming;    // Theme, Themes, Palette, BorderType
using Nutui.Widgets;    // Block, ListView, DataGrid, Dialog, ...
using Nutui.Layouts;    // Layout, Constraint
using Nutui.Focus;      // IFocusable, FocusScope, FocusManager
using Nutui.Input;      // KeyEvent, MouseEvent, InputEvent

Quick start

A complete app is an IAppView handed to NutuiApp.Run. Render paints a full‑screen ScreenBuffer; HandleKey returns false to quit.

using System;
using Nutui;
using Nutui.App;
using Nutui.Backend;
using Nutui.Input;
using Nutui.Rendering;
using Nutui.Theming;
using Nutui.Widgets;

var backend = new ConsoleBackend();          // auto-detects terminal capabilities
new NutuiApp(backend).Run(new HelloView());

sealed class HelloView : IAppView
{
    private static readonly Theme Theme = Themes.Dusk;

    public void Render(ScreenBuffer buf)
    {
        var block = Block.Panel(Theme, "nutui", focused: true);
        block.Render(buf.Area, buf);

        var inner = block.Inner(buf.Area);
        var msg = "Hello, terminal!  Press Esc to quit.";
        var x = inner.Left + Math.Max(0, (inner.Width - msg.Length) / 2);
        var y = inner.Top + inner.Height / 2;
        buf.SetString(x, y, msg, Theme.Panel);
    }

    // Return false to quit the run loop.
    public bool HandleKey(KeyEvent key) => !key.Is(ConsoleKey.Escape);
}

NutuiApp.Run enters the alternate screen, hides the cursor, switches the terminal to raw input mode (with a cooked fallback when stdin is redirected), then loops: it renders a frame and re‑renders whenever a key/mouse event arrives, the terminal is resized, or a background producer posts an update. It always restores the terminal on exit, even on exceptions or Ctrl‑C.


Core concepts

Concept What it is
IAppView Your application: Render(ScreenBuffer), HandleKey(KeyEvent), optional HandleMouse / OnMounted.
NutuiApp The run loop. Owns a backend and a UI dispatcher.
ITerminalBackend The I/O seam (ConsoleBackend is the built‑in). Init/Present/read‑input/size/shutdown.
ScreenBuffer A grid of Cells (char + Style). Widgets paint into it; the renderer diffs and flushes it.
Theme / Palette Semantic color/border tokens. Widgets never see raw colors — only named roles.
Widgets Plain objects you construct, mutate, and Render(area, buffer). They don't auto‑render.
Layout / Constraint Split a Rect into child rects deterministically.
FocusManager / FocusScope Track which widget is focused; support modal focus traps.

Two things worth internalizing:

  1. Widgets are explicit. You call widget.Render(area, buf) yourself in IAppView.Render. There is no retained tree — you decide layout and paint order every frame. This keeps the model tiny and predictable.
  2. Colors are always 24‑bit in your code (Color(r,g,b) / Color.FromHex). The renderer degrades them to the terminal's actual depth at the single output choke point — you never branch on capabilities in widget code.

Documentation

The application & run loop

public interface IAppView
{
    void Render(ScreenBuffer buf);                  // paint a full-screen buffer
    bool HandleKey(KeyEvent key);                   // return false to quit
    void HandleMouse(MouseEvent mouse) { }          // optional (default no-op)
    void OnMounted(IUiDispatcher dispatcher) { }    // optional: start background work
}

public sealed class NutuiApp
{
    public NutuiApp(ITerminalBackend backend, int idlePollMs = 16);
    public IUiDispatcher Dispatcher { get; }
    public void Run(IAppView view);
}

ConsoleBackend is the built‑in backend:

public sealed class ConsoleBackend : ITerminalBackend
{
    public ConsoleBackend(TerminalCapabilities? capabilities = null); // null = auto-detect
    public TerminalCapabilities Capabilities { get; }
}

Rendering primitives

public readonly record struct Color(byte R, byte G, byte B);
Color.FromHex("#3A96DD");          // or "3A96DD"

[Flags] public enum CellAttributes { None, Bold, Dim, Italic, Underline, Reverse }

public readonly record struct Style(
    Color? Foreground = null,      // null = inherit
    Color? Background = null,
    CellAttributes Attributes = CellAttributes.None);

public readonly record struct Rect(int X, int Y, int Width, int Height)
{
    // Left, Top, Right, Bottom, IsEmpty, Inner(margin), Inner(h, v), Contains(x, y)
}

public readonly record struct Cell(char Symbol, Style Style);

ScreenBuffer is the drawing surface:

var buf = new ScreenBuffer(width, height);         // optionally a fill Style
buf.Set(x, y, Glyphs.FullBlock, style);             // single cell (clips silently)
buf.Fill(new Rect(0, 0, 10, 3), ' ', style);        // a rectangle
buf.SetString(x, y, "hello", style);                // a run of text
Cell c = buf[x, y];                                 // indexer (throws out of bounds)
Rect area = buf.Area;                               // (0, 0, Width, Height)

Build styles fluently:

var s = new Style(Color.FromHex("#FFFFFF"), Color.FromHex("#0037DA"), CellAttributes.Bold);
var t = Style.Default.Fg(Color.FromHex("#FFFF55")).Plus(CellAttributes.Underline);

Glyphs. Rather than pasting raw box‑drawing/block characters into your code, use the named constants in Glyphs — blocks, shades, box lines, arrows, and symbols. Every glyph there is covered by the renderer's ASCII fold, so drawing with them still degrades cleanly on a non‑Unicode terminal.

buf.Set(x, y, Glyphs.FullBlock, style);          // █  (was '█')
buf.Set(x, y, Glyphs.TriangleRight, style);      // ►
buf.SetString(x, y, $"{Glyphs.Bullet} item", style);

// Draw a fractional bar column from the 0..8 vertical fill levels:
var level = Glyphs.VerticalEighths[fraction8];   // " ▁▂▃▄▅▆▇█"[0..8]

Available families: FullBlock, LightShade/MediumShade/DarkShade, the half blocks and VerticalEighths/HorizontalEighths; box lines Horizontal/Vertical/TopLeft/… /TeeRight/CrossLine; TriangleUp/Down/Left/Right and ArrowUp/Down/Left/Right; and Bullet, Ellipsis, Check, Cross, Degree.

Theming

Theming is two levels:

Level 1 — semantic tokens. A Palette names the roles every generic widget renders against; a Theme exposes them as ready‑made Styles:

public sealed record Palette
{
    // Surfaces & text
    public required Color Base, Surface, Text, Muted, Accent { get; init; }
    // Selection / highlight
    public required Color SelectionBg, SelectionText { get; init; }
    // Text inputs
    public required Color InputBg, InputText { get; init; }
    // Modal dialogs (may differ from panels)
    public required Color DialogBg, DialogText, DialogBorder { get; init; }
    // Menus (optional — fall back to panel/selection defaults when null)
    public Color? MenuBg, MenuText, MenuBorder, MenuSelectionBg, MenuSelectionText { get; init; }
    // Borders
    public required Color BorderIdle, BorderFocus { get; init; }
}

Theme turns those into style roles that widgets consume — a sampling:

Desktop  Panel  MutedText  Border(focused)  Title(focused)  Selection
Header   Footer FooterKey  ScrollTrack ScrollThumb  Input InputCaret  Button(focused)
DialogFill DialogBorderStyle DialogTitle DialogLabel
MenuFill MenuBorderStyle MenuSelection MenuKey ...

Because widgets only ever reference these roles, swapping a palette reskins everything at once.

Built‑in themes (Themes.All lists them):

Theme Look
Themes.DosBlue Classic DOS file‑manager: deep blue desktop, sky‑blue frames/menus, yellow accents.
Themes.Dusk Modern muted dark, rounded frames, soft green accent.
Themes.Bombshell High‑contrast dark, hot‑pink accent, violet selection.

Border styles (BorderType): None, Ascii, Single, Rounded, Double, Thick.

A custom theme is just a record copy — override a token or two:

var mine = Themes.Dusk with
{
    Name = "Ocean",
    Palette = Themes.Dusk.Palette with
    {
        Accent      = Color.FromHex("#38BDF8"),
        SelectionBg = Color.FromHex("#0EA5E9"),
    },
};

Widgets

Every widget is a plain object you construct (often via a ForTheme factory), mutate, and render with widget.Render(area, buffer).

Widget Purpose Construct with
Block Bordered, optionally‑titled panel — the workhorse container Block.Panel(theme, title?, focused?)
ListView Scrollable single‑selection list (+ scrollbar) ListView.ForTheme(theme, items, focused?)
DataGrid Table with fixed‑width columns, header row, ellipsis truncation DataGrid.ForTheme(theme, columns, rows, focused?)
TextField Single‑line editable input (caret, h‑scroll) TextField.ForTheme(theme, label)
Button Focusable push button new Button { Text = "OK" }
Dialog Modal form with fields + OK/Cancel (focus trap) new Dialog(theme, title, fields…)
Dropdown Focusable combo box with a scrollable popup Dropdown.ForTheme(theme, label?, placeholder?)
MenuBar / Menu / MenuItem Horizontal menu bar with dropdowns (F9 / Alt) new MenuBar(new Menu("File", items…))
TextViewer Read‑only text with syntax highlighting, gutter, scrollbar TextViewer.ForTheme(theme)
TextEditor Multi‑line plain‑text editor TextEditor.ForTheme(theme)
HexView Hex dump viewer/editor (offset + hex + ASCII panes) HexView.ForTheme(theme)
Gauge Horizontal progress bar with optional % label Gauge.ForTheme(theme)
Sparkline One‑row bar chart (eighth‑block glyphs) Sparkline.ForTheme(theme)
ScrollBar Vertical scrollbar with arrows + thumb ScrollBar.ForTheme(theme, total, visible, offset)
HotkeyBar Single‑row footer of hotkey hints HotkeyBar.ForTheme(theme, hints)

A list and a footer:

var theme = Themes.DosBlue;

var list = ListView.ForTheme(theme, new[] { "one", "two", "three" });
list.Render(new Rect(0, 0, 20, 10), buf);
list.MoveDown();                 // also MoveUp(), Selected, etc.

var footer = HotkeyBar.ForTheme(theme, new[]
{
    new HotkeyHint("F3", "View"),
    new HotkeyHint("F10", "Quit"),
});
footer.Render(new Rect(0, 23, 80, 1), buf);

A data grid:

var columns = new[] { new GridColumn("Name", 20), new GridColumn("Size", 10) };
var rows = new[]
{
    new[] { "README.md", "15917" },
    new[] { "LICENSE",   "1107"  },
};
var grid = DataGrid.ForTheme(theme, columns, rows);
grid.Render(new Rect(0, 0, 32, 12), buf);

A menu bar (open with F9 / Alt+letter; activation raises OnActivate with the item id):

var menu = new MenuBar(
    new Menu("File",
        new MenuItem("View", "view", "F3"),
        new MenuItem("Edit", "edit", "F4"),
        MenuItem.Separator,
        new MenuItem("Quit", "quit", "F10")));
menu.OnActivate = id => { if (id == "quit") { /* … */ } };
menu.Render(new Rect(0, 0, 80, 1), buf);   // draws the bar (+ dropdown if open)

A modal dialog (a capturing focus scope traps Tab; Esc cancels, Enter confirms):

var dialog = new Dialog(theme, "Connect",
    TextField.ForTheme(theme, "Host"),
    TextField.ForTheme(theme, "Port"));

// In HandleKey: forward keys while open, then read the result.
dialog.HandleKey(key);
if (!dialog.IsOpen && dialog.Result == DialogResult.Ok) { /* read fields */ }

Layout

Layout splits a Rect into child rects using constraints that always sum exactly to the area (deterministic largest‑remainder distribution):

var rows = Layout.Vertical(
        Constraint.Length(1),   // fixed: menu bar
        Constraint.Fill(1),     // takes the rest
        Constraint.Length(1))   // fixed: status bar
    .Split(area);
var (menu, body, status) = (rows[0], rows[1], rows[2]);

var cols = Layout.Horizontal(Constraint.Percentage(50), Constraint.Percentage(50))
    .WithSpacing(1)
    .Split(body);

Constraint kinds: Constraint.Length(cells), Percentage(pct), Ratio(num, den), Fill(weight = 1), Min(cells). There's also an allocation‑free Split(area, Span<Rect> dest) overload for hot paths.

Focus

The framework provides the mechanism; your app chooses the policy (which key moves between panes vs. fields).

public interface IFocusable { bool Focused { get; set; } }

Build a tree of FocusScopes (mark one capturing: true to make it a modal trap that Tab cannot escape), then drive it with a FocusManager:

var scope = new FocusScope()
    .Add(leftList)
    .Add(rightList);

var focus = new FocusManager(scope);
focus.Next();        // Tab within traps, wraps
focus.Prev();        // Shift+Tab
focus.NextGlobal();  // ignore traps
focus.Focus(rightList);
var current = focus.Current;

ListView, DataGrid, TextField, Button, and Dropdown implement IFocusable.

Input

The run loop routes each event to HandleKey or HandleMouse. If you read raw events yourself, an InputEvent discriminates the two:

public readonly record struct KeyEvent(ConsoleKey Key, char Char, bool Ctrl, bool Alt, bool Shift)
{
    public bool Is(ConsoleKey key);   // key.Is(ConsoleKey.Enter)
    public bool Is(char ch);          // key.Is('q')
    public static KeyEvent Type(char ch);
    public static KeyEvent Of(ConsoleKey key, char ch = '\0');
}

public readonly record struct MouseEvent(int X, int Y, MouseButton Button,
    MouseEventKind Kind, bool Ctrl, bool Alt, bool Shift)
{
    public bool IsWheel { get; }
    public int WheelDelta { get; }    // +1 up, -1 down, 0 otherwise
}

if (ev.IsKey && ev.Key.Is(ConsoleKey.Enter)) { /* … */ }
if (ev.IsMouse && ev.Mouse.IsWheel)          { int d = ev.Mouse.WheelDelta; }

MouseButton: None, Left, Right, Middle, WheelUp, WheelDown. MouseEventKind: Down, Up, Move, Drag. Mouse coordinates are 0‑based cells. Mouse reporting is enabled only when a real console is present; headless/redirected runs simply never call HandleMouse.

Async & background updates

Start background producers in OnMounted and marshal their results back to the UI thread with the dispatcher — no locks in widget code:

public void OnMounted(IUiDispatcher dispatcher)
{
    _ = Task.Run(async () =>
    {
        while (true)
        {
            var rows = await LoadRowsAsync();
            dispatcher.Post(() => _grid.Rows = rows);  // runs on the UI thread; triggers a redraw
            await Task.Delay(1000);
        }
    });
}

The loop wakes on a producer signal, a key/mouse event, or the idle poll, drains posted mutations on the UI thread, and repaints only if something changed.

Terminal capabilities & graceful degradation

nutui detects what the terminal can do from the environment (NO_COLOR, COLORTERM, TERM, LANG) and degrades at the single output choke point — your widget code never branches on it.

public enum ColorDepth { None, Truecolor, Ansi256 }

public readonly record struct TerminalCapabilities(ColorDepth Depth, bool Unicode)
{
    public static TerminalCapabilities Detect();
}

var caps = TerminalCapabilities.Detect();
var backend = new ConsoleBackend(caps);   // or new ConsoleBackend() to auto-detect
  • Color collapses Truecolor → Ansi256 → None (attributes only) as needed.
  • Unicode = false folds box‑drawing/blocks/arrows down to ASCII (┌│└ → +|+, █ → #, …), so a dumb terminal or NO_COLOR session still renders a clean, readable UI.

Custom backends

All terminal I/O sits behind a narrow interface, so you can host nutui somewhere other than the console (tests, a recording harness, a remote pipe) without touching widget code:

public interface ITerminalBackend
{
    (int Width, int Height) Size { get; }
    void Init();
    void Present(ScreenBuffer frame);
    bool TryReadInput(out InputEvent ev);
    void Shutdown();
}

Samples

Each sample has a headless --snapshot path (renders one frame and exits) for CI and screenshots.

dotnet run --project samples/Commander        # dual-pane file manager (DosBlue theme)
dotnet run --project samples/Query            # Postgres browser (catalog + results grid)
dotnet run --project samples/Dashboard        # live system monitor (gauges, sparklines)
dotnet run --project samples/ThemeGallery     # the same panel across every built-in theme

dotnet run --project samples/Commander -- --snapshot     # one static frame, no TTY needed
Sample Demonstrates
Commander Real‑filesystem dual‑pane browser, menu bar wired to F‑keys, copy/rename/mkdir dialogs, F3 view (text or hex) / F4 edit, focus switching between panes.
Query A real Npgsql connection, a connect dialog with a database dropdown, a catalog sidebar, streaming result rows fed via the dispatcher, row‑detail popups. Pass --conn "Host=…;…" or set NUTUI_PG.
Dashboard Auto‑refreshing 2×3 panel of CPU / memory / disk / network / system metrics using gauges, a sparkline, and semantic status colors — driven by a background timer + dispatcher.
ThemeGallery A Level‑1 token showcase: one panel rendered with every built‑in theme.

Building from source

git clone https://github.com/JessieWadman/nutui
cd nutui

dotnet build   nutui.slnx -c Release      # build library + samples + tests
dotnet test    nutui.slnx -c Release      # run the xUnit suite

Requires the .NET 10 SDK. The solution file is nutui.slnx.

Project layout:

src/Nutui            the framework (this is the NuGet package)
tests/Nutui.Tests    xUnit tests
samples/             Commander, Query, Dashboard, ThemeGallery

NativeAOT

The library is IsAotCompatible — no reflection, no dynamic code. Publish a sample as a small self‑contained native binary:

dotnet publish samples/Commander -c Release -r linux-x64 \
    -p:PublishAot=true -p:InvariantGlobalization=true

Cross‑platform & SSH

nutui runs on Windows, Linux, and macOS, and is designed to work over SSH to whatever terminal is on the other end:

  • Output is plain ANSI written as raw bytes — no console‑specific APIs on the render path.
  • Raw input mode is set via SetConsoleMode on Windows and termios on Unix, with a cooked fallback when stdin is redirected (headless/CI), so tests and pipelines still work.
  • On Unix the keyboard/mouse byte stream is read directly from the file descriptor (not through System.Console, which interferes with raw TTY input), so keys, arrows, function keys, and mouse escapes all arrive correctly over a real pseudo‑terminal.
  • Capability detection + degradation mean a TERM=dumb or NO_COLOR session still gets a usable, correctly‑bordered UI.

On minimal Linux hosts without ICU, publish/run with InvariantGlobalization (or DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1) as you would for any .NET app.

Design philosophy

  • Pure C#, no native core. Terminal work at TUI scale is ANSI text plus a couple of OS syscalls (termios / console mode) that .NET reaches natively — not a case for a Rust engine and per‑platform binaries. Staying managed keeps the package a single dependency‑free assembly that AOT‑compiles cleanly.
  • A narrow I/O seam. Everything above ITerminalBackend is pure rendering and state; the backend is the only thing that touches the outside world. Swap it for tests, recordings, or an alternate transport.
  • Two‑level theming. Widgets render only against semantic tokens, so a theme is portable data and a reskin is a one‑line palette swap.
  • Mechanism, not magic. No retained widget tree, no hidden reflection. You lay out and paint every frame; the framework gives you fast primitives (diffing, a constraint solver, a focus model) and stays out of the way.

License

MIT © 2026 Jessie Wadman

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.
  • net10.0

    • No dependencies.

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
1.0.4 399 7/20/2026