Add command interruption support with Ctrl+C and cancellation tokens

This commit is contained in:
Stone_Red
2026-06-11 21:27:06 +02:00
parent cda4dcf097
commit 0968d64fa3
8 changed files with 55 additions and 16 deletions
+2
View File
@@ -34,6 +34,8 @@ public class Kernel : Sys.Kernel
Sys.Mouse.MouseManager.Initialize(); Sys.Mouse.MouseManager.Initialize();
Sys.Mouse.MouseManager.SetScreenSize((int)canvas.Mode.Width, (int)canvas.Mode.Height); Sys.Mouse.MouseManager.SetScreenSize((int)canvas.Mode.Width, (int)canvas.Mode.Height);
Sys.Keyboard.KeyboardManager.Initialize(); Sys.Keyboard.KeyboardManager.Initialize();
ProcessManager.SpawnProcess<CliProcess>();
} }
protected override void Run() protected override void Run()
+17 -2
View File
@@ -11,6 +11,8 @@ internal class CliProcess() : Process("Cli")
private bool commandRunning = false; private bool commandRunning = false;
private CancellationTokenSource? commandCancellationTokenSource = null;
internal override void Start(string[] args) internal override void Start(string[] args)
{ {
Console.Clear(); Console.Clear();
@@ -23,10 +25,20 @@ internal class CliProcess() : Process("Cli")
internal override async void Tick() internal override async void Tick()
{ {
while (Console.KeyAvailable && !commandRunning) while (Console.KeyAvailable)
{ {
ConsoleKeyInfo key = Console.ReadKey(intercept: true); ConsoleKeyInfo key = Console.ReadKey(intercept: true);
if (commandRunning)
{
if (key.Key == ConsoleKey.C && key.Modifiers.HasFlag(ConsoleModifiers.Control) && commandCancellationTokenSource is not null)
{
await commandCancellationTokenSource.CancelAsync();
}
continue;
}
switch (key.Key) switch (key.Key)
{ {
case ConsoleKey.Enter: case ConsoleKey.Enter:
@@ -87,9 +99,10 @@ internal class CliProcess() : Process("Cli")
return; return;
} }
commandCancellationTokenSource = new CancellationTokenSource();
commandRunning = true; commandRunning = true;
bool handled = await CommandManager.TryExecute(input, Console.WriteLine); bool handled = await CommandManager.TryExecuteAsync(input, Console.WriteLine, commandCancellationTokenSource.Token);
if (!handled) if (!handled)
{ {
@@ -97,5 +110,7 @@ internal class CliProcess() : Process("Cli")
} }
commandRunning = false; commandRunning = false;
commandCancellationTokenSource.Dispose();
commandCancellationTokenSource = null;
} }
} }
+18 -3
View File
@@ -14,6 +14,8 @@ public class TerminalProcess() : Process("Terminal")
private Window window = null!; private Window window = null!;
private readonly List<string> history = []; private readonly List<string> history = [];
private int historyIndex = -1; private int historyIndex = -1;
private bool commandRunning = false;
private CancellationTokenSource? commandCancellationTokenSource = null;
private string currentInput = ""; private string currentInput = "";
private readonly List<Text> textLines = []; private readonly List<Text> textLines = [];
private readonly Lock textLinesLock = new(); private readonly Lock textLinesLock = new();
@@ -49,6 +51,16 @@ public class TerminalProcess() : Process("Terminal")
private async void HandleKey(KeyEvent keyEvent) private async void HandleKey(KeyEvent keyEvent)
{ {
if (commandRunning)
{
if (keyEvent.Key == ConsoleKeyEx.C && keyEvent.Modifiers.HasFlag(ConsoleModifiers.Control) && commandCancellationTokenSource is not null)
{
await commandCancellationTokenSource.CancelAsync();
}
return;
}
if (keyEvent.Key == ConsoleKeyEx.Enter) if (keyEvent.Key == ConsoleKeyEx.Enter)
{ {
string cmd = currentInput; string cmd = currentInput;
@@ -72,16 +84,19 @@ public class TerminalProcess() : Process("Terminal")
} }
else else
{ {
window.OnKeyEvent -= HandleKey; commandCancellationTokenSource = new CancellationTokenSource();
commandRunning = true;
bool handled = await CommandManager.TryExecute(cmd, PrintLine); bool handled = await CommandManager.TryExecuteAsync(cmd, PrintLine, commandCancellationTokenSource.Token);
if (!handled) if (!handled)
{ {
PrintLine($"\"{cmd}\" is not a command"); PrintLine($"\"{cmd}\" is not a command");
} }
window.OnKeyEvent += HandleKey; commandRunning = false;
commandCancellationTokenSource.Dispose();
commandCancellationTokenSource = null;
} }
historyIndex = CommandManager.GetCommandHistory().Count; historyIndex = CommandManager.GetCommandHistory().Count;
+4 -3
View File
@@ -29,7 +29,7 @@ public static class CommandManager
return commandHistory; return commandHistory;
} }
public static async Task<bool> TryExecute(string input, Action<string> printLine) public static async Task<bool> TryExecuteAsync(string input, Action<string> printLine, CancellationToken cancellationToken = default)
{ {
string trimmedInput = input.Trim(); string trimmedInput = input.Trim();
@@ -43,8 +43,7 @@ public static class CommandManager
commandHistory.Add(trimmedInput); commandHistory.Add(trimmedInput);
} }
foreach (KeyValuePair<string, ICommand> entry in commands foreach (KeyValuePair<string, ICommand> entry in commands.OrderByDescending(entry => entry.Key.Length))
.OrderByDescending(entry => entry.Key.Length))
{ {
string commandName = entry.Key; string commandName = entry.Key;
@@ -61,6 +60,8 @@ public static class CommandManager
arguments = trimmedInput[commandName.Length..].TrimStart(); arguments = trimmedInput[commandName.Length..].TrimStart();
} }
cancellationToken.Register(async () => await entry.Value.StopAsync());
await entry.Value.ExecuteAsync(arguments, printLine); await entry.Value.ExecuteAsync(arguments, printLine);
return true; return true;
} }
-5
View File
@@ -18,11 +18,6 @@ public class StartGuiCommand : ICommand
return Task.CompletedTask; return Task.CompletedTask;
} }
foreach (Process process in ProcessManager.GetProcessesOfType<CliProcess>())
{
ProcessManager.StopProcess(process.Id);
}
_ = ProcessManager.SpawnProcess<DesktopProcess>(); _ = ProcessManager.SpawnProcess<DesktopProcess>();
return Task.CompletedTask; return Task.CompletedTask;
} }
-2
View File
@@ -22,7 +22,5 @@ public class StopGuiCommand : ICommand
{ {
await ProcessManager.StopProcessAndWaitAsync(process.Id); await ProcessManager.StopProcessAndWaitAsync(process.Id);
} }
_ = ProcessManager.SpawnProcess<CliProcess>();
} }
} }
+9 -1
View File
@@ -12,10 +12,18 @@ namespace RemSox.UI.CLI.Commands
public string Description => "Start the YesNt interpreter"; public string Description => "Start the YesNt interpreter";
private int processId;
public async Task ExecuteAsync(string? arguments, Action<string> printLine) public async Task ExecuteAsync(string? arguments, Action<string> printLine)
{ {
int processId = ProcessManager.SpawnProcess<Processes.YesNtInterpreterProcess>(arguments?.Split(',') ?? []); processId = ProcessManager.SpawnProcess<Processes.YesNtInterpreterProcess>(arguments?.Split(',') ?? []);
await ProcessManager.WaitForProcessExitAsync(processId); await ProcessManager.WaitForProcessExitAsync(processId);
} }
public Task StopAsync()
{
ProcessManager.StopProcess(processId);
return Task.CompletedTask;
}
} }
} }
+5
View File
@@ -20,4 +20,9 @@ public interface ICommand
/// <param name="arguments">The arguments provided to the command.</param> /// <param name="arguments">The arguments provided to the command.</param>
/// <param name="printLine">A delegate to stream output lines to the current console or terminal.</param> /// <param name="printLine">A delegate to stream output lines to the current console or terminal.</param>
Task ExecuteAsync(string? arguments, Action<string> printLine); Task ExecuteAsync(string? arguments, Action<string> printLine);
/// <summary>
/// Interrupts the command if it's currently running. This is called when the user presses Ctrl+C in the CLI.
/// </summary>
Task StopAsync() => Task.CompletedTask;
} }