Leith.Term.Presentation.Integration 380.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 snapshots
  • Features — optional features discovered through TryGet<T>()
  • Input — bytes, text, or complete lines
  • Process — commands, working directory, reconnect, and stop
  • ResizeAsync — 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Ü.

Showing the top 20 packages that depend on Leith.Term.Presentation.Integration.

Packages Downloads
Leith.Term.Presentation.Gui.Definitions
Framework-neutral terminal presentation (frames, geometry, themes).
0
Leith.Term.Presentation.Session
Leith.Term.Presentation session host over the Leith.Term engine.
0

.NET 10.0

Version Downloads Last updated
387.0.0 0 09/29/2026
386.0.0 0 09/29/2026
382.0.0 0 09/28/2026
381.0.0 0 09/28/2026
380.0.0 0 09/28/2026
379.0.0 0 09/28/2026
378.0.0 0 09/28/2026
377.0.0 0 09/28/2026
376.0.0 0 09/28/2026
375.0.0 0 09/28/2026
374.0.0 0 09/28/2026
373.0.0 0 09/28/2026