Exclr8.Terminal.Pty
1.0.4
dotnet add package Exclr8.Terminal.Pty --version 1.0.4
NuGet\Install-Package Exclr8.Terminal.Pty -Version 1.0.4
<PackageReference Include="Exclr8.Terminal.Pty" Version="1.0.4" />
<PackageVersion Include="Exclr8.Terminal.Pty" Version="1.0.4" />
<PackageReference Include="Exclr8.Terminal.Pty" />
paket add Exclr8.Terminal.Pty --version 1.0.4
#r "nuget: Exclr8.Terminal.Pty, 1.0.4"
#:package Exclr8.Terminal.Pty@1.0.4
#addin nuget:?package=Exclr8.Terminal.Pty&version=1.0.4
#tool nuget:?package=Exclr8.Terminal.Pty&version=1.0.4
Exclr8.Terminal.Pty
Spawn-and-go PTY adapter for Exclr8.Terminal.
Exclr8.Terminal is intentionally PTY-agnostic — that's what makes
SSH channels, replay streams, and custom transports work. But hosts
that want a local shell don't need to reimplement the same ~500 lines
of glue (read loop, writer-lock-serialised input, resize forwarding,
exit-code propagation, dispose ordering) every time. This package
ships that glue.
dotnet add package Exclr8.Terminal.Pty
Pulls Exclr8.Terminal and Porta.Pty
transitively. Porta.Pty handles the cross-platform PTY layer
(macOS / Linux pty + Windows ConPTY) under the hood.
Quick start
using Exclr8.Terminal;
using Exclr8.Terminal.Pty;
using Porta.Pty;
var terminal = new TerminalControl();
// ... add the control to your view tree somehow ...
var adapter = new PtyTerminalAdapter(terminal);
// Wait for the first Resized so the shell starts at the actual cell
// grid (not 80x24 default → SIGWINCH on first paint).
terminal.Resized += async (_, size) =>
{
await adapter.StartAsync(new PtyOptions
{
Name = "xterm-256color",
App = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/zsh",
Cwd = Environment.GetEnvironmentVariable("HOME") ?? "/",
Cols = size.Cols,
Rows = size.Rows,
CommandLine = Array.Empty<string>(),
Environment = new Dictionary<string, string>
{
["TERM"] = "xterm-256color",
["COLORTERM"] = "truecolor",
},
});
};
adapter.ProcessExited += (_, e) => Console.WriteLine($"shell exited code={e.ExitCode}");
// On window close:
await adapter.DisposeAsync();
That's it. The adapter:
- Calls
terminal.PrepareForNewSession()before spawning so a recycled control doesn't carry SGR / scrollback state from the previous session. - Sets
terminal.RootProcessId = pty.Pidso the process-tree watcher hooks the spawned subtree automatically. - Subscribes to
terminal.Inputand forwards through a writer lock (mandatory on Windows — ConPTY corrupts the input pipe under concurrent writes). - Subscribes to
terminal.Resizedand forwards topty.Resize. - Runs a 16 KB-buffered async read loop, marshalling bytes onto the
Avalonia UI thread before calling
terminal.Write. - Surfaces
pty.ProcessExitedso you can drive UI on shell death. - Tears down everything in
DisposeAsync— read loop cancellation, await, handler unsubscribe, connection dispose. Idempotent.
API surface
| Member | Purpose |
|---|---|
new PtyTerminalAdapter(terminal) |
Bind an adapter to an existing TerminalControl. Spawns nothing yet. |
StartAsync(PtyOptions, ct) |
Spawn a Porta.Pty connection and wire it up. Idempotent re-call disposes the previous one first. |
Pid |
OS pid of the spawned shell, or null when not live. |
IsAlive |
true between successful StartAsync and read loop completion / dispose. |
ProcessExited |
EventHandler<PtyExitedEventArgs> — fires when the shell exits. May fire on a worker thread; marshal before touching UI. |
DisposeAsync() |
Tear down. Idempotent, safe from any thread. |
Beyond this, use the underlying terminal for everything else —
selection, search, copy/paste, theming, fonts, focus, recovery
primitives. The adapter only owns the PTY plumbing.
Threading
StartAsyncshould be called on the UI thread (it touchesTerminalControlproperties and subscribes events).- The read loop runs on a
Task.Runbackground task; it marshals bytes ontoDispatcher.UIThreadbeforeterminal.Write. ProcessExitedfires on whatever thread Porta.Pty surfaces the exit on (typically a wait worker thread). Marshal it before touching UI state.
When not to use this package
Skip it and wire terminal.Write / Input / Resized directly when:
- Your bytes come from an SSH channel (
SSH.NET,libssh). - You're driving a recorded session (asciicast / ttyrec / custom).
- The "PTY" is in-memory (replay tests, agent-driven sessions).
- You want a different PTY library than Porta.Pty.
- You're building a multiplexed transport (one PTY → N terminal panes).
The core Exclr8.Terminal covers all those cases without bringing
Porta.Pty along for the ride.
License
MIT (matches the core). Porta.Pty is MIT (Microsoft OSS); same
licence, no compatibility friction. See the root LICENSE
for the full text and the upstream Avalonia / xterm.js attributions
inherited from the core package.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Exclr8.Terminal (>= 1.0.7)
- Porta.Pty (>= 1.0.7)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.