From 6304c495074fd4c071721337a106588bfaf563cc Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 1 Feb 2026 16:07:09 +0100 Subject: [PATCH 1/7] Add `WriteLines` and `MarkupLines` extensions to `IAnsiConsole` and introduce `AnsiConsoleStringExtensions` for string markup utilities. --- .../AnsiConsoleExtensions.cs | 20 ++++++ .../AnsiConsoleStringExtensions.cs | 64 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleStringExtensions.cs diff --git a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs index 19531e17..898abcfa 100644 --- a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs +++ b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs @@ -83,4 +83,24 @@ public static void PrintTable(this IAnsiConsole ansiConsole, IEnumerable i ansiConsole.Write(table); } + + public static IAnsiConsole WriteLines(this IAnsiConsole ansiConsole, IEnumerable lines) + { + foreach (var line in lines) + { + ansiConsole.WriteLine(line); + } + + return ansiConsole; + } + + public static IAnsiConsole MarkupLines(this IAnsiConsole ansiConsole, IEnumerable lines) + { + foreach (var line in lines) + { + ansiConsole.MarkupLine(line); + } + + return ansiConsole; + } } diff --git a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleStringExtensions.cs b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleStringExtensions.cs new file mode 100644 index 00000000..86096b8e --- /dev/null +++ b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleStringExtensions.cs @@ -0,0 +1,64 @@ +using System.Text; +using Spectre.Console; + +namespace CreativeCoders.SysConsole.Core; + +public static class AnsiConsoleStringExtensions +{ + public static string ToErrorMarkup(this string text) + { + return $"[red]{text}[/]"; + } + + public static string ToSuccessMarkup(this string text) + { + return $"[green]{text}[/]"; + } + + public static string ToWarningMarkup(this string text) + { + return $"[yellow]{text}[/]"; + } + + public static string ToInfoMarkup(this string text) + { + return $"[blue]{text}[/]"; + } + + public static string ToWhiteMarkup(this string text) + { + return $"[white]{text}[/]"; + } + + public static string ToBoldMarkup(this string text) + { + return $"[bold]{text}[/]"; + } + + public static string ToItalicMarkup(this string text) + { + return $"[italic]{text}[/]"; + } + + public static string ToUnderlineMarkup(this string text) + { + return $"[underline]{text}[/]"; + } + + public static string ToStrikethroughMarkup(this string text) + { + return $"[strikethrough]{text}[/]"; + } + + public static string ToLinkMarkup(this string text, string url = "") + { + return string.IsNullOrWhiteSpace(url) + ? $"[link]{text}[/]" + : $"[link={url}]{text}[/]"; + } + + public static string ToEscapedMarkup(this string text) + { + return Markup.Escape(text); + } +} From ba16d67165390e852d5632b2540a7f09d59e636f Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 1 Feb 2026 18:49:07 +0100 Subject: [PATCH 2/7] Extend `IAnsiConsole` methods to support params for line collections, add nullability attributes, and update `DemoCliCommand` usage. --- samples/CliHostSampleApp/DemoCliCommand.cs | 6 +++++- .../CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs | 4 ++-- .../AnsiConsoleStringExtensions.cs | 5 ++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/samples/CliHostSampleApp/DemoCliCommand.cs b/samples/CliHostSampleApp/DemoCliCommand.cs index 2c5b8537..acef22a5 100644 --- a/samples/CliHostSampleApp/DemoCliCommand.cs +++ b/samples/CliHostSampleApp/DemoCliCommand.cs @@ -1,17 +1,21 @@ using CreativeCoders.Cli.Core; +using CreativeCoders.SysConsole.Core; using JetBrains.Annotations; +using Spectre.Console; namespace CliHostSampleApp; [UsedImplicitly] [CliCommand([DemoCommandGroup.Name, "do"] , AlternativeCommands = ["demo1"])] -public class DemoCliCommand : ICliCommand +public class DemoCliCommand(IAnsiConsole ansiConsole) : ICliCommand { public Task ExecuteAsync() { Console.WriteLine("Hello World from cli command !"); + ansiConsole.WriteLines("Test", 1234.ToString()); + return Task.FromResult(new CommandResult()); } } diff --git a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs index 898abcfa..10a32a80 100644 --- a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs +++ b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs @@ -84,7 +84,7 @@ public static void PrintTable(this IAnsiConsole ansiConsole, IEnumerable i ansiConsole.Write(table); } - public static IAnsiConsole WriteLines(this IAnsiConsole ansiConsole, IEnumerable lines) + public static IAnsiConsole WriteLines(this IAnsiConsole ansiConsole, params IEnumerable lines) { foreach (var line in lines) { @@ -94,7 +94,7 @@ public static IAnsiConsole WriteLines(this IAnsiConsole ansiConsole, IEnumerable return ansiConsole; } - public static IAnsiConsole MarkupLines(this IAnsiConsole ansiConsole, IEnumerable lines) + public static IAnsiConsole MarkupLines(this IAnsiConsole ansiConsole, params IEnumerable lines) { foreach (var line in lines) { diff --git a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleStringExtensions.cs b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleStringExtensions.cs index 86096b8e..4c5cfad4 100644 --- a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleStringExtensions.cs +++ b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleStringExtensions.cs @@ -1,8 +1,11 @@ -using System.Text; +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; using Spectre.Console; namespace CreativeCoders.SysConsole.Core; +[ExcludeFromCodeCoverage] +[PublicAPI] public static class AnsiConsoleStringExtensions { public static string ToErrorMarkup(this string text) From 6d2f9c576045c74310829c117d74427befae9a9f Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 1 Feb 2026 18:51:55 +0100 Subject: [PATCH 3/7] Refactor exception handling in `DefaultCliHost` to centralize `CliCommandAbortException` processing through a dedicated method. --- .../DefaultCliHost.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs index 87cd1c6d..0310aac4 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs @@ -135,6 +135,11 @@ public async Task RunAsync(string[] args) } catch (CliCommandConstructionFailedException e) { + if (e.InnerException is CliCommandAbortException abortException) + { + return HandleCommandAbortException(abortException); + } + _ansiConsole.Markup( $"[red]Error creating command: {e.InnerException?.Message ?? "Unknown error"}[/] "); @@ -156,12 +161,7 @@ public async Task RunAsync(string[] args) } catch (CliCommandAbortException e) { - if (e.PrintMessage) - { - _ansiConsole.MarkupLine(e.IsError ? $"[red]{e.Message}[/]" : $"[yellow]{e.Message}[/]"); - } - - return new CliResult(e.ExitCode); + return HandleCommandAbortException(e); } catch (CliExitException e) { @@ -171,6 +171,16 @@ public async Task RunAsync(string[] args) } } + private CliResult HandleCommandAbortException(CliCommandAbortException e) + { + if (e.PrintMessage) + { + _ansiConsole.MarkupLine(e.IsError ? $"[red]{e.Message}[/]" : $"[yellow]{e.Message}[/]"); + } + + return new CliResult(e.ExitCode); + } + private async Task ExecuteHelpPreProcessorsAsync(string[] args) { CliProcessorExecutionCondition[] conditions = From 308b1fd0bf067883ecfbcd18e74f35ca5cd3ab23 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 1 Feb 2026 18:57:11 +0100 Subject: [PATCH 4/7] Enhance exception handling in `DefaultCliHost` to handle base exceptions for `CliCommandAbortException` and add test for abort scenarios. --- .../DefaultCliHost.cs | 2 +- .../Hosting/DefaultCliHostTests.cs | 54 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs index 0310aac4..76b1c337 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs @@ -135,7 +135,7 @@ public async Task RunAsync(string[] args) } catch (CliCommandConstructionFailedException e) { - if (e.InnerException is CliCommandAbortException abortException) + if (e.InnerException?.GetBaseException() is CliCommandAbortException abortException) { return HandleCommandAbortException(abortException); } diff --git a/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs b/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs index 95f859ae..b6e7c3dc 100644 --- a/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs +++ b/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs @@ -650,6 +650,44 @@ public async Task RunAsync_WhenCommandCreationFails_PrintsErrorAndReturnsExitCod .Be(CliExitCodes.CommandCreationFailed); } + [Fact] + public async Task RunAsync_WhenCommandCreationFailsWithAbort_PrintsErrorAndReturnsExitCodeFromAbort() + { + // Arrange + var args = new[] { "run" }; + + var ansiConsole = A.Fake(); + var commandStore = A.Fake(); + var serviceProvider = A.Fake(); + var helpHandler = A.Fake(); + + SetupServiceProvider(serviceProvider, null); + + A.CallTo(() => helpHandler.ShouldPrintHelp(args)) + .Returns(false); + + var commandInfo = new CliCommandInfo + { + CommandAttribute = new CliCommandAttribute(["run"]), + CommandType = typeof(FailingCommandWithAbort) + }; + + var commandNode = new CliCommandNode(commandInfo, "run", null); + + A.CallTo(() => commandStore.FindCommandNode(args)) + .Returns(new FindCommandNodeResult(commandNode, [])); + + var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler, [], []); + + // Act + var result = await host.RunAsync(args); + + // Assert + result.ExitCode + .Should() + .Be(FailingCommandWithAbort.ExitCode); + } + [Fact] public async Task RunAsync_WithOptionsValidation_ExecutesAndReturnsResult() { @@ -863,6 +901,22 @@ public Task ExecuteAsync() } } + private sealed class FailingCommandWithAbort : ICliCommand + { + public const int ExitCode = 123876; + + [UsedImplicitly] + public FailingCommandWithAbort() + { + throw new CliCommandAbortException("Failure in constructor", ExitCode); + } + + public Task ExecuteAsync() + { + return Task.FromResult(new CommandResult()); + } + } + private sealed class DummyCommandWithErrorAbortException : ICliCommand { public const int ExitCode = 12349876; From ecbc62594a99df6b817055ca1865740bffe6c452 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 1 Feb 2026 19:12:39 +0100 Subject: [PATCH 5/7] Refactor `DefaultCliHost` to extract `FindAbortException` method for improved `CliCommandAbortException` handling. --- .../DefaultCliHost.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs index 76b1c337..05ade4f4 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs @@ -135,7 +135,9 @@ public async Task RunAsync(string[] args) } catch (CliCommandConstructionFailedException e) { - if (e.InnerException?.GetBaseException() is CliCommandAbortException abortException) + var abortException = FindAbortException(e.InnerException); + + if (abortException != null) { return HandleCommandAbortException(abortException); } @@ -171,6 +173,23 @@ public async Task RunAsync(string[] args) } } + private static CliCommandAbortException? FindAbortException(Exception? exception) + { + var e = exception; + + while (e != null) + { + if (e is CliCommandAbortException abortException) + { + return abortException; + } + + e = e.InnerException; + } + + return null; + } + private CliResult HandleCommandAbortException(CliCommandAbortException e) { if (e.PrintMessage) From 7f29276517449cc6015b9a6ebc4baa33f9a50a8f Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 1 Feb 2026 19:13:43 +0100 Subject: [PATCH 6/7] Remove unused `[PublicAPI]` annotations in `AnsiConsoleExtensions` for cleaner code. --- .../CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs index 10a32a80..f0cfc122 100644 --- a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs +++ b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs @@ -23,7 +23,6 @@ public static IAnsiConsolePrint PrintBlock(this IAnsiConsole ansiConsole, bool c return new AnsiConsolePrint(ansiConsole); } - [PublicAPI] public static IAnsiConsole Write(this IAnsiConsole ansiConsole, T value, Color foregroundColor, Color? backgroundColor = null) { @@ -34,7 +33,6 @@ public static IAnsiConsole Write(this IAnsiConsole ansiConsole, T value, Colo return ansiConsole; } - [PublicAPI] public static IAnsiConsole WriteLine(this IAnsiConsole ansiConsole, T value, Color foregroundColor, Color? backgroundColor = null) { @@ -45,7 +43,6 @@ public static IAnsiConsole WriteLine(this IAnsiConsole ansiConsole, T value, return ansiConsole; } - [PublicAPI] public static void PrintTable(this IAnsiConsole ansiConsole, IEnumerable items, TableColumnDef[] columns, Action? configureTable = null) { From bc1bcb0f227223d41b60082e930c9aa61c1d4d53 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 1 Feb 2026 19:15:40 +0100 Subject: [PATCH 7/7] Update `WriteLines` and `MarkupLines` extensions to use `params string[]` instead of `params IEnumerable` for improved usability. --- .../CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs index f0cfc122..ffcb62d4 100644 --- a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs +++ b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs @@ -81,7 +81,7 @@ public static void PrintTable(this IAnsiConsole ansiConsole, IEnumerable i ansiConsole.Write(table); } - public static IAnsiConsole WriteLines(this IAnsiConsole ansiConsole, params IEnumerable lines) + public static IAnsiConsole WriteLines(this IAnsiConsole ansiConsole, params string[] lines) { foreach (var line in lines) { @@ -91,7 +91,7 @@ public static IAnsiConsole WriteLines(this IAnsiConsole ansiConsole, params IEnu return ansiConsole; } - public static IAnsiConsole MarkupLines(this IAnsiConsole ansiConsole, params IEnumerable lines) + public static IAnsiConsole MarkupLines(this IAnsiConsole ansiConsole, params string[] lines) { foreach (var line in lines) {