From e0ba1e5ee980222a895d88a52c01a0e58d68d457 Mon Sep 17 00:00:00 2001 From: Christer C Date: Mon, 27 Nov 2023 20:16:32 +0100 Subject: [PATCH] Feature/monitor system commandline (#95) * Replace McMaster.Extensions.CommandLineUtils with System.CommandLine for parsing and executing in machine code monitor. * Scrollable SilkNet Native ImGUI monitor log --- .../GlobalUsings.cs | 2 +- ...hbyte.DotNet6502.App.ConsoleMonitor.csproj | 8 +- .../Program.cs | 12 +- .../Program.cs | 1 + .../SilkNetImgUIMonitor.cs | 65 +++- .../SilkNetNativeMonitor.cs | 2 +- .../CommandLineApp.cs | 97 +++--- .../Commands/BreakpointCommands.cs | 146 ++++----- .../Commands/CustomHelpTextGenerator.cs | 23 -- .../Commands/DisassemblyCommands.cs | 131 ++++---- .../Commands/ExecutionCommands.cs | 119 ++++---- .../Commands/FileCommands.cs | 245 +++++++++------ .../Commands/MemoryCommands.cs | 177 ++++++----- .../Commands/OptionCommands.cs | 111 +++---- .../Commands/RegisterCommands.cs | 280 +++++++++--------- .../Commands/ResetCommands.cs | 36 ++- .../Commands/ValidationHelpers.cs | 157 +++++----- .../CustomHelpBuilderWithourRootCommand.cs | 58 ++++ .../Highbyte.DotNet6502.Monitor.csproj | 4 +- .../MonitorBase.cs | 37 ++- .../MonitorConsole.cs | 105 +++---- .../SystemSpecific/ISystemMonitorCommands.cs | 6 +- .../Commodore64/Monitor/C64MonitorCommands.cs | 141 ++++----- .../Highbyte.DotNet6502.Systems.csproj | 2 +- 24 files changed, 1043 insertions(+), 922 deletions(-) delete mode 100644 src/libraries/Highbyte.DotNet6502.Monitor/Commands/CustomHelpTextGenerator.cs create mode 100644 src/libraries/Highbyte.DotNet6502.Monitor/CustomHelpBuilderWithourRootCommand.cs diff --git a/src/apps/Highbyte.DotNet6502.App.ConsoleMonitor/GlobalUsings.cs b/src/apps/Highbyte.DotNet6502.App.ConsoleMonitor/GlobalUsings.cs index 7e5a1c37..5c8cb17f 100644 --- a/src/apps/Highbyte.DotNet6502.App.ConsoleMonitor/GlobalUsings.cs +++ b/src/apps/Highbyte.DotNet6502.App.ConsoleMonitor/GlobalUsings.cs @@ -1,2 +1,2 @@ // If enable is added to the .csproj file, a bunch of standard .NET namespaces are added by default (and not needed to be added here). -global using McMaster.Extensions.CommandLineUtils; +global using System.CommandLine; diff --git a/src/apps/Highbyte.DotNet6502.App.ConsoleMonitor/Highbyte.DotNet6502.App.ConsoleMonitor.csproj b/src/apps/Highbyte.DotNet6502.App.ConsoleMonitor/Highbyte.DotNet6502.App.ConsoleMonitor.csproj index 765d2e40..d733bc55 100644 --- a/src/apps/Highbyte.DotNet6502.App.ConsoleMonitor/Highbyte.DotNet6502.App.ConsoleMonitor.csproj +++ b/src/apps/Highbyte.DotNet6502.App.ConsoleMonitor/Highbyte.DotNet6502.App.ConsoleMonitor.csproj @@ -8,13 +8,13 @@ - - - + - + + + diff --git a/src/apps/Highbyte.DotNet6502.App.ConsoleMonitor/Program.cs b/src/apps/Highbyte.DotNet6502.App.ConsoleMonitor/Program.cs index 4761dae8..278c557d 100644 --- a/src/apps/Highbyte.DotNet6502.App.ConsoleMonitor/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.ConsoleMonitor/Program.cs @@ -88,10 +88,12 @@ string? PromptInput() { - return Prompt.GetString(">", - promptColor: ConsoleColor.Gray, - promptBgColor: ConsoleColor.DarkBlue); + //return Prompt.GetString(">", + // promptColor: ConsoleColor.Gray, + // promptBgColor: ConsoleColor.DarkBlue); - // Console.Write(">"); - // return Console.ReadLine(); + Console.ForegroundColor = ConsoleColor.Gray; + //Console.BackgroundColor = ConsoleColor.DarkBlue; + Console.Write(">"); + return Console.ReadLine(); } diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs index fda76b0a..a3d05289 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs @@ -52,6 +52,7 @@ DefaultDrawScale = 3.0f, Monitor = new MonitorConfig { + MaxLineLength = 100, //DefaultDirectory = "../../../../../../samples/Assembler/C64/Build" //DefaultDirectory = "../../../../../../samples/Assembler/Generic/Build" diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImgUIMonitor.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImgUIMonitor.cs index bc854964..fb5744b9 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImgUIMonitor.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImgUIMonitor.cs @@ -1,6 +1,8 @@ +using System.Diagnostics; using System.Numerics; using Highbyte.DotNet6502.Monitor; using Highbyte.DotNet6502.Systems; +using ImGuiNET; using NativeFileDialogSharp; namespace Highbyte.DotNet6502.App.SilkNetNative; @@ -16,6 +18,7 @@ public class SilkNetImGuiMonitor : ISilkNetImGuiWindow public bool Quit = false; + private bool _scrollToEnd = false; private string _monitorCmdString = ""; @@ -25,8 +28,8 @@ public class SilkNetImGuiMonitor : ISilkNetImGuiWindow private const int POS_X = 300; private const int POS_Y = 2; - private const int WIDTH = 720; - private const int HEIGHT = 450; + private const int WIDTH = 750; + private const int HEIGHT = 642; const int MONITOR_CMD_LINE_LENGTH = 200; static Vector4 s_InformationColor = new Vector4(1.0f, 1.0f, 1.0f, 1.0f); @@ -35,6 +38,8 @@ public class SilkNetImGuiMonitor : ISilkNetImGuiWindow static Vector4 s_StatusColor = new Vector4(0.7f, 0.7f, 0.7f, 1.0f); + private bool _autoScroll = true; + public event EventHandler MonitorStateChange; protected virtual void OnMonitorStateChange(bool monitorEnabled) { @@ -78,34 +83,61 @@ public void PostOnRender() _hasBeenInitializedOnce = true; } - ImGui.Begin($"6502 Monitor: {_silkNetNativeMonitor.System.Name}"); + ImGui.Begin($"6502 Monitor: {_silkNetNativeMonitor.System.Name}", ImGuiWindowFlags.NoScrollbar); if (ImGui.IsWindowFocused()) { - _setFocusOnInput = true; + //_setFocusOnInput = true; // TODO: This is not working ok when child window contains a scrollbar (cannot select scrollbar when clicking outside child window) } - Vector4 textColor; - foreach (var cmd in _silkNetNativeMonitor.MonitorCmdHistory) + if (ImGui.BeginChild("##scrolling", Vector2.Zero, border: false, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar)) { - textColor = cmd.Severity switch + //ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + + Vector4 textColor; + foreach (var cmd in _silkNetNativeMonitor.MonitorCmdHistory) { - MessageSeverity.Information => s_InformationColor, - MessageSeverity.Warning => s_WarningColor, - MessageSeverity.Error => s_ErrorColor, - _ => s_InformationColor - }; - ImGui.PushStyleColor(ImGuiCol.Text, textColor); - ImGui.Text(cmd.Message); - ImGui.PopStyleColor(); + textColor = cmd.Severity switch + { + MessageSeverity.Information => s_InformationColor, + MessageSeverity.Warning => s_WarningColor, + MessageSeverity.Error => s_ErrorColor, + _ => s_InformationColor + }; + ImGui.PushStyleColor(ImGuiCol.Text, textColor); + ImGui.Text(cmd.Message); + ImGui.PopStyleColor(); + } + + //ImGui.PopStyleVar(); + + if (_autoScroll) + { + // If a command was entered, scroll to the bottom of the scroll region. + if (_scrollToEnd) + { + ImGui.SetScrollHereY(1.0f); // 0.0f:top, 0.5f:center, 1.0f:bottom + ImGui.SetScrollHereX(0.0f); // 0.0f:left, 0.5f:center, 1.0f:right + _scrollToEnd = false; + } + + // Keep up at the bottom of the scroll region if we were already at the bottom at the beginning of the frame. + // Using a scrollbar or mouse-wheel will take away from the bottom edge. + if (ImGui.GetScrollY() >= ImGui.GetScrollMaxY()) + ImGui.SetScrollHereY(1.0f); // 0.0f:top, 0.5f:center, 1.0f:bottom + } + } + ImGui.EndChild(); + + if (_setFocusOnInput) { ImGui.SetKeyboardFocusHere(); _setFocusOnInput = false; } - ImGui.PushItemWidth(600); + ImGui.PushItemWidth(700); if (ImGui.InputText("", ref _monitorCmdString, MONITOR_CMD_LINE_LENGTH, ImGuiInputTextFlags.EnterReturnsTrue)) { _silkNetNativeMonitor.WriteOutput(_monitorCmdString, MessageSeverity.Information); @@ -121,6 +153,7 @@ public void PostOnRender() Disable(); } _setFocusOnInput = true; + _scrollToEnd = true; } // When reaching this line, we may have destroyed the ImGui controller if we did a Quit or Continue as monitor command. diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetNativeMonitor.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetNativeMonitor.cs index 3376a47e..1058f5ae 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetNativeMonitor.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetNativeMonitor.cs @@ -7,7 +7,7 @@ public class SilkNetNativeMonitor : MonitorBase { private readonly MonitorConfig _monitorConfig; - public const int MONITOR_CMD_HISTORY_VIEW_ROWS = 20; + public const int MONITOR_CMD_HISTORY_VIEW_ROWS = 200; public List<(string Message, MessageSeverity Severity)> MonitorCmdHistory { get; private set; } = new(); public SilkNetNativeMonitor( diff --git a/src/libraries/Highbyte.DotNet6502.Monitor/CommandLineApp.cs b/src/libraries/Highbyte.DotNet6502.Monitor/CommandLineApp.cs index 70182fe7..440342e0 100644 --- a/src/libraries/Highbyte.DotNet6502.Monitor/CommandLineApp.cs +++ b/src/libraries/Highbyte.DotNet6502.Monitor/CommandLineApp.cs @@ -1,75 +1,72 @@ -using System.ComponentModel.DataAnnotations; +using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.Parsing; using Highbyte.DotNet6502.Monitor.Commands; using Highbyte.DotNet6502.Monitor.SystemSpecific; -using McMaster.Extensions.CommandLineUtils; namespace Highbyte.DotNet6502.Monitor; /// /// -public class CommandLineApp +public static class CommandLineApp { - public static CommandLineApplication Build(MonitorBase monitor, MonitorVariables monitorVariables, MonitorConfig options) + public static Parser Build(MonitorBase monitor, MonitorVariables monitorVariables, MonitorConfig options, IConsole console) { - //var app = new CommandLineApplication() - //var app = new CommandLineApplication(NullConsole.Singleton, monitor.Options.DefaultDirectory) - var app = new CommandLineApplication(MonitorConsole.BuildSingleton(monitor), monitor.Options.DefaultDirectory!) + + Parser? parser = null; + //var root = new RootCommand() + //{ + // Name = "DotNet6502Monitor", + // Description = "DotNet 6502 machine code monitor for the DotNet 6502 emulator library." + Environment.NewLine + + // "By Highbyte 2023" + Environment.NewLine + + // "Source at: https://github.com/highbyte/dotnet-6502" + //}; + var root = new Command( + "DotNet6502Monitor", + "DotNet 6502 machine code monitor for the DotNet 6502 emulator library." + Environment.NewLine + + "By Highbyte 2023" + Environment.NewLine + + "Source at: https://github.com/highbyte/dotnet-6502") { - Name = "", - Description = "DotNet 6502 machine code monitor for the DotNet 6502 emulator library." + Environment.NewLine + - "By Highbyte 2022" + Environment.NewLine + - "Source at: https://github.com/highbyte/dotnet-6502", - UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.StopParsingAndCollect }; - // Fix: Use custom Help Text Generator to avoid name/description of the application to be shown each time help text is shown. - app.HelpTextGenerator = new CustomHelpTextGenerator(options.MaxLineLength); - // Fix: To avoid CommandLineUtils to the name of the application at the end of the help text: Don't use HelpOption on app-level, instead set it on each command below. - //app.HelpOption(inherited: true); - - app.ConfigureRegisters(monitor, monitorVariables); - app.ConfigureMemory(monitor, monitorVariables); - app.ConfigureDisassembly(monitor, monitorVariables); - app.ConfigureExecution(monitor, monitorVariables); - app.ConfigureBreakpoints(monitor, monitorVariables); - app.ConfigureFiles(monitor, monitorVariables); - app.ConfigureReset(monitor, monitorVariables); - app.ConfigureOptions(monitor, monitorVariables); + root.ConfigureRegisters(monitor, monitorVariables); + root.ConfigureMemory(monitor, monitorVariables); + root.ConfigureDisassembly(monitor, monitorVariables); + root.ConfigureExecution(monitor, monitorVariables); + root.ConfigureBreakpoints(monitor, monitorVariables); + root.ConfigureFiles(monitor, monitorVariables); + root.ConfigureReset(monitor, monitorVariables); + root.ConfigureOptions(monitor, monitorVariables); // Add any system-specific monitor commands if the system implements it. if (monitor.SystemRunner.System is ISystemMonitor systemWithMonitor) { var monitorCommands = systemWithMonitor.GetSystemMonitorCommands(); - monitorCommands.Configure(app, monitor); + monitorCommands.Configure(root, monitor); } - app.Command("q", cmd => - { - cmd.HelpOption(inherited: true); - cmd.Description = "Quit monitor."; - cmd.AddName("quit"); - cmd.AddName("x"); - cmd.AddName("exit"); - - cmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); - - cmd.OnExecute(() => - { - return (int)CommandResult.Quit; - }); - }); + var quitCmd = new Command("q", "Quit monitor"); + quitCmd.AddAlias("quit"); + quitCmd.AddAlias("exit"); + quitCmd.SetHandler(() => Task.FromResult((int)CommandResult.Quit)); + root.AddCommand(quitCmd); - app.OnExecute(() => + var helpCmd = new Command("?", "Help"); + helpCmd.SetHandler(() => { - monitor.WriteOutput("Unknown command.", MessageSeverity.Error); - monitor.WriteOutput("Help: ?|help|-?|--help", MessageSeverity.Information); - monitor.WriteOutput("Help: command -?|-h|--help", MessageSeverity.Information); - return (int)CommandResult.Error; + parser?.Invoke($"{root.Name} -?", console); }); + root.AddCommand(helpCmd); + + int maxWidth = options.MaxLineLength ?? int.MaxValue; + var cmdLineBuilder = new CommandLineBuilder(root) + .UseHelpBuilder(_ => + { + return new CustomHelpBuilderWithourRootCommand(LocalizationResources.Instance, root.Name, maxWidth: maxWidth); + }) + .UseHelp(); - return app; + parser = cmdLineBuilder.Build(); + return parser; } } diff --git a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/BreakpointCommands.cs b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/BreakpointCommands.cs index 0eabad03..66ac8138 100644 --- a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/BreakpointCommands.cs +++ b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/BreakpointCommands.cs @@ -1,6 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System.CommandLine; using System.Globalization; -using McMaster.Extensions.CommandLineUtils; namespace Highbyte.DotNet6502.Monitor.Commands; @@ -8,86 +7,95 @@ namespace Highbyte.DotNet6502.Monitor.Commands; /// public static class BreakpointCommands { - public static CommandLineApplication ConfigureBreakpoints(this CommandLineApplication app, MonitorBase monitor, MonitorVariables monitorVariables) + public static Command ConfigureBreakpoints(this Command rootCommand, MonitorBase monitor, MonitorVariables monitorVariables) { - app.Command("b", cmd => - { - cmd.HelpOption(inherited: true); - cmd.Description = "Breakpoints"; - cmd.AddName("bp"); - cmd.AddName("breakpoint"); + rootCommand.AddCommand(BuildBreakpointCommand(monitor, monitorVariables)); + return rootCommand; + } - cmd.Command("l", bpCmd => - { - bpCmd.Description = "Lists all breakpoints."; - bpCmd.OnExecute(() => - { - return ListBreakpoints(monitor); - }); - }); + private static Command BuildBreakpointCommand(MonitorBase monitor, MonitorVariables monitorVariables) + { - cmd.Command("a", bpCmd => - { - bpCmd.Description = "Add a breakpoint."; - var memAddress = bpCmd.Argument("address", "Memory address 16 bits (hex).").IsRequired(); - memAddress.Validators.Add(new MustBe16BitHexValueValidator()); + // b l + var listSubCommand = new Command("l", "Lists all breakpoints.") + { + }; + listSubCommand.SetHandler(() => + { + return ListBreakpoints(monitor); + }); - bpCmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); + // b a + var addressArg = new Argument() + { + Name = "address", + Description = "Memory address 16 bits (hex).", + Arity = ArgumentArity.ExactlyOne + } + .MustBe16BitHex(); - bpCmd.OnExecute(() => - { - var address = ushort.Parse(memAddress.Value!, NumberStyles.AllowHexSpecifier, null); - if (!monitor.BreakPoints.ContainsKey(address)) - monitor.BreakPoints.Add(address, new BreakPoint { Enabled = true }); - else - monitor.BreakPoints[address].Enabled = true; - return (int)CommandResult.Ok; - }); - }); + var addSubCommand = new Command("a", "Add a breakpoint.") + { + addressArg + }; + addSubCommand.SetHandler((string memAddress) => + { + var address = ushort.Parse(memAddress, NumberStyles.AllowHexSpecifier, null); + if (!monitor.BreakPoints.ContainsKey(address)) + monitor.BreakPoints.Add(address, new BreakPoint { Enabled = true }); + else + monitor.BreakPoints[address].Enabled = true; + }, addressArg); - cmd.Command("d", bpCmd => - { - bpCmd.Description = "Delete a breakpoint."; - var memAddress = bpCmd.Argument("address", "Memory address 16 bits (hex).").IsRequired(); - memAddress.Validators.Add(new MustBe16BitHexValueValidator()); + // b d + var addressDelArg = new Argument() + { + Name = "address", + Description = "Memory address 16 bits (hex).", + Arity = ArgumentArity.ExactlyOne + } + .MustBe16BitHex(); - bpCmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); + var delSubCommand = new Command("d", "Delete a breakpoint.") + { + addressDelArg + }; + delSubCommand.SetHandler((string memAddress) => + { + var address = ushort.Parse(memAddress, NumberStyles.AllowHexSpecifier, null); + if (monitor.BreakPoints.ContainsKey(address)) + monitor.BreakPoints.Remove(address); - bpCmd.OnExecute(() => - { - var address = ushort.Parse(memAddress.Value!, NumberStyles.AllowHexSpecifier, null); - if (monitor.BreakPoints.ContainsKey(address)) - monitor.BreakPoints.Remove(address); - return (int)CommandResult.Ok; - }); - }); + }, addressDelArg); - cmd.Command("da", bpCmd => - { - bpCmd.Description = "Delete all breakpoints."; - bpCmd.OnExecute(() => - { - monitor.BreakPoints.Clear(); - return (int)CommandResult.Ok; - }); - }); + // b da + var delAllSubCommand = new Command("da", "Delete all breakpoints.") + { + }; + delAllSubCommand.SetHandler(() => + { + monitor.BreakPoints.Clear(); + }); - cmd.OnExecute(() => - { - return ListBreakpoints(monitor); - }); + // b + var command = new Command("b", "Breakpoints.") + { + listSubCommand, + addSubCommand, + delSubCommand, + delAllSubCommand + }; + command.AddAlias("bp"); + // Default command for just "b" without any subcommand + command.SetHandler(() => + { + return ListBreakpoints(monitor); }); - return app; + return command; } - private static int ListBreakpoints(MonitorBase monitor) + private static Task ListBreakpoints(MonitorBase monitor) { if (monitor.BreakPoints.Count == 0) monitor.WriteOutput($"No breakpoints."); @@ -100,6 +108,6 @@ private static int ListBreakpoints(MonitorBase monitor) var status = monitor.BreakPoints[bp].Enabled ? "Enabled" : "Disabled"; monitor.WriteOutput($"{addr} : {status}"); } - return (int)CommandResult.Ok; + return Task.FromResult((int)CommandResult.Ok); } } diff --git a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/CustomHelpTextGenerator.cs b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/CustomHelpTextGenerator.cs deleted file mode 100644 index 4878ca61..00000000 --- a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/CustomHelpTextGenerator.cs +++ /dev/null @@ -1,23 +0,0 @@ -using McMaster.Extensions.CommandLineUtils; -using McMaster.Extensions.CommandLineUtils.HelpText; - -namespace Highbyte.DotNet6502.Monitor.Commands; - -internal class CustomHelpTextGenerator : DefaultHelpTextGenerator -{ - public CustomHelpTextGenerator(int? maxLineLength = null) - { - // To make McMaster.Extensions.CommandLineUtils work in WASM, we have to set value for MaxLineLength in DefaultHelpTextGenerator - // because otherwise it will try to call Console.BufferWidth, which will throw exception under WASM. - base.MaxLineLength = maxLineLength; - } - - /// - /// Override GenerateHeader to avoid name/description of the application to be shown each time help text is shown. - /// - /// - /// - protected override void GenerateHeader(CommandLineApplication application, TextWriter output) - { - } -} diff --git a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/DisassemblyCommands.cs b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/DisassemblyCommands.cs index dd4db140..aedce2b4 100644 --- a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/DisassemblyCommands.cs +++ b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/DisassemblyCommands.cs @@ -1,6 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System.CommandLine; using System.Globalization; -using McMaster.Extensions.CommandLineUtils; namespace Highbyte.DotNet6502.Monitor.Commands; @@ -8,79 +7,87 @@ namespace Highbyte.DotNet6502.Monitor.Commands; /// public static class DisassemblyCommands { - public static CommandLineApplication ConfigureDisassembly(this CommandLineApplication app, MonitorBase monitor, MonitorVariables monitorVariables) + public static Command ConfigureDisassembly(this Command rootCommand, MonitorBase monitor, MonitorVariables monitorVariables) { - app.Command("d", cmd => + rootCommand.AddCommand(BuildDisassemblyCommand(monitor, monitorVariables)); + return rootCommand; + } + + private static Command BuildDisassemblyCommand(MonitorBase monitor, MonitorVariables monitorVariables) + { + var startArg = new Argument() { - cmd.HelpOption(inherited: true); - cmd.Description = "Disassembles 6502 code from emulator memory."; + Name = "start", + Description = "Start address (hex). If not specified, the current PC address is used.", + Arity = ArgumentArity.ZeroOrOne + } + .MustBe16BitHex(); - var start = cmd.Argument("start", "Start address (hex). If not specified, the current PC address is used."); - start.Validators.Add(new MustBe16BitHexValueValidator()); + var endArg = new Argument() + { + Name = "end", + Description = "End address (hex). If not specified, a default number of addresses will be shown from start.", + Arity = ArgumentArity.ZeroOrOne + } + .MustBe16BitHex() + .GreaterThan16bit(startArg); - var end = cmd.Argument("end", "End address (hex). If not specified, a default number of addresses will be shown from start."); - end.Validators.Add(new MustBe16BitHexValueValidator()); - end.Validators.Add(new GreaterThan16bitValidator(start)); + var command = new Command("d", "Disassembles 6502 code from emulator memory.") + { + startArg, + endArg + }; - cmd.OnValidationError((ValidationResult validationResult) => + command.SetHandler((string start, string end) => + { + ushort startAddress; + if (string.IsNullOrEmpty(start)) { - return monitor.WriteValidationError(validationResult); - }); + if (!monitorVariables.LatestDisassemblyAddress.HasValue) + monitorVariables.LatestDisassemblyAddress = monitor.Cpu.PC; + startAddress = monitorVariables.LatestDisassemblyAddress.Value; + } + else + { + startAddress = ushort.Parse(start, NumberStyles.AllowHexSpecifier, null); + } - cmd.OnExecute(() => + ushort? endAddress = null; + int? instructionShowCount = null; + if (string.IsNullOrEmpty(end)) { - ushort startAddress; - if (string.IsNullOrEmpty(start.Value)) - { - if (!monitorVariables.LatestDisassemblyAddress.HasValue) - monitorVariables.LatestDisassemblyAddress = monitor.Cpu.PC; - startAddress = monitorVariables.LatestDisassemblyAddress.Value; - } - else - { - startAddress = ushort.Parse(start.Value, NumberStyles.AllowHexSpecifier, null); - } + instructionShowCount = 10; + } + else + { + endAddress = ushort.Parse(end, NumberStyles.AllowHexSpecifier, null); + if (endAddress < startAddress) + endAddress = startAddress; + } - ushort? endAddress = null; - int? instructionShowCount = null; - if (string.IsNullOrEmpty(end.Value)) + ushort currentAddress = startAddress; + bool cont = true; + while (cont) + { + monitor.WriteOutput(OutputGen.GetInstructionDisassembly(monitor.Cpu, monitor.Mem, currentAddress)); + var nextInstructionAddress = monitor.Cpu.GetNextInstructionAddress(monitor.Mem, currentAddress); + + if (instructionShowCount.HasValue) { - instructionShowCount = 10; + instructionShowCount--; + if (instructionShowCount == 0) + cont = false; } else { - endAddress = ushort.Parse(end.Value, NumberStyles.AllowHexSpecifier, null); - if (endAddress < startAddress) - endAddress = startAddress; - } - - ushort currentAddress = startAddress; - bool cont = true; - while (cont) - { - monitor.WriteOutput(OutputGen.GetInstructionDisassembly(monitor.Cpu, monitor.Mem, currentAddress)); - var nextInstructionAddress = monitor.Cpu.GetNextInstructionAddress(monitor.Mem, currentAddress); - - if (instructionShowCount.HasValue) - { - instructionShowCount--; - if (instructionShowCount == 0) - cont = false; - } - else - { - if (nextInstructionAddress > endAddress || (currentAddress >= nextInstructionAddress)) - cont = false; - } - currentAddress = nextInstructionAddress; + if (nextInstructionAddress > endAddress || (currentAddress >= nextInstructionAddress)) + cont = false; } + currentAddress = nextInstructionAddress; + } - monitorVariables.LatestDisassemblyAddress = currentAddress; - - return (int)CommandResult.Ok; - }); - }); - - return app; + monitorVariables.LatestDisassemblyAddress = currentAddress; + }, startArg, endArg); + return command; } -} \ No newline at end of file +} diff --git a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/ExecutionCommands.cs b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/ExecutionCommands.cs index 4a020610..d468a9c6 100644 --- a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/ExecutionCommands.cs +++ b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/ExecutionCommands.cs @@ -1,6 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System.CommandLine; using System.Globalization; -using McMaster.Extensions.CommandLineUtils; namespace Highbyte.DotNet6502.Monitor.Commands; @@ -8,59 +7,73 @@ namespace Highbyte.DotNet6502.Monitor.Commands; /// public static class ExecutionCommands { - public static CommandLineApplication ConfigureExecution(this CommandLineApplication app, MonitorBase monitor, MonitorVariables monitorVariables) + public static Command ConfigureExecution(this Command rootCommand, MonitorBase monitor, MonitorVariables monitorVariables) { - app.Command("g", cmd => + rootCommand.AddCommand(BuildGoCommand(monitor)); + rootCommand.AddCommand(BuildSingleStepCommand(monitor)); + return rootCommand; + } + + private static Command BuildGoCommand(MonitorBase monitor) + { + var addressArg = new Argument() + { + Name = "address", + Description = "Optional address (hex) to start executing code at.", + Arity = ArgumentArity.ZeroOrOne + } + .MustBe16BitHex(); + + var command = new Command("g", "Change the PC (Program Counter) to the specified address continue execution.") + { + addressArg, + }; + + Func> handler = (string address) => + { + if (string.IsNullOrEmpty(address)) + return Task.FromResult((int)CommandResult.Continue); + + monitor.Cpu.PC = ushort.Parse(address, NumberStyles.AllowHexSpecifier, null); + //monitor.WriteOutput($"Starting executing code at {monitor.Cpu.PC.ToHex("", lowerCase: true)}"); + + return Task.FromResult((int)CommandResult.Continue); + + }; + + command.SetHandler(handler, addressArg); + return command; + } + + private static Command BuildSingleStepCommand(MonitorBase monitor) + { + var insCountArg = new Argument(() => 1) { - cmd.HelpOption(inherited: true); - cmd.Description = "Change the PC (Program Counter) to the specified address continue execution."; - cmd.AddName("goto"); - - var address = cmd.Argument("address", "Optional address (hex) to start executing code at."); - address.Validators.Add(new MustBe16BitHexValueValidator()); - - cmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); - - cmd.OnExecute(() => - { - if (string.IsNullOrEmpty(address.Value)) - return (int)CommandResult.Continue; - - monitor.Cpu.PC = ushort.Parse(address.Value, NumberStyles.AllowHexSpecifier, null); - //monitor.WriteOutput($"Starting executing code at {monitor.Cpu.PC.ToHex("", lowerCase: true)}"); - return (int)CommandResult.Continue; - }); - }); - - app.Command("z", cmd => + Name = "inscount", + Description = "Number of instructions to execute. Defaults to 1.", + Arity = ArgumentArity.ZeroOrOne, + }; + + var command = new Command("z", "Single step through instructions. Optionally execute a specified number of instructions.") { - cmd.HelpOption(inherited: true); - cmd.Description = "Single step through instructions. Optionally execute a specified number of instructions."; - - var inscount = cmd.Argument("inscount", "Number of instructions to execute. Defaults to 1."); - inscount.DefaultValue = 1; - - cmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); - - cmd.OnExecute(() => - { - //monitor.WriteOutput($"Executing code at {monitor.Cpu.PC.ToHex("",lowerCase:true)} for {inscount.Value} instruction(s)."); - monitor.Cpu.Execute(monitor.Mem, LegacyExecEvaluator.InstructionCountExecEvaluator(ulong.Parse(inscount.Value!))); - // Last instruction - // monitor.WriteOutput($"{OutputGen.GetLastInstructionDisassembly(monitor.Cpu, monitor.Mem)}"); - // Next instruction - monitor.WriteOutput($"{OutputGen.GetNextInstructionDisassembly(monitor.Cpu, monitor.Mem)}"); - - return (int)CommandResult.Ok; - }); - }); - - return app; + insCountArg + }; + + Func> handler = (ulong inscount) => + { + //monitor.WriteOutput($"Executing code at {monitor.Cpu.PC.ToHex("",lowerCase:true)} for {inscount.Value} instruction(s)."); + monitor.Cpu.Execute(monitor.Mem, LegacyExecEvaluator.InstructionCountExecEvaluator(inscount)); + // Last instruction + // monitor.WriteOutput($"{OutputGen.GetLastInstructionDisassembly(monitor.Cpu, monitor.Mem)}"); + // Next instruction + monitor.WriteOutput($"{OutputGen.GetNextInstructionDisassembly(monitor.Cpu, monitor.Mem)}"); + + return Task.FromResult((int)CommandResult.Ok); + + }; + + command.SetHandler(handler, insCountArg); + return command; } + } diff --git a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/FileCommands.cs b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/FileCommands.cs index 6c910b53..38ae6bbf 100644 --- a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/FileCommands.cs +++ b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/FileCommands.cs @@ -1,6 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System.CommandLine; using System.Globalization; -using McMaster.Extensions.CommandLineUtils; namespace Highbyte.DotNet6502.Monitor.Commands; @@ -8,126 +7,178 @@ namespace Highbyte.DotNet6502.Monitor.Commands; /// public static class FileCommands { - public static CommandLineApplication ConfigureFiles(this CommandLineApplication app, MonitorBase monitor, MonitorVariables monitorVariables) - { - app.Command("l", cmd => - { - cmd.HelpOption(inherited: true); - cmd.Description = "Load 6502 binary file from file pick dialog into emulator memory."; - cmd.AddName("load from file picker"); - var address = cmd.Argument("address", "Memory address (hex) to load the file into. If not specified, it's assumed the first two bytes of the file contains the load address."); - address.Validators.Add(new MustBe16BitHexValueValidator()); + public static Command ConfigureFiles(this Command rootCommand, MonitorBase monitor, MonitorVariables monitorVariables) + { + rootCommand.AddCommand(BuildLoadCommand(monitor)); + rootCommand.AddCommand(BuildLoadManualCommand(monitor)); + rootCommand.AddCommand(BuildSaveCommand(monitor)); + return rootCommand; + } - cmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); + private static Command BuildLoadCommand(MonitorBase monitor) + { + var addressArg = new Argument() + { + Name = "address", + Description = "Memory address (hex) to load the file into. If not specified, it's assumed the first two bytes of the file contains the load address.", + Arity = ArgumentArity.ZeroOrOne + } + .MustBe16BitHex(); - cmd.OnExecute(() => - { - ushort? forceLoadAtAddress; - - if (string.IsNullOrEmpty(address.Value)) - forceLoadAtAddress = null; - else - forceLoadAtAddress = ushort.Parse(address.Value, NumberStyles.AllowHexSpecifier, null); - - var loaded = monitor.LoadBinary(out var loadedAtAddress, out var fileLength, forceLoadAddress: forceLoadAtAddress); - if (!loaded) - { - // If file could not be loaded at this time, probably because a Web/WASM file picker dialog is asynchronus - return (int)CommandResult.Ok; - } - - monitor.WriteOutput($"File loaded at {loadedAtAddress.ToHex()}, length {fileLength.ToHex()}"); - // Set PC to start of loaded file. - monitor.Cpu.PC = loadedAtAddress; - return (int)CommandResult.Ok; - - }); - }); - app.Command("ll", cmd => + var command = new Command("l", "Load 6502 binary file from file pick dialog into emulator memory.") { - cmd.HelpOption(inherited: true); - cmd.Description = "Load specified 6502 binary file into emulator memory."; - cmd.AddName("load file"); + addressArg, + }; + command.AddAlias("load"); - var fileName = cmd.Argument("filename", "Name of the binary file.") - .IsRequired(); - //.Accepts(v => v.ExistingFile()); // Check file is done in LoadBinary(...) implementation + Func> handler = (string address) => + { + ushort? forceLoadAtAddress; - var address = cmd.Argument("address", "Memory address (hex) to load the file into. If not specified, it's assumed the first two bytes of the file contains the load address."); - address.Validators.Add(new MustBe16BitHexValueValidator()); + if (string.IsNullOrEmpty(address)) + forceLoadAtAddress = null; + else + forceLoadAtAddress = ushort.Parse(address, NumberStyles.AllowHexSpecifier, null); - cmd.OnValidationError((ValidationResult validationResult) => + var loaded = monitor.LoadBinary(out var loadedAtAddress, out var fileLength, forceLoadAddress: forceLoadAtAddress); + if (!loaded) { - return monitor.WriteValidationError(validationResult); - }); + // If file could not be loaded at this time, probably because a Web/WASM file picker dialog is asynchronus + return Task.FromResult((int)CommandResult.Ok); + } - cmd.OnExecute(() => - { - ushort? forceLoadAtAddress; + monitor.WriteOutput($"File loaded at {loadedAtAddress.ToHex()}, length {fileLength.ToHex()}"); + // Set PC to start of loaded file. + monitor.Cpu.PC = loadedAtAddress; + return Task.FromResult((int)CommandResult.Ok); - if (string.IsNullOrEmpty(address.Value)) - forceLoadAtAddress = null; - else - forceLoadAtAddress = ushort.Parse(address.Value, NumberStyles.AllowHexSpecifier, null); + }; - bool loaded = monitor.LoadBinary(fileName.Value!, out var loadedAtAddress, out var fileLength, forceLoadAddress: forceLoadAtAddress); - if (!loaded) - { - // If file could not be loaded, probably because it's not supported/implemented by the derived class. - return (int)CommandResult.Ok; - } + command.SetHandler(handler, addressArg); + return command; + } - monitor.WriteOutput($"File loaded at {loadedAtAddress.ToHex()}, length {fileLength.ToHex()}"); - // Set PC to start of loaded file. - monitor.Cpu.PC = loadedAtAddress; - return (int)CommandResult.Ok; + private static Command BuildLoadManualCommand(MonitorBase monitor) + { + var fileNameArg = new Argument() + { + Name = "filename", + Description = "Name of the binary file.", + Arity = ArgumentArity.ExactlyOne + }; + + var addressArg = new Argument() + { + Name = "address", + Description = "Memory address (hex) to load the file into. If not specified, it's assumed the first two bytes of the file contains the load address.", + Arity = ArgumentArity.ZeroOrOne + } + .MustBe16BitHex(); - }); - }); + var command = new Command("ll", "Load specified 6502 binary file into emulator memory.") + { + fileNameArg, + addressArg + }; - app.Command("s", cmd => + Func> handler = (string fileName, string address) => { - cmd.HelpOption(inherited: true); - cmd.Description = "Save a binary from 6502 emulator memory to host file system."; - cmd.AddName("save"); + ushort? forceLoadAtAddress; - var fileName = cmd.Argument("filename", "Name of the binary file.") - .IsRequired(); + if (string.IsNullOrEmpty(address)) + forceLoadAtAddress = null; + else + forceLoadAtAddress = ushort.Parse(address, NumberStyles.AllowHexSpecifier, null); - var startAddress = cmd.Argument("startAddress", "Start address (hex) of the memory area to save.") - .IsRequired(); - startAddress.Validators.Add(new MustBe16BitHexValueValidator()); + bool loaded = monitor.LoadBinary(fileName, out var loadedAtAddress, out var fileLength, forceLoadAddress: forceLoadAtAddress); + if (!loaded) + { + // If file could not be loaded, probably because it's not supported/implemented by the derived class. + return Task.FromResult((int)CommandResult.Ok); + } - var endAddress = cmd.Argument("endAddress", "End address (hex) of the memory area to save.") - .IsRequired(); - endAddress.Validators.Add(new MustBe16BitHexValueValidator()); + monitor.WriteOutput($"File loaded at {loadedAtAddress.ToHex()}, length {fileLength.ToHex()}"); + // Set PC to start of loaded file. + monitor.Cpu.PC = loadedAtAddress; + return Task.FromResult((int)CommandResult.Ok); - var addFileHeader = cmd.Argument("addFileHeader", "Optional. Set to n to NOT add a 2 byte file header with start address (usefull for data, not code)") - .Accepts(arg => arg.Values("y", "yes", "n", "no")); + }; - cmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); + command.SetHandler(handler, fileNameArg, addressArg); + return command; + } - cmd.OnExecute(() => + private static Command BuildSaveCommand(MonitorBase monitor) + { + var fileNameArg = new Argument() + { + Name = "filename", + Description = "Name of the binary file.", + Arity = ArgumentArity.ExactlyOne + }; + + var startAddressArg = new Argument() + { + Name = "startAddress", + Description = "Start address (hex) of the memory area to save.", + Arity = ArgumentArity.ExactlyOne + } + .MustBe16BitHex(); + + var endAddressArg = new Argument() + { + Name = "endAddress", + Description = "End address (hex) of the memory area to save.", + Arity = ArgumentArity.ExactlyOne + } + .MustBe16BitHex(); + + var addFileHeaderArg = new Argument() + { + Name = "addFileHeader", + Description = "Optional. Set to n to NOT add a 2 byte file header with start address (usefull for data, not code)", + Arity = ArgumentArity.ZeroOrOne, + }; + addFileHeaderArg.AddValidator( + a => { - ushort startAddressValue = ushort.Parse(startAddress.Value!, NumberStyles.AllowHexSpecifier, null); - ushort endAddressValue = ushort.Parse(endAddress.Value!, NumberStyles.AllowHexSpecifier, null); + var validationError = + a.Tokens + .Select(t => t.Value) + .Where(v => !string.IsNullOrEmpty(v) && !v.ToLower().Equals("y") && !v.ToLower().Equals("yes") && !v.ToLower().Equals("n") && !v.ToLower().Equals("no")) + .Select(_ => $"Argument '{addFileHeaderArg.Name}' must be either y, yes, n or no.") + .FirstOrDefault(); + if (validationError != null) + a.ErrorMessage = validationError; + } + ); + + + var command = new Command("s", "Save a binary from 6502 emulator memory to host file system.") + { + fileNameArg, + startAddressArg, + endAddressArg, + addFileHeaderArg + }; + command.AddAlias("save"); + + Func> handler = (string fileName, string startAddress, string endAddress, string addFileHeader) => + { + ushort startAddressValue = ushort.Parse(startAddress, NumberStyles.AllowHexSpecifier, null); + ushort endAddressValue = ushort.Parse(endAddress, NumberStyles.AllowHexSpecifier, null); + + bool addFileHeaderWithLoadAddress = string.IsNullOrEmpty(addFileHeader) + || (addFileHeader.ToLower() == "y" && addFileHeader.ToLower() == "yes"); - bool addFileHeaderWithLoadAddress = string.IsNullOrEmpty(addFileHeader.Value) - || (addFileHeader.Value.ToLower() == "y" && addFileHeader.Value.ToLower() == "yes"); + monitor.SaveBinary(fileName, startAddressValue, endAddressValue, addFileHeaderWithLoadAddress); - monitor.SaveBinary(fileName.Value!, startAddressValue, endAddressValue, addFileHeaderWithLoadAddress); + return Task.FromResult((int)CommandResult.Ok); - return (int)CommandResult.Ok; - }); - }); + }; - return app; + command.SetHandler(handler, fileNameArg, startAddressArg, endAddressArg, addFileHeaderArg); + return command; } } diff --git a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/MemoryCommands.cs b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/MemoryCommands.cs index 8267a50d..2afca6fe 100644 --- a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/MemoryCommands.cs +++ b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/MemoryCommands.cs @@ -1,6 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System.CommandLine; using System.Globalization; -using McMaster.Extensions.CommandLineUtils; namespace Highbyte.DotNet6502.Monitor.Commands; @@ -8,97 +7,113 @@ namespace Highbyte.DotNet6502.Monitor.Commands; /// public static class MemoryCommands { - public static CommandLineApplication ConfigureMemory(this CommandLineApplication app, MonitorBase monitor, MonitorVariables monitorVariables) + public static Command ConfigureMemory(this Command rootCommand, MonitorBase monitor, MonitorVariables monitorVariables) { - app.Command("m", cmd => - { - cmd.HelpOption(inherited: true); - cmd.Description = "Show contents of emulator memory in bytes."; - cmd.AddName("mem"); + rootCommand.AddCommand(BuildMemoryDumpCommand(monitor, monitorVariables)); + rootCommand.AddCommand(BuildMemoryFillCommand(monitor, monitorVariables)); + return rootCommand; + } - var start = cmd.Argument("start", "Start address (hex). If not specified, the 0000 address is used."); - start.Validators.Add(new MustBe16BitHexValueValidator()); + private static Command BuildMemoryDumpCommand(MonitorBase monitor, MonitorVariables monitorVariables) + { + var startArg = new Argument() + { + Name = "start", + Description = "Start address (hex). If not specified, the 0000 address is used.", + Arity = ArgumentArity.ZeroOrOne + } + .MustBe16BitHex(); - var end = cmd.Argument("end", "End address (hex). If not specified, a default number of memory locations will be shown from start."); - end.Validators.Add(new MustBe16BitHexValueValidator()); - end.Validators.Add(new GreaterThan16bitValidator(start)); + var endArg = new Argument() + { + Name = "end", + Description = "End address (hex). If not specified, a default number of memory locations will be shown from start.", + Arity = ArgumentArity.ZeroOrOne + } + .MustBe16BitHex() + .GreaterThan16bit(startArg); + + var command = new Command("m", "Disassembles 6502 code from emulator memory.") + { + startArg, + endArg + }; - cmd.OnValidationError((ValidationResult validationResult) => + command.SetHandler((string start, string end) => + { + ushort startAddress; + if (string.IsNullOrEmpty(start)) { - return monitor.WriteValidationError(validationResult); - }); - - cmd.OnExecute(() => + if (!monitorVariables.LatestMemoryDumpAddress.HasValue) + monitorVariables.LatestMemoryDumpAddress = 0x0000; + startAddress = monitorVariables.LatestMemoryDumpAddress.Value; + } + else { - ushort startAddress; - if (string.IsNullOrEmpty(start.Value)) - { - if (!monitorVariables.LatestMemoryDumpAddress.HasValue) - monitorVariables.LatestMemoryDumpAddress = 0x0000; - startAddress = monitorVariables.LatestMemoryDumpAddress.Value; - } - else - { - startAddress = ushort.Parse(start.Value, NumberStyles.AllowHexSpecifier, null); - } + startAddress = ushort.Parse(start, NumberStyles.AllowHexSpecifier, null); + } - ushort endAddress; - if (string.IsNullOrEmpty(end.Value)) - { - const int DEFAULT_BYTES_TO_SHOW = (16 * 8); - ushort endAddressDelta = DEFAULT_BYTES_TO_SHOW - 1; - if ((uint)((uint)startAddress + (uint)endAddressDelta) <= 0xffff) - endAddress = (ushort)(startAddress + endAddressDelta); - else - endAddress = 0xffff; - } + ushort endAddress; + if (string.IsNullOrEmpty(end)) + { + const int DEFAULT_BYTES_TO_SHOW = (16 * 8); + ushort endAddressDelta = DEFAULT_BYTES_TO_SHOW - 1; + if ((uint)((uint)startAddress + (uint)endAddressDelta) <= 0xffff) + endAddress = (ushort)(startAddress + endAddressDelta); else - { - endAddress = ushort.Parse(end.Value, NumberStyles.AllowHexSpecifier, null); - if (endAddress < startAddress) - endAddress = startAddress; - } - - var list = OutputMemoryGen.GetFormattedMemoryList(monitor.Mem, startAddress, endAddress); - foreach (var line in list) - monitor.WriteOutput(line); - - monitorVariables.LatestMemoryDumpAddress = ++endAddress; - - return (int)CommandResult.Ok; - }); - }); + endAddress = 0xffff; + } + else + { + endAddress = ushort.Parse(end, NumberStyles.AllowHexSpecifier, null); + if (endAddress < startAddress) + endAddress = startAddress; + } + + var list = OutputMemoryGen.GetFormattedMemoryList(monitor.Mem, startAddress, endAddress); + foreach (var line in list) + monitor.WriteOutput(line); + + monitorVariables.LatestMemoryDumpAddress = ++endAddress; + }, startArg, endArg); + return command; + } - app.Command("f", cmd => + private static Command BuildMemoryFillCommand(MonitorBase monitor, MonitorVariables monitorVariables) + { + var addressArg = new Argument() { - cmd.HelpOption(inherited: true); - cmd.Description = $"Fill memory at specified address with a list of bytes.{Environment.NewLine} Example: f 1000 20 ff ab 30"; - cmd.AddName("fill"); - - var memAddress = cmd.Argument("address", "Memory address (hex).").IsRequired(); - memAddress.Validators.Add(new MustBe16BitHexValueValidator()); - - var memValues = cmd.Argument("values", "List of byte values (hex). Example: 20 ff ab 30").IsRequired(); - memValues.MultipleValues = true; - memValues.Validators.Add(new MustBe8BitHexValueValidator()); + Name = "address", + Description = "Memory address (hex).", + Arity = ArgumentArity.ExactlyOne + } + .MustBe16BitHex(); - cmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); + var valuesArg = new Argument() + { + Name = "values", + Description = "List of byte values (hex). Example: 20 ff ab 30", + Arity = ArgumentArity.OneOrMore + } + .MustBe8BitHex(); - cmd.OnExecute(() => - { - var address = ushort.Parse(memAddress.Value!, NumberStyles.AllowHexSpecifier, null); - List bytes = new(); - foreach (var val in memValues.Values) - bytes.Add(byte.Parse(val!, NumberStyles.AllowHexSpecifier, null)); - foreach (var val in bytes) - monitor.Mem[address++] = val; - return (int)CommandResult.Ok; - }); - }); + var command = new Command("f", $"Fill memory at specified address with a list of bytes.{Environment.NewLine}Example: f 1000 20 ff ab 30") + { + addressArg, + valuesArg + }; + command.AddAlias("fill"); - return app; + command.SetHandler((string memAddress, string[] memValues) => + { + var address = ushort.Parse(memAddress, NumberStyles.AllowHexSpecifier, null); + List bytes = new(); + foreach (var val in memValues) + bytes.Add(byte.Parse(val!, NumberStyles.AllowHexSpecifier, null)); + foreach (var val in bytes) + monitor.Mem[address++] = val; + + }, addressArg, valuesArg); + return command; } } diff --git a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/OptionCommands.cs b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/OptionCommands.cs index 547af4ee..4eb296f0 100644 --- a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/OptionCommands.cs +++ b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/OptionCommands.cs @@ -1,5 +1,4 @@ -using System.ComponentModel.DataAnnotations; -using McMaster.Extensions.CommandLineUtils; +using System.CommandLine; namespace Highbyte.DotNet6502.Monitor.Commands; @@ -7,68 +6,72 @@ namespace Highbyte.DotNet6502.Monitor.Commands; /// public static class OptionCommands { - public static CommandLineApplication ConfigureOptions(this CommandLineApplication app, MonitorBase monitor, MonitorVariables monitorVariables) + public static Command ConfigureOptions(this Command rootCommand, MonitorBase monitor, MonitorVariables monitorVariables) { - app.Command("o", cmd => - { - cmd.HelpOption(inherited: true); - cmd.Description = "Show global options"; - cmd.AddName("options"); + rootCommand.AddCommand(BuildOptionsCommand(monitor, monitorVariables)); + return rootCommand; + } - cmd.Command("u", setRegisterCmd => - { - setRegisterCmd.Description = "Flag how to handle unknown instructions (0 = continue, 1 = stop)."; - var uVal = setRegisterCmd.Argument("flag", "Unknown instruction flag").IsRequired(); - uVal.Validators.Add(new MustBeIntegerFlag()); + private static Command BuildOptionsCommand(MonitorBase monitor, MonitorVariables monitorVariables) + { - setRegisterCmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); + // o u + var uValArg = new Argument() + { + Name = "flag", + Description = "Unknown instruction flag", + Arity = ArgumentArity.ExactlyOne + } + .MustBeIntegerFlag(); - setRegisterCmd.OnExecute(() => - { - var value = uVal.Value; - monitor.Options.StopAfterUnknownInstruction = value == "1"; - monitor.ApplyOptionsOnBreakPointExecEvaluator(); - monitor.ShowOptions(); - return (int)CommandResult.Ok; - }); - }); + var unknownInsCommand = new Command("u", "Flag how to handle unknown instructions (0 = continue, 1 = stop).") + { + uValArg + }; + unknownInsCommand.SetHandler((string uVal) => + { + var value = uVal; + monitor.Options.StopAfterUnknownInstruction = value == "1"; + monitor.ApplyOptionsOnBreakPointExecEvaluator(); + monitor.ShowOptions(); + }, uValArg); - cmd.Command("b", setRegisterCmd => - { - setRegisterCmd.Description = "Flag how to handle BRK instruction (0 = continue, 1 = stop)."; - var bVal = setRegisterCmd.Argument("flag", "BRK instruction flag").IsRequired(); - bVal.Validators.Add(new MustBeIntegerFlag()); - setRegisterCmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); + // o b + var bValArg = new Argument() + { + Name = "flag", + Description = "BRK instruction flag", + Arity = ArgumentArity.ExactlyOne + } + .MustBeIntegerFlag(); - setRegisterCmd.OnExecute(() => - { - var value = bVal.Value; - monitor.Options.StopAfterBRKInstruction = value == "1"; - monitor.ApplyOptionsOnBreakPointExecEvaluator(); - monitor.ShowOptions(); - return (int)CommandResult.Ok; - }); - }); + var bpCommand = new Command("b", "Flag how to handle BRK instruction (0 = continue, 1 = stop).") + { + bValArg + }; + bpCommand.SetHandler((string bVal) => + { + var value = bVal; + monitor.Options.StopAfterBRKInstruction = value == "1"; + monitor.ApplyOptionsOnBreakPointExecEvaluator(); + monitor.ShowOptions(); + }, bValArg); - cmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); - cmd.OnExecute(() => - { - monitor.ShowOptions(); - return (int)CommandResult.Ok; - }); + // o + var command = new Command("o", "Show global options.") + { + unknownInsCommand, + bpCommand + }; + command.AddAlias("options"); + // Default command for "o" if no subcommand is specified. + command.SetHandler(() => + { + monitor.ShowOptions(); }); - return app; + return command; } } diff --git a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/RegisterCommands.cs b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/RegisterCommands.cs index 5c1ce1d5..2c040b8b 100644 --- a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/RegisterCommands.cs +++ b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/RegisterCommands.cs @@ -1,6 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System.CommandLine; using System.Globalization; -using McMaster.Extensions.CommandLineUtils; namespace Highbyte.DotNet6502.Monitor.Commands; @@ -8,147 +7,146 @@ namespace Highbyte.DotNet6502.Monitor.Commands; /// public static class RegisterCommands { - public static CommandLineApplication ConfigureRegisters(this CommandLineApplication app, MonitorBase monitor, MonitorVariables monitorVariables) + public static Command ConfigureRegisters(this Command rootCommand, MonitorBase monitor, MonitorVariables monitorVariables) { - app.Command("r", cmd => - { - cmd.HelpOption(inherited: true); - cmd.Description = "Show processor status and registers. CY = #cycles executed."; - cmd.AddName("reg"); - - cmd.Command("a", setRegisterCmd => - { - setRegisterCmd.Description = "Sets A register."; - var regVal = setRegisterCmd.Argument("value", "Value of A register (hex).").IsRequired(); - regVal.Validators.Add(new MustBe8BitHexValueValidator()); - - setRegisterCmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); - - setRegisterCmd.OnExecute(() => - { - var value = regVal.Value!; - monitor.Cpu.A = byte.Parse(value, NumberStyles.AllowHexSpecifier, null); - monitor.WriteOutput($"{OutputGen.GetRegisters(monitor.Cpu)}"); - return (int)CommandResult.Ok; - }); - }); - - cmd.Command("x", setRegisterCmd => - { - setRegisterCmd.Description = "Sets X register."; - var regVal = setRegisterCmd.Argument("value", "Value of X register (hex).").IsRequired(); - regVal.Validators.Add(new MustBe8BitHexValueValidator()); - - setRegisterCmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); - - setRegisterCmd.OnExecute(() => - { - var value = regVal.Value!; - monitor.Cpu.X = byte.Parse(value, NumberStyles.AllowHexSpecifier, null); - monitor.WriteOutput($"{OutputGen.GetRegisters(monitor.Cpu)}"); - return (int)CommandResult.Ok; - }); - }); - - cmd.Command("y", setRegisterCmd => - { - setRegisterCmd.Description = "Sets Y register."; - var regVal = setRegisterCmd.Argument("value", "Value of Y register (hex).").IsRequired(); - regVal.Validators.Add(new MustBe8BitHexValueValidator()); - - setRegisterCmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); - - setRegisterCmd.OnExecute(() => - { - var value = regVal.Value!; - monitor.Cpu.Y = byte.Parse(value, NumberStyles.AllowHexSpecifier, null); - monitor.WriteOutput($"{OutputGen.GetRegisters(monitor.Cpu)}"); - return (int)CommandResult.Ok; - }); - }); - - cmd.Command("sp", setRegisterCmd => - { - setRegisterCmd.Description = "Sets SP (Stack Pointer)."; - var regVal = setRegisterCmd.Argument("value", "Value of SP (hex).").IsRequired(); - regVal.Validators.Add(new MustBe8BitHexValueValidator()); - - setRegisterCmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); - - setRegisterCmd.OnExecute(() => - { - var value = regVal.Value!; - monitor.Cpu.SP = byte.Parse(value, NumberStyles.AllowHexSpecifier, null); - monitor.WriteOutput($"{OutputGen.GetPCandSP(monitor.Cpu)}"); - return (int)CommandResult.Ok; - }); - }); - - cmd.Command("ps", setRegisterCmd => - { - setRegisterCmd.Description = "Sets processor status register."; - var regVal = setRegisterCmd.Argument("value", "Value of processor status register (hex).").IsRequired(); - regVal.Validators.Add(new MustBe8BitHexValueValidator()); - - setRegisterCmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); - - setRegisterCmd.OnExecute(() => - { - var value = regVal.Value!; - monitor.Cpu.ProcessorStatus.Value = byte.Parse(value, NumberStyles.AllowHexSpecifier, null); - monitor.WriteOutput($"PS={value}"); - monitor.WriteOutput($"{OutputGen.GetStatus(monitor.Cpu)}"); - return (int)CommandResult.Ok; - }); - }); - - cmd.Command("pc", setRegisterCmd => - { - setRegisterCmd.Description = "Sets PC (Program Counter)."; - var regVal = setRegisterCmd.Argument("value", "Value of PC (hex).").IsRequired(); - regVal.Validators.Add(new MustBe16BitHexValueValidator()); - - setRegisterCmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); - - setRegisterCmd.OnExecute(() => - { - var value = regVal.Value; - monitor.Cpu.PC = ushort.Parse(value!, NumberStyles.AllowHexSpecifier, null); - monitor.WriteOutput($"{OutputGen.GetPCandSP(monitor.Cpu)}"); - return (int)CommandResult.Ok; - }); - }); - - cmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); - - cmd.OnExecute(() => - { - monitor.WriteOutput(OutputGen.GetProcessorState(monitor.Cpu, includeCycles: true)); - return (int)CommandResult.Ok; - }); + rootCommand.AddCommand(BuildRegistersCommand(monitor, monitorVariables)); + return rootCommand; + } + + private static Command BuildRegistersCommand(MonitorBase monitor, MonitorVariables monitorVariables) + { + + // r a + var regAArg = new Argument() + { + Name = "value", + Description = "Value of A register (hex).", + Arity = ArgumentArity.ExactlyOne + } + .MustBe8BitHex(); + + var setRegACommand = new Command("a", "Sets A register.") + { + regAArg + }; + setRegACommand.SetHandler((string reg) => + { + monitor.Cpu.A = byte.Parse(reg, NumberStyles.AllowHexSpecifier, null); + monitor.WriteOutput($"{OutputGen.GetRegisters(monitor.Cpu)}"); + }, regAArg); + + + // r x + var regXArg = new Argument() + { + Name = "value", + Description = "Value of X register (hex).", + Arity = ArgumentArity.ExactlyOne + } + .MustBe8BitHex(); + + var setRegXCommand = new Command("x", "Sets X register.") + { + regXArg + }; + setRegXCommand.SetHandler((string reg) => + { + monitor.Cpu.X = byte.Parse(reg, NumberStyles.AllowHexSpecifier, null); + monitor.WriteOutput($"{OutputGen.GetRegisters(monitor.Cpu)}"); + }, regXArg); + + // r y + var regYArg = new Argument() + { + Name = "value", + Description = "Value of Y register (hex).", + Arity = ArgumentArity.ExactlyOne + } + .MustBe8BitHex(); + + var setRegYCommand = new Command("y", "Sets Y register.") + { + regYArg + }; + setRegYCommand.SetHandler((string reg) => + { + monitor.Cpu.Y = byte.Parse(reg, NumberStyles.AllowHexSpecifier, null); + monitor.WriteOutput($"{OutputGen.GetRegisters(monitor.Cpu)}"); + }, regYArg); + + // r sp + var regSPArg = new Argument() + { + Name = "value", + Description = "Value of SP register (hex).", + Arity = ArgumentArity.ExactlyOne + } + .MustBe8BitHex(); + + var setRegSPCommand = new Command("sp", "Sets SP register.") + { + regSPArg + }; + setRegSPCommand.SetHandler((string reg) => + { + monitor.Cpu.SP = byte.Parse(reg, NumberStyles.AllowHexSpecifier, null); + monitor.WriteOutput($"{OutputGen.GetPCandSP(monitor.Cpu)}"); + }, regSPArg); + + // r ps + var regPSArg = new Argument() + { + Name = "value", + Description = "Value of PS register (hex).", + Arity = ArgumentArity.ExactlyOne + } + .MustBe8BitHex(); + + var setRegPSCommand = new Command("ps", "Sets PS register.") + { + regPSArg + }; + setRegPSCommand.SetHandler((string reg) => + { + monitor.Cpu.ProcessorStatus.Value = byte.Parse(reg, NumberStyles.AllowHexSpecifier, null); + monitor.WriteOutput($"{OutputGen.GetStatus(monitor.Cpu)}"); + }, regPSArg); + + // r pc + var regPCArg = new Argument() + { + Name = "value", + Description = "Value of PC register (hex).", + Arity = ArgumentArity.ExactlyOne + } + .MustBe16BitHex(); + + var setRegPCCommand = new Command("pc", "Sets PC register.") + { + regPCArg + }; + setRegPCCommand.SetHandler((string reg) => + { + monitor.Cpu.PC = ushort.Parse(reg, NumberStyles.AllowHexSpecifier, null); + monitor.WriteOutput($"{OutputGen.GetPCandSP(monitor.Cpu)}"); + }, regPCArg); + + // r + var command = new Command("r", "Show processor status and registers. CY = #cycles executed.") + { + setRegACommand, + setRegXCommand, + setRegYCommand, + setRegSPCommand, + setRegPSCommand, + setRegPCCommand + }; + command.AddAlias("reg"); + command.SetHandler(() => + { + monitor.WriteOutput(OutputGen.GetProcessorState(monitor.Cpu, includeCycles: true)); }); - return app; + return command; } } diff --git a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/ResetCommands.cs b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/ResetCommands.cs index d2520e59..28c82f00 100644 --- a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/ResetCommands.cs +++ b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/ResetCommands.cs @@ -1,5 +1,4 @@ -using System.ComponentModel.DataAnnotations; -using McMaster.Extensions.CommandLineUtils; +using System.CommandLine; namespace Highbyte.DotNet6502.Monitor.Commands; @@ -7,25 +6,24 @@ namespace Highbyte.DotNet6502.Monitor.Commands; /// public static class ResetCommands { - public static CommandLineApplication ConfigureReset(this CommandLineApplication app, MonitorBase monitor, MonitorVariables monitorVariables) + public static Command ConfigureReset(this Command rootCommand, MonitorBase monitor, MonitorVariables monitorVariables) { - app.Command("reset", cmd => - { - cmd.HelpOption(inherited: true); - cmd.Description = "Resets the computer (soft, memory intact)."; - - cmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); + rootCommand.AddCommand(BuildResetCommand(monitor, monitorVariables)); + return rootCommand; + } - cmd.OnExecute(() => - { - monitor.Cpu.Reset(monitor.Mem); // A soft reset, memory not cleared. - return (int)CommandResult.Continue; - }); + private static Command BuildResetCommand(MonitorBase monitor, MonitorVariables monitorVariables) + { + // r + var command = new Command("reset", "Resets the computer (soft, memory intact).") + { + }; + command.SetHandler(() => + { + monitor.Cpu.Reset(monitor.Mem); // A soft reset, memory not cleared. + return Task.FromResult((int)CommandResult.Continue); }); - return app; + return command; } -} \ No newline at end of file +} diff --git a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/ValidationHelpers.cs b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/ValidationHelpers.cs index 13f7677d..6329560b 100644 --- a/src/libraries/Highbyte.DotNet6502.Monitor/Commands/ValidationHelpers.cs +++ b/src/libraries/Highbyte.DotNet6502.Monitor/Commands/ValidationHelpers.cs @@ -1,7 +1,6 @@ +using System.CommandLine; using System.ComponentModel.DataAnnotations; using System.Globalization; -using McMaster.Extensions.CommandLineUtils; -using McMaster.Extensions.CommandLineUtils.Validation; namespace Highbyte.DotNet6502.Monitor.Commands; @@ -9,106 +8,98 @@ namespace Highbyte.DotNet6502.Monitor.Commands; /// public static class ValidationHelpers { - public static int WriteValidationError(this MonitorBase monitor, ValidationResult validationResult) + public static Argument MustBe16BitHex(this Argument argument) { - monitor.WriteOutput(!string.IsNullOrEmpty(validationResult.ErrorMessage) - ? validationResult.ErrorMessage - : "Unknown validation message", MessageSeverity.Error); - return (int)CommandResult.Ok; - } -} - -public class MustBe16BitHexValueValidator : IArgumentValidator -{ - public ValidationResult GetValidationResult(CommandArgument argument, ValidationContext context) - { - // This validator only runs if there is a value - if (string.IsNullOrEmpty(argument.Value)) - return ValidationResult.Success!; //return new ValidationResult($"{argument.Name} cannot be empty"); + argument.AddValidator( + a => + { + var validationError = + a.Tokens + .Select(t => t.Value) + .Where(v => !ushort.TryParse(v, NumberStyles.AllowHexSpecifier, null, out ushort _)) + .Select(_ => $"Argument '{argument.Name}' is not a 16-bit hex value.") + .FirstOrDefault(); + if (validationError != null) + a.ErrorMessage = validationError; + } + ); - var addressString = argument.Value; - bool validAddress = ushort.TryParse(addressString, NumberStyles.AllowHexSpecifier, null, out ushort word); - if (!validAddress) - { - return new ValidationResult($"The value for {argument.Name} must be a 16-bit hex address"); - } - return ValidationResult.Success!; + return argument; } -} -public class MustBe8BitHexValueValidator : IArgumentValidator -{ - public ValidationResult GetValidationResult(CommandArgument argument, ValidationContext context) + public static Argument MustBe8BitHex(this Argument argument) { - if (argument.MultipleValues) - { - bool atLeastOneValueIsInvalid = false; - foreach (var value in argument.Values) + argument.AddValidator( + a => { - if (!IsValidValue(value!)) - { - atLeastOneValueIsInvalid = true; - } + var validationError = + a.Tokens + .Select(t => t.Value) + .Where(v => !byte.TryParse(v, NumberStyles.AllowHexSpecifier, null, out byte _)) + .Select(_ => $"Argument '{argument.Name}' has one or more values that are not a 8-bit hex value.") + .FirstOrDefault(); + if (validationError != null) + a.ErrorMessage = validationError; } + ); - if (!atLeastOneValueIsInvalid) - return ValidationResult.Success!; - else - return new ValidationResult($"The value for {argument.Name} must be a space-separated list of 8-bit hex numbers"); - } - else - { - if (IsValidValue(argument.Value!)) - return ValidationResult.Success!; - else - return new ValidationResult($"The value for {argument.Name} must be a 8-bit hex number"); - } + return argument; } - private bool IsValidValue(string value) + public static Argument MustBe8BitHex(this Argument argument) { - // This validator only runs if there is a value - if (string.IsNullOrEmpty(value)) - return true; - return byte.TryParse(value, NumberStyles.AllowHexSpecifier, null, out byte byteValue); - } -} - -public class GreaterThan16bitValidator : IArgumentValidator -{ - private readonly CommandArgument _otherArgument; - private readonly bool _ignoreUndefined; + argument.AddValidator( + a => + { + var validationError = + a.Tokens + .Select(t => t.Value) + .Where(v => !byte.TryParse(v, NumberStyles.AllowHexSpecifier, null, out byte _)) + .Select(_ => $"Argument '{argument.Name}' has one or more values that are not a 8-bit hex value.") + .FirstOrDefault(); + if (validationError != null) + a.ErrorMessage = validationError; + } + ); - public GreaterThan16bitValidator(CommandArgument otherArgument, bool ignoreUndefined = true) - { - _otherArgument = otherArgument; - _ignoreUndefined = ignoreUndefined; + return argument; } - public ValidationResult GetValidationResult(CommandArgument argument, ValidationContext context) + + public static Argument GreaterThan16bit(this Argument argument, Argument otherArgument, bool ignoreUndefined = true) { - if (_ignoreUndefined && string.IsNullOrEmpty(argument.Value)) - return ValidationResult.Success!; + argument.AddValidator( + a => + { + var valueRaw = a.Tokens[0].Value; + if (ignoreUndefined && string.IsNullOrEmpty(valueRaw)) + return; - var value = ushort.Parse(argument.Value!, NumberStyles.AllowHexSpecifier, null); - var otherValue = ushort.Parse(_otherArgument.Value!, NumberStyles.AllowHexSpecifier, null); - return value > otherValue ? ValidationResult.Success! : new ValidationResult($"The 16 bit value {argument.Name} ({argument.Value}) must higher than {_otherArgument.Name} ({_otherArgument.Value})"); + var value = ushort.Parse(valueRaw, NumberStyles.AllowHexSpecifier, null); + var otherValue = ushort.Parse(a.GetValueForArgument(otherArgument), NumberStyles.AllowHexSpecifier, null); + if (value <= otherValue) + a.ErrorMessage = $"The 16 bit value {argument.Name} ({value}) must higher than {otherArgument.Name} ({otherValue})"; + } + ); + return argument; } -} -public class MustBeIntegerFlag : IArgumentValidator -{ - public ValidationResult GetValidationResult(CommandArgument argument, ValidationContext context) + public static Argument MustBeIntegerFlag(this Argument argument) { - // This validator only runs if there is a value - if (string.IsNullOrEmpty(argument.Value)) - return ValidationResult.Success!; //return new ValidationResult($"{argument.Name} cannot be empty"); + argument.AddValidator( + a => + { + // This validator only runs if there is a value + if (string.IsNullOrEmpty(a.Tokens[0].Value)) + return; - bool validByte = byte.TryParse(argument.Value, NumberStyles.AllowHexSpecifier, null, out byte byteValue); - if (!validByte || byteValue > 1) - { - return new ValidationResult($"The value for {argument.Name} must be 0 or 1"); - } - return ValidationResult.Success!; + bool validByte = byte.TryParse(a.Tokens[0].Value, NumberStyles.AllowHexSpecifier, null, out byte byteValue); + if (!validByte || byteValue > 1) + { + a.ErrorMessage = $"The value for {argument.Name} must be 0 or 1"; + } + } + ); + return argument; } } diff --git a/src/libraries/Highbyte.DotNet6502.Monitor/CustomHelpBuilderWithourRootCommand.cs b/src/libraries/Highbyte.DotNet6502.Monitor/CustomHelpBuilderWithourRootCommand.cs new file mode 100644 index 00000000..a8c4257e --- /dev/null +++ b/src/libraries/Highbyte.DotNet6502.Monitor/CustomHelpBuilderWithourRootCommand.cs @@ -0,0 +1,58 @@ +using System.CommandLine; +using System.CommandLine.Help; +using System.Text; + +namespace Highbyte.DotNet6502.Monitor; + +/// +/// HelpBuilder that removes the root command name from the help text. +/// +internal class CustomHelpBuilderWithourRootCommand : HelpBuilder +{ + private readonly string _rootCommandName; + + internal CustomHelpBuilderWithourRootCommand( + LocalizationResources localizationResources, + string rootCommandName, + int maxWidth = int.MaxValue) + : base(localizationResources, maxWidth) + { + _rootCommandName = rootCommandName; + } + + public override void Write(HelpContext context) + { + var customTextWriter = new CustomTextWriterWithoutRootCommand(context.Output, _rootCommandName); + var newHelpContext = new HelpContext(context.HelpBuilder, context.Command, customTextWriter, context.ParseResult); + + base.Write(newHelpContext); + } + + private class CustomTextWriterWithoutRootCommand : TextWriter + { + private readonly TextWriter _originalTextWriter; + private readonly string _rootCommandName; + + public CustomTextWriterWithoutRootCommand(TextWriter originalTextWriter, string rootCommandName) + { + _originalTextWriter = originalTextWriter; + _rootCommandName = rootCommandName; + } + + public override void Write(char value) + { + _originalTextWriter.Write(value); + } + + public override void Write(string? value) + { + if (value != null && value.StartsWith(_rootCommandName)) + { + value = value.Replace(_rootCommandName, "").TrimStart(); + } + _originalTextWriter.Write(value); + } + + public override Encoding Encoding => _originalTextWriter.Encoding; + } +} diff --git a/src/libraries/Highbyte.DotNet6502.Monitor/Highbyte.DotNet6502.Monitor.csproj b/src/libraries/Highbyte.DotNet6502.Monitor/Highbyte.DotNet6502.Monitor.csproj index 026c6071..c003e062 100644 --- a/src/libraries/Highbyte.DotNet6502.Monitor/Highbyte.DotNet6502.Monitor.csproj +++ b/src/libraries/Highbyte.DotNet6502.Monitor/Highbyte.DotNet6502.Monitor.csproj @@ -25,11 +25,11 @@ - + - + diff --git a/src/libraries/Highbyte.DotNet6502.Monitor/MonitorBase.cs b/src/libraries/Highbyte.DotNet6502.Monitor/MonitorBase.cs index 582ed3f4..d406db2c 100644 --- a/src/libraries/Highbyte.DotNet6502.Monitor/MonitorBase.cs +++ b/src/libraries/Highbyte.DotNet6502.Monitor/MonitorBase.cs @@ -1,6 +1,7 @@ using Highbyte.DotNet6502.Monitor.SystemSpecific; using Highbyte.DotNet6502.Systems; -using McMaster.Extensions.CommandLineUtils; +using System.CommandLine; +using System.CommandLine.Parsing; namespace Highbyte.DotNet6502.Monitor; @@ -24,7 +25,8 @@ public abstract class MonitorBase private readonly Dictionary _breakPoints = new(); public Dictionary BreakPoints => _breakPoints; - private CommandLineApplication _commandLineApp; + private readonly Parser _commandLineApp; + private readonly MonitorConsole _console; public MonitorBase(SystemRunner systemRunner, MonitorConfig options) { @@ -38,7 +40,8 @@ public MonitorBase(SystemRunner systemRunner, MonitorConfig options) ApplyOptionsOnBreakPointExecEvaluator(); _variables = new MonitorVariables(); - _commandLineApp = CommandLineApp.Build(this, _variables, options); + _console = MonitorConsole.BuildSingleton(this); + _commandLineApp = CommandLineApp.Build(this, _variables, options, _console); } public CommandResult SendCommand(string command) @@ -46,19 +49,16 @@ public CommandResult SendCommand(string command) if (string.IsNullOrEmpty(command)) return CommandResult.Ok; - if (string.Equals(command, "?", StringComparison.InvariantCultureIgnoreCase) - || string.Equals(command, "-?", StringComparison.InvariantCultureIgnoreCase) - || string.Equals(command, "help", StringComparison.InvariantCultureIgnoreCase) - || string.Equals(command, "--help", StringComparison.InvariantCultureIgnoreCase)) + var cmdLine = $"{_commandLineApp.Configuration.RootCommand.Name} {command}".Split(' '); + var parseResult = _commandLineApp.Parse(cmdLine); + if (parseResult.Errors.Count > 0) { - ShowHelp(); - return CommandResult.Ok; + foreach (var error in parseResult.Errors) + WriteOutput(error.Message, MessageSeverity.Error); + return CommandResult.Error; } - - // Workaround for CommandLineUtils after showing help once, it will always show it for every command, even if syntax is correct. - // Create new instance for every time we parse input - _commandLineApp = CommandLineApp.Build(this, _variables, Options); - var result = (CommandResult)_commandLineApp.Execute(command.Split(' ')); + var invokeResult = _commandLineApp.Invoke(cmdLine, _console); + var result = (CommandResult)invokeResult; return result; } @@ -109,16 +109,13 @@ public void ShowInfoAfterBreakTriggerEnabled(ExecEvaluatorTriggerResult execEval } public void ShowDescription() { - if (_commandLineApp.Description != null) - WriteOutput(_commandLineApp.Description); + if (_commandLineApp.Configuration.RootCommand.Description != null) + WriteOutput(_commandLineApp.Configuration.RootCommand.Description); } public void ShowHelp() { - var helpText = _commandLineApp.GetHelpText(); - var helpTextLines = helpText.Split(Environment.NewLine); - foreach (var line in helpTextLines) - WriteOutput(line); + _commandLineApp.Invoke("?", _console); } public virtual void ShowOptions() diff --git a/src/libraries/Highbyte.DotNet6502.Monitor/MonitorConsole.cs b/src/libraries/Highbyte.DotNet6502.Monitor/MonitorConsole.cs index 15e2bffa..3ea1816a 100644 --- a/src/libraries/Highbyte.DotNet6502.Monitor/MonitorConsole.cs +++ b/src/libraries/Highbyte.DotNet6502.Monitor/MonitorConsole.cs @@ -1,105 +1,54 @@ +using System.CommandLine; +using System.CommandLine.IO; using System.Text; -using McMaster.Extensions.CommandLineUtils; namespace Highbyte.DotNet6502.Monitor; /// -/// McMaster CommandLine console implementation that does nothing except prints to our MonitorBase. +/// System.CommandLine.IConsole implementation that does nothing except prints to our MonitorBase. /// This is for output that cannot be controlled by our application, like help texts details per command. -/// By default McMaster CommandLine writes to a system console, which doesn't exist unless hosted in a .NET Console app. +/// By default System.CommandLine writes to a system console, which doesn't exist unless hosted in a .NET Console app. /// public class MonitorConsole : IConsole { private readonly MonitorBase _monitor; + private readonly MonitorStandardStreamWriter _monitorStandardStreamWriter; private MonitorConsole(MonitorBase monitor) { - Error = Out = new MonitorTextWriter(monitor); _monitor = monitor; + _monitorStandardStreamWriter = new MonitorStandardStreamWriter(_monitor); } - /// - /// A shared instance of . - /// - public static MonitorConsole BuildSingleton(MonitorBase monitor) - { - return new MonitorConsole(monitor); - } + public IStandardStreamWriter Out => _monitorStandardStreamWriter; - /// - /// A writer that does nothing. - /// - public TextWriter Out { get; } - - /// - /// A writer that does nothing. - /// - public TextWriter Error { get; } + public IStandardStreamWriter Error => _monitorStandardStreamWriter; - /// - /// An empty reader. - /// - public TextReader In { get; } = new StringReader(string.Empty); + public bool IsOutputRedirected { get; protected set; } - /// - /// Always false. - /// - public bool IsInputRedirected => false; + public bool IsErrorRedirected { get; protected set; } - /// - /// Always false. - /// - public bool IsOutputRedirected => false; + public bool IsInputRedirected { get; protected set; } /// - /// Always false. - /// - public bool IsErrorRedirected => false; - - public ConsoleColor ForegroundColor { get; set; } - - public ConsoleColor BackgroundColor { get; set; } - - /// - /// This event never fires. + /// A shared instance of . /// - public event ConsoleCancelEventHandler? CancelKeyPress - { - add { } - remove { } - } - - // public override bool Equals(object? obj) - // { - // return base.Equals(obj); - // } - - // public override int GetHashCode() - // { - // return base.GetHashCode(); - // } - - public void ResetColor() - { - } - - public override string? ToString() + public static MonitorConsole BuildSingleton(MonitorBase monitor) { - return base.ToString(); + return new MonitorConsole(monitor); } - private sealed class MonitorTextWriter : TextWriter + internal class MonitorStandardStreamWriter : TextWriter, IStandardStreamWriter { - List _printedChars = new(); + readonly List _printedChars = new(); private readonly MonitorBase _monitor; - public override Encoding Encoding => Encoding.Unicode; - - public MonitorTextWriter(MonitorBase monitor) + public MonitorStandardStreamWriter(MonitorBase monitor) { _monitor = monitor; } + public override void Write(char value) { _printedChars.Add(value); @@ -115,5 +64,21 @@ public override void Write(char value) } } } + + public override void Write(string? value) + { + if (value is null) + return; + foreach (var c in value) + Write(c); + } + + public override Encoding Encoding { get; } = Encoding.Unicode; + + public override string ToString() + { + var str = _printedChars.ToString(); + return str is null ? "" : str; + } } -} \ No newline at end of file +} diff --git a/src/libraries/Highbyte.DotNet6502.Monitor/SystemSpecific/ISystemMonitorCommands.cs b/src/libraries/Highbyte.DotNet6502.Monitor/SystemSpecific/ISystemMonitorCommands.cs index 0eb92a5d..2d2a680e 100644 --- a/src/libraries/Highbyte.DotNet6502.Monitor/SystemSpecific/ISystemMonitorCommands.cs +++ b/src/libraries/Highbyte.DotNet6502.Monitor/SystemSpecific/ISystemMonitorCommands.cs @@ -1,4 +1,4 @@ -using McMaster.Extensions.CommandLineUtils; +using System.CommandLine; namespace Highbyte.DotNet6502.Monitor.SystemSpecific; @@ -6,6 +6,6 @@ namespace Highbyte.DotNet6502.Monitor.SystemSpecific; /// public interface ISystemMonitorCommands { - public void Configure(CommandLineApplication app, MonitorBase monitor); + public void Configure(Command rootCommand, MonitorBase monitor); public void Reset(MonitorBase monitor); -} \ No newline at end of file +} diff --git a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Monitor/C64MonitorCommands.cs b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Monitor/C64MonitorCommands.cs index 88270ea0..0ba38212 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Monitor/C64MonitorCommands.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Monitor/C64MonitorCommands.cs @@ -1,8 +1,6 @@ -using System.ComponentModel.DataAnnotations; +using System.CommandLine; using Highbyte.DotNet6502.Monitor; -using Highbyte.DotNet6502.Monitor.Commands; using Highbyte.DotNet6502.Monitor.SystemSpecific; -using McMaster.Extensions.CommandLineUtils; namespace Highbyte.DotNet6502.Systems.Commodore64.Monitor; @@ -11,91 +9,100 @@ namespace Highbyte.DotNet6502.Systems.Commodore64.Monitor; /// public class C64MonitorCommands : ISystemMonitorCommands { - public void Configure(CommandLineApplication app, MonitorBase monitor) + public void Configure(Command rootCommand, MonitorBase monitor) { - app.Command("lb", cmd => + rootCommand.AddCommand(BuildLoadBasicCommand(monitor)); + rootCommand.AddCommand(BuildLoadBasicManualCommand(monitor)); + rootCommand.AddCommand(BuildSaveBasicCommand(monitor)); + } + + private static Command BuildLoadBasicCommand(MonitorBase monitor) + { + var command = new Command("lb", "C64 - Load a CBM Basic 2.0 PRG file from file picker dialog.") { - cmd.HelpOption(inherited: true); - cmd.Description = "C64 - Load a CBM Basic 2.0 PRG file from file picker dialog."; - cmd.AddName("loadbasic from filepicker"); + }; + command.AddAlias("loadbasic"); - cmd.OnValidationError((ValidationResult validationResult) => + Func> handler = () => + { + // Basic file should have a start address of 0801 stored as the two first bytes (little endian order, 01 08). + var loaded = monitor.LoadBinary(out var loadedAtAddress, out var fileLength, null, AfterLoadBasic); + if (!loaded) { - return monitor.WriteValidationError(validationResult); - }); + // If file could not be loaded at this time, probably because a Web/WASM file picker dialog is asynchronus + return Task.FromResult((int)CommandResult.Ok); + } + AfterLoadBasic(monitor, loadedAtAddress, fileLength); + return Task.FromResult((int)CommandResult.Ok); + }; - cmd.OnExecute(() => - { - // Basic file should have a start address of 0801 stored as the two first bytes (little endian order, 01 08). - var loaded = monitor.LoadBinary(out var loadedAtAddress, out var fileLength, null, AfterLoadBasic); - if (!loaded) - { - // If file could not be loaded at this time, probably because a Web/WASM file picker dialog is asynchronus - return (int)CommandResult.Ok; - } - AfterLoadBasic(monitor, loadedAtAddress, fileLength); - return (int)CommandResult.Ok; - }); - }); - app.Command("llb", cmd => - { - cmd.HelpOption(inherited: true); - cmd.Description = "C64 - Load a CBM Basic 2.0 PRG file from host file system."; - cmd.AddName("loadbasic file"); + command.SetHandler(handler); + return command; + } - var fileName = cmd.Argument("filename", "Name of the Basic file.") - .IsRequired(); - //.Accepts(v => v.ExistingFile()); // File exists check is done in LoadBinary(...) implementation. + private static Command BuildLoadBasicManualCommand(MonitorBase monitor) + { + var fileNameArg = new Argument() + { + Name = "filename", + Description = "Name of the binary file.", + Arity = ArgumentArity.ExactlyOne + }; - cmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); + var command = new Command("llb", "C64 - Load a CBM Basic 2.0 PRG file from host file system.") + { + fileNameArg + }; - cmd.OnExecute(() => + Func> handler = (string fileName) => + { + // Basic file should have a start address of 0801 stored as the two first bytes (little endian order, 01 08). + bool loaded = monitor.LoadBinary(fileName, out var loadedAtAddress, out var fileLength); + if (!loaded) { - // Basic file should have a start address of 0801 stored as the two first bytes (little endian order, 01 08). - bool loaded = monitor.LoadBinary(fileName.Value!, out var loadedAtAddress, out var fileLength); - if (!loaded) - { - // If file could not be loaded, probably because it's not supported/implemented by the derived class. - return (int)CommandResult.Ok; - } - AfterLoadBasic(monitor, loadedAtAddress, fileLength); - return (int)CommandResult.Ok; + // If file could not be loaded, probably because it's not supported/implemented by the derived class. + return Task.FromResult((int)CommandResult.Ok); + } + AfterLoadBasic(monitor, loadedAtAddress, fileLength); + return Task.FromResult((int)CommandResult.Ok); + }; - }); - }); + command.SetHandler(handler, fileNameArg); + return command; + } - app.Command("sb", cmd => + private static Command BuildSaveBasicCommand(MonitorBase monitor) + { + var fileNameArg = new Argument() { - cmd.HelpOption(inherited: true); - cmd.Description = "C64 - Save a CBM Basic 2.0 PRG file to host file system."; - cmd.AddName("savebasic"); + Name = "filename", + Description = "Name of the Basic file.", + Arity = ArgumentArity.ExactlyOne + }; - var fileName = cmd.Argument("filename", "Name of the Basic file.") - .IsRequired(); + var command = new Command("sb", "C64 - Save a CBM Basic 2.0 PRG file to host file system.") + { + fileNameArg, + }; + command.AddAlias("savebasic"); - cmd.OnValidationError((ValidationResult validationResult) => - { - return monitor.WriteValidationError(validationResult); - }); + Func> handler = (string fileName) => + { + ushort startAddressValue = C64.BASIC_LOAD_ADDRESS; + var endAddressValue = ((C64)monitor.System).GetBasicProgramEndAddress(); + monitor.SaveBinary(fileName, startAddressValue, endAddressValue, addFileHeaderWithLoadAddress: true); + return Task.FromResult((int)CommandResult.Ok); + }; - cmd.OnExecute(() => - { - ushort startAddressValue = C64.BASIC_LOAD_ADDRESS; - var endAddressValue = ((C64)monitor.System).GetBasicProgramEndAddress(); - monitor.SaveBinary(fileName.Value!, startAddressValue, endAddressValue, addFileHeaderWithLoadAddress: true); - return (int)CommandResult.Ok; - }); - }); + command.SetHandler(handler, fileNameArg); + return command; } public void Reset(MonitorBase monitor) { } - public void AfterLoadBasic(MonitorBase monitor, ushort loadedAtAddress, ushort fileLength) + public static void AfterLoadBasic(MonitorBase monitor, ushort loadedAtAddress, ushort fileLength) { monitor.WriteOutput($"Basic program loaded at {loadedAtAddress.ToHex()}, length {fileLength.ToHex()}"); ((C64)monitor.System).InitBasicMemoryVariables(loadedAtAddress, fileLength); diff --git a/src/libraries/Highbyte.DotNet6502.Systems/Highbyte.DotNet6502.Systems.csproj b/src/libraries/Highbyte.DotNet6502.Systems/Highbyte.DotNet6502.Systems.csproj index ed4578d8..ef0ee952 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/Highbyte.DotNet6502.Systems.csproj +++ b/src/libraries/Highbyte.DotNet6502.Systems/Highbyte.DotNet6502.Systems.csproj @@ -26,8 +26,8 @@ - +