The custom Porta.Pty bypass (own read loop, key tunneling, resize sync) rendered wrong, lagged, and dropped input. The spike proved TerminalControl.LaunchProcess() renders correctly and stays responsive, so hand pty/input/render/resize/focus back to the library. PtyTerminalSession shrinks to a thin wrapper: apply descriptor.Env process-wide (Porta.Pty inherits the process env; no per-launch env seam), set Process/Args/StartingDirectory, LaunchProcess(). Process="" still suppresses the control's auto-launch so exactly one process starts.
74 lines
2.6 KiB
C#
74 lines
2.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Iciclecreek.Terminal;
|
|
|
|
namespace ClaudeDo.Ui.Services;
|
|
|
|
/// <summary>
|
|
/// Thin wrapper around <see cref="TerminalControl.LaunchProcess()"/> — the library owns the
|
|
/// Porta.Pty spawn, keyboard input, rendering, resize, and focus end to end (see
|
|
/// <c>spikes/ConPtyTerminal/MainWindow.axaml.cs</c>, which proved this renders correctly and
|
|
/// stays responsive). We only:
|
|
/// 1) apply <see cref="TerminalLaunchDescriptor.Env"/> onto the current process environment
|
|
/// before launching — Porta.Pty inherits the calling process's environment and there is no
|
|
/// per-launch env seam on <see cref="TerminalControl"/>/<see cref="TerminalControl.LaunchProcess()"/> —
|
|
/// 2) relay the control's own <see cref="TerminalControl.ProcessExited"/> event and
|
|
/// <see cref="TerminalControl.Kill()"/> method.
|
|
/// </summary>
|
|
public sealed class PtyTerminalSession : IDisposable
|
|
{
|
|
private TerminalControl? _control;
|
|
private bool _disposed;
|
|
|
|
public bool IsRunning { get; private set; }
|
|
public int? ExitCode { get; private set; }
|
|
|
|
/// <summary>Raised when the child process exits, on the UI thread, with its exit code.</summary>
|
|
public event EventHandler<int>? ProcessExited;
|
|
|
|
/// <summary>
|
|
/// Applies <paramref name="descriptor"/>'s env vars to the current process, then drives
|
|
/// <paramref name="control"/> to launch it via <see cref="TerminalControl.LaunchProcess()"/>.
|
|
/// </summary>
|
|
public async Task StartAsync(TerminalLaunchDescriptor descriptor, TerminalControl control, CancellationToken ct = default)
|
|
{
|
|
if (_control is not null) throw new InvalidOperationException("Session already started.");
|
|
_control = control;
|
|
control.ProcessExited += OnControlProcessExited;
|
|
|
|
foreach (var (key, value) in descriptor.Env)
|
|
Environment.SetEnvironmentVariable(key, value);
|
|
|
|
control.Process = descriptor.Exe;
|
|
control.Args = new List<string>(descriptor.Args);
|
|
control.StartingDirectory = descriptor.Cwd;
|
|
|
|
await control.LaunchProcess();
|
|
IsRunning = true;
|
|
}
|
|
|
|
private void OnControlProcessExited(object? sender, ProcessExitedEventArgs e)
|
|
{
|
|
IsRunning = false;
|
|
ExitCode = e.ExitCode;
|
|
ProcessExited?.Invoke(this, e.ExitCode);
|
|
}
|
|
|
|
public void Kill()
|
|
{
|
|
try { _control?.Kill(); }
|
|
catch (Exception) { /* already exited */ }
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
|
|
if (_control is not null)
|
|
_control.ProcessExited -= OnControlProcessExited;
|
|
}
|
|
}
|