Leith.Term.Presentation.Host.Consolonia 375.0.0
Leith.Term.Presentation
Leith — pronounced /liːθ/; in Scottish English, like leaf.
Terminal hosting for .NET: open local PTY or SSH sessions, drive input and process APIs, and render the visible terminal grid with framework-neutral presentation contracts or SkiaSharp.
Status: 0.1.0 pre-publication, not yet published — a private prototype under active
development. There is no compatibility promise yet: the public API baselines are a drift
detector, not a freeze, and the surface is expected to be reshaped before the first release.
During the current Leith.Term refactor, baseline snapshot tests are suspended — see
Docs/BASELINE_TESTS_SUSPENDED.md.
Read ../Leith.Workspace/PRE_PUBLICATION.md for which constraints in
this documentation are live and which describe the state at publication; see the
0.1.0 shipping record before publishing.
Packages
NuGet IDs match assembly names (Leith.Term.Presentation.*). The IDs below describe the current release
candidate.
| Package ID | Role |
|---|---|
Leith.Term.Presentation.Abstractions |
Host/session contracts, snapshots, transports, and features |
Leith.Term.Presentation.Session |
Host factory and session host over the Leith.Term engine |
Leith.Term.Presentation.Transport.Local |
Cross-platform local PTY through sibling Leith.Pty |
Leith.Term.Presentation.Transport.Ssh |
SSH PTY through sibling Tmds.Ssh |
Leith.Term.Presentation.Host.Platform |
A ready-to-open request for the current OS shell |
Leith.Term.Presentation.Host.Avalonia |
Avalonia composition root for hosts and transports |
Leith.Term.Presentation.Host.Consolonia |
Consolonia composition root for hosts and transports |
Leith.Term.Presentation.Host.Maui |
.NET MAUI composition root for hosts and transports |
Leith.Term.Presentation.Host.ProGpu |
ProGPU composition root for hosts and transports |
Leith.Term.Presentation.Host.WinUI |
WinUI 3 composition root for hosts and transports |
Leith.Term.Presentation.Host.Wpf |
WPF composition root for hosts and transports |
Leith.Term.Presentation.Gui.Definitions |
Framework-neutral frames, geometry, presentation, and themes |
Leith.Term.Presentation.Rendering.Skia |
Font metrics and a canvas presenter for SkiaSharp |
Leith.Term.Presentation.Control.Avalonia |
Avalonia terminal control |
All packages target .NET 10.
Engine (Leith.Term)
The VT emulator engine lives in the sibling repo Leith.Term.Engine
(../Leith.Term.Engine/ next to this checkout). Leith.Term.Presentation.Session references the
Leith.Term package from that repo via ProjectReference. Engine tests and benchmarks run in
Leith.Term.Engine, not here.
Current product path: Session owns Leith.Term.Terminal directly. Local transport is sibling Leith.Pty
Dedicated. Performance status:
Benchmarks/results/PERFORMANCE_STATUS.md.
Avalonia quick start
Install the UI package, plus Leith.Term.Presentation.Host.Avalonia for the local shell request:
<PackageReference Include="Leith.Term.Presentation.Control.Avalonia" Version="0.1.0" />
<PackageReference Include="Leith.Term.Presentation.Host.Avalonia" Version="0.1.0" />
Control.Avalonia deliberately does not depend on Host.Avalonia: which shell to launch is an
application decision, and an SSH-only consumer has no use for it. Take Host.Avalonia when you want
the ready-made local shell request below.
Apply the styles once during application initialization, then open and display a session:
using Leith.Term.Presentation.Abstractions;
using Leith.Term.Presentation.Control.Avalonia.Controls;
using Leith.Term.Presentation.Control.Avalonia.Styling;
using Leith.Term.Presentation.Host;
using Leith.Term.Presentation.Host.Avalonia;
TerminalControlStyles.Apply(application);
var host = AvaloniaTerminalHost.CreateLocal();
var open = await host.OpenAsync(LocalTerminalRequestFactory.CreateDefault());
if (open.Status == TerminalOperationStatus.Succeeded && open.Session is { } session)
{
Content = SessionTerminalControlFactory.CreateWithSkiaMetrics(session);
}
// Keep the host for the lifetime of the UI. During application shutdown:
// await host.DisposeAsync();
The complete runnable version is in src/samples/AvaloniaHelloTerminal. For a full capability gallery, see AvaloniaFeatureGallery.
Custom Skia canvas quick start
WPF, WinUI 3, .NET MAUI, and other Skia hosts use the same public presenter:
using SkiaSharp;
using Leith.Term.Presentation.Rendering.Skia;
using var presenter = new TerminalSkiaCanvasPresenter(session);
presenter.Invalidated += (_, _) => InvalidateCanvas();
// In the framework's size callback:
await presenter.ResizeAsync(width, height, renderScale);
// In the framework's paint callback:
presenter.Render(canvas, SKRect.Create(width, height), renderScale);
The terminal host must receive a real UI dispatcher. ImmediateGuiDispatcher is intended for
tests and headless tools, not for a live UI session.
Composition and lifetime
Leith.Term.Presentation uses Pure DI by default. The composition root lives in the application
(or a platform package such as Leith.Term.Presentation.Host.Avalonia or Leith.Term.Presentation.Host.WinUI), not inside GUI controls.
| Component | Lifetime | Notes |
|---|---|---|
ITerminalHost |
Singleton per app | Owns open sessions; dispose on shutdown |
ITerminalSession |
Per open | Created by OpenAsync; dispose with host or CloseAsync |
ITerminalTransportProvider |
Singleton | Registered in the composition root |
IGuiDispatcher |
Singleton | Must match the UI thread |
ITerminalFontMetricsProvider |
Singleton or transient | Injected into controls via SessionTerminalControlFactory |
TerminalHostFactory.Create is the canonical Pure DI entry point. If you already use a
container, every package registers the piece it owns:
using Leith.Term.Presentation.Host.Avalonia; // AddAvaloniaTerminalHost
using Leith.Term.Presentation.Transport.Local; // AddLocalPtyTransport
services.AddAvaloniaTerminalHost() // Avalonia dispatcher + ITerminalHost
.AddLocalPtyTransport(); // the "local" transport
// Shut down asynchronously - the host owns child processes and PTYs:
await serviceProvider.DisposeAsync();
Host.Wpf, Host.WinUI and Host.Maui expose the same shape and take the framework
dispatcher: AddWpfTerminalHost(Application.Current.Dispatcher),
AddWinUiTerminalHost(window.DispatcherQueue), AddMauiTerminalHost(). Add
AddSshTransport() for SSH; transports accumulate rather than replace each other.
Call Add…TerminalHost from the UI thread — the dispatcher is built there — and dispose
the container with DisposeAsync, not Dispose.
GUI packages (Control.Avalonia) depend only on abstractions and rendering contracts — they
do not reference Session or Transport.*. Use Host.Avalonia to wire dispatcher, transports,
and host together.
Headless or custom composition
Packages: Leith.Term.Presentation.Session, Leith.Term.Presentation.Transport.Local, Leith.Term.Presentation.Host.Platform.
using Leith.Term.Presentation.Abstractions;
using Leith.Term.Presentation.Host;
using Leith.Term.Presentation.Session;
using Leith.Term.Presentation.Transport.Local;
ITerminalHost host = TerminalHostFactory.Create(
dispatcher,
[new LocalPtyTerminalTransportProvider()]);
var open = await host.OpenAsync(LocalTerminalRequestFactory.CreateDefault());
if (open.Session is { } session)
{
await session.Input.SendLineAsync("dotnet --info");
var snapshot = session.Screen.GetSnapshot();
}
Runnable samples
| Sample | What it demonstrates | Requirement |
|---|---|---|
| AvaloniaHelloTerminal | Full Avalonia terminal control | Windows, macOS, or Linux |
| WpfSkiaHelloTerminal | Local PTY rendered in WPF SKElement |
Windows |
| WinUiSkiaHelloTerminal | Local PTY rendered in WinUI 3 SKXamlCanvas |
Windows x64 |
| MauiSkiaHelloTerminal | Local PTY rendered in MAUI SKCanvasView |
Windows + maui-windows workload |
The three canvas samples deliberately use a command box instead of pretending to be complete terminal controls. A production control must additionally handle key chords, IME, paste, selection, mouse protocols, focus, and accessibility.
In-tree backends
Alongside the shipped Avalonia package the repository contains Consolonia, WinUI 3, ProGPU, WPF and
MAUI controls under src/Gui/Gui.Control/. These are not packable and are not published; build
them from source. Whether any of them ships is an open decision — see
0.1.0 shipping record.
To write a backend for another framework, see Docs/BACKEND_AUTHOR_GUIDE.md.
Consolonia console host (must run in a VT-capable terminal)
The Consolonia control renders into a real console. If you
launch it and see raw escape sequences printed literally — [38;2;204;204;204m, [?25l,
[2;81f, and similar — the process was attached to a console that does not interpret virtual
terminal sequences. The terminal grid is parsed correctly; it is Consolonia's own frame output
that the host console is failing to render.
The cause is host detection, not this library. Consolonia's UseAutoDetectedConsole() picks its
Windows output backend from environment variables: with WT_SESSION or VSAPPIDNAME set (its
IsWindowsTerminal() check) it emits ANSI/VT sequences and assumes the host interprets them;
otherwise it drives the console through the legacy Win32 API. Both variables are inherited by
child processes, so a process spawned from a Windows Terminal or Visual Studio session carries
WT_SESSION even when it is actually handed a fresh legacy conhost window — and a fresh
conhost starts with ENABLE_VIRTUAL_TERMINAL_PROCESSING off. The detection then sends VT bytes
to a console that prints them verbatim.
Run it in a terminal that genuinely interprets VT:
# Directly inside a Windows Terminal tab (ConPTY always interprets VT):
wt.exe new-tab --title ConsoloniaHelloTerminal path\to\your.exe
# Or from a real terminal via the SDK, which attaches a VT-capable console:
dotnet run --project src/samples/ConsoloniaHelloTerminal
Avoid spawning a fresh legacy console (for example Start-Process your.exe from a Windows
Terminal-hosted shell): the new window inherits WT_SESSION but is not Windows Terminal, which is
exactly the mismatch above.
To make the app robust regardless of how it is launched, enable VT processing on standard output before Consolonia starts drawing (on Windows, once the console is attached):
if (OperatingSystem.IsWindows())
{
const int StdOutputHandle = -11;
const uint EnableVirtualTerminalProcessing = 0x0004;
var stdout = GetStdHandle(StdOutputHandle);
if (GetConsoleMode(stdout, out var mode))
{
SetConsoleMode(stdout, mode | EnableVirtualTerminalProcessing);
}
}
// [DllImport("kernel32.dll")] GetStdHandle / GetConsoleMode / SetConsoleMode
On macOS and Linux this specific Windows VT-mode mismatch does not arise by design: Consolonia
selects a different (curses) backend there and native terminal emulators interpret ANSI/VT
directly. This has not yet been smoke-tested in this repository, so treat the Unix path as
unverified for now. Consolonia renders into the terminal itself rather than a window, so no
graphical display server is required and it is expected to work over an SSH session that provides
an interactive PTY. It still needs a real foreground TTY with a sensible TERM (for example
xterm-256color), not captured or redirected stdio.
Consumer API
The canonical boundary is ITerminalHost / ITerminalSession:
Screen— read-only size, title, and neutral snapshotsFeatures— optional features discovered throughTryGet<T>()Input— bytes, text, or complete linesProcess— commands, working directory, reconnect, and stopResizeAsync— grid resize propagated to the transport
Shipping assemblies expose no engine types. Expected operational failures return
TerminalOperationResult or TerminalOpenResult; programmer errors throw. Cancellation cancels
the current operation, not the lifetime of the child process.
Build, test, and pack
The repository pins the stable .NET 10 SDK in global.json.
dotnet build Term.Presentation.slnx -c Release
dotnet test Term.Presentation.slnx -c Release --no-build
dotnet pack Term.Presentation.slnx -c Release -o artifacts/nupkg
The normal suite is hermetic. The SSH live smoke is opt-in through
TERMINAL_SHARP_SSH_LIVE=1 and TERMINAL_SHARP_SSH_LIVE_ALIAS=<ssh-config-alias>.
Repository layout
| Path | Role |
|---|---|
src/Abstractions |
Public contracts |
src/Session |
Host and internal engine session |
src/Transport/Transport.Local, Transport.Ssh |
Local PTY and SSH transports |
src/Host/Host.Platform |
OS shell resolvers and the default local-shell request |
src/Host/Host.* |
Per-framework composition roots (dispatcher + transports + host) |
src/Gui/Gui.Definitions |
Framework-neutral presentation, framing, and input decisions |
src/Gui/Gui.Rendering/Rendering.Skia |
Skia glyph shaping and canvas presenter |
src/Gui/Gui.Control/Control.* |
UI backends; only Control.Avalonia ships as a package |
src/samples |
Runnable integration examples |
src/Tests |
Contract, session, architecture, and backend tests |
Docs |
Architecture, adapter, and release notes |
License
Leith.Term.Presentation is MIT-licensed; see LICENSE.
The embedded Symbols Nerd Font Mono carries its own license in
THIRD-PARTY.
Builtin box-drawing tables follow Windows Terminal's MIT-licensed BuiltinGlyphs.cpp; source
provenance is recorded in the relevant files. NuGet dependencies retain their own licenses.
Governance: ../Leith.Workspace/. Build/test: product Docs/ and this README.
Avalonia is a registered trademark of AvaloniaUI OÜ. Leith.Term.Presentation is an independent project and is not affiliated with or endorsed by AvaloniaUI OÜ.
No packages depend on Leith.Term.Presentation.Host.Consolonia.
.NET 10.0
- Leith.Term.Presentation.Host.Platform (>= 375.0.0)
- Leith.Term.Presentation.Session (>= 375.0.0)
- Leith.Term.Presentation.Transport.Local (>= 375.0.0)
- GuiDispatcher.Sharp.Consolonia (>= 1.1.1)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)