RoyalTerminal

June 22, 2026 · View on GitHub

RoyalTerminal logo RoyalTerminal logo

image

High-performance .NET 10 terminal stack with a backend-neutral Avalonia core (RoyalTerminal.Avalonia), framebuffer shader support, official native Ghostty VT integration (libghostty-vt), and a separate fully managed VT implementation.

.NET 10 Avalonia Documentation License: MIT

Documentation

Read the published documentation at royalapplications.github.io/RoyalTerminal. Source lives in docs/ and is published through the VitePress/GitHub Pages workflow in .github/workflows/docs.yml.

NuGet Packages

Primary Packages

PackageNuGetDescription
RoyalApps.RoyalTerminal.AvaloniaNuGetBackend-neutral Avalonia terminal control (TerminalControl) and presentation services (no Ghostty dependency)
RoyalApps.RoyalTerminal.GhosttySharpNuGetCore Ghostty VT bindings (libghostty-vt) with RID-aware native package selection
RoyalApps.RoyalTerminal.GhosttySharp.Native.OSXNuGetNative runtime assets selected for macOS by runtime.json (libghostty-vt, libghostty-renderer-capi)
RoyalApps.RoyalTerminal.GhosttySharp.Native.Win64NuGetNative runtime assets selected for Windows x64/arm64 by runtime.json (ghostty-vt.dll, ghostty-renderer-capi.dll)
RoyalApps.RoyalTerminal.GhosttySharp.Native.Linux64NuGetNative runtime assets selected for Linux by runtime.json (libghostty-vt.so, libghostty-renderer-capi.so)

Modular Managed Packages (Packable Composition Units)

PackageResponsibility
RoyalApps.RoyalTerminal.Avalonia.SettingsReusable Avalonia settings panel controls and state for session, connection, terminal, appearance, SSH, logging, and highlighting configuration
RoyalApps.RoyalTerminal.TerminalCore terminal contracts (ITerminalEndpoint, ITerminalInputSink, ITerminalSelectionSource, ITerminalModeSource), SSH bootstrap helpers, and screen model
RoyalApps.RoyalTerminal.UnicodeDeterministic Unicode data and terminal width helpers
RoyalApps.RoyalTerminal.SixelReusable managed sixel decoder and image payload model
RoyalApps.RoyalTerminal.Terminal.Vt.ManagedManaged VT processor (BasicVtProcessor)
RoyalApps.RoyalTerminal.Terminal.Vt.GhosttyNative VT processor (GhosttyVtProcessor over official libghostty-vt terminal/render APIs) + GhosttyVtProcessorProvider
RoyalApps.RoyalTerminal.Terminal.Vt.DefaultPreference-based VT processor factory (VtProcessorPreference)
RoyalApps.RoyalTerminal.Terminal.Pty.UnixUnix PTY implementation (forkpty)
RoyalApps.RoyalTerminal.Terminal.Pty.WindowsWindows PTY implementation (ConPTY)
RoyalApps.RoyalTerminal.Terminal.Pty.PlatformPlatform PTY factory (DefaultPtyFactory)
RoyalApps.RoyalTerminal.Terminal.Transport.PtyPTY transport provider and wrapper (PtyTerminalTransportProvider)
RoyalApps.RoyalTerminal.Terminal.Transport.PipeProcess pipe transport provider (PipeTerminalTransportProvider)
RoyalApps.RoyalTerminal.Terminal.Transport.RawRaw TCP terminal transport provider
RoyalApps.RoyalTerminal.Terminal.Transport.TelnetTelnet terminal transport provider with option negotiation
RoyalApps.RoyalTerminal.Terminal.Transport.SerialSerial line terminal transport provider
RoyalApps.RoyalTerminal.Terminal.Transport.Ssh.AbstractionsSSH host-key validation contracts
RoyalApps.RoyalTerminal.Terminal.Transport.Ssh.SshNetSSH transport provider (SshNetTerminalTransportProvider)
RoyalApps.RoyalTerminal.Terminal.Transport.Ssh.SshNet.AgentOptional SSH agent auth contributor for SSH.NET
RoyalApps.RoyalTerminal.Terminal.Services.ContractsTerminal session service contracts
RoyalApps.RoyalTerminal.Terminal.ServicesTerminal session service implementations
RoyalApps.RoyalTerminal.Rendering.TextReusable text shaping/fallback subsystem (HarfBuzzTextShaper, TerminalFontResolver)
RoyalApps.RoyalTerminal.ShadersDependency-free shader source models and compatibility translation for Skia Runtime Effect terminal shaders
RoyalApps.RoyalTerminal.Rendering.SkiaCPU cell renderer core (SkiaTerminalRenderer, GlyphCache) with HarfBuzz shaping, fallback font resolution, and framebuffer shader post-processing
RoyalApps.RoyalTerminal.Rendering.ContractsBackend-agnostic render contracts (RenderTargetDescriptor, capabilities)
RoyalApps.RoyalTerminal.Rendering.Interop.GhosttyManaged wrapper for ghostty-renderer-capi
RoyalApps.RoyalTerminal.Rendering.Interop.Ghostty.SkiaSkia bridge (SkiaInteropRenderer) with CPU fallback
RoyalApps.RoyalTerminal.Avalonia.Rendering.GhosttyInteropAvalonia render-target acquisition and texture interop draw handler

