Files
Servers/MoonlightServers.Frontend/UI/Components/XtermConsole.razor

238 lines
6.7 KiB
Plaintext

@using System.Collections.Concurrent
@using Microsoft.Extensions.Logging
@using MoonCore.Blazor.FlyonUi.Modals
@using MoonCore.Helpers
@using XtermBlazor
@using MoonCore.Blazor.FlyonUi.Components
@inject IJSRuntime JsRuntime
@inject ModalService ModalService
@inject ILogger<XtermConsole> Logger
@implements IAsyncDisposable
<div class="bg-black rounded-lg p-2 relative">
@if (IsInitialized)
{
<Xterm @ref="Terminal"
Addons="Addons"
Options="Options"
Class="h-full w-full"
OnFirstRender="HandleFirstRender"/>
}
<div class="flex flex-row w-full mt-1.5">
<input @bind="CommandInput" @onkeyup="HandleKey" type="text" placeholder="Type here..." class="input grow"/>
<WButton OnClick="_ => SubmitCommand()" CssClasses="btn btn-square btn-primary grow-0 ms-1.5">
<i class="icon-send-horizontal text-lg"></i>
</WButton>
</div>
<div class="absolute top-4 right-4">
<div class="flex flex-col gap-y-1.5">
@if (IsPaused)
{
<button @onclick="TogglePause" class="btn btn-primary btn-square">
<i class="icon-play text-lg"></i>
</button>
}
else
{
<button @onclick="TogglePause" class="btn btn-secondary btn-square">
<i class="icon-pause text-lg"></i>
</button>
}
<button @onclick="OpenFullscreen" class="btn btn-secondary btn-square">
<i class="icon-maximize text-lg"></i>
</button>
</div>
</div>
</div>
@code
{
[Parameter] public Func<Task>? OnAfterInitialized { get; set; }
[Parameter] public Func<Task>? OnFirstRender { get; set; }
[Parameter] public bool ShowActions { get; set; } = true;
[Parameter] public bool ShowInput { get; set; } = false;
[Parameter] public int MaxOutputCacheSize { get; set; } = 250;
[Parameter] public Func<string, Task>? OnCommand { get; set; }
[Parameter] public IList<string> CommandHistory { get; set; } = new ConcurrentList<string>();
public event Func<string, Task>? OnWrite;
private Xterm Terminal;
public HashSet<string> Addons { get; } = ["addon-fit"];
public TerminalOptions Options { get; } = new()
{
CursorBlink = false,
CursorStyle = CursorStyle.Bar,
CursorWidth = 1,
FontFamily = "Space Mono, monospace",
DisableStdin = true,
CursorInactiveStyle = CursorInactiveStyle.None,
Theme =
{
Background = "#000000"
},
};
public ConcurrentList<string> OutputCache { get; private set; } = new();
public ConcurrentList<string> WriteQueue { get; private set; } = new();
private int CommandIndex = -1;
private bool IsReadyToWrite = false;
private bool IsPaused = false;
private bool IsInitialized = false;
private string CommandInput = "";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
return;
try
{
await JsRuntime.InvokeVoidAsync("moonlightServers.loadAddons");
}
catch (Exception e)
{
Logger.LogError("An error occured while initializing addons: {e}", e);
}
IsInitialized = true;
await InvokeAsync(StateHasChanged);
if (OnAfterInitialized != null)
await OnAfterInitialized.Invoke();
}
private async Task HandleFirstRender()
{
try
{
await Terminal.Addon("addon-fit").InvokeVoidAsync("fit");
}
catch (Exception e)
{
Logger.LogError("An error occured while calling addons: {e}", e);
}
IsReadyToWrite = true;
// Write queued content since initialisation started
var queueContent = string.Concat(WriteQueue);
WriteQueue.Clear();
await Terminal.Write(queueContent);
if (OnFirstRender != null)
await OnFirstRender.Invoke();
}
public async Task Write(string content)
{
// We cache messages here as there is the chance that the console isn't ready for input while receiving write tasks
if (IsReadyToWrite && !IsPaused)
await HandleWrite(content);
else
WriteQueue.Add(content);
}
private async Task HandleWrite(string content)
{
// Update output cache and prune it if required
if (OutputCache.Count > MaxOutputCacheSize)
{
for (var i = 0; i < 50; i++)
OutputCache.RemoveAt(i);
}
OutputCache.Add(content);
// Trigger events
if (OnWrite != null)
await OnWrite.Invoke(content);
// Write in own terminal
await Terminal.Write(content);
}
private async Task OpenFullscreen()
{
await ModalService.Launch<FullScreenModal>(parameters => { parameters["Parent"] = this; }, size: "max-w-none");
}
private async Task TogglePause()
{
IsPaused = !IsPaused;
await InvokeAsync(StateHasChanged);
if (IsPaused)
return;
var queueContent = string.Concat(WriteQueue);
WriteQueue.Clear();
await HandleWrite(queueContent);
}
private async Task SubmitCommand()
{
CommandHistory.Add(CommandInput);
if (OnCommand != null)
await OnCommand.Invoke(CommandInput);
CommandIndex = -1;
CommandInput = "";
await InvokeAsync(StateHasChanged);
}
private async Task HandleKey(KeyboardEventArgs keyboard)
{
switch (keyboard.Code)
{
case "Enter":
await SubmitCommand();
break;
case "ArrowUp" or "ArrowDown":
{
var highestIndex = CommandHistory.Count - 1;
if (CommandIndex == -1)
CommandIndex = highestIndex;
else
{
if (keyboard.Code is "ArrowUp")
CommandIndex++;
else if (keyboard.Code is "ArrowDown")
CommandIndex--;
}
if (CommandIndex > highestIndex)
CommandIndex = highestIndex;
if (CommandIndex < 0)
CommandIndex = 0;
if (CommandIndex <= highestIndex || CommandHistory.Count > 0)
CommandInput = CommandHistory[CommandIndex];
await InvokeAsync(StateHasChanged);
break;
}
}
}
public async ValueTask DisposeAsync()
{
await Terminal.DisposeAsync();
}
}