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.
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.
| Package | Responsibility |
|---|---|
RoyalApps.RoyalTerminal.Avalonia.Settings |
Reusable Avalonia settings panel controls and state for session, connection, terminal, appearance, SSH, logging, and highlighting configuration |
RoyalApps.RoyalTerminal.Terminal |
Core terminal contracts (ITerminalEndpoint, ITerminalInputSink, ITerminalSelectionSource, ITerminalModeSource), SSH bootstrap helpers, and screen model |
RoyalApps.RoyalTerminal.Unicode |
Deterministic Unicode data and terminal width helpers |
RoyalApps.RoyalTerminal.Sixel |
Reusable managed sixel decoder and image payload model |
RoyalApps.RoyalTerminal.Terminal.Vt.Managed |
Managed VT processor (BasicVtProcessor) |
RoyalApps.RoyalTerminal.Terminal.Vt.Ghostty |
Native VT processor (GhosttyVtProcessor over official libghostty-vt terminal/render APIs) + GhosttyVtProcessorProvider |
RoyalApps.RoyalTerminal.Terminal.Vt.Default |
Preference-based VT processor factory (VtProcessorPreference) |
RoyalApps.RoyalTerminal.Terminal.Pty.Unix |
Unix PTY implementation (forkpty) |
RoyalApps.RoyalTerminal.Terminal.Pty.Windows |
Windows PTY implementation (ConPTY) |
RoyalApps.RoyalTerminal.Terminal.Pty.Platform |
Platform PTY factory (DefaultPtyFactory) |
RoyalApps.RoyalTerminal.Terminal.Transport.Pty |
PTY transport provider and wrapper (PtyTerminalTransportProvider) |
RoyalApps.RoyalTerminal.Terminal.Transport.Pipe |
Process pipe transport provider (PipeTerminalTransportProvider) |
RoyalApps.RoyalTerminal.Terminal.Transport.Raw |
Raw TCP terminal transport provider |
RoyalApps.RoyalTerminal.Terminal.Transport.Telnet |
Telnet terminal transport provider with option negotiation |
RoyalApps.RoyalTerminal.Terminal.Transport.Serial |
Serial line terminal transport provider |
RoyalApps.RoyalTerminal.Terminal.Transport.Ssh.Abstractions |
SSH host-key validation contracts |
RoyalApps.RoyalTerminal.Terminal.Transport.Ssh.SshNet |
SSH transport provider (SshNetTerminalTransportProvider) |
RoyalApps.RoyalTerminal.Terminal.Transport.Ssh.SshNet.Agent |
Optional SSH agent auth contributor for SSH.NET |
RoyalApps.RoyalTerminal.Terminal.Services.Contracts |
Terminal session service contracts |
RoyalApps.RoyalTerminal.Terminal.Services |
Terminal session service implementations |
RoyalApps.RoyalTerminal.Rendering.Text |
Reusable text shaping/fallback subsystem (HarfBuzzTextShaper, TerminalFontResolver) |
RoyalApps.RoyalTerminal.Shaders |
Dependency-free shader source models and compatibility translation for Skia Runtime Effect terminal shaders |
RoyalApps.RoyalTerminal.Rendering.Skia |
CPU cell renderer core (SkiaTerminalRenderer, GlyphCache) with HarfBuzz shaping, fallback font resolution, and framebuffer shader post-processing |
RoyalApps.RoyalTerminal.Rendering.Contracts |
Backend-agnostic render contracts (RenderTargetDescriptor, capabilities) |
RoyalApps.RoyalTerminal.Rendering.Interop.Ghostty |
Managed wrapper for ghostty-renderer-capi |
RoyalApps.RoyalTerminal.Rendering.Interop.Ghostty.Skia |
Skia bridge (SkiaInteropRenderer) with CPU fallback |
RoyalApps.RoyalTerminal.Avalonia.Rendering.GhosttyInterop |
Avalonia render-target acquisition and texture interop draw handler |
- Core/native VT split:
RoyalApps.RoyalTerminal.Avalonia: backend-neutral control and services.RoyalApps.RoyalTerminal.Terminal.Vt.Ghostty: official native VT integration over upstreamlibghostty-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 POSIXexportcommand composition across SSH backends. - Pluggable SSH secret persistence via
ISshSecretStore+ISshSecretProtectorwith cross-platform secure defaults (SshSecretProtectionFactory). - Session profiles + persistent settings model via
TerminalSessionProfile*contracts,TerminalSessionProfileSerializer, andJsonFileTerminalSessionProfileStore. - 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)
- CPU cell rendering path (
- Official native VT engine via
libghostty-vtterminal/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.ContractsandTerminal.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) showingAvalonia.Win32.Interoperability, PerMonitorV2 DPI, Win32 focus forwarding, andTerminalControl.Paddingfor embedded margins - macOS SwiftUI native tabbed demo (
samples/RoyalTerminal.MacNativeTabbed) that hosts GhosttyKit directly as a separate native sample, outside the managedRoyalTerminal.GhosttySharpsurface - VT/PTy control catalog CLI (
samples/RoyalTerminal.ControlCatalog) with managed/Ghostty VT probes, ncurses/TUI parity scenarios, and rich visual rendering galleries
- Avalonia demo (
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:
| Mode | Behavior |
|---|---|
Static |
Caches matched cells for unchanged row text, rule revision, and theme state. This is the default for normal terminal use. |
Realtime |
Recomputes matching rows whenever they are rendered. |
Disabled |
Keeps 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.
TerminalControl now runs through a transport abstraction, not a PTY-only runtime:
TerminalControl.StartSessionAsync(ITerminalTransportOptions)selects a transport.ITerminalTransportFactoryresolves a provider byTransportId.- The chosen
ITerminalTransport(pty,pipe, orssh) owns I/O and lifecycle. TerminalSessionServiceremains the coordination point for endpoint/input/selection/mode contracts.
Supported transport option models:
| Transport | Options Type | Notes |
|---|---|---|
| PTY | PtyTransportOptions |
Interactive local shell semantics (ConPTY/forkpty) |
| Pipe | PipeTransportOptions |
Non-PTY process streams, useful for command/log scenarios |
| SSH | SshTransportOptions |
Remote terminal sessions with optional PTY request and host-key checks (OpenSSH known_hosts and optional SHA-256 pinning) |
| Raw TCP | RawTcpTransportOptions |
Unframed TCP byte stream sessions |
| Telnet | TelnetTransportOptions |
Telnet remote sessions with option negotiation handling |
| Serial | SerialTransportOptions |
Direct serial line sessions (baud/parity/stop bits/handshake) |
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
ITerminalCaptureSessionFormatandTerminalCaptureSessionFormatRegistry - recommended extensions:
.rtcap.jsonand.cast
- native RoyalTerminal JSON via
- 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
The demo toolbar includes:
Start Capture/Stop Capture- capture format selector (
RoyalTerminal JSONorAsciicast 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 explicitApplyandSave)
When replay is active, the replay timeline bar is shown with play/pause, stop, slider seek, elapsed/total display, and source label.
RoyalTerminal.Avalonia.Capture.TerminalCaptureRuntime- runtime orchestration for capture + replay against a
TerminalControl
- runtime orchestration for capture + replay against a
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();| Mode | Control | Package Set | VT Engine | Renderer | PTY | Platform | Best For |
|---|---|---|---|---|---|---|---|
| Native VT | TerminalControl |
RoyalApps.RoyalTerminal.Avalonia + RoyalApps.RoyalTerminal.Terminal.Vt.Ghostty + native assets |
official libghostty-vt terminal/render-state APIs |
Skia cell renderer | Unix PTY / ConPTY | macOS/Linux/Windows | Cross-platform native VT parser on the upstream Ghostty C API |
| Managed VT | TerminalControl |
RoyalApps.RoyalTerminal.Avalonia |
BasicVtProcessor (C#) |
Skia cell renderer | Unix PTY / ConPTY | macOS/Linux/Windows | Explicit managed VT path |
| Rendered (Auto VT) | TerminalControl |
RoyalApps.RoyalTerminal.Avalonia (+ optional native VT provider packages) |
Auto (libghostty-vt when available, otherwise BasicVtProcessor) |
Skia cell renderer | Unix PTY / ConPTY | macOS/Linux/Windows | Default backend-neutral mode |
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 Mode | Fallback Chain (next supported mode in resolver order) |
|---|---|
Native VT |
Managed VT -> Rendered (Auto VT) |
Managed VT |
Rendered (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.
The Avalonia demo uses a glyph + color marker in each tab header:
| Mode | Marker | Color |
|---|---|---|
Native VT |
■ |
Green (#6AB04C) |
Managed VT |
▲ |
Teal (#4EC9B0) |
Rendered (Auto VT) |
▼ |
Olive (#6A9955) |
| Demo Mode Label | VtProcessorPreference |
Behavior |
|---|---|---|
| Native VT | Native |
Requires the official native Ghostty VT provider (libghostty-vt), throws when unavailable |
| Managed VT | Managed |
Forces BasicVtProcessor for deterministic pure-managed behavior |
| Rendered (Auto VT) | Auto |
Uses official native VT when available and falls back to managed VT otherwise |
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
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.
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);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.
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)));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, orNUL.
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.
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",
});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)));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);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);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);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.
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();using RoyalTerminal.Avalonia.Controls;
using RoyalTerminal.Terminal;
var terminal = new TerminalControl
{
VtProcessorPreference = VtProcessorPreference.Managed,
};
terminal.StartPty();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:
ITerminalEndpointonly (byte-stream integration): fallback key/text/mouse/focus VT sequences are written throughSendText(...).ITerminalEndpoint+ITerminalInputSink(full-fidelity event integration): structured key/text/pointer events are passed directly to your backend.
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 DECSET1000/1002/1003with SGR1006). - Focus in/out (
CSI I/CSI O) is sent throughSendText(...)when focus mode DECSET1004is enabled. WriteOutput(...)is safe from background callback threads;TerminalControlmarshals 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 viaITerminalModeSource(and optionallyITerminalFocusEventModeSource).
If you implement ITerminalInputSink, fallback byte-sequence encoding is bypassed and your backend receives structured TerminalKeyEvent / TerminalPointerEvent.
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);
}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);TerminalControl.StartPty(...)is still supported and remains the compatibility API.TerminalControl.PtyandTerminalControl.HasPtystill work for PTY sessions.- VT response writeback now goes through session input routing, so no PTY-specific handling is required in callers.
- New code should use
StartSessionAsync(...),StartPipeAsync(...), orStartSshAsync(...). - Query active session state with:
TerminalControl.HasActiveSessionTerminalControl.ActiveTransportId
- Core control rename:
GhosttyTerminalControl->TerminalControlGhosttyTerminalPresenter->TerminalPresenter
- VT selection moved from
UseNativeVtProcessortoVtProcessorPreference. - Legacy surface-coupled
ITerminalSurfacecontract was removed.
# Core Avalonia control + PTY + managed VT defaults
dotnet add package RoyalApps.RoyalTerminal.Avalonia# 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-arm64RoyalApps.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.
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.AgentIf 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.TerminalUse 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.GhosttyInteropFor source builds or internal feeds, create the same package set with bash scripts/pack-nuget.sh --configuration Release --output artifacts --version <version>.
NuGet publishing is handled by the Release GitHub Actions workflow in
.github/workflows/release.yml. The workflow
does not publish from ordinary branch pushes. It runs only when a v* tag is
pushed, builds native artifacts for all supported platforms, packs every
packable project, pushes packages to NuGet.org, and then creates the GitHub
release. The repository must have the NUGET_API_KEY secret configured.
Release tags must use one of these forms:
vMAJOR.MINOR.PATCH
vMAJOR.MINOR.PATCH-prerelease
The package version comes from the tag without the leading v. For example,
v0.4.0 publishes 0.4.0, and v0.4.0-preview.0 publishes
0.4.0-preview.0. Prerelease tags are marked as GitHub prereleases because the
workflow treats any tag containing - as prerelease.
Use this path for public stable packages.
- Merge the release-ready PRs to
main. - Make sure the local
mainmatchesorigin/main. - Optionally keep
Directory.Build.propsaligned with the stable version by settingVersionPrefixtoMAJOR.MINOR.PATCHandVersionSuffixto empty. The tag is still the source of truth for published package versions. - Create and push a stable tag from
main.
git switch main
git pull --ff-only origin main
# Optional repository metadata alignment before tagging:
# <VersionPrefix>0.4.0</VersionPrefix>
# <VersionSuffix></VersionSuffix>
git tag -a v0.4.0 -m "v0.4.0"
git push origin v0.4.0Monitor the workflow:
gh run list --workflow release.yml --limit 5
gh run watchUse this path when a preview package should be published before a branch is
merged, or when a preview should be cut directly from main. A branch push
alone will not publish packages; the preview release is started by tagging the
exact commit to release.
- Checkout the branch or
maincommit that contains the preview changes. - Merge or rebase latest
origin/mainif the preview should include current mainline fixes. - Optionally set
Directory.Build.propsto the preview version for local/default metadata. This is recommended for preview branches. For0.4.0-preview.0:
<VersionPrefix>0.4.0</VersionPrefix>
<VersionSuffix>preview.0</VersionSuffix>- Commit and push the metadata change if one was made.
- Tag the selected HEAD with the matching prerelease tag and push the tag.
git switch feature/my-preview
git fetch origin
git merge origin/main
git add Directory.Build.props
git commit -m "Bump version to 0.4.0 preview"
git push
git tag -a v0.4.0-preview.0 -m "v0.4.0-preview.0"
git push origin v0.4.0-preview.0For a preview from main, replace the branch checkout/merge commands with:
git switch main
git pull --ff-only origin mainThe release workflow checks out the tag ref, so the tag can point at either a
branch commit or a main commit. NuGet packages and the GitHub prerelease will
use the tag version.
- Do not reuse a version that has already been published to NuGet.org.
- Do not move or retag a version after packages have been published.
- Stable releases should normally be tagged from
main. - Preview releases may be tagged from a branch or
main, as long as the tag points at the exact commit intended for the preview. - If a release job fails after some packages are already published, rerun the same workflow/tag after confirming the existing packages are the same version.
This repository includes a Codex skill:
skills/royalterminal-development
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"test -f "${CODEX_HOME:-$HOME/.codex}/skills/royalterminal-development/SKILL.md" && echo "skill installed"- Skill instructions:
skills/royalterminal-development/SKILL.md - Granular references:
skills/royalterminal-development/references/
| Capability | Native VT (TerminalControl) |
Managed VT (TerminalControl) |
Rendered (TerminalControl, Auto VT) |
|---|---|---|---|
| Package entry point | RoyalApps.RoyalTerminal.Avalonia + Terminal.Vt.Ghostty |
RoyalApps.RoyalTerminal.Avalonia |
RoyalApps.RoyalTerminal.Avalonia |
| Platform availability | macOS/Linux/Windows | macOS/Linux/Windows | macOS/Linux/Windows |
| VT engine | official libghostty-vt |
BasicVtProcessor |
auto-selects native VT when available, otherwise managed VT |
| Renderer path | Skia cell renderer | Skia cell renderer | Skia cell renderer |
Requires libghostty-vt |
Yes | No | Optional |
| Full Avalonia overlay support | Yes | Yes | Yes |
| Cross-platform mode | Yes | Yes | Yes |
| Demo fallback when unavailable | Routed to Managed VT then Rendered |
Routed to Rendered |
Always supported |
RoyalTerminal.Rendering.Contracts defines the backend-neutral model:
RenderBackendKind:Software,Metal,Vulkan,D3D11,D3D12,OpenGLRenderTargetDescriptor: native handle carrier for one render target submissionRenderBackendCapabilities: supported features/sample counts/pixel formatsRenderFeatureFlags:ExternalTextureTargets,ExternalFramebufferTargets,CpuRgbaFallback,ExplicitFrameLifecycle, etc.
RoyalTerminal.Rendering.Interop.Ghostty.Skia (SkiaInteropRenderer) behavior:
- Validate descriptor.
- Attempt direct interop only when surface capabilities advertise the target kind (
ExternalTextureTargetsorExternalFramebufferTargets). - Fall back to CPU RGBA path when direct interop is unavailable or fails and fallback is enabled.
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 testlibghostty-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=noneFor 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=falseCI 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.
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.
| Package | Implementation |
|---|---|
RoyalApps.RoyalTerminal.Terminal.Pty.Unix |
UnixPty (forkpty, TIOCSWINSZ) |
RoyalApps.RoyalTerminal.Terminal.Pty.Windows |
WindowsPty (ConPTY) |
RoyalApps.RoyalTerminal.Terminal.Pty.Platform |
DefaultPtyFactory selector |
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:
- Explicit env vars above
runtimes/<rid>/native/next to app base directoryruntimes/<rid>/native/next to assembly directory- Default OS loader paths
| Platform | Files |
|---|---|
| macOS | libghostty-vt.dylib, libghostty-renderer-capi.dylib |
| Linux | libghostty-vt.so, libghostty-renderer-capi.so |
| Windows | ghostty-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.
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
- .NET 10 SDK
- Zig 0.15.2+
- Ghostty submodule:
git submodule update --init --recursiveWindows-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.ps1uses a repository-local Zig global cache at.zig-global-cacheto avoid stale user-level cache state.
# macOS/Linux
bash scripts/build-native.sh --release
# Windows
pwsh scripts/build-native.ps1 -Release
# Managed build
dotnet build RoyalTerminal.sln -c Releasedotnet run --project samples/RoyalTerminal.Demodotnet run --project samples/RoyalTerminal.ControlCatalogOptional demo toggles:
ROYALTERMINAL_DISABLE_TEXT_SHAPING=1 \
ROYALTERMINAL_ENABLE_RENDER_DIAGNOSTICS=1 \
dotnet run --project samples/RoyalTerminal.Demoswift run --package-path samples/RoyalTerminal.MacNativeTabbed# 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.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.pyWhile it is running:
- Press keys (logs
KEY ...) - Click/scroll in the terminal viewport (logs
MOUSE ...) - Resize window (logs
RESIZE ...) - Press
qto quit (logsEXIT quit)
Watch events from another terminal:
tail -f /tmp/rt-harness.logIf a previous app (for example mc) is suspended, resume/terminate it before running the harness:
jobs
fg %1 # or: kill %1Current 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| Category | Status |
|---|---|
| Terminal lifecycle/process/resize/reset | Implemented |
| Render-state lifecycle and dirty tracking | Implemented |
| Row/cell/grapheme iteration via render state | Implemented |
| Default/palette color APIs | Implemented |
| Mode queries, focus, size-report, formatter, key/mouse helpers | Implemented |
| Category | Status |
|---|---|
| Context/surface lifecycle | Implemented |
| Target validation + render-to-target | Implemented |
| CPU RGBA fallback | Implemented |
| Backend descriptors (Metal/Vulkan/D3D11/D3D12/OpenGL/Software) | Implemented end-to-end (validation + direct-target render dispatch + CPU fallback) |
ReactiveUI,ReactiveUI.Avalonia, andReactiveUI.SourceGeneratorsare used by the demo app only.- Library packages remain framework/service oriented and avoid app-level ReactiveUI dependencies.