Features

  • Core/native VT split:
    • RoyalApps.RoyalTerminal.Avalonia: backend-neutral control and services.
    • RoyalApps.RoyalTerminal.Terminal.Vt.Ghostty: official native VT integration over upstream libghostty-vt.
  • Backend-neutral endpoint contracts (ITerminalEndpoint, ITerminalInputSink, ITerminalSelectionSource, ITerminalModeSource) for control reuse across backends.
  • Pluggable transport runtime (ITerminalTransportFactory) supporting PTY, process pipe, SSH, raw TCP, Telnet, and serial sessions.
  • Shared SSH bootstrap helper (SshShellBootstrapCommandBuilder) for consistent POSIX export command composition across SSH backends.
  • Pluggable SSH secret persistence via ISshSecretStore + ISshSecretProtector with cross-platform secure defaults (SshSecretProtectionFactory).
  • Session profiles + persistent settings model via TerminalSessionProfile* contracts, TerminalSessionProfileSerializer, and JsonFileTerminalSessionProfileStore.
  • Regex text highlighting with one or more user-configurable rules, static cached or realtime matching, optional foreground/background colors, dark-theme overrides, and persisted settings/profile support.
  • Thread-safe output ingestion: TerminalControl.WriteOutput(...) can be called from background SSH/network callbacks (marshaled to UI thread internally).
  • Preference-based VT selection via VtProcessorPreference (Auto, Managed, Native).
  • Three integration modes with explicit trade-offs between fidelity, portability, and native dependencies.
  • Split rendering architecture:
    • CPU cell rendering path (RoyalTerminal.Rendering.Skia)
    • Managed framebuffer shader pipeline with direct Skia Runtime Effect source plus Ghostty/Shadertoy and Windows Terminal HLSL compatibility adapters
    • GPU interop path (RoyalApps.RoyalTerminal.Rendering.* + ghostty-renderer-capi)
  • Official native VT engine via libghostty-vt terminal/render-state APIs on all supported platforms.
  • Modular PTY and VT packages (Terminal.Pty.*, Terminal.Vt.*).
  • HarfBuzz-backed text shaping with grid-safe fallback behavior and optional diagnostics counters.
  • Grapheme-aware cell model in managed VT and official native VT render-state paths.
  • Terminal session service split (Terminal.Services.Contracts and Terminal.Services).
  • Sample applications:
    • Avalonia demo (samples/RoyalTerminal.Demo) with structured settings categories (Session/Connection/Terminal/Appearance/SSH/Logging), transport forms (PTY/Pipe/Raw TCP/Telnet/Serial/SSH), a tabbed Settings flyout with profile CRUD (new/duplicate/delete/set default) and explicit apply/save, session/event logging, shader samples, and terminal behavior toggles (copy-on-select, bell notifications, backspace mode, paste safety, text shaping/ligatures)
    • Windows Forms host sample (samples/RoyalTerminal.WinFormsHost) showing Avalonia.Win32.Interoperability, PerMonitorV2 DPI, Win32 focus forwarding, and TerminalControl.Padding for embedded margins
    • macOS SwiftUI native tabbed demo (samples/RoyalTerminal.MacNativeTabbed) that hosts GhosttyKit directly as a separate native sample, outside the managed RoyalTerminal.GhosttySharp surface
    • VT/PTy control catalog CLI (samples/RoyalTerminal.ControlCatalog) with managed/Ghostty VT probes, ncurses/TUI parity scenarios, and rich visual rendering galleries

Regex Text Highlighting

TerminalControl can apply ordered regex highlight rules to rendered terminal rows. Each rule can set foreground color, background color, both, or neither color independently. When a color is unset, matching cells keep the color already provided by the terminal application.

Runtime API:

using RoyalTerminal.Avalonia.Controls;
using RoyalTerminal.Avalonia.Rendering;
using RoyalTerminal.Terminal;

TerminalControl terminal = new()
{
    TextHighlightingMode = TerminalTextHighlightingMode.Static,
    TextHighlightRules =
    [
        new TerminalTextHighlightRule
        {
            Name = "Errors",
            Pattern = @"\b(ERROR|FAIL|FATAL)\b",
            Foreground = 0xFFFFE6E6,
            Background = 0xFF7F1D1D,
            DarkForeground = 0xFFFFB4B4,
            DarkBackground = 0xFF450A0A,
        },
        new TerminalTextHighlightRule
        {
            Name = "IPv4 addresses",
            Pattern = @"\b(?:25[0-5]|2[0-4]\d|1?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|1?\d?\d)){3}\b",
            Foreground = 0xFF93C5FD,
        },
    ],
};

Modes:

ModeBehavior
StaticCaches matched cells for unchanged row text, rule revision, and theme state. This is the default for normal terminal use.
RealtimeRecomputes matching rows whenever they are rendered.
DisabledKeeps configured rules but skips regex matching and rendering overrides.

Persisted profiles store the feature on TerminalSessionAppearanceSettings.TextHighlightingMode and TerminalSessionAppearanceSettings.TextHighlightRules. The reusable settings panel exposes it from the Appearance tab under Text Highlighting, including rule add/remove, enable/disable, mode selection, foreground/background checkboxes, and dark-theme color overrides.

The main foreground/background checkboxes control whether a rule changes that color. Dark foreground/background checkboxes are theme-specific overrides; when they are unchecked, the normal color is reused for dark themes.

See Regex Text Highlighting for the full profile JSON format, settings API, renderer behavior, performance details, and limitations.

Transport Session Model

TerminalControl now runs through a transport abstraction, not a PTY-only runtime:

  1. TerminalControl.StartSessionAsync(ITerminalTransportOptions) selects a transport.
  2. ITerminalTransportFactory resolves a provider by TransportId.
  3. The chosen ITerminalTransport (pty, pipe, or ssh) owns I/O and lifecycle.
  4. TerminalSessionService remains the coordination point for endpoint/input/selection/mode contracts.

Supported transport option models:

TransportOptions TypeNotes
PTYPtyTransportOptionsInteractive local shell semantics (ConPTY/forkpty)
PipePipeTransportOptionsNon-PTY process streams, useful for command/log scenarios
SSHSshTransportOptionsRemote terminal sessions with optional PTY request and host-key checks (OpenSSH known_hosts and optional SHA-256 pinning)
Raw TCPRawTcpTransportOptionsUnframed TCP byte stream sessions
TelnetTelnetTransportOptionsTelnet remote sessions with option negotiation handling
SerialSerialTransportOptionsDirect serial line sessions (baud/parity/stop bits/handshake)

Terminal Capture and Replay

Capture/replay is available for TerminalControl and is designed to be reusable outside the demo app.

  • Captured timeline events:
    • terminal output bytes
    • terminal input bytes sent through session routing
    • terminal resize events
    • process exit status events
    • marker events when loaded from formats that support them
  • Persistence:
    • native RoyalTerminal JSON via TerminalCaptureSessionSerializer
    • asciicast v3 via TerminalCaptureSessionFormats.AsciicastV3
    • pluggable formats via ITerminalCaptureSessionFormat and TerminalCaptureSessionFormatRegistry
    • recommended extensions: .rtcap.json and .cast
  • Replay controls:
    • play, pause, stop, and seek by timeline position
    • replay surface reset to captured initial dimensions on load, stop, and reset seeks
    • paused or static replay frames rebuild at the current control size after host resize

Demo Integration (samples/RoyalTerminal.Demo)

The demo toolbar includes:

  • Start Capture / Stop Capture
  • capture format selector (RoyalTerminal JSON or Asciicast v3)
  • Save Capture (writes the capture session using the selected format)
  • Load Replay (opens RoyalTerminal JSON or asciicast v3 capture files in a replay tab)
  • Settings (opens tabbed session/profile editor with explicit Apply and Save)

When replay is active, the replay timeline bar is shown with play/pause, stop, slider seek, elapsed/total display, and source label.

