ConsoleForge 0.3.0

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

ConsoleForge

An Elm-architecture TUI framework for .NET 8. Immutable model → pure update → declarative view.

Docs: popplywop.github.io/ConsoleForge  | NuGet: ConsoleForge  | SourceGen: ConsoleForge.SourceGen

Built for developers who want the predictability of Bubble Tea in C#: no mutable widget state, no hidden side effects, and a render pipeline that only touches the cells that actually changed.

Features

  • Elm loopInitUpdateView. Your model is an immutable record. Update returns a new copy. View is pure.
  • 13 built-in widgets — TextBlock, TextInput, TextArea, List, Table, Checkbox, Tabs, ProgressBar, Spinner, BorderBox, Container, Modal, ZStack
  • 6 named themes — Dark, Light, Dracula, Nord, Monokai, Tokyo Night. Switch at runtime with one message.
  • Mouse support — SGR 1006 extended mouse tracking. Click-to-focus, scroll wheel, button/motion events.
  • Unicode-aware layout — CJK, emoji, and full-width characters render at correct column widths.
  • Composable sub-programsIComponent / IComponent<TResult> for self-contained pages with own state, keymaps, and lifecycle.
  • Declarative keybindingsKeyMap + KeyPattern replace giant switch statements. Composable, context-aware.
  • Virtualized scrolling — List and Table render only visible rows. 1,000 items costs the same as 20.
  • Double-buffered renderer — Cell-level diff with per-widget dirty tracking. Only changed cells hit the terminal.
  • Margin & paddingStyle.Padding(1) and Style.Margin(1) enforced by the layout engine.
  • Async commandsCmd.Run, Cmd.Batch, Cmd.Sequence, Cmd.Tick, Cmd.Debounce, Cmd.Throttle.
  • SubscriptionsSub.Interval, Sub.FromAsyncEnumerable, Sub.FromObservable for continuous data streams.

Quick Start

dotnet add package ConsoleForge
using ConsoleForge.Core;
using ConsoleForge.Layout;
using ConsoleForge.Styling;
using ConsoleForge.Widgets;

sealed record HelloModel(int Count = 0) : IModel
{
    public ICmd? Init() => null;

    public (IModel Model, ICmd? Cmd) Update(IMsg msg) => msg switch
    {
        KeyMsg { Key: ConsoleKey.UpArrow }   => (this with { Count = Count + 1 }, null),
        KeyMsg { Key: ConsoleKey.DownArrow } => (this with { Count = Count - 1 }, null),
        KeyMsg { Key: ConsoleKey.Q }         => (this, Cmd.Quit()),
        _ => (this, null),
    };

    public IWidget View() =>
        new BorderBox("ConsoleForge",
            body: new Container(Axis.Vertical, [
                new TextBlock($"Count: {Count}"),
                new TextBlock("↑↓ to change, Q to quit",
                    style: Style.Default.Faint(true)),
            ]));
}

await Program.Run(new HelloModel(), theme: Theme.Dark);

Widgets

Widget Description
TextBlock Text display with word-wrap. Supports \n, padding, and alignment.
TextInput Single-line input with cursor, backspace, delete, and arrow keys.
TextArea Multi-line editor with cursor navigation, line splitting, and scroll.
List Scrollable list with selection highlight. Virtualized — only visible rows render.
Table Columnar data with headers, selection, separators. Virtualized scrolling.
Checkbox Toggle [✓] Label / [ ] Label. Customizable indicator characters.
Tabs Tab bar + body content. Left/Right arrows, number keys 1–9.
ProgressBar Horizontal fill bar with optional percentage label.
Spinner Animated spinner with braille, ASCII, and arc frame sets.
BorderBox Bordered box with title. 6 border styles: Normal, Rounded, Thick, Double, ASCII, Hidden.
Container Flex layout along horizontal or vertical axis. Supports scrolling.
Modal Centered dialog overlay. Compose with ZStack for layered UIs.
ZStack Renders layers back-to-front. The foundation for overlays and modals.

Themes

Six built-in themes with background colours, accent styles, and semantic colour slots:

await Program.Run(model, theme: Theme.Dracula);
Theme Background Accent Focus
Theme.Dark #1C1C1C Teal Gold
Theme.Light #F0F0F0 Blue Red
Theme.Dracula #282A36 Purple Cyan
Theme.Nord #2E3440 Teal Green
Theme.Monokai #272822 Green Yellow
Theme.TokyoNight #1A1B26 Blue Orange

