From 58b6261264164d87af28fa380ce8eeebe20d9452 Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Sat, 9 Aug 2025 23:52:41 +1000 Subject: [PATCH] display run logs in terminal --- .../Features/Run/RunService.cs | 13 ++- .../VsPersistence/SharpIdeModels.cs | 6 +- .../Components/RunOutputDisplay.razor | 106 ++++++++++++++++++ .../Components/RunPanel.razor | 9 +- 4 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 src/SharpIDE.Photino/Components/RunOutputDisplay.razor diff --git a/src/SharpIDE.Application/Features/Run/RunService.cs b/src/SharpIDE.Application/Features/Run/RunService.cs index 947a5e5..d32acd7 100644 --- a/src/SharpIDE.Application/Features/Run/RunService.cs +++ b/src/SharpIDE.Application/Features/Run/RunService.cs @@ -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(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) { diff --git a/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/SharpIdeModels.cs b/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/SharpIdeModels.cs index 84a92a9..58d87c5 100644 --- a/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/SharpIdeModels.cs +++ b/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/SharpIdeModels.cs @@ -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? RunningOutputChannel { get; set; } + public event Func ProjectStartedRunning = () => Task.CompletedTask; + public void InvokeProjectStartedRunning() => ProjectStartedRunning?.Invoke(); } diff --git a/src/SharpIDE.Photino/Components/RunOutputDisplay.razor b/src/SharpIDE.Photino/Components/RunOutputDisplay.razor new file mode 100644 index 0000000..42d59fd --- /dev/null +++ b/src/SharpIDE.Photino/Components/RunOutputDisplay.razor @@ -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 + + +
+ +
+ +@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 _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); + } + }); + } +} diff --git a/src/SharpIDE.Photino/Components/RunPanel.razor b/src/SharpIDE.Photino/Components/RunPanel.razor index 600df72..5d9d48f 100644 --- a/src/SharpIDE.Photino/Components/RunPanel.razor +++ b/src/SharpIDE.Photino/Components/RunPanel.razor @@ -11,14 +11,17 @@ .lowercase-tab-header { text-transform: none; } + .panels-full-height { + height: 100%; + } @* Run *@ - + @foreach (var tab in OpenTabs) { - - Tab content + + }