Reusable API Surface

  • RoyalTerminal.Avalonia.Capture.TerminalCaptureRuntime
    • runtime orchestration for capture + replay against a TerminalControl
  • RoyalTerminal.Terminal.TerminalCaptureRecorder
    • event recorder with snapshot/finalize semantics
  • RoyalTerminal.Terminal.TerminalCaptureSession
    • serializable capture payload (metadata + ordered events)
  • RoyalTerminal.Terminal.TerminalCaptureSessionSerializer
    • stream/file load + save helpers for native JSON plus explicit-format overloads
  • RoyalTerminal.Terminal.ITerminalCaptureSessionFormat
    • pluggable recording format contract
  • RoyalTerminal.Terminal.TerminalCaptureSessionFormatRegistry
    • format lookup, save by id, and load probing
  • RoyalTerminal.Terminal.TerminalCaptureSessionFormats
    • built-in RoyalTerminal JSON and asciicast v3 format instances
using RoyalTerminal.Avalonia.Capture;
using RoyalTerminal.Avalonia.Controls;
using RoyalTerminal.Terminal;

var terminal = new TerminalControl();
var captureRuntime = new TerminalCaptureRuntime(terminal);

captureRuntime.StartCapture();
terminal.SendInput("ls\r");
terminal.WriteOutput("file1\nfile2\n"u8);

TerminalCaptureSession captured = captureRuntime.StopCapture();
await TerminalCaptureSessionSerializer.SaveToFileAsync(captured, "session.rtcap.json");
await TerminalCaptureSessionSerializer.SaveToFileAsync(
    captured,
    "session.cast",
    TerminalCaptureSessionFormats.AsciicastV3);

TerminalCaptureSession loaded =
    await TerminalCaptureSessionSerializer.LoadFromFileAsync("session.rtcap.json");

await using FileStream asciicast = File.OpenRead("session.cast");
TerminalCaptureSession loadedAsciicast =
    await TerminalCaptureSessionFormats.DefaultRegistry.LoadAsync(asciicast, "session.cast");

captureRuntime.LoadReplay(loaded, "session.rtcap.json");
captureRuntime.PlayReplay();
captureRuntime.SeekReplay(2.5);
captureRuntime.PauseReplay();
captureRuntime.StopReplay();

Integration Modes