Switch at runtime:

// In Update:
return (this with { ThemeIdx = next }, Cmd.Msg(new ThemeChangedMsg(newTheme)));

Access theme colours with extension methods:

theme.Accent()       // IColor? — border brand colour
theme.AccentStyle()  // Style — accent as foreground, bold-ready
theme.MutedStyle()   // Style — dim secondary text
theme.Success()      // Style — green/equivalent
theme.Warning()      // Style — yellow/equivalent
theme.Error()        // Style — red/equivalent
theme.Bg()           // IColor? — background from BaseStyle
theme.BgStyle()      // Style — background only

Layout

Children declare Width and Height as SizeConstraint:

SizeConstraint.Fixed(24)        // exact columns/rows
SizeConstraint.Flex(1)          // proportional share of free space
SizeConstraint.Auto             // shrink to content
SizeConstraint.Min(10, inner)   // minimum bound
SizeConstraint.Max(40, inner)   // maximum bound

Container runs a two-pass layout: fixed children first, then flex children share the remainder. Supports padding and margin:

new Container(Axis.Horizontal,
    style: Style.Default.Padding(1),          // insets child region
    children: [
        new TextBlock("A") { Style = Style.Default.Margin(0, 1, 0, 1) },  // spacing around widget
        new TextBlock("B"),
    ]);

Styling

Style is an immutable value type. All methods return a new Style:

Style.Default
    .Foreground(Color.FromHex("#FF5733"))
    .Background(Color.Blue)
    .Bold()
    .Italic()
    .Underline()
    .Padding(1, 2)
    .Border(Borders.Rounded)
    .BorderForeground(Color.Cyan)

Styles inherit from the active theme when properties are unset — set only what you need.

Mouse Support

await Program.Run(model, theme: Theme.Dark, enableMouse: true);
  • Click-to-focus — left-click moves focus to the clicked widget (automatic)
  • Scroll wheelMouseMsg with MouseButton.ScrollUp / ScrollDown
  • Button events — press, release, motion tracking via MouseMsg

Handle in your model:

case MouseMsg { Button: MouseButton.ScrollDown } => // scroll handler

KeyMap — Declarative Keybindings

Replace switch statements with composable, context-aware binding maps:

static readonly KeyMap SidebarKeys = new KeyMap()
    .On(ConsoleKey.UpArrow,   () => new NavUpMsg())
    .On(ConsoleKey.DownArrow, () => new NavDownMsg())
    .On(ConsoleKey.Enter,     () => new SelectMsg())
    .On(ConsoleKey.Escape,    () => new QuitMsg())
    .On(KeyPattern.WithCtrl(ConsoleKey.C), () => new QuitMsg())
    .OnScroll(m => m.Button == MouseButton.ScrollUp
        ? new NavUpMsg() : new NavDownMsg());

// In Update:
if (SidebarKeys.Handle(msg) is { } action) msg = action;

KeyPattern supports modifier wildcards: Of(key), WithCtrl(key), WithAlt(key), Plain(key).

Compose maps: globalKeys.Merge(pageKeys) — first map takes priority.

IComponent — Sub-Programs

Self-contained pages with own state, keybindings, and view. The Bubble Tea tea.Model-per-page pattern:

// Pages/CounterPage.cs
sealed record CounterPage(int Count = 0) : IComponent
{
    static readonly KeyMap Keys = new KeyMap()
        .On(ConsoleKey.UpArrow,   () => new IncrMsg())
        .On(ConsoleKey.DownArrow, () => new DecrMsg());

    public ICmd? Init() => null;

    public (IModel Model, ICmd? Cmd) Update(IMsg msg)
    {
        if (Keys.Handle(msg) is { } action) msg = action;
        return msg switch
        {
            IncrMsg => (this with { Count = Count + 1 }, null),
            DecrMsg => (this with { Count = Count - 1 }, null),
            _       => (this, null),
        };
    }

    public IWidget View() => new TextBlock($"Count: {Count}");
}

Embed in a parent model:

record AppModel(CounterPage Counter) : IModel
{
    public (IModel, ICmd?) Update(IMsg msg)
    {
        var (next, cmd) = Component.Delegate(Counter, msg);
        return (this with { Counter = next! }, cmd);
    }
    public IWidget View() => Counter.View();
}

For components that return results (file pickers, confirm dialogs):

sealed record FilePicker(string? Result = null) : IComponent<string>
{
    string IComponent<string>.Result => Result!;
    // ... Update sets Result when user picks a file
}

// Parent checks completion:
var (next, cmd) = Component.Delegate(picker, msg);
if (next.IsCompleted())
    return (this with { Picker = null, ChosenFile = next.Result }, cmd);

Commands

Cmd.Quit()                                    // exit the program
Cmd.Msg(new SomeMsg())                        // synchronous follow-up message
Cmd.Run(async ct => { ... return msg; })      // async work → message
Cmd.Batch(cmd1, cmd2, cmd3)                   // run concurrently
Cmd.Sequence(cmd1, cmd2, cmd3)                // run serially
Cmd.Tick(TimeSpan, ts => new TickMsg(ts))     // delayed single fire
Cmd.Debounce(TimeSpan, ts => msg)             // debounced (last wins)
Cmd.Throttle(TimeSpan, ts => msg)             // throttled (first wins)

Subscriptions

For continuous data streams, implement IHasSubscriptions:

public IEnumerable<(string Key, ISub Sub)> Subscriptions() =>
[
    ("timer", Sub.Interval(TimeSpan.FromSeconds(1), ts => new TickMsg(ts))),
    ("data",  Sub.FromAsyncEnumerable(ct => GetDataStream(ct))),
];

Samples

Sample Description
ConsoleForge.Gallery Widget showcase — all 13 widgets, 6 themes, mouse support, IComponent pages
ConsoleForge.TodoApp Todo list — browse, add, toggle, delete
ConsoleForge.SysMonitor Live system stats via async subscriptions
dotnet run --project samples/ConsoleForge.Gallery

Project Structure

src/ConsoleForge/
  Core/       Program, IModel, IMsg, ICmd, IComponent, KeyMap, KeyPattern,
              Component, Cmd, Sub, FocusManager, Renderer, Messages
  Layout/     IWidget, IContainer, IFocusable, ISingleBodyWidget, ILayeredContainer,
              LayoutEngine, RenderContext, TextUtils, SizeConstraint, Region
  Styling/    Style, Theme, ThemeExtensions, Color, Borders, BorderSpec, ColorProfile
  Terminal/   ITerminal, AnsiTerminal, Termios (Unix), WindowsConsole
  Widgets/    TextBlock, TextInput, TextArea, List, Table, Checkbox, Tabs,
              ProgressBar, Spinner, BorderBox, Container, Modal, ZStack

tests/ConsoleForge.Tests/        398 unit tests (xUnit v3)
tests/ConsoleForge.Benchmarks/   BenchmarkDotNet render + cmd benchmarks

samples/
  ConsoleForge.Gallery/          Widget browser with IComponent page architecture
  ConsoleForge.SysMonitor/       System monitor dashboard
  ConsoleForge.TodoApp/          Todo list app

Building

dotnet build ConsoleForge.slnx
dotnet test  ConsoleForge.slnx
dotnet run --project samples/ConsoleForge.Gallery

Requires .NET 8 SDK. Single dependency: System.Reactive.

Performance

Double-buffered cell diff + per-widget render cache. Benchmarks (80×24 terminal):

Scenario Time Allocations
20-widget cold render ~20 µs 68 KB
20-widget warm (no changes) ~21 µs 14 KB
1,000-row Table (24 visible) ~30 µs 67 KB
Dirty-skip (model unchanged) 3 ns 0

License

MIT — see LICENSE.

Product 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 was computed.  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 was computed.  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

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.3.0 107 4/21/2026
0.2.2 96 4/20/2026
0.2.1 112 4/17/2026
0.2.0 103 4/16/2026
0.1.0 109 4/14/2026
0.0.2 107 4/14/2026

v0.2.2: Fix issue with wide-characters not rendering properly and causing artifacts on screen.

       v0.2.1: Performance optimizations (AggressiveInlining on hot-path layout methods, lazy widget cache allocation, composite-only render caching), publish workflow for both NuGet packages + GitHub Releases, Doxyfile improvements (README as landing page, full sidebar, sorted members).