feat(interactive): embedded ConPTY terminal host in UI
Adds a self-contained terminal host: PtyTerminalSession drives a Porta.Pty child directly (custom env) and pumps it through Iciclecreek's XTerm.NET renderer via TerminalControl.Terminal, bypassing LaunchProcess() so a fully-populated environment can be passed. InteractiveTerminalView/ViewModel host it with an order-independent attach/start. Adds Iciclecreek.Avalonia.Terminal 2.0.3. Sets Process="" to suppress the control's auto-launch of a stray shell.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using Iciclecreek.Terminal;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Hosts a <see cref="PtyTerminalSession"/> for an embedded ConPTY terminal. The view attaches
|
||||
/// its <see cref="TerminalControl"/> once loaded via <see cref="AttachControl"/>; <see cref="Start"/>
|
||||
/// can be called before or after attach — whichever happens second triggers the launch.
|
||||
/// </summary>
|
||||
public sealed partial class InteractiveTerminalViewModel : ViewModelBase, IDisposable
|
||||
{
|
||||
private readonly PtyTerminalSession _session = new();
|
||||
private TerminalControl? _control;
|
||||
private TerminalLaunchDescriptor? _pendingDescriptor;
|
||||
|
||||
[ObservableProperty] private bool _isRunning;
|
||||
[ObservableProperty] private bool _hasExited;
|
||||
[ObservableProperty] private int? _exitCode;
|
||||
[ObservableProperty] private string? _startError;
|
||||
|
||||
public InteractiveTerminalViewModel()
|
||||
{
|
||||
_session.ProcessExited += OnSessionProcessExited;
|
||||
}
|
||||
|
||||
/// <summary>Called by the view once its <see cref="TerminalControl"/> has loaded (template applied).</summary>
|
||||
public void AttachControl(TerminalControl control)
|
||||
{
|
||||
_control = control;
|
||||
if (_pendingDescriptor is { } descriptor)
|
||||
{
|
||||
_pendingDescriptor = null;
|
||||
_ = StartCoreAsync(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
public void Start(TerminalLaunchDescriptor descriptor)
|
||||
{
|
||||
if (_control is null)
|
||||
{
|
||||
_pendingDescriptor = descriptor;
|
||||
return;
|
||||
}
|
||||
_ = StartCoreAsync(descriptor);
|
||||
}
|
||||
|
||||
private async Task StartCoreAsync(TerminalLaunchDescriptor descriptor)
|
||||
{
|
||||
if (_control is null) return;
|
||||
try
|
||||
{
|
||||
StartError = null;
|
||||
await _session.StartAsync(descriptor, _control);
|
||||
IsRunning = true;
|
||||
HasExited = false;
|
||||
ExitCode = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IsRunning = false;
|
||||
HasExited = true;
|
||||
StartError = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSessionProcessExited(object? sender, int exitCode)
|
||||
{
|
||||
IsRunning = false;
|
||||
HasExited = true;
|
||||
ExitCode = exitCode;
|
||||
}
|
||||
|
||||
public void Kill() => _session.Kill();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_session.ProcessExited -= OnSessionProcessExited;
|
||||
_session.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user