ModeControlPackage SetVT EngineRendererPTYPlatformBest For
Native VTTerminalControlRoyalApps.RoyalTerminal.Avalonia + RoyalApps.RoyalTerminal.Terminal.Vt.Ghostty + native assetsofficial libghostty-vt terminal/render-state APIsSkia cell rendererUnix PTY / ConPTYmacOS/Linux/WindowsCross-platform native VT parser on the upstream Ghostty C API
Managed VTTerminalControlRoyalApps.RoyalTerminal.AvaloniaBasicVtProcessor (C#)Skia cell rendererUnix PTY / ConPTYmacOS/Linux/WindowsExplicit managed VT path
Rendered (Auto VT)TerminalControlRoyalApps.RoyalTerminal.Avalonia (+ optional native VT provider packages)Auto (libghostty-vt when available, otherwise BasicVtProcessor)Skia cell rendererUnix PTY / ConPTYmacOS/Linux/WindowsDefault backend-neutral mode

Mode Availability and Fallback Policy (Demo)

The demo exposes only cross-platform modes. When native VT is unavailable, mode routing falls back deterministically to the next supported mode.

Resolver cycle order:

Native VT -> Managed VT -> Rendered (Auto VT)

Fallback chains when requested mode is unavailable:

Requested ModeFallback Chain (next supported mode in resolver order)
Native VTManaged VT -> Rendered (Auto VT)
Managed VTRendered (Auto VT)
Rendered (Auto VT)Always supported (no fallback required)

Mode cycle in the demo also skips unsupported modes and always lands on a runnable mode.

Demo Tab Mode Indicators

The Avalonia demo uses a glyph + color marker in each tab header:

ModeMarkerColor
Native VTGreen (#6AB04C)
Managed VTTeal (#4EC9B0)
Rendered (Auto VT)Olive (#6A9955)

TerminalControl VT Preference Modes

Demo Mode LabelVtProcessorPreferenceBehavior
Native VTNativeRequires the official native Ghostty VT provider (libghostty-vt), throws when unavailable
Managed VTManagedForces BasicVtProcessor for deterministic pure-managed behavior
Rendered (Auto VT)AutoUses official native VT when available and falls back to managed VT otherwise

Architecture

flowchart TD
    App["Application / Avalonia Host"]

    subgraph CoreUI["RoyalTerminal.Avalonia (Core)"]
      C1["TerminalControl"]
      C2["TerminalDrawHandler / TerminalPresenter"]
    end

    subgraph Terminal["Terminal Modules"]
      T0["RoyalTerminal.Terminal"]
      T1["Terminal.Vt.Managed"]
      T2["Terminal.Vt.Ghostty"]
      T3["Terminal.Vt.Default"]
      P1["Terminal.Pty.Unix"]
      P2["Terminal.Pty.Windows"]
      P3["Terminal.Pty.Platform"]
      P4["Terminal.Transport.Pty"]
      P5["Terminal.Transport.Pipe"]
      P6["Terminal.Transport.Ssh.Abstractions"]
      P7["Terminal.Transport.Ssh.SshNet"]
      S1["Terminal.Services.Contracts"]
      S2["Terminal.Services"]
    end

    subgraph CoreRender["Rendering Core"]
      R4["Rendering.Skia"]
    end

    subgraph GhosttyRender["Ghostty Rendering Interop (Optional)"]
      R0["Rendering.Contracts"]
      R1["Rendering.Interop.Ghostty"]
      R2["Rendering.Interop.Ghostty.Skia"]
      R3["Avalonia.Rendering.GhosttyInterop"]
    end

    subgraph Native["Native Libraries"]
      N1["libghostty-vt"]
      N2["libghostty-renderer-capi"]
    end

    App --> CoreUI
    CoreUI --> Terminal
    CoreUI --> CoreRender
    Terminal --> Native
    GhosttyRender --> Native

Usage

1. Backend-Neutral Cross-Platform Control (RoyalTerminal.Avalonia)

using RoyalTerminal.Avalonia.Controls;
using RoyalTerminal.Terminal;

var terminal = new TerminalControl
{
    FontFamilyName = "JetBrains Mono",
    TerminalFontSize = 14,
    Columns = 120,
    Rows = 40,
    ScrollbackLimit = 10_000,
    VtProcessorPreference = VtProcessorPreference.Auto,
};

terminal.StartPty(
    workingDirectory: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));

StartPty(...) remains supported for backwards compatibility. The preferred session API is StartSessionAsync(...) with transport options.

1a. Unified Session Start (StartSessionAsync)

using RoyalTerminal.Avalonia.Controls;
using RoyalTerminal.Terminal;

var terminal = new TerminalControl();

ITerminalTransportOptions options = new PtyTransportOptions(
    Command: null,
    WorkingDirectory: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
    Environment: null,
    Dimensions: new TerminalSessionDimensions(120, 40, 1200, 800));

await terminal.StartSessionAsync(options);

Preserve Session Scrollback

New sessions start with a clean buffer by default. Reconnect flows can keep the previous session history in the same control:

terminal.PreserveScrollbackOnSessionStart = true;
await terminal.StartSessionAsync(options);

await terminal.StartSessionAsync(nextOptions, preserveScrollback: true);

terminal.ClearScrollback();

ClearScrollback() removes history while keeping the active viewport intact. See the Session History And Scrollback guide for managed VT, Ghostty VT, and reference-terminal behavior details.

1b. Start a Pipe Session (PipeTransportOptions)

using RoyalTerminal.Avalonia.Controls;
using RoyalTerminal.Terminal;

var terminal = new TerminalControl();

await terminal.StartPipeAsync(
    new PipeTransportOptions(
        Command: new TerminalCommandSpec(
            FileName: "/bin/sh",
            Arguments: new[] { "-lc", "dotnet --info" }),
        WorkingDirectory: null,
        Environment: null,
        MergeStdErrIntoStdOut: true,
        Dimensions: new TerminalSessionDimensions(120, 40, 1200, 800)));

1c. Start an SSH Session (SshTransportOptions)

using System;
using System.Collections.Generic;
using RoyalTerminal.Avalonia.Controls;
using RoyalTerminal.Terminal;

var terminal = new TerminalControl();

SshTransportOptions options = new(
    Endpoint: new SshEndpointOptions("example.com", 22, "alice"),
    RequestPty: true,
    TerminalType: "xterm-256color",
    InitialCommand: null,
    Authentication: new SshAuthenticationOptions(
        UsePassword: true,
        PasswordSecretId: "demo-password",
        PrivateKeySecretIds: Array.Empty<string>(),
        UseAgent: false),
    Dimensions: new TerminalSessionDimensions(120, 40, 1200, 800))
{
    ExpectedHostKeyFingerprintSha256 = "SHA256:BASE64_FINGERPRINT",
    EnvironmentVariables = new Dictionary<string, string>(StringComparer.Ordinal)
    {
        ["LANG"] = "en_US.UTF-8",
        ["LC_CTYPE"] = "en_US.UTF-8",
        ["TERM"] = "xterm-256color",
    },
};

await terminal.StartSshAsync(options);

ExpectedHostKeyFingerprintSha256 is optional. When omitted, host-key trust falls back to KnownHostsSshHostKeyValidator (OpenSSH known_hosts).

EnvironmentVariables is optional. When provided, values are applied through a POSIX-shell bootstrap command (export KEY='value') before InitialCommand.

Environment-variable validation rules:

  • Name must match [A-Za-z_][A-Za-z0-9_]*.
  • Value must be non-null.
  • Value must not contain CR, LF, or NUL.

1c.0 Advanced SSH configuration surface (proxy, forwarding, policy)

options = options with
{
    Proxy = new SshProxyOptions(
        Type: SshProxyType.Socks5,
        Host: "proxy.example.com",
        Port: 1080,
        Username: "proxy-user",
        Password: "proxy-password"),
    PortForwardings =
    [
        new SshPortForwardOptions(
            Mode: SshPortForwardMode.Local,
            BindAddress: "127.0.0.1",
            SourcePort: 15432,
            DestinationHost: "db.internal",
            DestinationPort: 5432),
    ],
    Policy = new SshPolicyOptions(
        KeepAliveIntervalSeconds: 30,
        ConnectTimeoutSeconds: 15),
};

X11 has an options surface (SshX11Options) but is currently not supported by the SSH.NET backend transport and will throw when enabled.

1c.1 Reuse SSH Bootstrap Composition in Custom Backends (Rebex, etc.)

If you run SSH with a custom backend instead of RoyalTerminal.Terminal.Transport.Ssh.SshNet, use the same bootstrap helper for consistent behavior:

using RoyalTerminal.Terminal;

SshTransportOptions options = /* build options */;
string? bootstrap = SshShellBootstrapCommandBuilder.Build(options);

if (!string.IsNullOrWhiteSpace(bootstrap))
{
    // WriteLine to your custom shell channel (for example Rebex shell stream).
    SendLineToRemoteShell(bootstrap);
}

You can also use the overload:

string? bootstrap = SshShellBootstrapCommandBuilder.Build(
    initialCommand: "exec $SHELL -il",
    environmentVariables: new Dictionary<string, string>
    {
        ["LANG"] = "en_US.UTF-8",
    });

1d. PTY with Custom Shell Profile

using RoyalTerminal.Avalonia.Controls;
using RoyalTerminal.Terminal;

IShellProfileCatalog shellCatalog = new DefaultShellProfileCatalog();
ShellProfile shellProfile = shellCatalog.GetDefaultProfile();

var terminal = new TerminalControl();
await terminal.StartSessionAsync(
    new PtyTransportOptions(
        Command: shellProfile.Command,
        WorkingDirectory: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
        Environment: null,
        Dimensions: new TerminalSessionDimensions(120, 40, 1200, 800)));

1e. SSH Credential Provider Injection

using System;
using System.Threading;
using RoyalTerminal.Avalonia.Controls;
using RoyalTerminal.Avalonia.Services;
using RoyalTerminal.Terminal;
using RoyalTerminal.Terminal.Services;
using RoyalTerminal.Terminal.Transport.Ssh;
using RoyalTerminal.Terminal.Transport.Ssh.SshNet;

sealed class RuntimeCredentialProvider : ISshCredentialProvider
{
    public ValueTask<SshResolvedCredentials> ResolveAsync(
        SshCredentialRequest request,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        return ValueTask.FromResult(
            new SshResolvedCredentials(
                Password: Environment.GetEnvironmentVariable("SSH_PASSWORD"),
                PrivateKeyPemOrPath: Array.Empty<string>(),
                UseAgent: false));
    }
}

var terminal = new TerminalControl(
    new TerminalSessionService(),
    new DefaultTerminalInputAdapter(),
    new DefaultTerminalSelectionService(),
    new DefaultTerminalScrollService(),
    new DefaultVtProcessorFactory(),
    new DefaultPtyFactory(),
    new RuntimeCredentialProvider(),
    new KnownHostsSshHostKeyValidator(),
    transportFactory: null);

1f. Protected SSH Secret Store (Cross-Platform Default)

using System;
using RoyalTerminal.Terminal;

ISshSecretStore secretStore = SshSecretProtectionFactory.CreateDefaultSecretStore();

await secretStore.SaveSecretAsync("ssh/password", "my-password");
await secretStore.SaveSecretAsync("ssh/key/main", "/home/user/.ssh/id_ed25519");

ISshCredentialProvider credentialProvider = new SecretStoreSshCredentialProvider(secretStore);

1g. Session Profiles and Persistent Settings

using RoyalTerminal.Avalonia.Controls;
using RoyalTerminal.Terminal;

TerminalSessionProfilesDocument profiles = new()
{
    Profiles =
    [
        new TerminalSessionProfile
        {
            Id = "dev-ssh",
            DisplayName = "Dev SSH",
            Transport = new TerminalSessionTransportProfile
            {
                TransportId = TerminalTransportIds.Ssh,
                Ssh = new TerminalSessionSshSettings
                {
                    Host = "example.com",
                    Port = 22,
                    Username = "alice",
                    Authentication = new TerminalSessionSshAuthenticationSettings
                    {
                        UsePassword = true,
                        PasswordSecretId = "ssh/dev/password",
                    },
                },
            },
        },
    ],
};

ITerminalSessionProfileStore store = TerminalSessionProfileStoreFactory.CreateDefault();
await store.SaveAsync(profiles);

TerminalSessionProfilesDocument loaded = await store.LoadAsync();
ITerminalTransportOptions options = TerminalSessionProfileMapper.ToTransportOptions(loaded.Profiles[0]);

var terminal = new TerminalControl();
await terminal.StartSessionAsync(options);

1h. Embed in Windows Forms

Use Avalonia.Win32.Interoperability from a Windows Forms project and start Avalonia once with SetupWithoutStarting() before creating the host control. TerminalControl.Padding is the supported embedded-margin API; it defaults to 0 for existing layouts, and 8 works well when the control is hosted edge-to-edge in a form.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows7.0</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <EnableWindowsTargeting>true</EnableWindowsTargeting>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Avalonia.Desktop" />
    <PackageReference Include="Avalonia.Themes.Fluent" />
    <PackageReference Include="Avalonia.Win32.Interoperability" />
    <PackageReference Include="RoyalApps.RoyalTerminal.Avalonia" />
  </ItemGroup>
</Project>
using Avalonia;
using Avalonia.Themes.Fluent;
using Avalonia.Win32.Interoperability;
using RoyalTerminal.Avalonia.Controls;
using WinForms = System.Windows.Forms;

WinForms.Application.SetHighDpiMode(WinForms.HighDpiMode.PerMonitorV2);
WinForms.Application.EnableVisualStyles();
WinForms.Application.SetCompatibleTextRenderingDefault(false);

AppBuilder.Configure<App>()
    .UsePlatformDetect()
    .SetupWithoutStarting();

TerminalControl terminal = new()
{
    Padding = new Thickness(8),
};

WinFormsAvaloniaControlHost host = new()
{
    Dock = WinForms.DockStyle.Fill,
    Content = terminal,
};

For focus interop, handle WM_SETFOCUS on the WinForms host and post the Avalonia focus call at input priority. Avoid focus work from WM_KILLFOCUS; Windows is already moving focus at that point.

private const int WmSetFocus = 0x0007;

protected override void WndProc(ref WinForms.Message m)
{
    base.WndProc(ref m);

    if (m.Msg == WmSetFocus)
    {
        Dispatcher.UIThread.Post(
            () => terminal.Focus(),
            DispatcherPriority.Input);
    }
}

For DPI, keep TerminalFontSize in Avalonia device-independent pixels. Avalonia top-level RenderScaling is forwarded automatically by TerminalControl; a WinForms host should also forward DeviceDpi / 96.0 after parent DPI changes so native or external render endpoints implementing ITerminalScaleSink receive physical scale through SetContentScale.

protected override void OnDpiChangedAfterParent(EventArgs e)
{
    base.OnDpiChangedAfterParent(e);

    double scale = DeviceDpi > 0 ? DeviceDpi / 96d : 1d;
    Dispatcher.UIThread.Post(
        () => terminal.SetContentScale(scale, scale),
        DispatcherPriority.Input);
}

See samples/RoyalTerminal.WinFormsHost for a complete Windows-only sample targeting net10.0-windows7.0.

2. Core Control with Native VT Provider (official libghostty-vt)

using RoyalTerminal.Avalonia.Controls;
using RoyalTerminal.Avalonia.Services;
using RoyalTerminal.Terminal;
using RoyalTerminal.Terminal.Services;

var terminal = new TerminalControl(
    new TerminalSessionService(),
    new DefaultTerminalInputAdapter(),
    new DefaultTerminalSelectionService(),
    new DefaultTerminalScrollService(),
    new DefaultVtProcessorFactory([new GhosttyVtProcessorProvider()]),
    new DefaultPtyFactory())
{
    VtProcessorPreference = VtProcessorPreference.Native,
};

terminal.StartPty();

3. Core Control with Explicit Managed VT (BasicVtProcessor)

using RoyalTerminal.Avalonia.Controls;
using RoyalTerminal.Terminal;

var terminal = new TerminalControl
{
    VtProcessorPreference = VtProcessorPreference.Managed,
};

terminal.StartPty();

4. Attach a Custom Endpoint (ITerminalEndpoint)

using System;
using System.Text;
using RoyalTerminal.Avalonia.Controls;
using RoyalTerminal.Terminal;

sealed class LoggingEndpoint : ITerminalEndpoint
{
    public void SendText(ReadOnlySpan<byte> utf8)
        => Console.WriteLine($"INPUT: {Encoding.UTF8.GetString(utf8)}");

    public void SetFocus(bool focused)
        => Console.WriteLine($"FOCUS: {focused}");

    public void SetSize(int widthPx, int heightPx)
        => Console.WriteLine($"SIZE: {widthPx}x{heightPx}");
}

var terminal = new TerminalControl();
terminal.AttachEndpoint(new LoggingEndpoint());
terminal.SendInput("echo hello\n");
terminal.DetachEndpoint();

TerminalControl supports two endpoint integration levels:

  • ITerminalEndpoint only (byte-stream integration): fallback key/text/mouse/focus VT sequences are written through SendText(...).
  • ITerminalEndpoint + ITerminalInputSink (full-fidelity event integration): structured key/text/pointer events are passed directly to your backend.

4a. Comprehensive Custom SSH Endpoint Setup (Rebex or Other SSH SDKs)

Use this model when you own the SSH channel/session lifecycle and only need TerminalControl for VT parsing, rendering, and input mapping:

using System;
using RoyalTerminal.Avalonia.Controls;
using RoyalTerminal.Terminal;

sealed class ByteStreamSshEndpoint : ITerminalEndpoint
{
    private readonly Action<byte[]> _sendBytes;
    private readonly Action<bool> _setRemoteFocus;
    private readonly Action<int, int> _setRemoteSizePx;

    public ByteStreamSshEndpoint(
        Action<byte[]> sendBytes,
        Action<bool> setRemoteFocus,
        Action<int, int> setRemoteSizePx)
    {
        _sendBytes = sendBytes;
        _setRemoteFocus = setRemoteFocus;
        _setRemoteSizePx = setRemoteSizePx;
    }

    public void SendText(ReadOnlySpan<byte> utf8) => _sendBytes(utf8.ToArray());
    public void SetFocus(bool focused) => _setRemoteFocus(focused);
    public void SetSize(int widthPx, int heightPx) => _setRemoteSizePx(widthPx, heightPx);
}

var terminal = new TerminalControl
{
    VtProcessorPreference = VtProcessorPreference.Managed,
};

var endpoint = new ByteStreamSshEndpoint(
    sendBytes: bytes => SendBytesToSshShell(bytes),          // your SSH write path
    setRemoteFocus: focused => NotifySshFocus(focused),      // optional backend-specific signal
    setRemoteSizePx: (w, h) => NotifySshPixelSize(w, h));    // optional backend-specific signal

terminal.AttachEndpoint(endpoint);

// Route remote output to TerminalControl from any thread (network callback thread is fine).
OnSshData((buffer, length) =>
{
    terminal.WriteOutput(buffer.AsSpan(0, length));
});

Behavior when using endpoint-only mode:

  • Keyboard and text input are encoded to VT byte sequences and sent through ITerminalEndpoint.SendText(...).
  • Mouse reporting sequences are sent through SendText(...) when mouse modes are enabled (for example DECSET 1000/1002/1003 with SGR 1006).
  • Focus in/out (CSI I / CSI O) is sent through SendText(...) when focus mode DECSET 1004 is enabled.
  • WriteOutput(...) is safe from background callback threads; TerminalControl marshals processing to the UI thread internally.
  • Mode state is normally learned from the stream you feed into WriteOutput(...); if your backend tracks modes separately, expose them via ITerminalModeSource (and optionally ITerminalFocusEventModeSource).

If you implement ITerminalInputSink, fallback byte-sequence encoding is bypassed and your backend receives structured TerminalKeyEvent / TerminalPointerEvent.

5. Renderer Shaping Controls and Diagnostics

using RoyalTerminal.Avalonia.Controls;
using RoyalTerminal.Avalonia.Rendering;

var terminal = new TerminalControl();
terminal.StartPty();

if (terminal.Renderer is { } renderer)
{
    renderer.EnableTextShaping = true;
    renderer.TextDirectionMode = TextDirectionMode.Auto;
    renderer.EnableLigatures = false;
    renderer.EnableTextRenderDiagnostics = true;

    TextRenderDiagnostics diagnostics = renderer.GetTextRenderDiagnostics(reset: true);
}

6. Direct Renderer Interop (No Avalonia Adapter)

using RoyalTerminal.Rendering.Contracts;
using RoyalTerminal.Rendering.Interop.Ghostty;

using var context = new GhosttyRenderContext();
using var surface = context.CreateSurface(RenderBackendKind.Software);

surface.SetSize(800, 600);
surface.SetScale(1.0, 1.0);

ulong frameToken = surface.BeginFrame();
byte[] rgba = new byte[800 * 600 * 4];
RenderFrameResult frame = surface.RenderToRgba(rgba, 800, 600, 800 * 4);
surface.EndFrame(frameToken);

Migration Guide

Existing PTY Users

  • TerminalControl.StartPty(...) is still supported and remains the compatibility API.
  • TerminalControl.Pty and TerminalControl.HasPty still work for PTY sessions.
  • VT response writeback now goes through session input routing, so no PTY-specific handling is required in callers.

Preferred New Session API

  • New code should use StartSessionAsync(...), StartPipeAsync(...), or StartSshAsync(...).
  • Query active session state with:
    • TerminalControl.HasActiveSession
    • TerminalControl.ActiveTransportId

API Name and Package Changes

  • Core control rename:
    • GhosttyTerminalControl -> TerminalControl
    • GhosttyTerminalPresenter -> TerminalPresenter
  • VT selection moved from UseNativeVtProcessor to VtProcessorPreference.
  • Legacy surface-coupled ITerminalSurface contract was removed.

Installation

Backend-Neutral Setup (No Ghostty Dependency)

# Core Avalonia control + PTY + managed VT defaults

dotnet add package RoyalApps.RoyalTerminal.Avalonia

Optional Native VT for Core Control

# Native VT provider over official libghostty-vt bindings

dotnet add package RoyalApps.RoyalTerminal.Terminal.Vt.Ghostty

# Restore/publish for the RID you target so NuGet selects the matching native package.
dotnet publish -r osx-arm64

RoyalApps.RoyalTerminal.GhosttySharp and RoyalApps.RoyalTerminal.Rendering.Interop.Ghostty ship runtime.json metadata that maps supported RIDs to the matching RoyalApps.RoyalTerminal.GhosttySharp.Native.* package. Direct native package references are only needed when you intentionally want to force a specific native asset package.

Optional SSH Transport Packages

dotnet add package RoyalApps.RoyalTerminal.Terminal.Transport.Ssh.Abstractions
dotnet add package RoyalApps.RoyalTerminal.Terminal.Transport.Ssh.SshNet

# Optional SSH agent auth adapter
dotnet add package RoyalApps.RoyalTerminal.Terminal.Transport.Ssh.SshNet.Agent

If you integrate a custom SSH SDK (for example Rebex) via AttachEndpoint(...), you can skip SSH.NET packages and use:

dotnet add package RoyalApps.RoyalTerminal.Avalonia
dotnet add package RoyalApps.RoyalTerminal.Terminal

Modular Rendering Interop Setup

Use this when embedding the renderer interop pipeline directly (for TextureInterop or custom integrations):

dotnet add package RoyalApps.RoyalTerminal.Rendering.Contracts
dotnet add package RoyalApps.RoyalTerminal.Rendering.Interop.Ghostty
dotnet add package RoyalApps.RoyalTerminal.Shaders
dotnet add package RoyalApps.RoyalTerminal.Rendering.Skia
dotnet add package RoyalApps.RoyalTerminal.Rendering.Interop.Ghostty.Skia
dotnet add package RoyalApps.RoyalTerminal.Avalonia.Rendering.GhosttyInterop

For source builds or internal feeds, create the same package set with bash scripts/pack-nuget.sh --configuration Release --output artifacts --version <version>.

Codex SKILL

This repository includes a Codex skill:

  • skills/royalterminal-development

Install to Codex Skills Directory

From the repository root:

SKILL_NAME="royalterminal-development"
CODEX_SKILLS_DIR="${CODEX_HOME:-$HOME/.codex}/skills"

mkdir -p "$CODEX_SKILLS_DIR"
cp -R "skills/$SKILL_NAME" "$CODEX_SKILLS_DIR/$SKILL_NAME"
SKILL_NAME="royalterminal-development"
CODEX_SKILLS_DIR="${CODEX_HOME:-$HOME/.codex}/skills"

mkdir -p "$CODEX_SKILLS_DIR"
ln -sfn "$(pwd)/skills/$SKILL_NAME" "$CODEX_SKILLS_DIR/$SKILL_NAME"

Verify Install

test -f "${CODEX_HOME:-$HOME/.codex}/skills/royalterminal-development/SKILL.md" && echo "skill installed"

Skill Entry Files

  • Skill instructions: skills/royalterminal-development/SKILL.md
  • Granular references: skills/royalterminal-development/references/

Feature Comparison

CapabilityNative VT (TerminalControl)Managed VT (TerminalControl)Rendered (TerminalControl, Auto VT)
Package entry pointRoyalApps.RoyalTerminal.Avalonia + Terminal.Vt.GhosttyRoyalApps.RoyalTerminal.AvaloniaRoyalApps.RoyalTerminal.Avalonia
Platform availabilitymacOS/Linux/WindowsmacOS/Linux/WindowsmacOS/Linux/Windows
VT engineofficial libghostty-vtBasicVtProcessorauto-selects native VT when available, otherwise managed VT
Renderer pathSkia cell rendererSkia cell rendererSkia cell renderer
Requires libghostty-vtYesNoOptional
Full Avalonia overlay supportYesYesYes
Cross-platform modeYesYesYes
Demo fallback when unavailableRouted to Managed VT then RenderedRouted to RenderedAlways supported

Rendering Interop Contract

RoyalTerminal.Rendering.Contracts defines the backend-neutral model:

  • RenderBackendKind: Software, Metal, Vulkan, D3D11, D3D12, OpenGL
  • RenderTargetDescriptor: native handle carrier for one render target submission
  • RenderBackendCapabilities: supported features/sample counts/pixel formats
  • RenderFeatureFlags: ExternalTextureTargets, ExternalFramebufferTargets, CpuRgbaFallback, ExplicitFrameLifecycle, etc.

RoyalTerminal.Rendering.Interop.Ghostty.Skia (SkiaInteropRenderer) behavior:

  1. Validate descriptor.
  2. Attempt direct interop only when surface capabilities advertise the target kind (ExternalTextureTargets or ExternalFramebufferTargets).
  3. Fall back to CPU RGBA path when direct interop is unavailable or fails and fallback is enabled.

Native Renderer C API (ghostty-renderer-capi)

native/ghostty-renderer-capi exports:

  • Context/surface lifecycle
  • Surface configuration (set_size, set_scale, set_focus, set_color_scheme)
  • Explicit frame lifecycle (begin_frame / end_frame)
  • Target validation and rendering (validate_target, render_to_target)
  • CPU fallback rendering (render_to_rgba)

Header: native/ghostty-renderer-capi/include/ghostty_renderer.h

Build directly:

cd native/ghostty-renderer-capi
bash build.sh release
bash build.sh test

Official VT Library (libghostty-vt)

libghostty-vt is now the only native VT engine used by RoyalTerminal. GhosttyVtProcessor drives the upstream terminal and render-state APIs directly, with no custom standalone wrapper.

Build directly:

cd external/ghostty
zig build -Doptimize=ReleaseFast -Dapp-runtime=none

For distributable Windows x64 artifacts, build a scalar compatibility DLL with an explicit baseline CPU. This avoids AVX/VEX instructions in startup paths on older CPUs, constrained VMs, and Windows ARM64 x64 emulation:

.\scripts\build-native.ps1 -Arch x64 -Release
# or, from external/ghostty:
zig build -Doptimize=ReleaseFast -Dapp-runtime=none -Dtarget=x86_64-windows-msvc -Dcpu=x86_64-vzeroupper -Dsimd=false

CI verifies the Windows x64 native artifacts with scripts/verify-windows-x64-no-avx.ps1. Install LLVM or pass the verifier an explicit -ObjdumpPath when running the check locally.

Ghostty Submodule Status

RoyalTerminal now tracks upstream Ghostty directly in external/ghostty; the local Ghostty fork patches and managed libghostty wrapper layer were removed. The managed/native integration now targets upstream libghostty-vt directly, with ghostty-renderer-capi retained only for optional render-target interop.

PTY Layer

PackageImplementation
RoyalApps.RoyalTerminal.Terminal.Pty.UnixUnixPty (forkpty, TIOCSWINSZ)
RoyalApps.RoyalTerminal.Terminal.Pty.WindowsWindowsPty (ConPTY)
RoyalApps.RoyalTerminal.Terminal.Pty.PlatformDefaultPtyFactory selector

Native Library Resolution

Renderer interop (RoyalTerminal.Rendering.Interop.Ghostty) supports:

  • GHOSTTY_RENDERER_CAPI_LIBRARY_PATH (absolute file path)
  • GHOSTTY_RENDERER_CAPI_LIBRARY_DIR (directory containing the library)

Probe order includes:

  1. Explicit env vars above
  2. runtimes/<rid>/native/ next to app base directory
  3. runtimes/<rid>/native/ next to assembly directory
  4. Default OS loader paths

Native Libraries and Placement

PlatformFiles
macOSlibghostty-vt.dylib, libghostty-renderer-capi.dylib
Linuxlibghostty-vt.so, libghostty-renderer-capi.so
Windowsghostty-vt.dll, ghostty-renderer-capi.dll

Primary runtime package locations:

  • src/RoyalTerminal.GhosttySharp.Native.OSX/runtimes/<rid>/native/
  • src/RoyalTerminal.GhosttySharp.Native.Linux64/runtimes/<rid>/native/
  • src/RoyalTerminal.GhosttySharp.Native.Win64/runtimes/<rid>/native/

Package consumers normally do not reference those native packages directly. RID-aware restore/publish resolves them from the runtime.json files in RoyalApps.RoyalTerminal.GhosttySharp and RoyalApps.RoyalTerminal.Rendering.Interop.Ghostty.

Project Structure

RoyalTerminal/
├── Directory.Build.props
├── Directory.Packages.props
├── RoyalTerminal.sln
├── native/
│   └── ghostty-renderer-capi/
├── src/
│   ├── RoyalTerminal.GhosttySharp/
│   ├── RoyalTerminal.Avalonia/
│   ├── RoyalTerminal.Avalonia.Settings/
│   ├── RoyalTerminal.Avalonia.Rendering.GhosttyInterop/
│   ├── RoyalTerminal.Terminal/
│   ├── RoyalTerminal.Unicode/
│   ├── RoyalTerminal.Sixel/
│   ├── RoyalTerminal.Terminal.Vt.Managed/
│   ├── RoyalTerminal.Terminal.Vt.Ghostty/
│   ├── RoyalTerminal.Terminal.Vt.Default/
│   ├── RoyalTerminal.Terminal.Pty.Unix/
│   ├── RoyalTerminal.Terminal.Pty.Windows/
│   ├── RoyalTerminal.Terminal.Pty.Platform/
│   ├── RoyalTerminal.Terminal.Transport.Pty/
│   ├── RoyalTerminal.Terminal.Transport.Pipe/
│   ├── RoyalTerminal.Terminal.Transport.Raw/
│   ├── RoyalTerminal.Terminal.Transport.Telnet/
│   ├── RoyalTerminal.Terminal.Transport.Serial/
│   ├── RoyalTerminal.Terminal.Transport.Ssh.Abstractions/
│   ├── RoyalTerminal.Terminal.Transport.Ssh.SshNet/
│   ├── RoyalTerminal.Terminal.Transport.Ssh.SshNet.Agent/
│   ├── RoyalTerminal.Terminal.Services.Contracts/
│   ├── RoyalTerminal.Terminal.Services/
│   ├── RoyalTerminal.Rendering.Text/
│   ├── RoyalTerminal.Shaders/
│   ├── RoyalTerminal.Rendering.Contracts/
│   ├── RoyalTerminal.Rendering.Interop.Ghostty/
│   ├── RoyalTerminal.Rendering.Skia/
│   ├── RoyalTerminal.Rendering.Interop.Ghostty.Skia/
│   ├── RoyalTerminal.GhosttySharp.Native.OSX/
│   ├── RoyalTerminal.GhosttySharp.Native.Linux64/
│   └── RoyalTerminal.GhosttySharp.Native.Win64/
├── samples/RoyalTerminal.Demo/
├── samples/RoyalTerminal.WinFormsHost/
├── samples/RoyalTerminal.MacNativeTabbed/
├── samples/RoyalTerminal.ControlCatalog/
├── tests/
│   ├── RoyalTerminal.Benchmarks/
│   ├── RoyalTerminal.Tests/
│   └── RoyalTerminal.IntegrationTests/
└── scripts/
    ├── build-native.sh
    └── build-native.ps1

Building

Prerequisites

  • .NET 10 SDK
  • Zig 0.15.2+
  • Ghostty submodule:
git submodule update --init --recursive

Windows-specific prerequisite:

  • Symlink creation must be available for Zig package extraction.
  • Enable Developer Mode (start ms-settings:developers) or run PowerShell as Administrator.
  • scripts/build-native.ps1 uses a repository-local Zig global cache at .zig-global-cache to avoid stale user-level cache state.

Build Native + Managed

# macOS/Linux
bash scripts/build-native.sh --release

# Windows
pwsh scripts/build-native.ps1 -Release

# Managed build
dotnet build RoyalTerminal.sln -c Release

Run Demo

dotnet run --project samples/RoyalTerminal.Demo

Run Control Catalog CLI

dotnet run --project samples/RoyalTerminal.ControlCatalog

Optional demo toggles:

ROYALTERMINAL_DISABLE_TEXT_SHAPING=1 \
ROYALTERMINAL_ENABLE_RENDER_DIAGNOSTICS=1 \
dotnet run --project samples/RoyalTerminal.Demo

Run macOS Native SwiftUI Sample

swift run --package-path samples/RoyalTerminal.MacNativeTabbed

Testing

# Full test pass
dotnet test RoyalTerminal.sln -c Release

# Rendering-focused tests
dotnet test tests/RoyalTerminal.Tests/RoyalTerminal.Tests.csproj -c Release --filter "RenderingInteropTests|RenderingSkiaInteropTests|RenderingAvaloniaAdapterTests|RenderingContractsTests"

# Optional SSH transport integration tests (env-gated)
ROYALTERMINAL_IT_SSH_HOST=127.0.0.1 \
ROYALTERMINAL_IT_SSH_PORT=22 \
ROYALTERMINAL_IT_SSH_USERNAME=test-user \
ROYALTERMINAL_IT_SSH_PASSWORD=secret \
ROYALTERMINAL_IT_SSH_HOST_KEY_SHA256=SHA256:your-host-key-fingerprint \
dotnet test tests/RoyalTerminal.IntegrationTests/RoyalTerminal.IntegrationTests.csproj -c Release --filter "SshTransportIntegrationTests"

# Optional key-based auth for SSH integration tests
# ROYALTERMINAL_IT_SSH_PRIVATE_KEY can contain either a PEM payload or a private-key file path.

Ncurses Harness (Manual Mouse/Keyboard/Resize Validation)

The ncurses harness fixture lives at:

tests/RoyalTerminal.Tests/Fixtures/NcursesHarness.py

Important: it requires RT_HARNESS_LOG. If this variable is missing, the script exits immediately.

Run it manually:

RT_HARNESS_LOG=/tmp/rt-harness.log \
RT_HARNESS_TIMEOUT_SEC=300 \
TERM=xterm-256color \
python3 tests/RoyalTerminal.Tests/Fixtures/NcursesHarness.py

While it is running:

  • Press keys (logs KEY ...)
  • Click/scroll in the terminal viewport (logs MOUSE ...)
  • Resize window (logs RESIZE ...)
  • Press q to quit (logs EXIT quit)

Watch events from another terminal:

tail -f /tmp/rt-harness.log

If a previous app (for example mc) is suspended, resume/terminate it before running the harness:

jobs
fg %1   # or: kill %1

Current baseline in this repository:

  • Unit tests: 369 passed
  • Integration tests: 47 passed

Performance baseline harness:

dotnet run --project tests/RoyalTerminal.Benchmarks/RoyalTerminal.Benchmarks.csproj -c Release -- --output /tmp/royalterminal-render-baseline.md

API Coverage

libghostty-vt

CategoryStatus
Terminal lifecycle/process/resize/resetImplemented
Render-state lifecycle and dirty trackingImplemented
Row/cell/grapheme iteration via render stateImplemented
Default/palette color APIsImplemented
Mode queries, focus, size-report, formatter, key/mouse helpersImplemented

ghostty-renderer-capi

CategoryStatus
Context/surface lifecycleImplemented
Target validation + render-to-targetImplemented
CPU RGBA fallbackImplemented
Backend descriptors (Metal/Vulkan/D3D11/D3D12/OpenGL/Software)Implemented end-to-end (validation + direct-target render dispatch + CPU fallback)

Notes

  • ReactiveUI, ReactiveUI.Avalonia, and ReactiveUI.SourceGenerators are used by the demo app only.
  • Library packages remain framework/service oriented and avoid app-level ReactiveUI dependencies.

License

MIT

Acknowledgements