display run logs in terminal

This commit is contained in:
Matt Parker
2025-08-09 23:52:41 +10:00
parent ff23f39591
commit 58b6261264
4 changed files with 128 additions and 6 deletions

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System.Threading.Channels;
using Ardalis.GuardClauses;
using AsyncReadProcess;
using SharpIDE.Application.Features.Events;
@@ -31,21 +32,28 @@ public class RunService
RedirectStandardError = true
};
var process = new AsyncReadProcess.Process2
var process = new Process2
{
StartInfo = processStartInfo
};
process.Start();
project.RunningOutputChannel = Channel.CreateUnbounded<string>(new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = false,
});
var logsDrained = new TaskCompletionSource();
_ = Task.Run(async () =>
{
await foreach(var log in process.CombinedOutputChannel.Reader.ReadAllAsync())
{
var logString = System.Text.Encoding.UTF8.GetString(log, 0, log.Length);
Console.Write(logString);
//Console.Write(logString);
await project.RunningOutputChannel.Writer.WriteAsync(logString).ConfigureAwait(false);
}
project.RunningOutputChannel.Writer.Complete();
logsDrained.TrySetResult();
});
@@ -53,6 +61,7 @@ public class RunService
project.OpenInRunPanel = true;
GlobalEvents.InvokeProjectsRunningChanged();
GlobalEvents.InvokeStartedRunningProject();
project.InvokeProjectStartedRunning();
await process.WaitForExitAsync().WaitAsync(project.RunningCancellationTokenSource.Token).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
if (project.RunningCancellationTokenSource.IsCancellationRequested)
{

View File

@@ -1,4 +1,5 @@
using Microsoft.Build.Evaluation;
using System.Threading.Channels;
using Microsoft.Build.Evaluation;
namespace SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
@@ -37,4 +38,7 @@ public class SharpIdeProjectModel : ISharpIdeNode
public bool IsRunnable => MsBuildEvaluationProject.Xml.Sdk is "Microsoft.NET.Sdk.BlazorWebAssembly" || MsBuildEvaluationProject.GetPropertyValue("OutputType") is "Exe" or "WinExe";
public bool OpenInRunPanel { get; set; }
public Channel<string>? RunningOutputChannel { get; set; }
public event Func<Task> ProjectStartedRunning = () => Task.CompletedTask;
public void InvokeProjectStartedRunning() => ProjectStartedRunning?.Invoke();
}

View File

@@ -0,0 +1,106 @@
@using Ardalis.GuardClauses
@using SharpIDE.Application.Features.Build
@using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence
@using XtermBlazor
@inject BuildService BuildService
@implements IDisposable
<style>
.xterm-underline-5.xterm-underline-5 { /* Specificity hack lol */
text-decoration: dotted underline;
}
</style>
<div style="width: 100%; height: 100%; overflow: visible">
<Xterm @ref="@_terminalRef" Options="@_options" Style="height: calc(100% - 1px)" Addons="@_addons" OnFirstRender="@OnFirstRender"/>
</div>
@code {
[Parameter, EditorRequired]
public SharpIdeProjectModel Project { get; set; } = null!;
private Xterm _terminalRef;
private readonly TerminalOptions _options = new TerminalOptions
{
CursorBlink = true,
CursorStyle = CursorStyle.Bar,
Columns = 140,
FontFamily = "Cascadia Code",
FontWeightBold = "400",
Theme =
{
BrightGreen = "#98c379",
BrightRed = "#e06c75",
Foreground = "#dcdfe4",
Background = "#282c34",
},
};
private HashSet<string> _addons = ["addon-fit"];
protected override async Task OnInitializedAsync()
{
Project.ProjectStartedRunning += OnProjectStartedRunning;
// This event may/will be raised before the component is initialized, so we call OnProjectStartedRunning directly, for the first render.
await OnProjectStartedRunning();
}
private async Task OnProjectStartedRunning()
{
Guard.Against.Null(Project);
Guard.Against.Null(Project.RunningOutputChannel, nameof(Project.RunningOutputChannel));
await ClearPreviousOutput();
_ = Task.Run(async () =>
{
try
{
await foreach (var log in Project.RunningOutputChannel.Reader.ReadAllAsync())
{
await _terminalRef.Write(log);
}
}
catch (Exception e)
{
await DispatchExceptionAsync(e);
}
});
}
private async Task ClearPreviousOutput()
{
if (_terminalRef is not null)
{
await _terminalRef.Clear();
await InvokeAsync(StateHasChanged);
}
}
public void Dispose() => Project.ProjectStartedRunning -= OnProjectStartedRunning;
private async Task OnFirstRender()
{
await _terminalRef.Addon("addon-fit").InvokeVoidAsync("fit");
_ = Task.Run(async () =>
{
try
{
while (true)
{
await Task.Delay(500).ConfigureAwait(false);
await InvokeAsync(async () =>
{
await _terminalRef.Addon("addon-fit").InvokeVoidAsync("fit");
});
}
}
catch (Exception e)
{
await DispatchExceptionAsync(e);
}
});
}
}

View File

@@ -11,14 +11,17 @@
.lowercase-tab-header {
text-transform: none;
}
.panels-full-height {
height: 100%;
}
</style>
@* <MudText>Run</MudText> *@
<MudTabs KeepPanelsAlive="true" TabPanelClass="lowercase-tab-header">
<MudTabs Style="height: 100%" KeepPanelsAlive="true" PanelClass="panels-full-height" TabPanelClass="lowercase-tab-header">
<ChildContent>
@foreach (var tab in OpenTabs)
{
<MudTabPanel ID="@tab" Text="@tab.Name">
Tab content
<MudTabPanel Style="height: 100%" ID="@tab" Text="@tab.Name">
<RunOutputDisplay Project="@tab"/>
</MudTabPanel>
}
</ChildContent>