- Filter variable completions and hover by script type so only relevant variables appear (e.g. $AllocatedKey only in KeyChange scripts) - Add inline validation warnings for package scripts missing New-Package and for variables used outside their valid script type - Extend debug console to support tools and redistributables, add stop button and elapsed time display - Disable minimap, enable bracket pair colorization and word wrap - Add snippet insertion via Monaco snippet controller with selection replacement support - Add script templates that auto-populate on new script creation - Add Variables dropdown in editor toolbar based on current script type - Change Add Script button to type-selection dropdown, auto-populate script name from type, remove Type field from editor dialog - Move Requires Admin checkbox to toolbar row, move toolbar and editor outside Form to prevent dropdown clicks from closing the modal - Set MaskClosable=false on script editor modals so Monaco autocomplete clicks don't dismiss the dialog
181 lines
4.8 KiB
Text
181 lines
4.8 KiB
Text
@using LANCommander.SDK.PowerShell
|
|
@using LANCommander.SDK.Services
|
|
@using LANCommander.Server.Services.PowerShell
|
|
@using XtermBlazor
|
|
@using LogLevel = Microsoft.Extensions.Logging.LogLevel
|
|
@inject ScriptDebugger ScriptDebugger
|
|
@inject ScriptClient ScriptClient
|
|
@inject GameService GameService
|
|
@inject ToolService ToolService
|
|
@inject RedistributableService RedistributableService
|
|
|
|
<div class="terminal @(Active ? "" : "hidden")">
|
|
@if (Active)
|
|
{
|
|
<div style="display: flex; align-items: center; justify-content: space-between; padding: 4px 8px; background: #1e1e1e; border-bottom: 1px solid #333; color: #ccc; font-size: 12px; font-family: monospace;">
|
|
<span>@FormatElapsed()</span>
|
|
<button @onclick="Stop" title="Stop script" style="background: #c0392b; color: white; border: none; padding: 2px 10px; cursor: pointer; font-size: 12px;">Stop</button>
|
|
</div>
|
|
}
|
|
|
|
<Terminal @ref="_terminal" Id="@Id.ToString()" Options="_options" />
|
|
</div>
|
|
|
|
@code {
|
|
[Parameter] public Guid Id { get; set; }
|
|
[Parameter] public bool Active { get; set; }
|
|
[Parameter] public EventCallback<bool> ActiveChanged { get; set; }
|
|
|
|
private Terminal? _terminal;
|
|
private CancellationTokenSource? _cts;
|
|
private DateTime? _startTime;
|
|
private System.Threading.Timer? _elapsedTimer;
|
|
|
|
private readonly TerminalOptions _options = new()
|
|
{
|
|
CursorBlink = true,
|
|
CursorStyle = CursorStyle.Bar,
|
|
};
|
|
|
|
async Task SetActive(bool active)
|
|
{
|
|
Active = active;
|
|
|
|
if (ActiveChanged.HasDelegate)
|
|
await ActiveChanged.InvokeAsync(Active);
|
|
}
|
|
|
|
private void InitializeDebugger()
|
|
{
|
|
ScriptDebugger.OnBreak = Break;
|
|
ScriptDebugger.OnStart = Start;
|
|
ScriptDebugger.OnEnd = End;
|
|
ScriptDebugger.OnOutput = Output;
|
|
|
|
ScriptClient.Debug = true;
|
|
|
|
_cts?.Dispose();
|
|
_cts = new CancellationTokenSource();
|
|
_startTime = DateTime.UtcNow;
|
|
|
|
_elapsedTimer = new System.Threading.Timer(
|
|
_ => InvokeAsync(StateHasChanged),
|
|
null,
|
|
TimeSpan.Zero,
|
|
TimeSpan.FromSeconds(1));
|
|
}
|
|
|
|
public async Task DebugPackagingScript(Guid gameId)
|
|
{
|
|
InitializeDebugger();
|
|
await GameService.PackageAsync(gameId);
|
|
}
|
|
|
|
public async Task DebugToolPackagingScript(Guid toolId)
|
|
{
|
|
InitializeDebugger();
|
|
await ToolService.PackageAsync(toolId);
|
|
}
|
|
|
|
public async Task DebugRedistributablePackagingScript(Guid redistributableId)
|
|
{
|
|
InitializeDebugger();
|
|
await RedistributableService.PackageAsync(redistributableId);
|
|
}
|
|
|
|
public async Task Start(IScriptDebugContext context)
|
|
{
|
|
if (_terminal is null)
|
|
return;
|
|
|
|
await _terminal.Clear();
|
|
await SetActive(true);
|
|
await InvokeAsync(StateHasChanged);
|
|
|
|
await _terminal.Focus();
|
|
await _terminal.FitAsync();
|
|
}
|
|
|
|
public async Task End(IScriptDebugContext context)
|
|
{
|
|
StopTimer();
|
|
|
|
if (_terminal != null && _startTime.HasValue)
|
|
{
|
|
var elapsed = DateTime.UtcNow - _startTime.Value;
|
|
await _terminal.WriteLine($"\nCompleted in {FormatTimeSpan(elapsed)}", LogLevel.Information);
|
|
}
|
|
|
|
await SetActive(false);
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
public async Task Break(IScriptDebugContext context)
|
|
{
|
|
if (_terminal is null)
|
|
return;
|
|
|
|
while (true)
|
|
{
|
|
var input = await _terminal.ReadLineAsync();
|
|
|
|
if (input.Trim().Equals("exit", StringComparison.OrdinalIgnoreCase))
|
|
break;
|
|
|
|
if (input.StartsWith('$'))
|
|
input = "Write-Host " + input;
|
|
|
|
await context.ExecuteAsync(input);
|
|
}
|
|
}
|
|
|
|
public async Task Output(IScriptDebugContext context, LogLevel level, string message)
|
|
{
|
|
if (_terminal is null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
await _terminal.WriteLine(message, level);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
|
|
}
|
|
}
|
|
|
|
async Task Stop()
|
|
{
|
|
_cts?.Cancel();
|
|
StopTimer();
|
|
|
|
if (_terminal != null)
|
|
await _terminal.WriteLine("\nScript stopped by user.", LogLevel.Warning);
|
|
|
|
await SetActive(false);
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
void StopTimer()
|
|
{
|
|
_elapsedTimer?.Dispose();
|
|
_elapsedTimer = null;
|
|
}
|
|
|
|
string FormatElapsed()
|
|
{
|
|
if (!_startTime.HasValue)
|
|
return "";
|
|
|
|
var elapsed = DateTime.UtcNow - _startTime.Value;
|
|
return FormatTimeSpan(elapsed);
|
|
}
|
|
|
|
static string FormatTimeSpan(TimeSpan ts)
|
|
{
|
|
if (ts.TotalHours >= 1)
|
|
return ts.ToString(@"h\:mm\:ss");
|
|
|
|
return ts.ToString(@"m\:ss");
|
|
}
|
|
}
|