From c70fd501bc739c383e4a2a3d43532f0872335a94 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:17:55 +0200 Subject: [PATCH] Code cleanup --- Cryptography/AesGcmCrypto.cs | 87 +- Cryptography/CryptoManager.cs | 14 +- Cryptography/PasswordKeyDeriver.cs | 31 +- Kernel.cs | 5 +- Processes/YesNtInterpreterProcess.cs | 68 +- Processing/Process.cs | 3 +- Processing/ProcessManager.cs | 2 +- UI/CLI/CommandManager.cs | 2 +- UI/CLI/Commands/YesNtCommand.cs | 39 +- UI/CLI/ICommand.cs | 5 +- Utils/YesNtWindowStatements.cs | 1186 +++++++++++++------------- 11 files changed, 735 insertions(+), 707 deletions(-) diff --git a/Cryptography/AesGcmCrypto.cs b/Cryptography/AesGcmCrypto.cs index 908846e..f1738e0 100644 --- a/Cryptography/AesGcmCrypto.cs +++ b/Cryptography/AesGcmCrypto.cs @@ -5,66 +5,65 @@ using Org.BouncyCastle.Crypto.Parameters; using System.Security.Cryptography; using System.Text; -namespace RemSox.Cryptography +namespace RemSox.Cryptography; + +public static class AesGcmCrypto { - public static class AesGcmCrypto + private const int NonceSize = 12; + private const int TagSize = 128; + + public static byte[] Encrypt(byte[] plainData, byte[] key) { - private const int NonceSize = 12; - private const int TagSize = 128; + byte[] nonce = new byte[NonceSize]; + RandomNumberGenerator.Fill(nonce); - public static byte[] Encrypt(byte[] plainData, byte[] key) - { - byte[] nonce = new byte[NonceSize]; - RandomNumberGenerator.Fill(nonce); + GcmBlockCipher cipher = new GcmBlockCipher(new AesEngine()); + cipher.Init(true, new AeadParameters(new KeyParameter(key), TagSize, nonce)); - GcmBlockCipher cipher = new GcmBlockCipher(new AesEngine()); - cipher.Init(true, new AeadParameters(new KeyParameter(key), TagSize, nonce)); + byte[] output = new byte[cipher.GetOutputSize(plainData.Length)]; - byte[] output = new byte[cipher.GetOutputSize(plainData.Length)]; + int len = cipher.ProcessBytes(plainData, 0, plainData.Length, output, 0); + _ = cipher.DoFinal(output, len); - int len = cipher.ProcessBytes(plainData, 0, plainData.Length, output, 0); - _ = cipher.DoFinal(output, len); + byte[] result = new byte[nonce.Length + output.Length]; - byte[] result = new byte[nonce.Length + output.Length]; + Buffer.BlockCopy(nonce, 0, result, 0, nonce.Length); + Buffer.BlockCopy(output, 0, result, nonce.Length, output.Length); - Buffer.BlockCopy(nonce, 0, result, 0, nonce.Length); - Buffer.BlockCopy(output, 0, result, nonce.Length, output.Length); + return result; + } - return result; - } + public static string Encrypt(string text, byte[] key) + { + byte[] data = Encoding.UTF8.GetBytes(text); + return Convert.ToBase64String(Encrypt(data, key)); + } - public static string Encrypt(string text, byte[] key) - { - byte[] data = Encoding.UTF8.GetBytes(text); - return Convert.ToBase64String(Encrypt(data, key)); - } + public static byte[] Decrypt(byte[] encryptedData, byte[] key) + { + byte[] nonce = new byte[NonceSize]; + Buffer.BlockCopy(encryptedData, 0, nonce, 0, NonceSize); - public static byte[] Decrypt(byte[] encryptedData, byte[] key) - { - byte[] nonce = new byte[NonceSize]; - Buffer.BlockCopy(encryptedData, 0, nonce, 0, NonceSize); + int cipherLength = encryptedData.Length - NonceSize; + byte[] cipherBytes = new byte[cipherLength]; - int cipherLength = encryptedData.Length - NonceSize; - byte[] cipherBytes = new byte[cipherLength]; + Buffer.BlockCopy(encryptedData, NonceSize, cipherBytes, 0, cipherLength); - Buffer.BlockCopy(encryptedData, NonceSize, cipherBytes, 0, cipherLength); + GcmBlockCipher cipher = new GcmBlockCipher(new AesEngine()); + cipher.Init(false, new AeadParameters(new KeyParameter(key), TagSize, nonce)); - GcmBlockCipher cipher = new GcmBlockCipher(new AesEngine()); - cipher.Init(false, new AeadParameters(new KeyParameter(key), TagSize, nonce)); + byte[] plain = new byte[cipher.GetOutputSize(cipherBytes.Length)]; - byte[] plain = new byte[cipher.GetOutputSize(cipherBytes.Length)]; + int len = cipher.ProcessBytes(cipherBytes, 0, cipherBytes.Length, plain, 0); + _ = cipher.DoFinal(plain, len); - int len = cipher.ProcessBytes(cipherBytes, 0, cipherBytes.Length, plain, 0); - _ = cipher.DoFinal(plain, len); + return plain; + } - return plain; - } - - public static string Decrypt(string cipherText, byte[] key) - { - byte[] data = Convert.FromBase64String(cipherText); - byte[] plain = Decrypt(data, key); - return Encoding.UTF8.GetString(plain); - } + public static string Decrypt(string cipherText, byte[] key) + { + byte[] data = Convert.FromBase64String(cipherText); + byte[] plain = Decrypt(data, key); + return Encoding.UTF8.GetString(plain); } } \ No newline at end of file diff --git a/Cryptography/CryptoManager.cs b/Cryptography/CryptoManager.cs index 485d2e9..3de0b76 100644 --- a/Cryptography/CryptoManager.cs +++ b/Cryptography/CryptoManager.cs @@ -1,13 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Numerics; -using System.Threading.Tasks; using Cosmos.Kernel.System.Keyboard; using Cosmos.Kernel.System.Mouse; + using RemSox.Plugs; +using System.Drawing; + namespace RemSox.Cryptography; public static class CryptoManager @@ -26,7 +23,10 @@ public static class CryptoManager RandomNumberGeneratorImplementationImpl.AddMouseEntropy(dx, dy, dz, x, y); - if (!KeyboardManager.KeyAvailable) return; + if (!KeyboardManager.KeyAvailable) + { + return; + } KeyEvent keyEvent = KeyboardManager.Peek(); diff --git a/Cryptography/PasswordKeyDeriver.cs b/Cryptography/PasswordKeyDeriver.cs index 57fbf17..7b7f525 100644 --- a/Cryptography/PasswordKeyDeriver.cs +++ b/Cryptography/PasswordKeyDeriver.cs @@ -3,25 +3,24 @@ using Org.BouncyCastle.Crypto.Digests; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Parameters; -namespace RemSox.Cryptography +namespace RemSox.Cryptography; + +public static class Pkcs5S2PasswordKeyDeriver { - public static class Pkcs5S2PasswordKeyDeriver + private const int KeySize = 32; // 256-bit key + private const int Iterations = 150_000; // PBKDF2 cost factor + + public static byte[] DeriveKey(string password, byte[] salt) { - private const int KeySize = 32; // 256-bit key - private const int Iterations = 150_000; // PBKDF2 cost factor + Pkcs5S2ParametersGenerator generator = new Pkcs5S2ParametersGenerator(new Sha256Digest()); - public static byte[] DeriveKey(string password, byte[] salt) - { - Pkcs5S2ParametersGenerator generator = new Pkcs5S2ParametersGenerator(new Sha256Digest()); + generator.Init( + PbeParametersGenerator.Pkcs5PasswordToBytes(password.ToCharArray()), + salt, + Iterations + ); - generator.Init( - PbeParametersGenerator.Pkcs5PasswordToBytes(password.ToCharArray()), - salt, - Iterations - ); - - return ((KeyParameter)generator.GenerateDerivedMacParameters(KeySize * 8)) - .GetKey(); - } + return ((KeyParameter)generator.GenerateDerivedMacParameters(KeySize * 8)) + .GetKey(); } } \ No newline at end of file diff --git a/Kernel.cs b/Kernel.cs index f6cb0e1..55a07bb 100644 --- a/Kernel.cs +++ b/Kernel.cs @@ -1,9 +1,11 @@ global using Sys = Cosmos.Kernel.System; + using RemSox.Cryptography; using RemSox.Processes; using RemSox.Processing; using RemSox.UI.CLI; using RemSox.UI.CLI.Commands; + using System.Runtime; namespace RemSox; @@ -34,8 +36,7 @@ public class Kernel : Sys.Kernel Sys.Mouse.MouseManager.Initialize(); Sys.Mouse.MouseManager.SetScreenSize((int)canvas.Mode.Width, (int)canvas.Mode.Height); Sys.Keyboard.KeyboardManager.Initialize(); - - ProcessManager.SpawnProcess(); + _ = ProcessManager.SpawnProcess(); } protected override void Run() diff --git a/Processes/YesNtInterpreterProcess.cs b/Processes/YesNtInterpreterProcess.cs index 57c346f..266fe79 100644 --- a/Processes/YesNtInterpreterProcess.cs +++ b/Processes/YesNtInterpreterProcess.cs @@ -1,48 +1,46 @@ -using System; using RemSox.Processing; using RemSox.Utils; + using YesNt.Interpreter.Runtime; -using YesNt.Interpreter.Utilities; -namespace RemSox.Processes +namespace RemSox.Processes; + +public class YesNtInterpreterProcess() : Process("YesNtInterpreter") { - public class YesNtInterpreterProcess() : Process("YesNtInterpreter") + private YesNtInterpreter interpreter = null!; + + internal override void Start(string[] args) { - YesNtInterpreter interpreter = null!; + // TODO: eventually we want to load these from a file instead of hardcoding them here + args = [ + "win_create \"Test\" 320 240", + "global winId = %win_last_id", + "win_flush ${winId}", + "print ${winId}", + "label test:", + "goto test" + ]; + interpreter = new(); - internal override void Start(string[] args) + YesNtWindowStatements.Register(interpreter, this); + + interpreter.Prepare([.. args]); + } + + internal override void Tick() + { + if (interpreter.IsRunning) { - // TODO: eventually we want to load these from a file instead of hardcoding them here - args = [ - "win_create \"Test\" 320 240", - "global winId = %win_last_id", - "win_flush ${winId}", - "print ${winId}", - "label test:", - "goto test" - ]; - interpreter = new(); - - YesNtWindowStatements.Register(interpreter, this); - - interpreter.Prepare([.. args]); + _ = interpreter.Step(); } - - internal override void Tick() + else { - if (interpreter.IsRunning) - { - interpreter.Step(); - } - else - { - RequestStop(); - } - } - - internal override void Stop() - { - interpreter.Stop(); + RequestStop(); } } + + internal override void Stop() + { + interpreter.Stop(); + } } \ No newline at end of file diff --git a/Processing/Process.cs b/Processing/Process.cs index f0e2ffa..b32be62 100644 --- a/Processing/Process.cs +++ b/Processing/Process.cs @@ -1,8 +1,9 @@ -using System.Drawing; using RemSox.Logging; using RemSox.Processing.IPC; using RemSox.UI.GUI.Windows; +using System.Drawing; + namespace RemSox.Processing; public abstract class Process(string name) diff --git a/Processing/ProcessManager.cs b/Processing/ProcessManager.cs index e4ca41d..563be07 100644 --- a/Processing/ProcessManager.cs +++ b/Processing/ProcessManager.cs @@ -216,7 +216,7 @@ public static class ProcessManager { process.Stop(); - if (processes.TryRemove(process.Id, out var processEntry)) + if (processes.TryRemove(process.Id, out (Process Process, ProcessMetrics Metrics, TaskCompletionSource ExitSource) processEntry)) { _ = processEntry.ExitSource.TrySetResult(); } diff --git a/UI/CLI/CommandManager.cs b/UI/CLI/CommandManager.cs index e6ec96f..393cd00 100644 --- a/UI/CLI/CommandManager.cs +++ b/UI/CLI/CommandManager.cs @@ -60,7 +60,7 @@ public static class CommandManager arguments = trimmedInput[commandName.Length..].TrimStart(); } - cancellationToken.Register(async () => await entry.Value.StopAsync()); + CancellationTokenRegistration unused = cancellationToken.Register(async () => await entry.Value.StopAsync()); await entry.Value.ExecuteAsync(arguments, printLine); return true; diff --git a/UI/CLI/Commands/YesNtCommand.cs b/UI/CLI/Commands/YesNtCommand.cs index 5fbd13e..5ad1636 100644 --- a/UI/CLI/Commands/YesNtCommand.cs +++ b/UI/CLI/Commands/YesNtCommand.cs @@ -1,29 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using RemSox.Processing; -namespace RemSox.UI.CLI.Commands +namespace RemSox.UI.CLI.Commands; + +public class YesNtCommand : ICommand { - public class YesNtCommand : ICommand + public string Name => "yesnt"; + + public string Description => "Start the YesNt interpreter"; + + private int processId; + + public async Task ExecuteAsync(string? arguments, Action printLine) { - public string Name => "yesnt"; + processId = ProcessManager.SpawnProcess(arguments?.Split(',') ?? []); + await ProcessManager.WaitForProcessExitAsync(processId); + } - public string Description => "Start the YesNt interpreter"; - - private int processId; - - public async Task ExecuteAsync(string? arguments, Action printLine) - { - processId = ProcessManager.SpawnProcess(arguments?.Split(',') ?? []); - await ProcessManager.WaitForProcessExitAsync(processId); - } - - public Task StopAsync() - { - ProcessManager.StopProcess(processId); - return Task.CompletedTask; - } + public Task StopAsync() + { + ProcessManager.StopProcess(processId); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/UI/CLI/ICommand.cs b/UI/CLI/ICommand.cs index d68dea6..be1387f 100644 --- a/UI/CLI/ICommand.cs +++ b/UI/CLI/ICommand.cs @@ -24,5 +24,8 @@ public interface ICommand /// /// Interrupts the command if it's currently running. This is called when the user presses Ctrl+C in the CLI. /// - Task StopAsync() => Task.CompletedTask; + Task StopAsync() + { + return Task.CompletedTask; + } } diff --git a/Utils/YesNtWindowStatements.cs b/Utils/YesNtWindowStatements.cs index cc63841..948058b 100644 --- a/Utils/YesNtWindowStatements.cs +++ b/Utils/YesNtWindowStatements.cs @@ -1,601 +1,633 @@ -using System; -using System.Collections.Generic; -using System.Drawing; using RemSox.Processing; using RemSox.UI.GUI.Windows; + +using System.Drawing; + using YesNt.Interpreter.Runtime; using YesNt.Interpreter.Utilities; -namespace RemSox.Utils +namespace RemSox.Utils; + +/// +/// Registers all YesNt statements that expose the WindowManager / UI element API. +/// +/// Window management: +/// win_create "My Window" 320 240 creates a window, read back id with %win_last_id +/// win_close ${myWinId} closes/destroys a window by id +/// win_flush ${myWinId} forces a redraw of the window +/// win_title ${myWinId} "New Title" changes the window title +/// win_autoflush ${myWinId} true enables/disables auto-flush +/// win_invalidate_all invalidates and redraws every window +/// +/// UI element creation (read back id with %ui_last_id after each call): +/// ui_button ${myWinId} 20 30 100 30 "Click Me" +/// ui_checkbox ${myWinId} 20 80 "Check Me" true +/// ui_label ${myWinId} 20 10 "Hello World" +/// ui_textbox ${myWinId} 20 50 160 24 "placeholder" +/// ui_line ${myWinId} 20 130 180 130 255 0 0 (x1 y1 x2 y2 R G B) +/// ui_rect ${myWinId} 20 20 100 60 0 128 255 (x y w h R G B) +/// +/// Inline substitutions (use inside any line, like %read_line): +/// %win_last_id expands to the id of the last created window +/// %ui_last_id expands to the id of the last created UI element +/// +public static class YesNtWindowStatements { + // Maps script-visible integer ids to actual Window objects. + private static readonly Dictionary s_windows = []; + private static int s_nextWindowId = 1; + private static int s_lastWindowId = 0; + + private static int s_nextUiId = 1; + private static int s_lastUiId = 0; + + private static Process s_ownerProcess = null!; + /// - /// Registers all YesNt statements that expose the WindowManager / UI element API. - /// - /// Window management: - /// win_create "My Window" 320 240 creates a window, read back id with %win_last_id - /// win_close ${myWinId} closes/destroys a window by id - /// win_flush ${myWinId} forces a redraw of the window - /// win_title ${myWinId} "New Title" changes the window title - /// win_autoflush ${myWinId} true enables/disables auto-flush - /// win_invalidate_all invalidates and redraws every window - /// - /// UI element creation (read back id with %ui_last_id after each call): - /// ui_button ${myWinId} 20 30 100 30 "Click Me" - /// ui_checkbox ${myWinId} 20 80 "Check Me" true - /// ui_label ${myWinId} 20 10 "Hello World" - /// ui_textbox ${myWinId} 20 50 160 24 "placeholder" - /// ui_line ${myWinId} 20 130 180 130 255 0 0 (x1 y1 x2 y2 R G B) - /// ui_rect ${myWinId} 20 20 100 60 0 128 255 (x y w h R G B) - /// - /// Inline substitutions (use inside any line, like %read_line): - /// %win_last_id expands to the id of the last created window - /// %ui_last_id expands to the id of the last created UI element + /// Call this from to register + /// every window-related statement with the given interpreter. /// - public static class YesNtWindowStatements + /// The live interpreter instance. + /// + /// The that will own created windows + /// (usually the itself). + /// + public static void Register(YesNtInterpreter interpreter, Process ownerProcess) { - // Maps script-visible integer ids to actual Window objects. - private static readonly Dictionary s_windows = new(); - private static int s_nextWindowId = 1; - private static int s_lastWindowId = 0; + s_ownerProcess = ownerProcess; - private static int s_nextUiId = 1; - private static int s_lastUiId = 0; + RegisterWindowStatements(interpreter); + RegisterButtonStatement(interpreter); + RegisterCheckBoxStatement(interpreter); + RegisterLabelStatement(interpreter); + RegisterTextBoxStatement(interpreter); + RegisterLineStatement(interpreter); + RegisterRectStatement(interpreter); + RegisterLastIdSubstitutions(interpreter); + } - private static Process s_ownerProcess = null!; - - /// - /// Call this from to register - /// every window-related statement with the given interpreter. - /// - /// The live interpreter instance. - /// - /// The that will own created windows - /// (usually the itself). - /// - public static void Register(YesNtInterpreter interpreter, Process ownerProcess) - { - s_ownerProcess = ownerProcess; - - RegisterWindowStatements(interpreter); - RegisterButtonStatement(interpreter); - RegisterCheckBoxStatement(interpreter); - RegisterLabelStatement(interpreter); - RegisterTextBoxStatement(interpreter); - RegisterLineStatement(interpreter); - RegisterRectStatement(interpreter); - RegisterLastIdSubstitutions(interpreter); - } - - private static void RegisterWindowStatements(YesNtInterpreter interpreter) - { - // win_create "Title" width height - // Sets %win_last_id to the new window's id. - // - // win_create "Control Test" 320 240 - // global winId = %win_last_id - interpreter.AddStatement( - new StatementInformation( - "win_create", - YesNt.Interpreter.Enums.SearchMode.StartOfLine, - YesNt.Interpreter.Enums.SpaceAround.End) - { - Priority = YesNt.Interpreter.Enums.Priority.Normal - }, - (args, context) => - { - string trimmed = args.Trim(); - if (!TryParseWindowArgs(trimmed, out string title, out int w, out int h)) - { - context.Exit($"[win_create] Invalid arguments: {trimmed}", false); - return; - } - - Window win = WindowManager.CreateWindow( - s_ownerProcess, - title, - new Size(w, h)); - - int id = s_lastWindowId = s_nextWindowId++; - s_windows[id] = win; - }); - - // win_close - interpreter.AddStatement( - new StatementInformation( - "win_close", - YesNt.Interpreter.Enums.SearchMode.StartOfLine, - YesNt.Interpreter.Enums.SpaceAround.End) - { - Priority = YesNt.Interpreter.Enums.Priority.Normal - }, - (args, context) => - { - string idStr = args.Trim(); - if (int.TryParse(idStr, out int id) && s_windows.TryGetValue(id, out Window? win)) - { - WindowManager.CloseWindow(win); - s_windows.Remove(id); - } - }); - - // win_flush - interpreter.AddStatement( - new StatementInformation( - "win_flush", - YesNt.Interpreter.Enums.SearchMode.StartOfLine, - YesNt.Interpreter.Enums.SpaceAround.End) - { - Priority = YesNt.Interpreter.Enums.Priority.Normal - }, - (args, context) => - { - string idStr = args.Trim(); - if (int.TryParse(idStr, out int id) && s_windows.TryGetValue(id, out Window? win)) - { - win.Flush(); - } - }); - - // win_title "New Title" - interpreter.AddStatement( - new StatementInformation( - "win_title", - YesNt.Interpreter.Enums.SearchMode.StartOfLine, - YesNt.Interpreter.Enums.SpaceAround.End) - { - Priority = YesNt.Interpreter.Enums.Priority.Normal - }, - (args, context) => - { - string rest = args.Trim(); - int spaceIdx = rest.IndexOf(' '); - if (spaceIdx < 0) { return; } - - string idStr = rest[..spaceIdx].Trim(); - string newTitle = rest[(spaceIdx + 1)..].Trim().Trim('"'); - - if (int.TryParse(idStr, out int id) && s_windows.TryGetValue(id, out Window? win)) - { - win.Title = newTitle; - } - }); - - // win_autoflush true|false - interpreter.AddStatement( - new StatementInformation( - "win_autoflush", - YesNt.Interpreter.Enums.SearchMode.StartOfLine, - YesNt.Interpreter.Enums.SpaceAround.End) - { - Priority = YesNt.Interpreter.Enums.Priority.Normal - }, - (args, context) => - { - string rest = args.Trim(); - string[] parts = rest.Split(' ', StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 2 - && int.TryParse(parts[0], out int id) - && bool.TryParse(parts[1], out bool enabled) - && s_windows.TryGetValue(id, out Window? win)) - { - win.AutoFlush = enabled; - } - }); - - // win_invalidate_all - interpreter.AddStatement( - new StatementInformation( - "win_invalidate_all", - YesNt.Interpreter.Enums.SearchMode.Contains, - YesNt.Interpreter.Enums.SpaceAround.None) - { - Priority = YesNt.Interpreter.Enums.Priority.Normal - }, - (args, context) => - { - WindowManager.InvalidateAll(); - }); - } - - // ui_button "Label" [R G B] - // ui_button ${winId} 20 30 100 30 "Click Me" - // ui_button ${winId} 20 30 100 30 "Click Me" 173 216 230 - private static void RegisterButtonStatement(YesNtInterpreter interpreter) - { - interpreter.AddStatement( - new StatementInformation( - "ui_button", - YesNt.Interpreter.Enums.SearchMode.StartOfLine, - YesNt.Interpreter.Enums.SpaceAround.End) - { - Priority = YesNt.Interpreter.Enums.Priority.Normal - }, - (args, context) => - { - string rest = args.Trim(); - if (!TryParseUiArgs(rest, 5, out int winId, out int[] nums, out string label, out Color color)) - { - context.Exit($"[ui_button] Invalid arguments: {rest}", false); - return; - } - - if (!s_windows.TryGetValue(winId, out Window? win)) - { - context.Exit($"[ui_button] No window with id {winId}", false); - return; - } - - int uiId = s_lastUiId = s_nextUiId++; - _ = win.CreateUIElement(b => - { - b.Position = new Point(nums[0], nums[1]); - b.Size = new Size(nums[2], nums[3]); - b.Text = label; - if (color != Color.Empty) - b.BackgroundColor = color; - }); - }); - } - - // ui_checkbox "Label" true|false - // ui_checkbox ${winId} 20 80 "Enable feature" false - private static void RegisterCheckBoxStatement(YesNtInterpreter interpreter) - { - interpreter.AddStatement( - new StatementInformation( - "ui_checkbox", - YesNt.Interpreter.Enums.SearchMode.StartOfLine, - YesNt.Interpreter.Enums.SpaceAround.End) - { - Priority = YesNt.Interpreter.Enums.Priority.Normal - }, - (args, context) => - { - string rest = args.Trim(); - if (!TryParseCheckboxArgs(rest, out int winId, out int x, out int y, out string label, out bool isChecked)) - { - context.Exit($"[ui_checkbox] Invalid arguments: {rest}", false); - return; - } - - if (!s_windows.TryGetValue(winId, out Window? win)) - { - context.Exit($"[ui_checkbox] No window with id {winId}", false); - return; - } - - int uiId = s_lastUiId = s_nextUiId++; - _ = win.CreateUIElement(c => - { - c.Position = new Point(x, y); - c.Text = label; - c.IsChecked = isChecked; - }); - }); - } - - // ui_label "Text" - // ui_label ${winId} 10 10 "Hello, World!" - private static void RegisterLabelStatement(YesNtInterpreter interpreter) - { - interpreter.AddStatement( - new StatementInformation( - "ui_label", - YesNt.Interpreter.Enums.SearchMode.StartOfLine, - YesNt.Interpreter.Enums.SpaceAround.End) - { - Priority = YesNt.Interpreter.Enums.Priority.Normal - }, - (args, context) => - { - string rest = args.Trim(); - if (!TryParseUiArgs(rest, 2, out int winId, out int[] nums, out string text, out _)) - { - context.Exit($"[ui_label] Invalid arguments: {rest}", false); - return; - } - - if (!s_windows.TryGetValue(winId, out Window? win)) - { - context.Exit($"[ui_label] No window with id {winId}", false); - return; - } - - //int uiId = s_lastUiId = s_nextUiId++; - //_ = win.CreateUIElement(l => - //{ - // l.Position = new Point(nums[0], nums[1]); - // l.Text = text; - //}); - }); - } - - // ui_textbox "Placeholder" - // ui_textbox ${winId} 20 50 160 24 "Enter name..." - private static void RegisterTextBoxStatement(YesNtInterpreter interpreter) - { - interpreter.AddStatement( - new StatementInformation( - "ui_textbox", - YesNt.Interpreter.Enums.SearchMode.StartOfLine, - YesNt.Interpreter.Enums.SpaceAround.End) - { - Priority = YesNt.Interpreter.Enums.Priority.Normal - }, - (args, context) => - { - string rest = args.Trim(); - if (!TryParseUiArgs(rest, 4, out int winId, out int[] nums, out string placeholder, out _)) - { - context.Exit($"[ui_textbox] Invalid arguments: {rest}", false); - return; - } - - if (!s_windows.TryGetValue(winId, out Window? win)) - { - context.Exit($"[ui_textbox] No window with id {winId}", false); - return; - } - - //int uiId = s_lastUiId = s_nextUiId++; - //_ = win.CreateUIElement(t => - //{ - // t.Position = new Point(nums[0], nums[1]); - // t.Size = new Size(nums[2], nums[3]); - // t.PlaceholderText = placeholder; - //}); - }); - } - - // ui_line - // ui_line ${winId} 20 130 180 130 255 0 0 - private static void RegisterLineStatement(YesNtInterpreter interpreter) - { - interpreter.AddStatement( - new StatementInformation( - "ui_line", - YesNt.Interpreter.Enums.SearchMode.StartOfLine, - YesNt.Interpreter.Enums.SpaceAround.End) - { - Priority = YesNt.Interpreter.Enums.Priority.Normal - }, - (args, context) => - { - string rest = args.Trim(); - string[] parts = rest.Split(' ', StringSplitOptions.RemoveEmptyEntries); - if (parts.Length < 8 - || !int.TryParse(parts[0], out int winId) - || !int.TryParse(parts[1], out int x1) - || !int.TryParse(parts[2], out int y1) - || !int.TryParse(parts[3], out int x2) - || !int.TryParse(parts[4], out int y2) - || !int.TryParse(parts[5], out int r) - || !int.TryParse(parts[6], out int g) - || !int.TryParse(parts[7], out int b)) - { - context.Exit($"[ui_line] Invalid arguments: {rest}", false); - return; - } - - if (!s_windows.TryGetValue(winId, out Window? win)) - { - context.Exit($"[ui_line] No window with id {winId}", false); - return; - } - - int uiId = s_lastUiId = s_nextUiId++; - _ = win.CreateUIElement(l => - { - l.Position = new Point(x1, y1); - l.EndPosition = new Point(x2, y2); - l.Color = Color.FromArgb(r, g, b); - }); - }); - } - - // ui_rect - // ui_rect ${winId} 20 20 100 60 0 128 255 - private static void RegisterRectStatement(YesNtInterpreter interpreter) - { - interpreter.AddStatement( - new StatementInformation( - "ui_rect", - YesNt.Interpreter.Enums.SearchMode.StartOfLine, - YesNt.Interpreter.Enums.SpaceAround.End) - { - Priority = YesNt.Interpreter.Enums.Priority.Normal - }, - (args, context) => - { - string rest = args.Trim(); - string[] parts = rest.Split(' ', StringSplitOptions.RemoveEmptyEntries); - if (parts.Length < 8 - || !int.TryParse(parts[0], out int winId) - || !int.TryParse(parts[1], out int x) - || !int.TryParse(parts[2], out int y) - || !int.TryParse(parts[3], out int w) - || !int.TryParse(parts[4], out int h) - || !int.TryParse(parts[5], out int r) - || !int.TryParse(parts[6], out int g) - || !int.TryParse(parts[7], out int b)) - { - context.Exit($"[ui_rect] Invalid arguments: {rest}", false); - return; - } - - if (!s_windows.TryGetValue(winId, out Window? win)) - { - context.Exit($"[ui_rect] No window with id {winId}", false); - return; - } - - int uiId = s_lastUiId = s_nextUiId++; - _ = win.CreateUIElement(rect => - { - rect.Position = new Point(x, y); - rect.Size = new Size(w, h); - rect.Color = Color.FromArgb(r, g, b); - }); - }); - } - - private static void RegisterLastIdSubstitutions(YesNtInterpreter interpreter) - { - // %win_last_id → replaced with the id of the last created window - interpreter.AddStatement( - new StatementInformation( - "%win_last_id", - YesNt.Interpreter.Enums.SearchMode.Contains, - YesNt.Interpreter.Enums.SpaceAround.None) - { - KeepStatementInArgs = true, - Priority = YesNt.Interpreter.Enums.Priority.PreProcessing - }, - (args, context) => - { - context.CurrentLine = TemplateProcessor.ProcessSimplePlaceholders( - args, "%win_last_id", s_lastWindowId.ToString()); - }); - - // %ui_last_id → replaced with the id of the last created UI element - interpreter.AddStatement( - new StatementInformation( - "%ui_last_id", - YesNt.Interpreter.Enums.SearchMode.Contains, - YesNt.Interpreter.Enums.SpaceAround.None) - { - KeepStatementInArgs = true, - Priority = YesNt.Interpreter.Enums.Priority.PreProcessing - }, - (args, context) => - { - context.CurrentLine = TemplateProcessor.ProcessSimplePlaceholders( - args, "%ui_last_id", s_lastUiId.ToString()); - }); - } - - /// - /// Parses: "Title" width height - /// The title may be quoted or unquoted. - /// - private static bool TryParseWindowArgs(string input, out string title, out int w, out int h) - { - title = string.Empty; w = 0; h = 0; - - input = input.Trim(); - string remaining; - - if (input.StartsWith('"')) + private static void RegisterWindowStatements(YesNtInterpreter interpreter) + { + // win_create "Title" width height + // Sets %win_last_id to the new window's id. + // + // win_create "Control Test" 320 240 + // global winId = %win_last_id + interpreter.AddStatement( + new StatementInformation( + "win_create", + YesNt.Interpreter.Enums.SearchMode.StartOfLine, + YesNt.Interpreter.Enums.SpaceAround.End) { - int end = input.IndexOf('"', 1); - if (end < 0) return false; - title = input[1..end]; - remaining = input[(end + 1)..].Trim(); - } - else + Priority = YesNt.Interpreter.Enums.Priority.Normal + }, + (args, context) => { - int space = input.IndexOf(' '); - if (space < 0) return false; - title = input[..space]; - remaining = input[(space + 1)..].Trim(); - } - - string[] nums = remaining.Split(' ', StringSplitOptions.RemoveEmptyEntries); - return nums.Length >= 2 - && int.TryParse(nums[0], out w) - && int.TryParse(nums[1], out h); - } - - /// - /// Generic UI arg parser. - /// Input format: winId n0 n1 ... n{numCount-1} "Label" [R G B] - /// - private static bool TryParseUiArgs( - string input, - int numCount, - out int winId, - out int[] nums, - out string label, - out Color color) - { - winId = 0; nums = Array.Empty(); label = string.Empty; color = Color.Empty; - - string[] parts = input.Split(' ', numCount + 2, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length < numCount + 1) return false; - - if (!int.TryParse(parts[0], out winId)) return false; - - nums = new int[numCount]; - for (int i = 0; i < numCount; i++) - { - if (!int.TryParse(parts[i + 1], out nums[i])) return false; - } - - string rest = string.Join(" ", parts[(numCount + 1)..]).Trim(); - - if (rest.StartsWith('"')) - { - int end = rest.IndexOf('"', 1); - if (end < 0) return false; - label = rest[1..end]; - rest = rest[(end + 1)..].Trim(); - } - else - { - int sp = rest.IndexOf(' '); - label = sp < 0 ? rest : rest[..sp]; - rest = sp < 0 ? string.Empty : rest[(sp + 1)..].Trim(); - } - - if (!string.IsNullOrWhiteSpace(rest)) - { - string[] rgb = rest.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); - if (rgb.Length >= 3 - && int.TryParse(rgb[0], out int r) - && int.TryParse(rgb[1], out int g) - && int.TryParse(rgb[2], out int b)) + string trimmed = args.Trim(); + if (!TryParseWindowArgs(trimmed, out string title, out int w, out int h)) { - color = Color.FromArgb(r, g, b); + context.Exit($"[win_create] Invalid arguments: {trimmed}", false); + return; } - } - return true; - } + Window win = WindowManager.CreateWindow( + s_ownerProcess, + title, + new Size(w, h)); - /// Parses: winId x y "Label" true|false - private static bool TryParseCheckboxArgs( - string input, - out int winId, - out int x, out int y, - out string label, - out bool isChecked) + int id = s_lastWindowId = s_nextWindowId++; + s_windows[id] = win; + }); + + // win_close + interpreter.AddStatement( + new StatementInformation( + "win_close", + YesNt.Interpreter.Enums.SearchMode.StartOfLine, + YesNt.Interpreter.Enums.SpaceAround.End) + { + Priority = YesNt.Interpreter.Enums.Priority.Normal + }, + (args, context) => + { + string idStr = args.Trim(); + if (int.TryParse(idStr, out int id) && s_windows.TryGetValue(id, out Window? win)) + { + WindowManager.CloseWindow(win); + bool unused = s_windows.Remove(id); + } + }); + + // win_flush + interpreter.AddStatement( + new StatementInformation( + "win_flush", + YesNt.Interpreter.Enums.SearchMode.StartOfLine, + YesNt.Interpreter.Enums.SpaceAround.End) + { + Priority = YesNt.Interpreter.Enums.Priority.Normal + }, + (args, context) => + { + string idStr = args.Trim(); + if (int.TryParse(idStr, out int id) && s_windows.TryGetValue(id, out Window? win)) + { + win.Flush(); + } + }); + + // win_title "New Title" + interpreter.AddStatement( + new StatementInformation( + "win_title", + YesNt.Interpreter.Enums.SearchMode.StartOfLine, + YesNt.Interpreter.Enums.SpaceAround.End) + { + Priority = YesNt.Interpreter.Enums.Priority.Normal + }, + (args, context) => + { + string rest = args.Trim(); + int spaceIdx = rest.IndexOf(' '); + if (spaceIdx < 0) { return; } + + string idStr = rest[..spaceIdx].Trim(); + string newTitle = rest[(spaceIdx + 1)..].Trim().Trim('"'); + + if (int.TryParse(idStr, out int id) && s_windows.TryGetValue(id, out Window? win)) + { + win.Title = newTitle; + } + }); + + // win_autoflush true|false + interpreter.AddStatement( + new StatementInformation( + "win_autoflush", + YesNt.Interpreter.Enums.SearchMode.StartOfLine, + YesNt.Interpreter.Enums.SpaceAround.End) + { + Priority = YesNt.Interpreter.Enums.Priority.Normal + }, + (args, context) => + { + string rest = args.Trim(); + string[] parts = rest.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 2 + && int.TryParse(parts[0], out int id) + && bool.TryParse(parts[1], out bool enabled) + && s_windows.TryGetValue(id, out Window? win)) + { + win.AutoFlush = enabled; + } + }); + + // win_invalidate_all + interpreter.AddStatement( + new StatementInformation( + "win_invalidate_all", + YesNt.Interpreter.Enums.SearchMode.Contains, + YesNt.Interpreter.Enums.SpaceAround.None) + { + Priority = YesNt.Interpreter.Enums.Priority.Normal + }, + (args, context) => + { + WindowManager.InvalidateAll(); + }); + } + + // ui_button "Label" [R G B] + // ui_button ${winId} 20 30 100 30 "Click Me" + // ui_button ${winId} 20 30 100 30 "Click Me" 173 216 230 + private static void RegisterButtonStatement(YesNtInterpreter interpreter) + { + interpreter.AddStatement( + new StatementInformation( + "ui_button", + YesNt.Interpreter.Enums.SearchMode.StartOfLine, + YesNt.Interpreter.Enums.SpaceAround.End) + { + Priority = YesNt.Interpreter.Enums.Priority.Normal + }, + (args, context) => + { + string rest = args.Trim(); + if (!TryParseUiArgs(rest, 5, out int winId, out int[] nums, out string label, out Color color)) + { + context.Exit($"[ui_button] Invalid arguments: {rest}", false); + return; + } + + if (!s_windows.TryGetValue(winId, out Window? win)) + { + context.Exit($"[ui_button] No window with id {winId}", false); + return; + } + + int uiId = s_lastUiId = s_nextUiId++; + _ = win.CreateUIElement(b => + { + b.Position = new Point(nums[0], nums[1]); + b.Size = new Size(nums[2], nums[3]); + b.Text = label; + if (color != Color.Empty) + { + b.BackgroundColor = color; + } + }); + }); + } + + // ui_checkbox "Label" true|false + // ui_checkbox ${winId} 20 80 "Enable feature" false + private static void RegisterCheckBoxStatement(YesNtInterpreter interpreter) + { + interpreter.AddStatement( + new StatementInformation( + "ui_checkbox", + YesNt.Interpreter.Enums.SearchMode.StartOfLine, + YesNt.Interpreter.Enums.SpaceAround.End) + { + Priority = YesNt.Interpreter.Enums.Priority.Normal + }, + (args, context) => + { + string rest = args.Trim(); + if (!TryParseCheckboxArgs(rest, out int winId, out int x, out int y, out string label, out bool isChecked)) + { + context.Exit($"[ui_checkbox] Invalid arguments: {rest}", false); + return; + } + + if (!s_windows.TryGetValue(winId, out Window? win)) + { + context.Exit($"[ui_checkbox] No window with id {winId}", false); + return; + } + + int uiId = s_lastUiId = s_nextUiId++; + _ = win.CreateUIElement(c => + { + c.Position = new Point(x, y); + c.Text = label; + c.IsChecked = isChecked; + }); + }); + } + + // ui_label "Text" + // ui_label ${winId} 10 10 "Hello, World!" + private static void RegisterLabelStatement(YesNtInterpreter interpreter) + { + interpreter.AddStatement( + new StatementInformation( + "ui_label", + YesNt.Interpreter.Enums.SearchMode.StartOfLine, + YesNt.Interpreter.Enums.SpaceAround.End) + { + Priority = YesNt.Interpreter.Enums.Priority.Normal + }, + (args, context) => + { + string rest = args.Trim(); + if (!TryParseUiArgs(rest, 2, out int winId, out int[] nums, out string text, out _)) + { + context.Exit($"[ui_label] Invalid arguments: {rest}", false); + return; + } + + if (!s_windows.TryGetValue(winId, out Window? win)) + { + context.Exit($"[ui_label] No window with id {winId}", false); + return; + } + + //int uiId = s_lastUiId = s_nextUiId++; + //_ = win.CreateUIElement(l => + //{ + // l.Position = new Point(nums[0], nums[1]); + // l.Text = text; + //}); + }); + } + + // ui_textbox "Placeholder" + // ui_textbox ${winId} 20 50 160 24 "Enter name..." + private static void RegisterTextBoxStatement(YesNtInterpreter interpreter) + { + interpreter.AddStatement( + new StatementInformation( + "ui_textbox", + YesNt.Interpreter.Enums.SearchMode.StartOfLine, + YesNt.Interpreter.Enums.SpaceAround.End) + { + Priority = YesNt.Interpreter.Enums.Priority.Normal + }, + (args, context) => + { + string rest = args.Trim(); + if (!TryParseUiArgs(rest, 4, out int winId, out int[] nums, out string placeholder, out _)) + { + context.Exit($"[ui_textbox] Invalid arguments: {rest}", false); + return; + } + + if (!s_windows.TryGetValue(winId, out Window? win)) + { + context.Exit($"[ui_textbox] No window with id {winId}", false); + return; + } + + //int uiId = s_lastUiId = s_nextUiId++; + //_ = win.CreateUIElement(t => + //{ + // t.Position = new Point(nums[0], nums[1]); + // t.Size = new Size(nums[2], nums[3]); + // t.PlaceholderText = placeholder; + //}); + }); + } + + // ui_line + // ui_line ${winId} 20 130 180 130 255 0 0 + private static void RegisterLineStatement(YesNtInterpreter interpreter) + { + interpreter.AddStatement( + new StatementInformation( + "ui_line", + YesNt.Interpreter.Enums.SearchMode.StartOfLine, + YesNt.Interpreter.Enums.SpaceAround.End) + { + Priority = YesNt.Interpreter.Enums.Priority.Normal + }, + (args, context) => + { + string rest = args.Trim(); + string[] parts = rest.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 8 + || !int.TryParse(parts[0], out int winId) + || !int.TryParse(parts[1], out int x1) + || !int.TryParse(parts[2], out int y1) + || !int.TryParse(parts[3], out int x2) + || !int.TryParse(parts[4], out int y2) + || !int.TryParse(parts[5], out int r) + || !int.TryParse(parts[6], out int g) + || !int.TryParse(parts[7], out int b)) + { + context.Exit($"[ui_line] Invalid arguments: {rest}", false); + return; + } + + if (!s_windows.TryGetValue(winId, out Window? win)) + { + context.Exit($"[ui_line] No window with id {winId}", false); + return; + } + + int uiId = s_lastUiId = s_nextUiId++; + _ = win.CreateUIElement(l => + { + l.Position = new Point(x1, y1); + l.EndPosition = new Point(x2, y2); + l.Color = Color.FromArgb(r, g, b); + }); + }); + } + + // ui_rect + // ui_rect ${winId} 20 20 100 60 0 128 255 + private static void RegisterRectStatement(YesNtInterpreter interpreter) + { + interpreter.AddStatement( + new StatementInformation( + "ui_rect", + YesNt.Interpreter.Enums.SearchMode.StartOfLine, + YesNt.Interpreter.Enums.SpaceAround.End) + { + Priority = YesNt.Interpreter.Enums.Priority.Normal + }, + (args, context) => + { + string rest = args.Trim(); + string[] parts = rest.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 8 + || !int.TryParse(parts[0], out int winId) + || !int.TryParse(parts[1], out int x) + || !int.TryParse(parts[2], out int y) + || !int.TryParse(parts[3], out int w) + || !int.TryParse(parts[4], out int h) + || !int.TryParse(parts[5], out int r) + || !int.TryParse(parts[6], out int g) + || !int.TryParse(parts[7], out int b)) + { + context.Exit($"[ui_rect] Invalid arguments: {rest}", false); + return; + } + + if (!s_windows.TryGetValue(winId, out Window? win)) + { + context.Exit($"[ui_rect] No window with id {winId}", false); + return; + } + + int uiId = s_lastUiId = s_nextUiId++; + _ = win.CreateUIElement(rect => + { + rect.Position = new Point(x, y); + rect.Size = new Size(w, h); + rect.Color = Color.FromArgb(r, g, b); + }); + }); + } + + private static void RegisterLastIdSubstitutions(YesNtInterpreter interpreter) + { + // %win_last_id → replaced with the id of the last created window + interpreter.AddStatement( + new StatementInformation( + "%win_last_id", + YesNt.Interpreter.Enums.SearchMode.Contains, + YesNt.Interpreter.Enums.SpaceAround.None) + { + KeepStatementInArgs = true, + Priority = YesNt.Interpreter.Enums.Priority.PreProcessing + }, + (args, context) => + { + context.CurrentLine = TemplateProcessor.ProcessSimplePlaceholders( + args, "%win_last_id", s_lastWindowId.ToString()); + }); + + // %ui_last_id → replaced with the id of the last created UI element + interpreter.AddStatement( + new StatementInformation( + "%ui_last_id", + YesNt.Interpreter.Enums.SearchMode.Contains, + YesNt.Interpreter.Enums.SpaceAround.None) + { + KeepStatementInArgs = true, + Priority = YesNt.Interpreter.Enums.Priority.PreProcessing + }, + (args, context) => + { + context.CurrentLine = TemplateProcessor.ProcessSimplePlaceholders( + args, "%ui_last_id", s_lastUiId.ToString()); + }); + } + + /// + /// Parses: "Title" width height + /// The title may be quoted or unquoted. + /// + private static bool TryParseWindowArgs(string input, out string title, out int w, out int h) + { + title = string.Empty; w = 0; h = 0; + + input = input.Trim(); + string remaining; + + if (input.StartsWith('"')) { - winId = 0; x = 0; y = 0; label = string.Empty; isChecked = false; - - string[] parts = input.Split(' ', 4, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length < 4) return false; - - if (!int.TryParse(parts[0], out winId) - || !int.TryParse(parts[1], out x) - || !int.TryParse(parts[2], out y)) return false; - - string rest = parts[3].Trim(); - if (rest.StartsWith('"')) + int end = input.IndexOf('"', 1); + if (end < 0) { - int end = rest.IndexOf('"', 1); - if (end < 0) return false; - label = rest[1..end]; - rest = rest[(end + 1)..].Trim(); - } - else - { - int sp = rest.IndexOf(' '); - if (sp < 0) { label = rest; rest = "false"; } - else { label = rest[..sp]; rest = rest[(sp + 1)..].Trim(); } + return false; } - bool.TryParse(rest, out isChecked); - return true; + title = input[1..end]; + remaining = input[(end + 1)..].Trim(); } + else + { + int space = input.IndexOf(' '); + if (space < 0) + { + return false; + } + + title = input[..space]; + remaining = input[(space + 1)..].Trim(); + } + + string[] nums = remaining.Split(' ', StringSplitOptions.RemoveEmptyEntries); + return nums.Length >= 2 + && int.TryParse(nums[0], out w) + && int.TryParse(nums[1], out h); + } + + /// + /// Generic UI arg parser. + /// Input format: winId n0 n1 ... n{numCount-1} "Label" [R G B] + /// + private static bool TryParseUiArgs( + string input, + int numCount, + out int winId, + out int[] nums, + out string label, + out Color color) + { + winId = 0; nums = Array.Empty(); label = string.Empty; color = Color.Empty; + + string[] parts = input.Split(' ', numCount + 2, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < numCount + 1) + { + return false; + } + + if (!int.TryParse(parts[0], out winId)) + { + return false; + } + + nums = new int[numCount]; + for (int i = 0; i < numCount; i++) + { + if (!int.TryParse(parts[i + 1], out nums[i])) + { + return false; + } + } + + string rest = string.Join(" ", parts[(numCount + 1)..]).Trim(); + + if (rest.StartsWith('"')) + { + int end = rest.IndexOf('"', 1); + if (end < 0) + { + return false; + } + + label = rest[1..end]; + rest = rest[(end + 1)..].Trim(); + } + else + { + int sp = rest.IndexOf(' '); + label = sp < 0 ? rest : rest[..sp]; + rest = sp < 0 ? string.Empty : rest[(sp + 1)..].Trim(); + } + + if (!string.IsNullOrWhiteSpace(rest)) + { + string[] rgb = rest.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (rgb.Length >= 3 + && int.TryParse(rgb[0], out int r) + && int.TryParse(rgb[1], out int g) + && int.TryParse(rgb[2], out int b)) + { + color = Color.FromArgb(r, g, b); + } + } + + return true; + } + + /// Parses: winId x y "Label" true|false + private static bool TryParseCheckboxArgs( + string input, + out int winId, + out int x, out int y, + out string label, + out bool isChecked) + { + winId = 0; x = 0; y = 0; label = string.Empty; isChecked = false; + + string[] parts = input.Split(' ', 4, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 4) + { + return false; + } + + if (!int.TryParse(parts[0], out winId) + || !int.TryParse(parts[1], out x) + || !int.TryParse(parts[2], out y)) + { + return false; + } + + string rest = parts[3].Trim(); + if (rest.StartsWith('"')) + { + int end = rest.IndexOf('"', 1); + if (end < 0) + { + return false; + } + + label = rest[1..end]; + rest = rest[(end + 1)..].Trim(); + } + else + { + int sp = rest.IndexOf(' '); + if (sp < 0) { label = rest; rest = "false"; } + else { label = rest[..sp]; rest = rest[(sp + 1)..].Trim(); } + } + + _ = bool.TryParse(rest, out isChecked); + return true; } } \ No newline at end of file