Compare commits

...

40 Commits

Author SHA1 Message Date
tk
c2f21b9eb0 * Support command description localization
eg.
 [CommandOption("-a|--args")]
 [Description(nameof(Str.GitArgs))]
 [Localization(typeof(Str))]
 public string Args { get; set; }
2025-05-21 11:21:56 +08:00
Bartosz Ogiński
d836ad1805
Add ShowRowSeparators in Table Widget docs (#1807) 2025-04-14 18:34:09 +02:00
Marek
57dd8ee410
#1718 TestConsole can now be configured and accessed in CommandAppTester (#1803)
* TestConsole can now be configured and accessed in CommandAppTester
* Add test with mocked user inputs for interactive command
* Add documentation for using the CommandAppTester

Co-authored-by: Patrik Svensson <patriksvensson@users.noreply.github.com>
Co-authored-by: Marek Magath <Marek.Magath@solarwinds.com>
2025-04-14 10:38:03 +02:00
martincostello
6105ee2a86 Fix IndexOutOfRangeException
Fix `IndexOutOfRangeException` if an exception does not have an associated stack trace.

Resolves #1798.
2025-04-09 12:36:53 +02:00
Patrik Svensson
b5c839030c Blog post: Fix broken PR links 2025-04-08 20:45:50 +02:00
Patrik Svensson
b08ca1c4d7 Add blog post 2025-04-08 18:29:25 +02:00
Patrik Svensson
68fcfe0de4 Update dependencies and .NET SDK 2025-04-08 17:30:03 +02:00
Frank Ray
b0f82d787d
Documentation improvements for 1.0 release (#1620)
* Rewrote the Dependency Injection section for better clarity and readability.
* Syntax fix: Should reference interfaces, not implementation
* Renamed TypeRegistrar to MyTypeRegistrar, making it more obvious it is a custom class the user must provide.
2025-04-08 17:01:17 +02:00
Frank Ray
1dabf25e1c
Add testing documentation (#1631) 2025-04-08 16:58:25 +02:00
Frank Ray
958820dd66 Tighten up when to show/hide the application "-v|--version" option. 2025-04-08 16:53:43 +02:00
Frank Ray
c4a97f3c89 Improved unit test coverage using Spectre.Console.Tests.Data.VersionCommand 2025-04-08 16:53:43 +02:00
Frank Ray
4ac88b5d3f Help writer unit tests; including coverage of when the -v|--version should (and shouldn't) appear in the help output 2025-04-08 16:53:43 +02:00
Frank Ray
c937c8800a Updated test names for better self-documentation 2025-04-08 16:43:49 +02:00
Frank Ray
349eac1e22 Bug fix: Tests with remaining args should not be expected to pass strict parsing 2025-04-08 16:43:49 +02:00
Frank Ray
2f8a38f169 Add strict parsing to all version tests, an obvious omission in main 2025-04-08 16:43:49 +02:00
Frank Ray
e9f9f56189 Check if the command has a version option on its setting class 2025-04-08 16:43:49 +02:00
Frank Ray
cefb51df7b Unit tests to ensure VersionCommand executes when -v|--version is specified, rather than showing the ApplicationVersion number 2025-04-08 16:43:49 +02:00
Frank Ray
75b3b83210 Removed unnecessary using directive 2025-04-08 16:43:49 +02:00
Frank Ray
dfdd129dd0 Display the application version (if set) when a version flag is the first argument, even if a default command has been set. 2025-04-08 16:43:49 +02:00
Frank Ray
17c7a4f7d6 Cover -v and --version options in unit tests 2025-04-08 16:43:49 +02:00
Frank Ray
520efe07e2 Significant improvement to the command line parsing 2025-04-08 16:38:37 +02:00
Frank Ray
c81bc5fe1d Fix tests with incorrect validation that were in the main branch
Tests had incorrect expectations for the parsing of branch + default command arguments
2025-04-08 16:38:37 +02:00
Frank Ray
edf7f23957 Add strict parsing to almost all branches tests, an obvious omission 2025-04-08 16:38:37 +02:00
Frank Ray
80a8b0e406 Cosmetic: Remove ugly looking variable naming 2025-04-08 16:38:37 +02:00
Frank Ray
dca67da8cd New unit tests to ensure unknown flags are added to remaining args 2025-04-08 16:38:37 +02:00
Patrik Svensson
78272e62e6
Merge pull request #1779 from Moustafaa91/bug/1776 2025-03-24 00:05:14 +01:00
Mostafa Attia
540fb0195f Upgrade SixLabors.ImageSharp to 3.1.7 2025-03-23 13:44:18 +00:00
Melvin Dommer
93668e92b6
Changed IConfigurator to return IConfigurator instead of void for (#1762) 2025-02-24 20:57:23 +00:00
Tonttu
11a320c7c9
Conditionally trim trailing periods of argument and option descriptions (#1740) 2025-02-11 21:13:30 +00:00
Patrik Svensson
c1eb94c1db
Merge pull request #1755 from 0xced/fix-generic-exception-formatting 2025-02-05 18:39:38 +01:00
Cédric Luthi
9d8d3c1d6d Fix generic exception formatting with shortened types
Fixes #1754
2025-02-05 18:20:21 +01:00
Cédric Luthi
7e1142df58 Add tests for generic exception formatting 2025-02-05 18:20:21 +01:00
Eduardo Tolino
a6b96e9297
Include resource files for additional cultures in HelpProvider. (#1717)
* Creation of Resource files for HelpProvider in the following cultures: it, ja, ko, pt, ru, and zh-Hans.
* Add unit test files and update InlineData for new cultures.
* Include --version option in CommandAppTests.Help.cs and update related output files
2025-01-31 21:23:40 +00:00
Patrik Svensson
f1f633cc72
Merge pull request #1747 from phil-scott-78/spinner-extension 2025-01-28 18:41:34 +01:00
Phil Scott
05ce33615e Add async spinner extension methods and related documentation 2025-01-28 10:20:24 -05:00
Patrik Svensson
97715f2553
Merge pull request #1743 from spectreconsole/FrankRay78-disable-blank-issue 2025-01-25 20:20:59 +01:00
Frank Ray
039553efbb
Disable the GitHub 'Blank issue' option offered to users 2025-01-25 18:30:02 +00:00
Patrik Svensson
f704f2a0e8
Merge pull request #1739 from FrankRay78/1738-CommandAppTester-is-trimming-TestConsole-output 2025-01-21 10:33:25 +01:00
Frank Ray
8c5264d117 Trimming of TestConsole output by CommandAppTester is configurable 2025-01-21 08:39:31 +00:00
Martijn Straathof
58bf89a56a
Implement 3 digit hex parsing (#1708) 2024-12-04 16:31:21 +01:00
99 changed files with 3714 additions and 407 deletions

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -57,15 +57,15 @@ Task("Test")
});
Task("Package")
.IsDependentOn("Test")
//.IsDependentOn("Test")
.Does(context =>
{
context.DotNetPack($"./src/Spectre.Console.sln", new DotNetPackSettings {
Configuration = configuration,
Verbosity = DotNetVerbosity.Minimal,
NoLogo = true,
NoRestore = true,
NoBuild = true,
NoRestore = false,
NoBuild = false,
OutputDirectory = "./.artifacts",
MSBuildSettings = new DotNetMSBuildSettings()
.TreatAllWarningsAs(MSBuildTreatAllWarningsAs.Error)
@ -73,7 +73,7 @@ Task("Package")
});
Task("Publish-NuGet")
.WithCriteria(ctx => BuildSystem.IsRunningOnGitHubActions, "Not running on GitHub Actions")
//.WithCriteria(ctx => BuildSystem.IsRunningOnGitHubActions, "Not running on GitHub Actions")
.IsDependentOn("Package")
.Does(context =>
{
@ -90,6 +90,7 @@ Task("Publish-NuGet")
{
Source = "https://api.nuget.org/v3/index.json",
ApiKey = apiKey,
SkipDuplicate = true
});
}
});

View File

@ -38,7 +38,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Playwright" Version="1.49.0" />
<PackageReference Include="Microsoft.Playwright" Version="1.51.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Statiq.CodeAnalysis" Version="1.0.0-beta.72" />
<PackageReference Include="Statiq.Common" Version="1.0.0-beta.72" />

View File

@ -23,6 +23,7 @@ namespace Docs
{
"../../src/Spectre.Console/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs",
"../../src/Spectre.Console.Cli/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs",
"../../src/Spectre.Console.Testing/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs",
"../../src/Extensions/Spectre.Console.ImageSharp/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs",
"../../src/Extensions/Spectre.Console.Json/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs"
})

View File

@ -1,6 +1,6 @@
{
"sdk": {
"version": "9.0.100",
"version": "9.0.202",
"rollForward": "latestFeature"
}
}

View File

@ -0,0 +1,332 @@
{"version": 2, "width": 42, "height": 4, "title": "await-spinner (plain)", "env": {"TERM": "Spectre.Console"}}
[0, "o", "Loading the rocket ship "]
[0, "o", "\u001B[?25l"]
[0, "o", "\u280B"]
[0, "o", "\u001B[1D"]
[0.094, "o", "\u001B[?25l"]
[0.094, "o", "\u2819"]
[0.094, "o", "\u001B[1D"]
[0.188, "o", "\u001B[?25l"]
[0.188, "o", "\u2839"]
[0.188, "o", "\u001B[1D"]
[0.266, "o", "\u001B[?25l"]
[0.266, "o", "\u2838"]
[0.266, "o", "\u001B[1D"]
[0.36, "o", "\u001B[?25l"]
[0.36, "o", "\u283C"]
[0.36, "o", "\u001B[1D"]
[0.453, "o", "\u001B[?25l"]
[0.453, "o", "\u2834"]
[0.453, "o", "\u001B[1D"]
[0.563, "o", "\u001B[?25l"]
[0.563, "o", "\u2826"]
[0.563, "o", "\u001B[1D"]
[0.656, "o", "\u001B[?25l"]
[0.656, "o", "\u2827"]
[0.656, "o", "\u001B[1D"]
[0.75, "o", "\u001B[?25l"]
[0.75, "o", "\u2807"]
[0.75, "o", "\u001B[1D"]
[0.844, "o", "\u001B[?25l"]
[0.844, "o", "\u280F"]
[0.844, "o", "\u001B[1D"]
[0.922, "o", "\u001B[?25l"]
[0.922, "o", "\u280B"]
[0.922, "o", "\u001B[1D"]
[1.016, "o", "\u001B[?25l"]
[1.016, "o", "\u2819"]
[1.016, "o", "\u001B[1D"]
[1.11, "o", "\u001B[?25l"]
[1.11, "o", "\u2839"]
[1.11, "o", "\u001B[1D"]
[1.203, "o", "\u001B[?25l"]
[1.203, "o", "\u2838"]
[1.203, "o", "\u001B[1D"]
[1.297, "o", "\u001B[?25l"]
[1.297, "o", "\u283C"]
[1.297, "o", "\u001B[1D"]
[1.391, "o", "\u001B[?25l"]
[1.391, "o", "\u2834"]
[1.391, "o", "\u001B[1D"]
[1.485, "o", "\u001B[?25l"]
[1.485, "o", "\u2826"]
[1.485, "o", "\u001B[1D"]
[1.578, "o", "\u001B[?25l"]
[1.578, "o", "\u2827"]
[1.578, "o", "\u001B[1D"]
[1.672, "o", "\u001B[?25l"]
[1.672, "o", "\u2807"]
[1.672, "o", "\u001B[1D"]
[1.75, "o", "\u001B[?25l"]
[1.75, "o", "\u280F"]
[1.75, "o", "\u001B[1D"]
[1.828, "o", "\u001B[?25l"]
[1.828, "o", "\u280B"]
[1.828, "o", "\u001B[1D"]
[1.906, "o", "\u001B[?25l"]
[1.906, "o", "\u2819"]
[1.906, "o", "\u001B[1D"]
[2, "o", "\u001B[?25l"]
[2, "o", "\u2839"]
[2, "o", "\u001B[1D"]
[2.11, "o", "\u001B[?25l"]
[2.11, "o", "\u2838"]
[2.11, "o", "\u001B[1D"]
[2.203, "o", "\u001B[?25l"]
[2.203, "o", "\u283C"]
[2.203, "o", "\u001B[1D"]
[2.297, "o", "\u001B[?25l"]
[2.297, "o", "\u2834"]
[2.297, "o", "\u001B[1D"]
[2.391, "o", "\u001B[?25l"]
[2.391, "o", "\u2826"]
[2.391, "o", "\u001B[1D"]
[2.485, "o", "\u001B[?25l"]
[2.485, "o", "\u2827"]
[2.485, "o", "\u001B[1D"]
[2.578, "o", "\u001B[?25l"]
[2.578, "o", "\u2807"]
[2.578, "o", "\u001B[1D"]
[2.656, "o", "\u001B[?25l"]
[2.656, "o", "\u280F"]
[2.656, "o", "\u001B[1D"]
[2.735, "o", "\u001B[?25l"]
[2.735, "o", "\u280B"]
[2.735, "o", "\u001B[1D"]
[2.828, "o", "\u001B[?25l"]
[2.828, "o", "\u2819"]
[2.828, "o", "\u001B[1D"]
[2.922, "o", "\u001B[?25l"]
[2.922, "o", "\u2839"]
[2.922, "o", "\u001B[1D"]
[3.016, "o", "\u001B[?25l"]
[3.016, "o", "\u2838"]
[3.016, "o", "\u001B[1D"]
[3.094, "o", "\u001B[?25l"]
[3.094, "o", "\u283C"]
[3.094, "o", "\u001B[1D"]
[3.188, "o", "\u001B[?25l"]
[3.188, "o", "\u2834"]
[3.188, "o", "\u001B[1D"]
[3.281, "o", "\u001B[?25l"]
[3.281, "o", "\u2826"]
[3.281, "o", "\u001B[1D"]
[3.375, "o", "\u001B[?25l"]
[3.375, "o", "\u2827"]
[3.375, "o", "\u001B[1D"]
[3.453, "o", "\u001B[?25l"]
[3.453, "o", "\u2807"]
[3.453, "o", "\u001B[1D"]
[3.516, "o", " "]
[3.516, "o", "\u001B[1D"]
[3.516, "o", "\u001B[?25h"]
[3.516, "o", "\u001B[32mDone\u001B[0m\r\n"]
[3.516, "o", "Firing up the engines "]
[3.516, "o", "\u001B[?25l"]
[3.516, "o", "[ ]"]
[3.516, "o", "\u001B[6D"]
[3.61, "o", "\u001B[?25l"]
[3.61, "o", "[= ]"]
[3.61, "o", "\u001B[6D"]
[3.703, "o", "\u001B[?25l"]
[3.703, "o", "[== ]"]
[3.703, "o", "\u001B[6D"]
[3.797, "o", "\u001B[?25l"]
[3.797, "o", "[=== ]"]
[3.797, "o", "\u001B[6D"]
[3.875, "o", "\u001B[?25l"]
[3.875, "o", "[ ===]"]
[3.875, "o", "\u001B[6D"]
[3.953, "o", "\u001B[?25l"]
[3.953, "o", "[ ==]"]
[3.953, "o", "\u001B[6D"]
[4.063, "o", "\u001B[?25l"]
[4.063, "o", "[ =]"]
[4.063, "o", "\u001B[6D"]
[4.156, "o", "\u001B[?25l"]
[4.156, "o", "[ ]"]
[4.156, "o", "\u001B[6D"]
[4.25, "o", "\u001B[?25l"]
[4.25, "o", "[ =]"]
[4.25, "o", "\u001B[6D"]
[4.328, "o", "\u001B[?25l"]
[4.328, "o", "[ ==]"]
[4.328, "o", "\u001B[6D"]
[4.406, "o", "\u001B[?25l"]
[4.406, "o", "[ ===]"]
[4.406, "o", "\u001B[6D"]
[4.5, "o", "\u001B[?25l"]
[4.5, "o", "[====]"]
[4.5, "o", "\u001B[6D"]
[4.594, "o", "\u001B[?25l"]
[4.594, "o", "[=== ]"]
[4.594, "o", "\u001B[6D"]
[4.688, "o", "\u001B[?25l"]
[4.688, "o", "[== ]"]
[4.688, "o", "\u001B[6D"]
[4.781, "o", "\u001B[?25l"]
[4.781, "o", "[= ]"]
[4.781, "o", "\u001B[6D"]
[4.86, "o", "\u001B[?25l"]
[4.86, "o", "[ ]"]
[4.86, "o", "\u001B[6D"]
[4.953, "o", "\u001B[?25l"]
[4.953, "o", "[= ]"]
[4.953, "o", "\u001B[6D"]
[5.031, "o", "\u001B[?25l"]
[5.031, "o", "[== ]"]
[5.031, "o", "\u001B[6D"]
[5.125, "o", "\u001B[?25l"]
[5.125, "o", "[=== ]"]
[5.125, "o", "\u001B[6D"]
[5.219, "o", "\u001B[?25l"]
[5.219, "o", "[ ===]"]
[5.219, "o", "\u001B[6D"]
[5.313, "o", "\u001B[?25l"]
[5.313, "o", "[ ==]"]
[5.313, "o", "\u001B[6D"]
[5.422, "o", "\u001B[?25l"]
[5.422, "o", "[ =]"]
[5.422, "o", "\u001B[6D"]
[5.5, "o", "\u001B[?25l"]
[5.5, "o", "[ ]"]
[5.5, "o", "\u001B[6D"]
[5.594, "o", "\u001B[?25l"]
[5.594, "o", "[ =]"]
[5.594, "o", "\u001B[6D"]
[5.672, "o", "\u001B[?25l"]
[5.672, "o", "[ ==]"]
[5.672, "o", "\u001B[6D"]
[5.766, "o", "\u001B[?25l"]
[5.766, "o", "[ ===]"]
[5.766, "o", "\u001B[6D"]
[5.86, "o", "\u001B[?25l"]
[5.86, "o", "[====]"]
[5.86, "o", "\u001B[6D"]
[5.953, "o", "\u001B[?25l"]
[5.969, "o", "[=== ]"]
[5.969, "o", "\u001B[6D"]
[6.063, "o", "\u001B[?25l"]
[6.063, "o", "[== ]"]
[6.063, "o", "\u001B[6D"]
[6.156, "o", "\u001B[?25l"]
[6.156, "o", "[= ]"]
[6.156, "o", "\u001B[6D"]
[6.25, "o", "\u001B[?25l"]
[6.25, "o", "[ ]"]
[6.25, "o", "\u001B[6D"]
[6.328, "o", "\u001B[?25l"]
[6.328, "o", "[= ]"]
[6.328, "o", "\u001B[6D"]
[6.422, "o", "\u001B[?25l"]
[6.422, "o", "[== ]"]
[6.422, "o", "\u001B[6D"]
[6.516, "o", "\u001B[?25l"]
[6.516, "o", "[=== ]"]
[6.516, "o", "\u001B[6D"]
[6.61, "o", "\u001B[?25l"]
[6.61, "o", "[ ===]"]
[6.61, "o", "\u001B[6D"]
[6.703, "o", "\u001B[?25l"]
[6.703, "o", "[ ==]"]
[6.703, "o", "\u001B[6D"]
[6.797, "o", "\u001B[?25l"]
[6.797, "o", "[ =]"]
[6.797, "o", "\u001B[6D"]
[6.891, "o", "\u001B[?25l"]
[6.891, "o", "[ ]"]
[6.891, "o", "\u001B[6D"]
[6.922, "o", " "]
[6.922, "o", "\u001B[6D"]
[6.922, "o", "\u001B[?25h"]
[6.922, "o", "\u001B[32mDone\u001B[0m\r\n"]
[6.922, "o", "Blasting into orbit "]
[6.922, "o", "\u001B[?25l"]
[6.922, "o", "\u2631"]
[6.922, "o", "\u001B[1D"]
[7.031, "o", "\u001B[?25l"]
[7.031, "o", "\u2632"]
[7.031, "o", "\u001B[1D"]
[7.141, "o", "\u001B[?25l"]
[7.141, "o", "\u2634"]
[7.141, "o", "\u001B[1D"]
[7.25, "o", "\u001B[?25l"]
[7.25, "o", "\u2631"]
[7.25, "o", "\u001B[1D"]
[7.36, "o", "\u001B[?25l"]
[7.36, "o", "\u2632"]
[7.36, "o", "\u001B[1D"]
[7.485, "o", "\u001B[?25l"]
[7.485, "o", "\u2634"]
[7.485, "o", "\u001B[1D"]
[7.594, "o", "\u001B[?25l"]
[7.594, "o", "\u2631"]
[7.594, "o", "\u001B[1D"]
[7.703, "o", "\u001B[?25l"]
[7.703, "o", "\u2632"]
[7.703, "o", "\u001B[1D"]
[7.813, "o", "\u001B[?25l"]
[7.813, "o", "\u2634"]
[7.813, "o", "\u001B[1D"]
[7.922, "o", "\u001B[?25l"]
[7.922, "o", "\u2631"]
[7.922, "o", "\u001B[1D"]
[8.031, "o", "\u001B[?25l"]
[8.031, "o", "\u2632"]
[8.031, "o", "\u001B[1D"]
[8.141, "o", "\u001B[?25l"]
[8.141, "o", "\u2634"]
[8.141, "o", "\u001B[1D"]
[8.25, "o", "\u001B[?25l"]
[8.25, "o", "\u2631"]
[8.25, "o", "\u001B[1D"]
[8.375, "o", "\u001B[?25l"]
[8.375, "o", "\u2632"]
[8.375, "o", "\u001B[1D"]
[8.485, "o", "\u001B[?25l"]
[8.485, "o", "\u2634"]
[8.485, "o", "\u001B[1D"]
[8.594, "o", "\u001B[?25l"]
[8.594, "o", "\u2631"]
[8.594, "o", "\u001B[1D"]
[8.703, "o", "\u001B[?25l"]
[8.703, "o", "\u2632"]
[8.703, "o", "\u001B[1D"]
[8.813, "o", "\u001B[?25l"]
[8.813, "o", "\u2634"]
[8.813, "o", "\u001B[1D"]
[8.938, "o", "\u001B[?25l"]
[8.938, "o", "\u2631"]
[8.938, "o", "\u001B[1D"]
[9.047, "o", "\u001B[?25l"]
[9.047, "o", "\u2632"]
[9.047, "o", "\u001B[1D"]
[9.156, "o", "\u001B[?25l"]
[9.156, "o", "\u2634"]
[9.156, "o", "\u001B[1D"]
[9.266, "o", "\u001B[?25l"]
[9.266, "o", "\u2631"]
[9.266, "o", "\u001B[1D"]
[9.375, "o", "\u001B[?25l"]
[9.375, "o", "\u2632"]
[9.375, "o", "\u001B[1D"]
[9.485, "o", "\u001B[?25l"]
[9.485, "o", "\u2634"]
[9.485, "o", "\u001B[1D"]
[9.594, "o", "\u001B[?25l"]
[9.594, "o", "\u2631"]
[9.594, "o", "\u001B[1D"]
[9.719, "o", "\u001B[?25l"]
[9.719, "o", "\u2632"]
[9.719, "o", "\u001B[1D"]
[9.828, "o", "\u001B[?25l"]
[9.828, "o", "\u2634"]
[9.828, "o", "\u001B[1D"]
[9.938, "o", "\u001B[?25l"]
[9.938, "o", "\u2631"]
[9.938, "o", "\u001B[1D"]
[9.953, "o", " "]
[9.953, "o", "\u001B[1D"]
[9.953, "o", "\u001B[?25h"]
[9.953, "o", "\u001B[31mOh no\u001B[0m\r\n"]

View File

@ -0,0 +1,326 @@
{"version": 2, "width": 42, "height": 4, "title": "await-spinner (rich)", "env": {"TERM": "Spectre.Console"}}
[0, "o", "Loading the rocket ship "]
[0, "o", "\u001B[?25l"]
[0, "o", "\u280B"]
[0, "o", "\u001B[1D"]
[0.094, "o", "\u001B[?25l"]
[0.094, "o", "\u2819"]
[0.094, "o", "\u001B[1D"]
[0.172, "o", "\u001B[?25l"]
[0.172, "o", "\u2839"]
[0.172, "o", "\u001B[1D"]
[0.266, "o", "\u001B[?25l"]
[0.266, "o", "\u2838"]
[0.266, "o", "\u001B[1D"]
[0.36, "o", "\u001B[?25l"]
[0.36, "o", "\u283C"]
[0.36, "o", "\u001B[1D"]
[0.453, "o", "\u001B[?25l"]
[0.453, "o", "\u2834"]
[0.453, "o", "\u001B[1D"]
[0.547, "o", "\u001B[?25l"]
[0.547, "o", "\u2826"]
[0.547, "o", "\u001B[1D"]
[0.641, "o", "\u001B[?25l"]
[0.641, "o", "\u2827"]
[0.641, "o", "\u001B[1D"]
[0.719, "o", "\u001B[?25l"]
[0.719, "o", "\u2807"]
[0.719, "o", "\u001B[1D"]
[0.797, "o", "\u001B[?25l"]
[0.797, "o", "\u280F"]
[0.797, "o", "\u001B[1D"]
[0.891, "o", "\u001B[?25l"]
[0.891, "o", "\u280B"]
[0.891, "o", "\u001B[1D"]
[0.969, "o", "\u001B[?25l"]
[0.969, "o", "\u2819"]
[0.969, "o", "\u001B[1D"]
[1.078, "o", "\u001B[?25l"]
[1.078, "o", "\u2839"]
[1.094, "o", "\u001B[1D"]
[1.25, "o", "\u001B[?25l"]
[1.25, "o", "\u2838"]
[1.266, "o", "\u001B[1D"]
[1.407, "o", "\u001B[?25l"]
[1.407, "o", "\u283C"]
[1.422, "o", "\u001B[1D"]
[1.532, "o", "\u001B[?25l"]
[1.532, "o", "\u2834"]
[1.532, "o", "\u001B[1D"]
[1.61, "o", "\u001B[?25l"]
[1.61, "o", "\u2826"]
[1.61, "o", "\u001B[1D"]
[1.703, "o", "\u001B[?25l"]
[1.703, "o", "\u2827"]
[1.703, "o", "\u001B[1D"]
[1.782, "o", "\u001B[?25l"]
[1.782, "o", "\u2807"]
[1.782, "o", "\u001B[1D"]
[1.86, "o", "\u001B[?25l"]
[1.86, "o", "\u280F"]
[1.86, "o", "\u001B[1D"]
[1.953, "o", "\u001B[?25l"]
[1.953, "o", "\u280B"]
[1.953, "o", "\u001B[1D"]
[2.047, "o", "\u001B[?25l"]
[2.047, "o", "\u2819"]
[2.047, "o", "\u001B[1D"]
[2.125, "o", "\u001B[?25l"]
[2.125, "o", "\u2839"]
[2.125, "o", "\u001B[1D"]
[2.219, "o", "\u001B[?25l"]
[2.219, "o", "\u2838"]
[2.219, "o", "\u001B[1D"]
[2.313, "o", "\u001B[?25l"]
[2.313, "o", "\u283C"]
[2.313, "o", "\u001B[1D"]
[2.407, "o", "\u001B[?25l"]
[2.407, "o", "\u2834"]
[2.407, "o", "\u001B[1D"]
[2.5, "o", "\u001B[?25l"]
[2.5, "o", "\u2826"]
[2.5, "o", "\u001B[1D"]
[2.594, "o", "\u001B[?25l"]
[2.594, "o", "\u2827"]
[2.594, "o", "\u001B[1D"]
[2.688, "o", "\u001B[?25l"]
[2.688, "o", "\u2807"]
[2.688, "o", "\u001B[1D"]
[2.782, "o", "\u001B[?25l"]
[2.782, "o", "\u280F"]
[2.782, "o", "\u001B[1D"]
[2.875, "o", "\u001B[?25l"]
[2.875, "o", "\u280B"]
[2.875, "o", "\u001B[1D"]
[2.969, "o", "\u001B[?25l"]
[2.969, "o", "\u2819"]
[2.969, "o", "\u001B[1D"]
[3.063, "o", "\u001B[?25l"]
[3.063, "o", "\u2839"]
[3.063, "o", "\u001B[1D"]
[3.157, "o", "\u001B[?25l"]
[3.157, "o", "\u2838"]
[3.157, "o", "\u001B[1D"]
[3.25, "o", "\u001B[?25l"]
[3.25, "o", "\u283C"]
[3.25, "o", "\u001B[1D"]
[3.344, "o", "\u001B[?25l"]
[3.344, "o", "\u2834"]
[3.344, "o", "\u001B[1D"]
[3.469, "o", "\u001B[?25l"]
[3.469, "o", "\u2826"]
[3.469, "o", "\u001B[1D"]
[3.563, "o", "\u001B[?25l"]
[3.563, "o", "\u2827"]
[3.563, "o", "\u001B[1D"]
[3.563, "o", " "]
[3.563, "o", "\u001B[1D"]
[3.563, "o", "\u001B[?25h"]
[3.563, "o", "\u001B[38;5;2mDone\u001B[0m\r\n"]
[3.563, "o", "Firing up the engines "]
[3.563, "o", "\u001B[?25l"]
[3.563, "o", "[ ]"]
[3.563, "o", "\u001B[6D"]
[3.672, "o", "\u001B[?25l"]
[3.672, "o", "[= ]"]
[3.672, "o", "\u001B[6D"]
[3.75, "o", "\u001B[?25l"]
[3.75, "o", "[== ]"]
[3.75, "o", "\u001B[6D"]
[3.844, "o", "\u001B[?25l"]
[3.844, "o", "[=== ]"]
[3.844, "o", "\u001B[6D"]
[3.953, "o", "\u001B[?25l"]
[3.953, "o", "[ ===]"]
[3.953, "o", "\u001B[6D"]
[4.047, "o", "\u001B[?25l"]
[4.047, "o", "[ ==]"]
[4.047, "o", "\u001B[6D"]
[4.157, "o", "\u001B[?25l"]
[4.157, "o", "[ =]"]
[4.157, "o", "\u001B[6D"]
[4.25, "o", "\u001B[?25l"]
[4.25, "o", "[ ]"]
[4.25, "o", "\u001B[6D"]
[4.344, "o", "\u001B[?25l"]
[4.344, "o", "[ =]"]
[4.344, "o", "\u001B[6D"]
[4.438, "o", "\u001B[?25l"]
[4.438, "o", "[ ==]"]
[4.438, "o", "\u001B[6D"]
[4.532, "o", "\u001B[?25l"]
[4.532, "o", "[ ===]"]
[4.532, "o", "\u001B[6D"]
[4.625, "o", "\u001B[?25l"]
[4.625, "o", "[====]"]
[4.625, "o", "\u001B[6D"]
[4.719, "o", "\u001B[?25l"]
[4.719, "o", "[=== ]"]
[4.719, "o", "\u001B[6D"]
[4.828, "o", "\u001B[?25l"]
[4.844, "o", "[== ]"]
[4.86, "o", "\u001B[6D"]
[4.953, "o", "\u001B[?25l"]
[4.953, "o", "[= ]"]
[4.953, "o", "\u001B[6D"]
[5.047, "o", "\u001B[?25l"]
[5.047, "o", "[ ]"]
[5.047, "o", "\u001B[6D"]
[5.141, "o", "\u001B[?25l"]
[5.141, "o", "[= ]"]
[5.141, "o", "\u001B[6D"]
[5.235, "o", "\u001B[?25l"]
[5.235, "o", "[== ]"]
[5.235, "o", "\u001B[6D"]
[5.328, "o", "\u001B[?25l"]
[5.328, "o", "[=== ]"]
[5.328, "o", "\u001B[6D"]
[5.422, "o", "\u001B[?25l"]
[5.422, "o", "[ ===]"]
[5.422, "o", "\u001B[6D"]
[5.532, "o", "\u001B[?25l"]
[5.532, "o", "[ ==]"]
[5.532, "o", "\u001B[6D"]
[5.625, "o", "\u001B[?25l"]
[5.625, "o", "[ =]"]
[5.625, "o", "\u001B[6D"]
[5.719, "o", "\u001B[?25l"]
[5.719, "o", "[ ]"]
[5.719, "o", "\u001B[6D"]
[5.813, "o", "\u001B[?25l"]
[5.813, "o", "[ =]"]
[5.813, "o", "\u001B[6D"]
[5.907, "o", "\u001B[?25l"]
[5.907, "o", "[ ==]"]
[5.907, "o", "\u001B[6D"]
[6, "o", "\u001B[?25l"]
[6, "o", "[ ===]"]
[6, "o", "\u001B[6D"]
[6.094, "o", "\u001B[?25l"]
[6.094, "o", "[====]"]
[6.094, "o", "\u001B[6D"]
[6.188, "o", "\u001B[?25l"]
[6.188, "o", "[=== ]"]
[6.188, "o", "\u001B[6D"]
[6.282, "o", "\u001B[?25l"]
[6.282, "o", "[== ]"]
[6.282, "o", "\u001B[6D"]
[6.375, "o", "\u001B[?25l"]
[6.375, "o", "[= ]"]
[6.375, "o", "\u001B[6D"]
[6.453, "o", "\u001B[?25l"]
[6.453, "o", "[ ]"]
[6.453, "o", "\u001B[6D"]
[6.547, "o", "\u001B[?25l"]
[6.547, "o", "[= ]"]
[6.547, "o", "\u001B[6D"]
[6.625, "o", "\u001B[?25l"]
[6.625, "o", "[== ]"]
[6.625, "o", "\u001B[6D"]
[6.703, "o", "\u001B[?25l"]
[6.703, "o", "[=== ]"]
[6.703, "o", "\u001B[6D"]
[6.797, "o", "\u001B[?25l"]
[6.797, "o", "[ ===]"]
[6.797, "o", "\u001B[6D"]
[6.891, "o", "\u001B[?25l"]
[6.891, "o", "[ ==]"]
[6.891, "o", "\u001B[6D"]
[6.969, "o", " "]
[6.969, "o", "\u001B[6D"]
[6.969, "o", "\u001B[?25h"]
[6.969, "o", "\u001B[38;5;2mDone\u001B[0m\r\n"]
[6.969, "o", "Blasting into orbit "]
[6.969, "o", "\u001B[?25l"]
[6.969, "o", "\u2631"]
[6.969, "o", "\u001B[1D"]
[7.078, "o", "\u001B[?25l"]
[7.078, "o", "\u2632"]
[7.078, "o", "\u001B[1D"]
[7.172, "o", "\u001B[?25l"]
[7.172, "o", "\u2634"]
[7.172, "o", "\u001B[1D"]
[7.282, "o", "\u001B[?25l"]
[7.282, "o", "\u2631"]
[7.282, "o", "\u001B[1D"]
[7.375, "o", "\u001B[?25l"]
[7.375, "o", "\u2632"]
[7.375, "o", "\u001B[1D"]
[7.485, "o", "\u001B[?25l"]
[7.485, "o", "\u2634"]
[7.485, "o", "\u001B[1D"]
[7.594, "o", "\u001B[?25l"]
[7.594, "o", "\u2631"]
[7.594, "o", "\u001B[1D"]
[7.703, "o", "\u001B[?25l"]
[7.703, "o", "\u2632"]
[7.703, "o", "\u001B[1D"]
[7.813, "o", "\u001B[?25l"]
[7.813, "o", "\u2634"]
[7.813, "o", "\u001B[1D"]
[7.922, "o", "\u001B[?25l"]
[7.922, "o", "\u2631"]
[7.922, "o", "\u001B[1D"]
[8.032, "o", "\u001B[?25l"]
[8.032, "o", "\u2632"]
[8.032, "o", "\u001B[1D"]
[8.125, "o", "\u001B[?25l"]
[8.125, "o", "\u2634"]
[8.125, "o", "\u001B[1D"]
[8.235, "o", "\u001B[?25l"]
[8.235, "o", "\u2631"]
[8.235, "o", "\u001B[1D"]
[8.344, "o", "\u001B[?25l"]
[8.344, "o", "\u2632"]
[8.344, "o", "\u001B[1D"]
[8.453, "o", "\u001B[?25l"]
[8.453, "o", "\u2634"]
[8.453, "o", "\u001B[1D"]
[8.563, "o", "\u001B[?25l"]
[8.563, "o", "\u2631"]
[8.563, "o", "\u001B[1D"]
[8.657, "o", "\u001B[?25l"]
[8.657, "o", "\u2632"]
[8.657, "o", "\u001B[1D"]
[8.766, "o", "\u001B[?25l"]
[8.766, "o", "\u2634"]
[8.766, "o", "\u001B[1D"]
[8.86, "o", "\u001B[?25l"]
[8.86, "o", "\u2631"]
[8.86, "o", "\u001B[1D"]
[8.969, "o", "\u001B[?25l"]
[8.969, "o", "\u2632"]
[8.969, "o", "\u001B[1D"]
[9.078, "o", "\u001B[?25l"]
[9.078, "o", "\u2634"]
[9.078, "o", "\u001B[1D"]
[9.203, "o", "\u001B[?25l"]
[9.203, "o", "\u2631"]
[9.203, "o", "\u001B[1D"]
[9.313, "o", "\u001B[?25l"]
[9.313, "o", "\u2632"]
[9.313, "o", "\u001B[1D"]
[9.407, "o", "\u001B[?25l"]
[9.407, "o", "\u2634"]
[9.407, "o", "\u001B[1D"]
[9.516, "o", "\u001B[?25l"]
[9.516, "o", "\u2631"]
[9.516, "o", "\u001B[1D"]
[9.625, "o", "\u001B[?25l"]
[9.625, "o", "\u2632"]
[9.625, "o", "\u001B[1D"]
[9.719, "o", "\u001B[?25l"]
[9.719, "o", "\u2634"]
[9.735, "o", "\u001B[1D"]
[9.828, "o", "\u001B[?25l"]
[9.828, "o", "\u2631"]
[9.828, "o", "\u001B[1D"]
[9.938, "o", "\u001B[?25l"]
[9.938, "o", "\u2632"]
[9.938, "o", "\u001B[1D"]
[10, "o", " "]
[10, "o", "\u001B[1D"]
[10, "o", "\u001B[?25h"]
[10, "o", "\u001B[38;5;9mOh no\u001B[0m\r\n"]

View File

@ -85,10 +85,8 @@ on the main thread.
### Unit Testing Best Practices
For testing of console output, Spectre.Console has [`IAnsiConsole`](xref:T:Spectre.Console.IAnsiConsole) that can be
injected into your application.
The [Spectre.Console.Test](https://www.nuget.org/packages/Spectre.Console.Testing/) contains a set of utilities for
capturing the output for verification, either manually or via a tool such
as [Verify](https://github.com/VerifyTests/Verify).
injected into your application. The [Spectre.Console.Test](https://www.nuget.org/packages/Spectre.Console.Testing/)
NuGet package contains utilities for capturing the console output for verification. See the [Unit Testing](cli/unit-testing) page for further guidance.
### Analyzer for Best Practices

View File

@ -26,9 +26,9 @@ New features have been added, such as the ability to show separators between tab
## Rendering
* Add .NET 8 support by [@patriksvensson](https://github.com/patriksvensson) in [#1367](https://github.com/spectreconsole/spectre.console/pull/1367)
* Fixed render issue where writeline inside status caused corrupt output #415 #694 by [@fredrikbentzen](https://github.com/fredrikbentzen) in [#1132](https://github.com/spectreconsole/spectre.console/pull/1132))
* Relax the SDK requirements by rolling forward to the latest feature by [@0xced](https://github.com/0xced) in [#1237](https://github.com/spectreconsole/spectre.console/pull/1237))
* Add fix to avoid exception on rows with no children by [@jeppevammenkristensen](https://github.com/jeppevammenkristensen) in [#1241](https://github.com/spectreconsole/spectre.console/pull/1241))
* Fixed render issue where writeline inside status caused corrupt output #415 #694 by [@fredrikbentzen](https://github.com/fredrikbentzen) in [#1132](https://github.com/spectreconsole/spectre.console/pull/1132)
* Relax the SDK requirements by rolling forward to the latest feature by [@0xced](https://github.com/0xced) in [#1237](https://github.com/spectreconsole/spectre.console/pull/1237)
* Add fix to avoid exception on rows with no children by [@jeppevammenkristensen](https://github.com/jeppevammenkristensen) in [#1241](https://github.com/spectreconsole/spectre.console/pull/1241)
* Set `end_of_line` to `LF` instead of `CRLF` by [@0xced](https://github.com/0xced) in [#1256](https://github.com/spectreconsole/spectre.console/pull/1256)
* Fix `Rule` widget docs by [@tomaszprasolek](https://github.com/tomaszprasolek) in [#1257](https://github.com/spectreconsole/spectre.console/pull/1257)
* Added the missing columns-cast by [@nils](https://github.com/nils)-a in [#1294](https://github.com/spectreconsole/spectre.console/pull/1294)
@ -46,7 +46,7 @@ New features have been added, such as the ability to show separators between tab
## CLI
* Add async command unit tests by [@FrankRay78](https://github.com/FrankRay78) in [#1228](https://github.com/spectreconsole/spectre.console/pull/1228)
* Add support for async delegate by [@icalvo](https://github.com/icalvo) in [#1215](https://github.com/spectreconsole/spectre.console/pull/1215))
* Add support for async delegate by [@icalvo](https://github.com/icalvo) in [#1215](https://github.com/spectreconsole/spectre.console/pull/1215)
* Remove unnecessary `[NotNull]` attributes by [@0xced](https://github.com/0xced) in [#1255](https://github.com/spectreconsole/spectre.console/pull/1255)
* Allow custom help providers by [@FrankRay78](https://github.com/FrankRay78) in [#1259](https://github.com/spectreconsole/spectre.console/pull/1259)
* Specified details for settings for the argument vector by [@nils](https://github.com/nils)-a in [#1301](https://github.com/spectreconsole/spectre.console/pull/1301)

View File

@ -0,0 +1,71 @@
Title: Spectre.Console 0.50 released!
Description: Now with 25% less lead!
Published: 2025-04-08
Category: Release Notes
Excluded: false
---
Version 0.50 of Spectre.Console has been released!
## New Contributors
* [@Kissaki](https://github.com/Kissaki) made their first contribution in [#1575](https://github.com/spectreconsole/spectre.console/pull/1575)
* [@z4ryy](https://github.com/z4ryy) made their first contribution in [#1590](https://github.com/spectreconsole/spectre.console/pull/1590)
* [@TonWin618](https://github.com/TonWin618) made their first contribution in [#1595](https://github.com/spectreconsole/spectre.console/pull/1595)
* [@KirillOsenkov](https://github.com/KirillOsenkov) made their first contribution in [#1623](https://github.com/spectreconsole/spectre.console/pull/1623)
* [@davide-pi](https://github.com/davide-pi) made their first contribution in [#1246](https://github.com/spectreconsole/spectre.console/pull/1246)
* [@armanossiloko](https://github.com/armanossiloko) made their first contribution in [#1668](https://github.com/spectreconsole/spectre.console/pull/1668)
* [@PascalSenn](https://github.com/PascalSenn) made their first contribution in [#1687](https://github.com/spectreconsole/spectre.console/pull/1687)
* [@tpill90](https://github.com/tpill90) made their first contribution in [#904](https://github.com/spectreconsole/spectre.console/pull/904)
* [@tmds](https://github.com/tmds) made their first contribution in [#1194](https://github.com/spectreconsole/spectre.console/pull/1194)
* [@TheMarteh](https://github.com/TheMarteh) made their first contribution in [#1708](https://github.com/spectreconsole/spectre.console/pull/1708)
* [@Tolitech](https://github.com/Tolitech) made their first contribution in [#1717](https://github.com/spectreconsole/spectre.console/pull/1717)
* [@TheTonttu](https://github.com/TheTonttu) made their first contribution in [#1740](https://github.com/spectreconsole/spectre.console/pull/1740)
* [@byte2pixel](https://github.com/byte2pixel) made their first contribution in [#1762](https://github.com/spectreconsole/spectre.console/pull/1762)
* [@Moustafaa91](https://github.com/Moustafaa91) made their first contribution in [#1779](https://github.com/spectreconsole/spectre.console/pull/1779)
### General
* Strong name the assemblies by [@KirillOsenkov](https://github.com/KirillOsenkov) in [#1623](https://github.com/spectreconsole/spectre.console/pull/1623)
* Update MSDN link to learn.microsoft.com by [@Kissaki](https://github.com/Kissaki) in [#1575](https://github.com/spectreconsole/spectre.console/pull/1575)
* Add spanish translation for help strings by [@kzu](https://github.com/kzu) in [#1597](https://github.com/spectreconsole/spectre.console/pull/1597)
* Update documentation: add example for the Text Prompt usage by [@davide-pi](https://github.com/davide-pi) in [#1636](https://github.com/spectreconsole/spectre.console/pull/1636)
* Fix typos xml docs by [@devlead](https://github.com/devlead) in [#1684](https://github.com/spectreconsole/spectre.console/pull/1684)
* Upgrade SixLabors.ImageSharp to 3.1.7 by [@Moustafaa91](https://github.com/Moustafaa91) in [#1779](https://github.com/spectreconsole/spectre.console/pull/1779)
### Console
* AOT Support for Spectre.Console by [@phil-scott-78](https://github.com/phil-scott-78) in [#1690](https://github.com/spectreconsole/spectre.console/pull/1690)
* Make method reference to Markup.Escape more obvious by [@Kissaki](https://github.com/Kissaki) in [#1574](https://github.com/spectreconsole/spectre.console/pull/1574)
* Fix `HtmlEncoder` Incorrectly Applying Italics to Bold Text by [@z4ryy](https://github.com/z4ryy) in [#1590](https://github.com/spectreconsole/spectre.console/pull/1590)
* Fix Console Display Issue with Deleting Wide Characters by [@TonWin618](https://github.com/TonWin618) in [#1595](https://github.com/spectreconsole/spectre.console/pull/1595)
* Fix search bug in prompt related to custom item types by [@patriksvensson](https://github.com/patriksvensson) in [#1627](https://github.com/spectreconsole/spectre.console/pull/1627)
* Cleanup the prompt tests by [@0xced](https://github.com/0xced) in [#1635](https://github.com/spectreconsole/spectre.console/pull/1635)
* Add custom style for each calendar event by [@davide-pi](https://github.com/davide-pi) in [#1246](https://github.com/spectreconsole/spectre.console/pull/1246)
* Fix tree expansion bug by [@davide-pi](https://github.com/davide-pi) in [#1245](https://github.com/spectreconsole/spectre.console/pull/1245)
* Enhance the style of the checkboxes for multi-selection by [@davide-pi](https://github.com/davide-pi) in [#1244](https://github.com/spectreconsole/spectre.console/pull/1244)
* Improve exception if a (multi)selection prompt is used incorrectly by [@0xced](https://github.com/0xced) in [#1637](https://github.com/spectreconsole/spectre.console/pull/1637)
* Fix incorrect panel height calculation in complex layout by [@BlazeFace](https://github.com/BlazeFace) in [#1514](https://github.com/spectreconsole/spectre.console/pull/1514)
* Adding Enricher for Azure Pipelines by [@BlazeFace](https://github.com/BlazeFace) in [#1675](https://github.com/spectreconsole/spectre.console/pull/1675)
* Added hex color conversion by [@jsheely](https://github.com/jsheely) in [#1432](https://github.com/spectreconsole/spectre.console/pull/1432)
* Fixed type in Segment description by [@PascalSenn](https://github.com/PascalSenn) in [#1687](https://github.com/spectreconsole/spectre.console/pull/1687)
* Adding TransferSpeedColumn configuration to display bits/bytes + binary/decimal prefixes by [@tpill90](https://github.com/tpill90) in [#904](https://github.com/spectreconsole/spectre.console/pull/904)
* Changes Emoji dictionary to OrdinalIgnoreCase for performance by [@phil-scott-78](https://github.com/phil-scott-78) in [#1691](https://github.com/spectreconsole/spectre.console/pull/1691)
* ProgressTask.GetPercentage() returns 100 when max value is 0 by [@FrankRay78](https://github.com/FrankRay78) in [#1694](https://github.com/spectreconsole/spectre.console/pull/1694)
* Async overloads for AnsiConsole Prompt/Ask/Confirm. by [@tmds](https://github.com/tmds) in [#1194](https://github.com/spectreconsole/spectre.console/pull/1194)
* Support 3-digit hex codes in markup by [@TheMarteh](https://github.com/TheMarteh) in [#1708](https://github.com/spectreconsole/spectre.console/pull/1708)
* Add async spinner extension methods and related documentation by [@phil-scott-78](https://github.com/phil-scott-78) in [#1747](https://github.com/spectreconsole/spectre.console/pull/1747)
* Fix generic exception formatting by [@0xced](https://github.com/0xced) in [#1755](https://github.com/spectreconsole/spectre.console/pull/1755)
### CLI
* Remove redundant explain settings ctor by [@gitfool](https://github.com/gitfool) in [#1534](https://github.com/spectreconsole/spectre.console/pull/1534)
* Trim trailing comma in settings by [@devlead](https://github.com/devlead) in [#1550](https://github.com/spectreconsole/spectre.console/pull/1550)
* Consider -? as an alias to -h by [@kzu](https://github.com/kzu) in [#1552](https://github.com/spectreconsole/spectre.console/pull/1552)
* Trimming of TestConsole output by CommandAppTester is user configurable. by [@FrankRay78](https://github.com/FrankRay78) in [#1739](https://github.com/spectreconsole/spectre.console/pull/1739)
* Include resource files for additional cultures in HelpProvider. by [@Tolitech](https://github.com/Tolitech) in [#1717](https://github.com/spectreconsole/spectre.console/pull/1717)
* Conditionally trim trailing periods of argument and option descriptions by [@TheTonttu](https://github.com/TheTonttu) in [#1740](https://github.com/spectreconsole/spectre.console/pull/1740)
* Changed IConfigurator to return IConfigurator instead of void by [@byte2pixel](https://github.com/byte2pixel) in [#1762](https://github.com/spectreconsole/spectre.console/pull/1762)
* Add parsed unknown flag to remaining arguments for a branch with a default command by [@FrankRay78](https://github.com/FrankRay78) in [#1660](https://github.com/spectreconsole/spectre.console/pull/1660)
* Correctly show application version; execution of command with version option by [@FrankRay78](https://github.com/FrankRay78) in [#1663](https://github.com/spectreconsole/spectre.console/pull/1663)
* Help output correctly decides when to show the version option by [@FrankRay78](https://github.com/FrankRay78) in [#1664](https://github.com/spectreconsole/spectre.console/pull/1664)

View File

@ -67,7 +67,7 @@ registrations.AddSingleton<IGreeter, HelloWorldGreeter>();
// Create a type registrar and register any dependencies.
// A type registrar is an adapter for a DI framework.
var registrar = new TypeRegistrar(registrations);
var registrar = new MyTypeRegistrar(registrations);
// Create a new command app with the registrar
// and run it with the provided arguments.
@ -75,10 +75,13 @@ var app = new CommandApp<DefaultCommand>(registrar);
return app.Run(args);
```
`TypeRegistrar` is a custom class that must be created by the user. This [example using `Microsoft.Extensions.DependencyInjection` as the container](https://github.com/spectreconsole/examples/tree/main/examples/Cli/Injection) provides an example `TypeRegistrar` and `TypeResolver` that can be added to your application with small adjustments for your DI container.
<?# Alert ?>
`MyTypeRegistrar` is a custom class that implements [ITypeRegistrar](xref:T:Spectre.Console.Cli.ITypeRegistrar) and must be provided by the user.
<?#/ Alert ?>
Hint: If you do write your own implementation of `TypeRegistrar` and `TypeResolver` and you have some form of unit tests in place for your project,
there is a utility `TypeRegistrarBaseTests` available that can be used to ensure your implementations adhere to the required implementation. Simply call `TypeRegistrarBaseTests.RunAllTests()` and expect no `TypeRegistrarBaseTests.TestFailedException` to be thrown.
There is a working [example of dependency injection](https://github.com/spectreconsole/examples/tree/main/examples/Cli/Injection) that uses `Microsoft.Extensions.DependencyInjection` as the container. Example implementations of `ITypeRegistrar` and `ITypeResolver` are provided, which you can copy and paste to your application for dependency injection.
Unit testing your `ITypeRegistrar` and `ITypeResolver` implementations is done using the utility `TypeRegistrarBaseTests` included in `Spectre.Console.Testing`. Simply call `TypeRegistrarBaseTests.RunAllTests()` and expect no `TypeRegistrarBaseTests.TestFailedException` to be thrown.
## Interception
Interceptors can be registered with the `TypeRegistrar` (or with a custom DI-Container). Alternatively, `CommandApp` also provides a `SetInterceptor` configuration.
@ -89,4 +92,4 @@ This provides an opportunity to modify the result and also to tear down any infr
The `Intercept`-Method of each interceptor is run before the command is executed and the `InterceptResult`-Method is run after it. These are typically used for configuring logging or other infrastructure concerns.
For an example of using the interceptor to configure logging, see the [Serilog demo](https://github.com/spectreconsole/examples/tree/main/examples/Cli/Logging).
For an example of using the interceptor to configure logging, see the [Serilog demo](https://github.com/spectreconsole/examples/tree/main/examples/Cli/Logging)

View File

@ -0,0 +1,199 @@
Title: Unit Testing
Order: 14
Description: Instructions for unit testing a Spectre.Console application.
Reference:
- T:Spectre.Console.Testing.CommandAppTester
- T:Spectre.Console.Testing.TestConsole
- T:Spectre.Console.Testing.TestConsoleInput
---
`Spectre.Console` has a separate project that contains test harnesses for unit testing your own console applications.
The fastest way of getting started is to install the `Spectre.Console.Testing` NuGet package.
```text
> dotnet add package Spectre.Console.Testing
```
`Spectre.Console.Testing` is also the namespace containing the test classes.
## Testing a CommandApp
The `CommandAppTester` is a test implementation of `CommandApp` that's configured in a similar manner but designed for unit testing.
The following example validates the exit code and terminal output of a `Spectre.Console` command:
```csharp
/// <summary>
/// A Spectre.Console Command
/// </summary>
public class HelloWorldCommand : Command
{
private readonly IAnsiConsole _console;
public HelloWorldCommand(IAnsiConsole console)
{
// nb. AnsiConsole should not be called directly by the command
// since this doesn't play well with testing. Instead,
// the command should inject a IAnsiConsole and use that.
_console = console;
}
public override int Execute(CommandContext context)
{
_console.WriteLine("Hello world.");
return 0;
}
}
[TestMethod]
public void Should_Output_Hello_World()
{
// Given
var app = new CommandAppTester();
app.SetDefaultCommand<HelloWorldCommand>();
// When
var result = app.Run();
// Then
Assert.AreEqual(result.ExitCode, 0);
Assert.AreEqual(result.Output, "Hello world.");
}
```
The following example demonstrates how to mock user inputs for an interactive command.
This test (InteractiveCommand_WithMockedUserInputs_ProducesExpectedOutput) simulates user interactions by pushing predefined inputs to the console, then verifies that the resulting output is as expected.
```csharp
public sealed class InteractiveCommandTests
{
private sealed class InteractiveCommand : Command
{
private readonly IAnsiConsole _console;
public InteractiveCommand(IAnsiConsole console)
{
_console = console;
}
public override int Execute(CommandContext context)
{
var fruits = _console.Prompt(
new MultiSelectionPrompt<string>()
.Title("What are your [green]favorite fruits[/]?")
.NotRequired() // Not required to have a favorite fruit
.PageSize(10)
.MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]")
.InstructionsText(
"[grey](Press [blue]<space>[/] to toggle a fruit, " +
"[green]<enter>[/] to accept)[/]")
.AddChoices(new[] {
"Apple", "Apricot", "Avocado",
"Banana", "Blackcurrant", "Blueberry",
"Cherry", "Cloudberry", "Coconut",
}));
var fruit = _console.Prompt(
new SelectionPrompt<string>()
.Title("What's your [green]favorite fruit[/]?")
.PageSize(10)
.MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]")
.AddChoices(new[] {
"Apple", "Apricot", "Avocado",
"Banana", "Blackcurrant", "Blueberry",
"Cherry", "Cloudberry", "Cocunut",
}));
var name = _console.Ask<string>("What's your name?");
_console.WriteLine($"[{string.Join(',', fruits)};{fruit};{name}]");
return 0;
}
}
[Fact]
public void InteractiveCommand_WithMockedUserInputs_ProducesExpectedOutput()
{
// Given
TestConsole console = new();
console.Interactive();
// Your mocked inputs must always end with "Enter" for each prompt!
// Multi selection prompt: Choose first option
console.Input.PushKey(ConsoleKey.Spacebar);
console.Input.PushKey(ConsoleKey.Enter);
// Selection prompt: Choose second option
console.Input.PushKey(ConsoleKey.DownArrow);
console.Input.PushKey(ConsoleKey.Enter);
// Ask text prompt: Enter name
console.Input.PushTextWithEnter("Spectre Console");
var app = new CommandAppTester(null, new CommandAppTesterSettings(), console);
app.SetDefaultCommand<InteractiveCommand>();
// When
var result = app.Run();
// Then
result.ExitCode.ShouldBe(0);
result.Output.EndsWith("[Apple;Apricot;Spectre Console]");
}
}
```
## Testing console behaviour
`TestConsole` and `TestConsoleInput` are testable implementations of `IAnsiConsole` and `IAnsiConsoleInput`, allowing you fine-grain control over testing console output and interactivity.
The following example renders some widgets before then validating the console output:
```csharp
[TestMethod]
public void Should_Render_Panel()
{
// Given
var console = new TestConsole();
// When
console.Write(new Panel(new Text("Hello World")));
// Then
Assert.AreEqual(console.Output, """"
┌─────────────┐
│ Hello World │
└─────────────┘
"""");
}
```
While `Assert` is fine for validating simple output, more complex output may benefit from a tool like [Verify](https://github.com/VerifyTests/Verify).
The following example prompts the user for input before then validating the expected choice was made:
```csharp
[TestMethod]
public void Should_Select_Orange()
{
// Given
var console = new TestConsole();
console.Input.PushTextWithEnter("Orange");
// When
console.Prompt(
new TextPrompt<string>("Favorite fruit?")
.AddChoice("Banana")
.AddChoice("Orange"));
// Then
Assert.AreEqual(console.Output, "Favorite fruit? [Banana/Orange]: Orange\n");
}
```
`CommandAppTester` uses `TestConsole` internally, which in turn uses `TestConsoleInput`, offering a fully testable harness for `Spectre.Console` widgets, prompts and commands.

View File

@ -6,7 +6,7 @@ Order: 0
Spectre.Console is a `.NET` library that makes it easier
to create beautiful console applications.
## Spectre.Console.AnsiConsole Features
## Spectre.Console.AnsiConsole
* Easily output text with different colors and even styles such as bold, italic and blinking with a Rich inspired [markup language](markup).
* Supports `3`/`4`/`8`/`24`-bit colors in the terminal with auto-detection of the current terminal's capabilities.
@ -14,16 +14,19 @@ to create beautiful console applications.
* Display progress for long running tasks with live displays of [progress](live/progress) and [status](live/status) controls.
* Prompt user input with strongly typed [text input](prompts/text) or via [single-item select](prompts/selection) and [multiple item select](prompts/multiselection) controls.
* Format .NET [exceptions](exceptions) with custom color coded themes and styles.
* Written with unit testing in mind.
Spectre.Console.AnsiConsole has been heavily inspired
by the excellent [Rich](https://github.com/willmcgugan/rich) library
for Python written by Will McGugan.
Spectre.Console.AnsiConsole has been heavily inspired by the excellent [Rich](https://github.com/willmcgugan/rich) library for Python written by Will McGugan.
## Spectre.Console.Cli
* Create strongly typed settings and commands for parsing `args[]` to create complex command line applications like `git`, `gh`, or `dotnet`
## Spectre.Console.Testing
* Spectre.Console has been developed with unit testing in mind. The Spectre.Console library itself is covered by an extensive test suite, project maintainers require test coverage for all new commits, and the same extension points and test harnesses used internally for testing are available to you.
* The [Unit Testing](cli/unit-testing) page provides instructions for testing a Spectre.Console application.
## Examples
![Sample of Spectre.Console output](./assets/images/example.png)
@ -36,3 +39,4 @@ for Python written by Will McGugan.
Sorry, your browser doesn't support embedded videos.
</video>
The Spectre.Console [examples repository](https://github.com/spectreconsole/examples) contains many other examples.

73
docs/input/live/async.md Normal file
View File

@ -0,0 +1,73 @@
Title: Async Extensions
Order: 11
Description: "Async Extensions provides extension methods for running tasks with an inline animations."
Highlights:
- Extension methods for running tasks with spinner animations
- Support for both void and generic Task types
- Customizable spinner styles and console output
Reference:
- T:Spectre.Console.Extensions.SpinnerExtensions
Xref: spinner-extensions
---
The Async Spinner Extension provides convenient extension methods for running tasks with an inline spinner animations in the console.
<?# AsciiCast cast="await-spinner" /?>
<?# Alert ?>
The spinner animation is not thread safe, and using it together with other interactive
components such as prompts, progress displays or other status displays is not supported.
<?#/ Alert ?>
## Usage
The extension methods allow you to easily add spinner animations to any Task execution:
```csharp
// Basic usage with void Task
await someTask.Spinner();
// With generic Task<T>
var result = await someTaskWithResult.Spinner(
Spinner.Known.Star,
new Style(foreground: Color.Green));
// With custom console
await someTask.Spinner(
Spinner.Known.Dots,
style: Style.Plain,
ansiConsole: customConsole);
```
## Features
The spinner extensions provide:
- Support for both void Tasks and Tasks with return values
- Customizable spinner animations using any Spectre.Console Spinner
- Optional styling for the spinner animation
- Ability to specify a custom IAnsiConsole instance
## Examples
Here's a more complete example showing different ways to use the spinner extensions:
```csharp
// Basic spinner with default settings
await Task.Delay(1000)
.Spinner(Spinner.Known.Dots);
// Customized spinner with style
var result = await CalculateSomething()
.Spinner(
Spinner.Known.Star,
new Style(foreground: Color.Green));
// Using with a custom console
await ProcessData()
.Spinner(
new Spinner(new[] { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }, 80),
new Style(foreground: Color.Blue),
customConsole);
```

View File

@ -17,7 +17,7 @@ and the ascii terminal player.
/* temp styling for alerts */
.alert-warning {
@apply p-4 border border-yellow-300 bg-yellow-100 text-yellow-800 dark:border-orange-700/50 dark:bg-orange-800/50 dark:text-orange-300/90 rounded shadow-sm text-sm;
@apply p-4 border border-yellow-300 bg-yellow-100 text-yellow-800 dark:border-red-700/50 dark:bg-red-800/50 dark:text-red-100/90 rounded shadow-sm text-sm;
}
.alert-warning p {

View File

@ -138,3 +138,10 @@ table.Columns[0].NoWrap();
// Set the column width
table.Columns[0].Width(15);
```
### Show row separators
```csharp
// Shows separator between each row
table.ShowRowSeparators();
```

282
docs/package-lock.json generated
View File

@ -123,12 +123,13 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.0.tgz",
"integrity": "sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.13.4"
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
@ -315,12 +316,13 @@
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@ -475,10 +477,11 @@
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@ -500,6 +503,13 @@
"node": ">=4"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT"
},
"node_modules/defined": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
@ -587,10 +597,11 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@ -737,6 +748,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
@ -802,13 +814,14 @@
}
},
"node_modules/micromatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.1",
"picomatch": "^2.2.3"
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
@ -824,16 +837,27 @@
}
},
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/nanoid": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
"integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==",
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@ -931,10 +955,11 @@
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@ -949,21 +974,32 @@
}
},
"node_modules/postcss": {
"version": "8.4.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz",
"integrity": "sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==",
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.2.0",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
}
},
"node_modules/postcss-js": {
@ -1093,10 +1129,11 @@
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
"dev": true
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"dev": true,
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.0",
@ -1157,6 +1194,29 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/seroval": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.2.1.tgz",
"integrity": "sha512-yBxFFs3zmkvKNmR0pFSU//rIsYjuX418TnlDmc2weaq5XFDqDIV/NOMPBoLrbxjLH42p4UzRuXHryXh9dYcKcw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/seroval-plugins": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.2.1.tgz",
"integrity": "sha512-H5vs53+39+x4Udwp4J5rNZfgFuA+Lt+uU+09w1gYBVWomtAl98B+E9w7yC05Xc81/HgLvJdlyqJbU0fJCKCmdw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"seroval": "^1.0"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -1179,16 +1239,23 @@
}
},
"node_modules/solid-js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.3.5.tgz",
"integrity": "sha512-PUom2cCARfvvgxI7cwOhfXMrZZZxjp+vIrb5fzVNBFyICy8A30wTqExwfUv457eJYgKpii2D3qStW9ILtKnShw==",
"dev": true
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.5.tgz",
"integrity": "sha512-ogI3DaFcyn6UhYhrgcyRAMbu/buBJitYQASZz5WzfQVPP10RD2AbCoRZ517psnezrasyCbWzIxZ6kVqet768xw==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.1.0",
"seroval": "^1.1.0",
"seroval-plugins": "^1.1.0"
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
@ -1274,6 +1341,7 @@
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
@ -1407,12 +1475,12 @@
}
},
"@babel/runtime": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.0.tgz",
"integrity": "sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
"regenerator-runtime": "^0.14.0"
}
},
"@nodelib/fs.scandir": {
@ -1547,12 +1615,12 @@
"dev": true
},
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"requires": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
}
},
"browserslist": {
@ -1652,9 +1720,9 @@
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
@ -1668,6 +1736,12 @@
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true
},
"csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true
},
"defined": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
@ -1743,9 +1817,9 @@
}
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"requires": {
"to-regex-range": "^5.0.1"
@ -1905,13 +1979,13 @@
"dev": true
},
"micromatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"requires": {
"braces": "^3.0.1",
"picomatch": "^2.2.3"
"braces": "^3.0.3",
"picomatch": "^2.3.1"
}
},
"mini-svg-data-uri": {
@ -1921,15 +1995,15 @@
"dev": true
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true
},
"nanoid": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
"integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==",
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true
},
"node-releases": {
@ -1998,9 +2072,9 @@
"dev": true
},
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true
},
"picomatch": {
@ -2010,14 +2084,14 @@
"dev": true
},
"postcss": {
"version": "8.4.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz",
"integrity": "sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==",
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true,
"requires": {
"nanoid": "^3.2.0",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
}
},
"postcss-js": {
@ -2086,9 +2160,9 @@
}
},
"regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"dev": true
},
"resolve": {
@ -2123,6 +2197,19 @@
"queue-microtask": "^1.2.2"
}
},
"seroval": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.2.1.tgz",
"integrity": "sha512-yBxFFs3zmkvKNmR0pFSU//rIsYjuX418TnlDmc2weaq5XFDqDIV/NOMPBoLrbxjLH42p4UzRuXHryXh9dYcKcw==",
"dev": true
},
"seroval-plugins": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.2.1.tgz",
"integrity": "sha512-H5vs53+39+x4Udwp4J5rNZfgFuA+Lt+uU+09w1gYBVWomtAl98B+E9w7yC05Xc81/HgLvJdlyqJbU0fJCKCmdw==",
"dev": true,
"requires": {}
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -2139,15 +2226,20 @@
"dev": true
},
"solid-js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.3.5.tgz",
"integrity": "sha512-PUom2cCARfvvgxI7cwOhfXMrZZZxjp+vIrb5fzVNBFyICy8A30wTqExwfUv457eJYgKpii2D3qStW9ILtKnShw==",
"dev": true
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.5.tgz",
"integrity": "sha512-ogI3DaFcyn6UhYhrgcyRAMbu/buBJitYQASZz5WzfQVPP10RD2AbCoRZ517psnezrasyCbWzIxZ6kVqet768xw==",
"dev": true,
"requires": {
"csstype": "^3.1.0",
"seroval": "^1.1.0",
"seroval-plugins": "^1.1.0"
}
},
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true
},
"supports-color": {

View File

@ -99,8 +99,7 @@ public class Api : Pipeline
new ConcatDocuments(nameof(Code)),
new CacheDocuments(
new AnalyzeCSharp()
.WhereNamespaces(ns => ns.StartsWith("Spectre.Console") && !ns.Contains("Analyzer") &&
!ns.Contains("Testing") && !ns.Contains("Examples"))
.WhereNamespaces(ns => ns.StartsWith("Spectre.Console") && !ns.Contains("Analyzer") && !ns.Contains("Examples"))
.WherePublic(true)
.WithCssClasses("code", "cs")
.WithDestinationPrefix("api")

View File

@ -1,7 +1,7 @@
{
"$schema": "http://json.schemastore.org/global",
"sdk": {
"version": "9.0.100",
"version": "9.0.202",
"rollForward": "latestFeature"
}
}

View File

@ -0,0 +1,36 @@
using System.Threading.Tasks;
using Generator.Commands.Samples;
using Spectre.Console;
using Spectre.Console.Extensions;
namespace Generator.Commands.AsciiCast.Samples;
public class AwaitSpinnerSample : BaseSample
{
private static async Task DoSomethingAsync(int value)
{
await Task.Delay(value);
}
public override (int Cols, int Rows) ConsoleSize { get; } = (40, 4);
public override void Run(IAnsiConsole console)
{
Task.Run(async () =>
{
AnsiConsole.Write("Loading the rocket ship ");
await DoSomethingAsync(3500).Spinner(Spinner.Known.Dots);
AnsiConsole.MarkupLine("[green]Done[/]");
AnsiConsole.Write("Firing up the engines ");
await DoSomethingAsync(3400).Spinner(Spinner.Known.BouncingBar);
AnsiConsole.MarkupLine("[green]Done[/]");
AnsiConsole.Write("Blasting into orbit ");
await DoSomethingAsync(3025).Spinner(Spinner.Known.Hamburger);
AnsiConsole.MarkupLine("[red]Oh no[/]");
}).Wait();
}
}

View File

@ -43,10 +43,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.1.2" />
<PackageReference Include="AngleSharp" Version="1.2.0" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Scriban" Version="5.12.0" />
<PackageReference Include="Scriban" Version="6.2.0" />
<PackageReference Include="Spectre.IO" Version="0.18.0" />
</ItemGroup>

View File

@ -4,7 +4,6 @@
<LangVersion>12</LangVersion>
<DebugSymbols>true</DebugSymbols>
<DebugType>embedded</DebugType>
<MinVerSkip Condition="'$(Configuration)' == 'Debug'">true</MinVerSkip>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
@ -12,6 +11,7 @@
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)\..\resources\spectre.snk</AssemblyOriginatorKeyFile>
<PublicKey>00240000048000009400000006020000002400005253413100040000010001006146d3789d31477cf4a3b508dcf772ff9ccad8613f6bd6b17b9c4a960a7a7b551ecd22e4f4119ced70ee8bbdf3ca0a117c99fd6248c16255ea9033110c2233d42e74e81bf4f3f7eb09bfe8b53ad399d957514f427171a86f5fe9fe0014be121d571c80c4a0cfc3531bdbf5a2900d936d93f2c94171b9134f7644a1ac3612a0d0</PublicKey>
<Version>1.0.1</Version>
</PropertyGroup>
<PropertyGroup Label="Deterministic Build" Condition="'$(GITHUB_ACTIONS)' == 'true'">
@ -56,7 +56,6 @@
<!-- Allow folks to build with minimal dependencies (though they will need to provide their own Version data) -->
<ItemGroup Label="Build Tools Package References" Condition="'$(UseBuildTimeTools)' != 'false'">
<PackageReference Include="MinVer" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers">
<PrivateAssets>All</PrivateAssets>

View File

@ -1,8 +1,2 @@
<Project>
<Target Name="Versioning" BeforeTargets="MinVer">
<PropertyGroup Label="Build">
<MinVerDefaultPreReleaseIdentifiers>preview.0</MinVerDefaultPreReleaseIdentifiers>
<MinVerVerbosity>normal</MinVerVerbosity>
</PropertyGroup>
</Target>
</Project>

View File

@ -1,26 +1,27 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup Label="Dependencies">
<PackageVersion Include="MinVer" PrivateAssets="All" Version="6.0.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" Version="8.0.0" />
<PackageVersion Include="Wcwidth.Sources" Version="2.0.0" />
<ItemGroup>
<PackageVersion Include="IsExternalInit" Version="1.0.3"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.0"/>
<PackageVersion Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" Version="8.0.0"/>
<PackageVersion Include="MinVer" PrivateAssets="All" Version="6.0.0"/>
<PackageVersion Include="PolySharp" Version="1.15.0"/>
<PackageVersion Include="Shouldly" Version="4.2.1" />
<PackageVersion Include="Spectre.Verify.Extensions" Version="22.3.2-preview.0.1" />
<PackageVersion Include="Verify.Xunit" Version="28.2.1" />
<PackageVersion Include="xunit" Version="2.9.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="System.Memory" Version="4.6.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageVersion Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" Version="1.0.0-alpha.160" />
</ItemGroup>
<ItemGroup Label="Static Analysis">
<PackageVersion Include="Roslynator.Analyzers" PrivateAssets="All" Version="4.13.1"/>
<PackageVersion Include="Shouldly" Version="4.3.0"/>
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.8"/>
<PackageVersion Include="Spectre.Verify.Extensions" Version="28.16.0"/>
<PackageVersion Include="StyleCop.Analyzers" PrivateAssets="All" Version="1.2.0-beta.556"/>
<PackageVersion Include="Roslynator.Analyzers" PrivateAssets="All" Version="4.12.9" />
<PackageVersion Include="System.Memory" Version="4.6.3"/>
<PackageVersion Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" Version="1.0.0-alpha.160"/>
<PackageVersion Include="Verify.Xunit" Version="30.1.0"/>
<PackageVersion Include="Wcwidth.Sources" Version="2.0.0"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.0"/>
<PackageVersion Include="xunit" Version="2.9.3"/>
</ItemGroup>
</Project>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
<IsPackable>true</IsPackable>
<PackageId>NetAdmin.Spectre.Console.ImageSharp</PackageId>
<Description>A library that extends Spectre.Console with ImageSharp superpowers.</Description>
</PropertyGroup>
<PropertyGroup>

View File

@ -4,6 +4,7 @@
<TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks>
<ImplicitUsings>true</ImplicitUsings>
<IsPackable>true</IsPackable>
<PackageId>NetAdmin.Spectre.Console.Json</PackageId>
<Description>A library that extends Spectre.Console with JSON superpowers.</Description>
</PropertyGroup>
<PropertyGroup>

View File

@ -0,0 +1,24 @@
namespace Spectre.Console.Cli;
/// <summary>
/// Indicates that a "Description" feature should display its localization description.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)]
public class LocalizationAttribute : Attribute
{
/// <summary>
/// Gets or Sets a strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
public Type ResourceClass { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="LocalizationAttribute"/> class.
/// </summary>
/// <param name="resourceClass">
/// The type of the resource manager.
/// </param>
public LocalizationAttribute(Type resourceClass)
{
ResourceClass = resourceClass;
}
}

View File

@ -113,4 +113,9 @@ public sealed class CommandParseException : CommandRuntimeException
var text = $"[red]Error:[/] The value '[white]{value}[/]' is not in a correct format";
return new CommandParseException("Could not parse value", new Markup(text));
}
internal static CommandParseException UnknownParsingError()
{
return new CommandParseException("An unknown error occured when parsing the arguments.");
}
}

View File

@ -168,7 +168,7 @@ public static class ConfiguratorExtensions
}
/// <summary>
/// Tells the help writer whether or not to trim trailing period.
/// Tells the help provider whether or not to trim trailing period.
/// </summary>
/// <param name="configurator">The configurator.</param>
/// <param name="trimTrailingPeriods">True to trim trailing period (default), false to not.</param>

View File

@ -22,7 +22,7 @@ public class HelpProvider : IHelpProvider
protected virtual bool ShowOptionDefaultValues { get; }
/// <summary>
/// Gets a value indicating whether a trailing period of a command description is trimmed in the help text.
/// Gets a value indicating whether a trailing period of a description is trimmed in the help text.
/// </summary>
protected virtual bool TrimTrailingPeriod { get; }
@ -88,18 +88,29 @@ public class HelpProvider : IHelpProvider
new HelpOption("h", "help", null, null, resources.PrintHelpDescription, null),
};
// Version information applies to the entire application
// Include the "-v" option in the help when at the root of the command line application
// Don't allow the "-v" option if users have specified one or more sub-commands
if ((command?.Parent == null) && !(command?.IsBranch ?? false))
// Version information applies to the entire CLI application.
// Whether to show the "-v|--version" option in the help is determined as per:
// - If an application version has been set, and
// -- When at the root of the application, or
// -- When at the root of the application with a default command, unless
// --- The default command has a version option in its settings
if ((command?.Parent == null) && !(command?.IsBranch ?? false) && (command?.IsDefaultCommand ?? true))
{
// Only show the version command if there is an
// application version set.
// Check whether the default command has a version option in its settings.
var versionCommandOption = command?.Parameters?.OfType<CommandOption>()?.FirstOrDefault(o =>
(o.ShortNames.FirstOrDefault(v => v.Equals("v", StringComparison.OrdinalIgnoreCase)) != null) ||
(o.LongNames.FirstOrDefault(v => v.Equals("version", StringComparison.OrdinalIgnoreCase)) != null));
// Only show the version option if the default command doesn't have a version option in its settings.
if (versionCommandOption == null)
{
// Only show the version option if there is an application version set.
if (model.ApplicationVersion != null)
{
parameters.Add(new HelpOption("v", "version", null, null, resources.PrintVersionDescription, null));
}
}
}
parameters.AddRange(command?.Parameters.OfType<ICommandOption>().Where(o => !o.IsHidden).Select(o =>
new HelpOption(
@ -171,7 +182,7 @@ public class HelpProvider : IHelpProvider
var composer = NewComposer();
composer.Style(helpStyles?.Description?.Header ?? Style.Plain, $"{resources.Description}:").LineBreak();
composer.Text(command.Description).LineBreak();
composer.Text(NormalizeDescription(command.Description)).LineBreak();
yield return composer.LineBreak();
}
@ -364,14 +375,14 @@ public class HelpProvider : IHelpProvider
{
grid.AddRow(
NewComposer().Style(helpStyles?.Arguments?.RequiredArgument ?? Style.Plain, $"<{argument.Name}>"),
NewComposer().Text(argument.Description?.TrimEnd('.') ?? " "));
NewComposer().Text(NormalizeDescription(argument.Description)));
}
foreach (var argument in arguments.Where(x => !x.Required).OrderBy(x => x.Position))
{
grid.AddRow(
NewComposer().Style(helpStyles?.Arguments?.OptionalArgument ?? Style.Plain, $"[{argument.Name}]"),
NewComposer().Text(argument.Description?.TrimEnd('.') ?? " "));
NewComposer().Text(NormalizeDescription(argument.Description)));
}
result.Add(grid);
@ -428,7 +439,7 @@ public class HelpProvider : IHelpProvider
columns.Add(GetDefaultValueForOption(option.DefaultValue));
}
columns.Add(NewComposer().Text(option.Description?.TrimEnd('.') ?? " "));
columns.Add(NewComposer().Text(NormalizeDescription(option.Description)));
grid.AddRow(columns.ToArray());
}
@ -478,18 +489,9 @@ public class HelpProvider : IHelpProvider
arguments.Space();
}
if (TrimTrailingPeriod)
{
grid.AddRow(
NewComposer().Text(arguments.ToString().TrimEnd()),
NewComposer().Text(child.Description?.TrimEnd('.') ?? " "));
}
else
{
grid.AddRow(
NewComposer().Text(arguments.ToString().TrimEnd()),
NewComposer().Text(child.Description ?? " "));
}
NewComposer().Text(NormalizeDescription(child.Description)));
}
result.Add(grid);
@ -566,4 +568,16 @@ public class HelpProvider : IHelpProvider
_ => NewComposer().Style(helpStyles?.Options?.DefaultValue ?? Style.Plain, defaultValue?.ToString() ?? string.Empty),
};
}
private string NormalizeDescription(string? description)
{
if (description == null)
{
return " ";
}
return TrimTrailingPeriod
? description.TrimEnd('.')
: description;
}
}

View File

@ -37,7 +37,7 @@ public interface ICommandAppSettings
bool ShowOptionDefaultValues { get; set; }
/// <summary>
/// Gets or sets a value indicating whether a trailing period of a command description is trimmed in the help text.
/// Gets or sets a value indicating whether a trailing period of a description is trimmed in the help text.
/// </summary>
bool TrimTrailingPeriod { get; set; }

View File

@ -9,13 +9,15 @@ public interface IConfigurator
/// Sets the help provider for the application.
/// </summary>
/// <param name="helpProvider">The help provider to use.</param>
public void SetHelpProvider(IHelpProvider helpProvider);
/// <returns>A configurator that can be used for further configuration.</returns>
public IConfigurator SetHelpProvider(IHelpProvider helpProvider);
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <typeparam name="T">The type of the help provider to instantiate at runtime and use.</typeparam>
public void SetHelpProvider<T>()
/// <returns>A configurator that can be used for further configuration.</returns>
public IConfigurator SetHelpProvider<T>()
where T : IHelpProvider;
/// <summary>
@ -27,7 +29,8 @@ public interface IConfigurator
/// Adds an example of how to use the application.
/// </summary>
/// <param name="args">The example arguments.</param>
void AddExample(params string[] args);
/// <returns>A configurator that can be used for further configuration.</returns>
IConfigurator AddExample(params string[] args);
/// <summary>
/// Adds a command.

View File

@ -1,3 +1,5 @@
using static Spectre.Console.Cli.CommandTreeTokenizer;
namespace Spectre.Console.Cli;
internal sealed class CommandExecutor
@ -12,6 +14,8 @@ internal sealed class CommandExecutor
public async Task<int> Execute(IConfiguration configuration, IEnumerable<string> args)
{
CommandTreeParserResult parsedResult;
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
@ -27,20 +31,37 @@ internal sealed class CommandExecutor
_registrar.RegisterInstance(typeof(CommandModel), model);
_registrar.RegisterDependencies(model);
// No default command?
if (model.DefaultCommand == null)
{
// Got at least one argument?
var firstArgument = arguments.FirstOrDefault();
if (firstArgument != null)
{
// Asking for version? Kind of a hack, but it's alright.
// We should probably make this a bit better in the future.
if (firstArgument.Equals("--version", StringComparison.OrdinalIgnoreCase) ||
firstArgument.Equals("-v", StringComparison.OrdinalIgnoreCase))
// Asking for version?
if (firstArgument.Equals("-v", StringComparison.OrdinalIgnoreCase) ||
firstArgument.Equals("--version", StringComparison.OrdinalIgnoreCase))
{
if (configuration.Settings.ApplicationVersion != null)
{
// We need to check if the command has a version option on its setting class.
// Do this by first parsing the command line args and checking the remaining args.
try
{
// Parse and map the model against the arguments.
parsedResult = ParseCommandLineArguments(model, configuration.Settings, arguments);
}
catch (Exception)
{
// Something went wrong with parsing the command line arguments,
// however we know the first argument is a version option.
var console = configuration.Settings.Console.GetConsole();
console.MarkupLine(configuration.Settings.ApplicationVersion);
return 0;
}
// Check the parsed remaining args for the version options.
if ((firstArgument.Equals("-v", StringComparison.OrdinalIgnoreCase) && parsedResult.Remaining.Parsed.Contains("-v")) ||
(firstArgument.Equals("--version", StringComparison.OrdinalIgnoreCase) && parsedResult.Remaining.Parsed.Contains("--version")))
{
// The version option is not a member of the command settings.
var console = configuration.Settings.Console.GetConsole();
console.MarkupLine(configuration.Settings.ApplicationVersion);
return 0;
@ -50,7 +71,7 @@ internal sealed class CommandExecutor
}
// Parse and map the model against the arguments.
var parsedResult = ParseCommandLineArguments(model, configuration.Settings, arguments);
parsedResult = ParseCommandLineArguments(model, configuration.Settings, arguments);
// Register the arguments with the container.
_registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult);
@ -101,7 +122,89 @@ internal sealed class CommandExecutor
}
}
[SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1513:Closing brace should be followed by blank line", Justification = "Improves code readability by grouping together related statements into a block")]
private CommandTreeParserResult ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IReadOnlyList<string> args)
{
CommandTreeParserResult? parsedResult = null;
CommandTreeTokenizerResult tokenizerResult;
try
{
(parsedResult, tokenizerResult) = InternalParseCommandLineArguments(model, settings, args);
var lastParsedLeaf = parsedResult.Tree?.GetLeafCommand();
var lastParsedCommand = lastParsedLeaf?.Command;
if (lastParsedLeaf != null && lastParsedCommand != null &&
lastParsedCommand.IsBranch && !lastParsedLeaf.ShowHelp &&
lastParsedCommand.DefaultCommand != null)
{
// Adjust for any parsed remaining arguments by
// inserting the the default command ahead of them.
var position = tokenizerResult.Tokens.Position;
foreach (var parsedRemaining in parsedResult.Remaining.Parsed)
{
position--;
position -= parsedRemaining.Count(value => value != null);
}
position = position < 0 ? 0 : position;
// Insert this branch's default command into the command line
// arguments and try again to see if it will parse.
var argsWithDefaultCommand = new List<string>(args);
argsWithDefaultCommand.Insert(position, lastParsedCommand.DefaultCommand.Name);
(parsedResult, tokenizerResult) = InternalParseCommandLineArguments(model, settings, argsWithDefaultCommand);
}
}
catch (CommandParseException) when (parsedResult == null && settings.ParsingMode == ParsingMode.Strict)
{
// The parsing exception might be resolved by adding in the default command,
// but we can't know for sure. Take a brute force approach and try this for
// every position between the arguments.
for (int i = 0; i < args.Count; i++)
{
var argsWithDefaultCommand = new List<string>(args);
argsWithDefaultCommand.Insert(args.Count - i, "__default_command");
try
{
(parsedResult, tokenizerResult) = InternalParseCommandLineArguments(model, settings, argsWithDefaultCommand);
break;
}
catch (CommandParseException)
{
// Continue.
}
}
if (parsedResult == null)
{
// Failed to parse having inserted the default command between each argument.
// Repeat the parsing of the original arguments to throw the correct exception.
InternalParseCommandLineArguments(model, settings, args);
}
}
if (parsedResult == null)
{
// The arguments failed to parse despite everything we tried above.
// Exceptions should be thrown above before ever getting this far,
// however the following is the ulimately backstop and avoids
// the compiler from complaining about returning null.
throw CommandParseException.UnknownParsingError();
}
return parsedResult;
}
/// <summary>
/// Parse the command line arguments using the specified <see cref="CommandModel"/> and <see cref="CommandAppSettings"/>,
/// returning the parser and tokenizer results.
/// </summary>
/// <returns>The parser and tokenizer results as a tuple.</returns>
private (CommandTreeParserResult ParserResult, CommandTreeTokenizerResult TokenizerResult) InternalParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IReadOnlyList<string> args)
{
var parser = new CommandTreeParser(model, settings.CaseSensitivity, settings.ParsingMode, settings.ConvertFlagsToRemainingArguments);
@ -109,24 +212,7 @@ internal sealed class CommandExecutor
var tokenizerResult = CommandTreeTokenizer.Tokenize(args);
var parsedResult = parser.Parse(parserContext, tokenizerResult);
var lastParsedLeaf = parsedResult.Tree?.GetLeafCommand();
var lastParsedCommand = lastParsedLeaf?.Command;
if (lastParsedLeaf != null && lastParsedCommand != null &&
lastParsedCommand.IsBranch && !lastParsedLeaf.ShowHelp &&
lastParsedCommand.DefaultCommand != null)
{
// Insert this branch's default command into the command line
// arguments and try again to see if it will parse.
var argsWithDefaultCommand = new List<string>(args);
argsWithDefaultCommand.Insert(tokenizerResult.Tokens.Position, lastParsedCommand.DefaultCommand.Name);
parserContext = new CommandTreeParserContext(argsWithDefaultCommand, settings.ParsingMode);
tokenizerResult = CommandTreeTokenizer.Tokenize(argsWithDefaultCommand);
parsedResult = parser.Parse(parserContext, tokenizerResult);
}
return parsedResult;
return (parsedResult, tokenizerResult);
}
private static async Task<int> Execute(

View File

@ -20,22 +20,25 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig
Examples = new List<string[]>();
}
public void SetHelpProvider(IHelpProvider helpProvider)
public IConfigurator SetHelpProvider(IHelpProvider helpProvider)
{
// Register the help provider
_registrar.RegisterInstance(typeof(IHelpProvider), helpProvider);
return this;
}
public void SetHelpProvider<T>()
public IConfigurator SetHelpProvider<T>()
where T : IHelpProvider
{
// Register the help provider
_registrar.Register(typeof(IHelpProvider), typeof(T));
return this;
}
public void AddExample(params string[] args)
public IConfigurator AddExample(params string[] args)
{
Examples.Add(args);
return this;
}
public ConfiguredCommand SetDefaultCommand<TDefaultCommand>()

View File

@ -0,0 +1,21 @@
namespace Spectre.Console.Cli;
internal static class LocalizationExtensions
{
public static string? LocalizedDescription(this MemberInfo me)
{
var description = me.GetCustomAttribute<DescriptionAttribute>();
if (description is null)
{
return null;
}
var localization = me.GetCustomAttribute<LocalizationAttribute>();
string? localizedText;
return (localizedText = localization?.ResourceClass
.GetProperty(description.Description)?
.GetValue(default) as string) != null
? localizedText
: description.Description;
}
}

View File

@ -48,10 +48,10 @@ internal sealed class CommandInfo : ICommandContainer, ICommandInfo
if (CommandType != null && string.IsNullOrWhiteSpace(Description))
{
var description = CommandType.GetCustomAttribute<DescriptionAttribute>();
var description = CommandType.LocalizedDescription();
if (description != null)
{
Description = description.Description;
Description = description;
}
}
}

View File

@ -174,7 +174,7 @@ internal static class CommandModelBuilder
private static CommandOption BuildOptionParameter(PropertyInfo property, CommandOptionAttribute attribute)
{
var description = property.GetCustomAttribute<DescriptionAttribute>();
var description = property.LocalizedDescription();
var converter = property.GetCustomAttribute<TypeConverterAttribute>();
var deconstructor = property.GetCustomAttribute<PairDeconstructorAttribute>();
var valueProvider = property.GetCustomAttribute<ParameterValueProviderAttribute>();
@ -189,14 +189,14 @@ internal static class CommandModelBuilder
}
return new CommandOption(property.PropertyType, kind,
property, description?.Description, converter, deconstructor,
property, description, converter, deconstructor,
attribute, valueProvider, validators, defaultValue,
attribute.ValueIsOptional);
}
private static CommandArgument BuildArgumentParameter(PropertyInfo property, CommandArgumentAttribute attribute)
{
var description = property.GetCustomAttribute<DescriptionAttribute>();
var description = property.LocalizedDescription();
var converter = property.GetCustomAttribute<TypeConverterAttribute>();
var defaultValue = property.GetCustomAttribute<DefaultValueAttribute>();
var valueProvider = property.GetCustomAttribute<ParameterValueProviderAttribute>();
@ -206,7 +206,7 @@ internal static class CommandModelBuilder
return new CommandArgument(
property.PropertyType, kind, property,
description?.Description, converter,
description, converter,
defaultValue, attribute, valueProvider,
validators);
}

View File

@ -1,4 +1,4 @@
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
@ -124,7 +124,7 @@ namespace Spectre.Console.Cli.Resources {
}
/// <summary>
/// Looks up a localized string similar to Prints help information.
/// Looks up a localized string similar to Prints help information..
/// </summary>
internal static string PrintHelpDescription {
get {
@ -133,7 +133,7 @@ namespace Spectre.Console.Cli.Resources {
}
/// <summary>
/// Looks up a localized string similar to Prints version information.
/// Looks up a localized string similar to Prints version information..
/// </summary>
internal static string PrintVersionDescription {
get {

View File

@ -139,10 +139,10 @@
<value>OPTIONEN</value>
</data>
<data name="PrintHelpDescription" xml:space="preserve">
<value>Zeigt Hilfe an</value>
<value>Zeigt Hilfe an.</value>
</data>
<data name="PrintVersionDescription" xml:space="preserve">
<value>Zeigt Versionsinformationen an</value>
<value>Zeigt Versionsinformationen an.</value>
</data>
<data name="Usage" xml:space="preserve">
<value>VERWENDUNG</value>

View File

@ -139,10 +139,10 @@
<value>OPCIONES</value>
</data>
<data name="PrintHelpDescription" xml:space="preserve">
<value>Imprime información de ayuda</value>
<value>Imprime información de ayuda.</value>
</data>
<data name="PrintVersionDescription" xml:space="preserve">
<value>Imprime información de versión</value>
<value>Imprime información de versión.</value>
</data>
<data name="Usage" xml:space="preserve">
<value>USO</value>

View File

@ -139,10 +139,10 @@
<value>OPTIONS</value>
</data>
<data name="PrintHelpDescription" xml:space="preserve">
<value>Affiche l'aide</value>
<value>Affiche l'aide.</value>
</data>
<data name="PrintVersionDescription" xml:space="preserve">
<value>Affiche la version</value>
<value>Affiche la version.</value>
</data>
<data name="Usage" xml:space="preserve">
<value>UTILISATION</value>

View File

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Arguments" xml:space="preserve">
<value>ARGOMENTI</value>
</data>
<data name="Command" xml:space="preserve">
<value>COMANDO</value>
</data>
<data name="Commands" xml:space="preserve">
<value>COMANDI</value>
</data>
<data name="Default" xml:space="preserve">
<value>PREDEFINITO</value>
</data>
<data name="Description" xml:space="preserve">
<value>DESCRIZIONE</value>
</data>
<data name="Examples" xml:space="preserve">
<value>ESEMPI</value>
</data>
<data name="Options" xml:space="preserve">
<value>OPZIONI</value>
</data>
<data name="PrintHelpDescription" xml:space="preserve">
<value>Visualizza le informazioni di aiuto.</value>
</data>
<data name="PrintVersionDescription" xml:space="preserve">
<value>Visualizza le informazioni sulla versione.</value>
</data>
<data name="Usage" xml:space="preserve">
<value>USO</value>
</data>
</root>

View File

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Arguments" xml:space="preserve">
<value>引数</value>
</data>
<data name="Command" xml:space="preserve">
<value>コマンド</value>
</data>
<data name="Commands" xml:space="preserve">
<value>コマンド群</value>
</data>
<data name="Default" xml:space="preserve">
<value>デフォルト</value>
</data>
<data name="Description" xml:space="preserve">
<value>説明</value>
</data>
<data name="Examples" xml:space="preserve">
<value>例</value>
</data>
<data name="Options" xml:space="preserve">
<value>オプション</value>
</data>
<data name="PrintHelpDescription" xml:space="preserve">
<value>ヘルプ情報を表示</value>
</data>
<data name="PrintVersionDescription" xml:space="preserve">
<value>バージョン情報を表示</value>
</data>
<data name="Usage" xml:space="preserve">
<value>使用法</value>
</data>
</root>

View File

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Arguments" xml:space="preserve">
<value>인수</value>
</data>
<data name="Command" xml:space="preserve">
<value>명령</value>
</data>
<data name="Commands" xml:space="preserve">
<value>명령어</value>
</data>
<data name="Default" xml:space="preserve">
<value>기본값</value>
</data>
<data name="Description" xml:space="preserve">
<value>설명</value>
</data>
<data name="Examples" xml:space="preserve">
<value>예시</value>
</data>
<data name="Options" xml:space="preserve">
<value>옵션</value>
</data>
<data name="PrintHelpDescription" xml:space="preserve">
<value>도움말 정보를 출력</value>
</data>
<data name="PrintVersionDescription" xml:space="preserve">
<value>버전 정보를 출력</value>
</data>
<data name="Usage" xml:space="preserve">
<value>사용법</value>
</data>
</root>

View File

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Arguments" xml:space="preserve">
<value>ARGUMENTOS</value>
</data>
<data name="Command" xml:space="preserve">
<value>COMANDO</value>
</data>
<data name="Commands" xml:space="preserve">
<value>COMANDOS</value>
</data>
<data name="Default" xml:space="preserve">
<value>PADRÃO</value>
</data>
<data name="Description" xml:space="preserve">
<value>DESCRIÇÃO</value>
</data>
<data name="Examples" xml:space="preserve">
<value>EXEMPLOS</value>
</data>
<data name="Options" xml:space="preserve">
<value>OPÇÕES</value>
</data>
<data name="PrintHelpDescription" xml:space="preserve">
<value>Exibe informações de ajuda.</value>
</data>
<data name="PrintVersionDescription" xml:space="preserve">
<value>Exibe informações de versão.</value>
</data>
<data name="Usage" xml:space="preserve">
<value>USO</value>
</data>
</root>

View File

@ -139,10 +139,10 @@
<value>OPTIONS</value>
</data>
<data name="PrintHelpDescription" xml:space="preserve">
<value>Prints help information</value>
<value>Prints help information.</value>
</data>
<data name="PrintVersionDescription" xml:space="preserve">
<value>Prints version information</value>
<value>Prints version information.</value>
</data>
<data name="Usage" xml:space="preserve">
<value>USAGE</value>

View File

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Arguments" xml:space="preserve">
<value>АРГУМЕНТЫ</value>
</data>
<data name="Command" xml:space="preserve">
<value>КОМАНДА</value>
</data>
<data name="Commands" xml:space="preserve">
<value>КОМАНДЫ</value>
</data>
<data name="Default" xml:space="preserve">
<value>ПО УМОЛЧАНИЮ</value>
</data>
<data name="Description" xml:space="preserve">
<value>ОПИСАНИЕ</value>
</data>
<data name="Examples" xml:space="preserve">
<value>ПРИМЕРЫ</value>
</data>
<data name="Options" xml:space="preserve">
<value>ОПЦИИ</value>
</data>
<data name="PrintHelpDescription" xml:space="preserve">
<value>Выводит информацию о помощи.</value>
</data>
<data name="PrintVersionDescription" xml:space="preserve">
<value>Выводит информацию о версии.</value>
</data>
<data name="Usage" xml:space="preserve">
<value>ИСПОЛЬЗОВАНИЕ</value>
</data>
</root>

View File

@ -139,10 +139,10 @@
<value>VAL</value>
</data>
<data name="PrintHelpDescription" xml:space="preserve">
<value>Skriver ut hjälpinformation</value>
<value>Skriver ut hjälpinformation.</value>
</data>
<data name="PrintVersionDescription" xml:space="preserve">
<value>Skriver ut versionsnummer</value>
<value>Skriver ut versionsnummer.</value>
</data>
<data name="Usage" xml:space="preserve">
<value>ANVÄNDING</value>

View File

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Arguments" xml:space="preserve">
<value>参数</value>
</data>
<data name="Command" xml:space="preserve">
<value>命令</value>
</data>
<data name="Commands" xml:space="preserve">
<value>命令列表</value>
</data>
<data name="Default" xml:space="preserve">
<value>默认</value>
</data>
<data name="Description" xml:space="preserve">
<value>描述</value>
</data>
<data name="Examples" xml:space="preserve">
<value>示例</value>
</data>
<data name="Options" xml:space="preserve">
<value>选项</value>
</data>
<data name="PrintHelpDescription" xml:space="preserve">
<value>显示帮助信息</value>
</data>
<data name="PrintVersionDescription" xml:space="preserve">
<value>显示版本信息</value>
</data>
<data name="Usage" xml:space="preserve">
<value>用法</value>
</data>
</root>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks>
<IsPackable>true</IsPackable>
<PackageId>NetAdmin.Spectre.Console.Cli</PackageId>
</PropertyGroup>
<PropertyGroup>
<IsAotCompatible Condition="'$(TargetFramework)' != 'netstandard2.0'" >false</IsAotCompatible>

View File

@ -31,10 +31,5 @@ public sealed class CommandAppResult
Output = output ?? string.Empty;
Context = context;
Settings = settings;
Output = Output
.NormalizeLineEndings()
.TrimLines()
.Trim();
}
}

View File

@ -9,19 +9,46 @@ public sealed class CommandAppTester
private Action<IConfigurator>? _configuration;
/// <summary>
/// Initializes a new instance of the <see cref="CommandAppTester"/> class.
/// Gets the test console used by both the CommandAppTester and CommandApp.
/// </summary>
/// <param name="registrar">The registrar.</param>
public CommandAppTester(ITypeRegistrar? registrar = null)
{
Registrar = registrar;
}
public TestConsole Console { get; }
/// <summary>
/// Gets or sets the Registrar to use in the CommandApp.
/// </summary>
public ITypeRegistrar? Registrar { get; set; }
/// <summary>
/// Gets or sets the settings for the <see cref="CommandAppTester"/>.
/// </summary>
public CommandAppTesterSettings TestSettings { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="CommandAppTester"/> class.
/// </summary>
/// <param name="registrar">The registrar.</param>
/// <param name="settings">The settings.</param>
/// <param name="console">The test console that overrides the default one.</param>
public CommandAppTester(
ITypeRegistrar? registrar = null,
CommandAppTesterSettings? settings = null,
TestConsole? console = null)
{
Registrar = registrar;
TestSettings = settings ?? new CommandAppTesterSettings();
Console = console ?? new TestConsole().Width(int.MaxValue);
}
/// <summary>
/// Initializes a new instance of the <see cref="CommandAppTester"/> class.
/// </summary>
/// <param name="settings">The settings.</param>
public CommandAppTester(CommandAppTesterSettings settings)
{
TestSettings = settings;
Console = new TestConsole().Width(int.MaxValue);
}
/// <summary>
/// Sets the default command.
/// </summary>
@ -69,25 +96,23 @@ public sealed class CommandAppTester
public CommandAppFailure RunAndCatch<T>(params string[] args)
where T : Exception
{
var console = new TestConsole().Width(int.MaxValue);
try
{
Run(args, console, c => c.PropagateExceptions());
Run(args, Console, c => c.PropagateExceptions());
throw new InvalidOperationException("Expected an exception to be thrown, but there was none.");
}
catch (T ex)
{
if (ex is CommandAppException commandAppException && commandAppException.Pretty != null)
{
console.Write(commandAppException.Pretty);
Console.Write(commandAppException.Pretty);
}
else
{
console.WriteLine(ex.Message);
Console.WriteLine(ex.Message);
}
return new CommandAppFailure(ex, console.Output);
return new CommandAppFailure(ex, Console.Output);
}
catch (Exception ex)
{
@ -104,8 +129,7 @@ public sealed class CommandAppTester
/// <returns>The result.</returns>
public CommandAppResult Run(params string[] args)
{
var console = new TestConsole().Width(int.MaxValue);
return Run(args, console);
return Run(args, Console);
}
private CommandAppResult Run(string[] args, TestConsole console, Action<IConfigurator>? config = null)
@ -135,10 +159,8 @@ public sealed class CommandAppTester
var result = app.Run(args);
var output = console.Output
.NormalizeLineEndings()
.TrimLines()
.Trim();
var output = console.Output.NormalizeLineEndings();
output = TestSettings.TrimConsoleOutput ? output.TrimLines().Trim() : output;
return new CommandAppResult(result, output, context, settings);
}
@ -150,8 +172,7 @@ public sealed class CommandAppTester
/// <returns>The result.</returns>
public async Task<CommandAppResult> RunAsync(params string[] args)
{
var console = new TestConsole().Width(int.MaxValue);
return await RunAsync(args, console);
return await RunAsync(args, Console);
}
private async Task<CommandAppResult> RunAsync(string[] args, TestConsole console, Action<IConfigurator>? config = null)
@ -181,10 +202,8 @@ public sealed class CommandAppTester
var result = await app.RunAsync(args);
var output = console.Output
.NormalizeLineEndings()
.TrimLines()
.Trim();
var output = console.Output.NormalizeLineEndings();
output = TestSettings.TrimConsoleOutput ? output.TrimLines().Trim() : output;
return new CommandAppResult(result, output, context, settings);
}

View File

@ -0,0 +1,15 @@
namespace Spectre.Console.Testing;
/// <summary>
/// Represents the configuration settings for the <see cref="CommandAppTester"/> class.
/// </summary>
public sealed class CommandAppTesterSettings
{
/// <summary>
/// Gets or sets a value indicating whether whitespace should be trimmed from the console output.
/// </summary>
/// <remarks>
/// When enabled, leading and trailing whitespace from the console output and trailing whitespace from each line will be trimmed.
/// </remarks>
public bool TrimConsoleOutput { get; set; } = true;
}

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks>
<IsTestProject>false</IsTestProject>
<IsPackable>true</IsPackable>
<IsPackable>false</IsPackable>
<Description>Contains testing utilities for Spectre.Console.</Description>
</PropertyGroup>

View File

@ -230,6 +230,13 @@ public partial struct Color : IEquatable<Color>
hex = hex.Substring(1);
}
// 3 digit hex codes are expanded to 6 digits
// by doubling each digit, conform to CSS color codes
if (hex.Length == 3)
{
hex = string.Concat(hex.Select(c => new string(c, 2)));
}
var r = byte.Parse(hex.Substring(0, 2), NumberStyles.HexNumber);
var g = byte.Parse(hex.Substring(2, 2), NumberStyles.HexNumber);
var b = byte.Parse(hex.Substring(4, 2), NumberStyles.HexNumber);

View File

@ -0,0 +1,92 @@
namespace Spectre.Console.Extensions;
/// <summary>
/// Provides extension methods for running tasks with a spinner animation.
/// </summary>
public static class SpinnerExtensions
{
/// <summary>
/// Runs a task with a spinner animation.
/// </summary>
/// <param name="task">The task to run.</param>
/// <param name="spinner">The spinner to use.</param>
/// <param name="style">The style to apply to the spinner.</param>
/// <param name="ansiConsole">The console to write to.</param>
/// <returns>The result of the task.</returns>
public static async Task Spinner(this Task task, Spinner? spinner = null, Style? style = null, IAnsiConsole? ansiConsole = null)
{
await SpinnerInternal<object>(task, spinner ?? Console.Spinner.Known.Default, style, ansiConsole);
}
/// <summary>
/// Runs a task with a spinner animation.
/// </summary>
/// <typeparam name="T">The type of the task result.</typeparam>
/// <param name="task">The task to run.</param>
/// <param name="spinner">The spinner to use.</param>
/// <param name="style">The style to apply to the spinner.</param>
/// <param name="ansiConsole">The console to write to.</param>
/// <returns>The result of the task.</returns>
public static async Task<T> Spinner<T>(this Task<T> task, Spinner? spinner = null, Style? style = null, IAnsiConsole? ansiConsole = null)
{
return (await SpinnerInternal<T>(task, spinner ?? Console.Spinner.Known.Default, style, ansiConsole))!;
}
private static async Task<T?> SpinnerInternal<T>(Task task, Spinner spinner, Style? style = null, IAnsiConsole? ansiConsole = null)
{
ansiConsole ??= AnsiConsole.Console;
style ??= Style.Plain;
var currentFrame = 0;
var cancellationTokenSource = new CancellationTokenSource();
// Start spinner animation in background
var spinnerTask = Task.Run(
async () =>
{
while (!cancellationTokenSource.Token.IsCancellationRequested)
{
ansiConsole.Cursor.Show(false);
var spinnerFrame = spinner.Frames[currentFrame];
// Write the spinner frame
ansiConsole.Write(new Text(spinnerFrame, style));
ansiConsole.Write(new ControlCode(AnsiSequences.CUB(spinnerFrame.Length)));
currentFrame = (currentFrame + 1) % spinner.Frames.Count;
await Task.Delay(spinner.Interval, cancellationTokenSource.Token);
}
}, cancellationTokenSource.Token);
try
{
// Wait for the actual task to complete
if (task is Task<T> taskWithResult)
{
var result = await taskWithResult;
await cancellationTokenSource.CancelAsync();
await spinnerTask.ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnCanceled);
return result;
}
else
{
await task;
await cancellationTokenSource.CancelAsync();
await spinnerTask.ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnCanceled);
return default;
}
}
finally
{
var spinnerFrame = spinner.Frames[currentFrame];
ansiConsole.Write(new string(' ', spinnerFrame.Length));
ansiConsole.Write(new ControlCode(AnsiSequences.CUB(spinnerFrame.Length)));
ansiConsole.Cursor.Show();
await cancellationTokenSource.CancelAsync();
}
}
}

View File

@ -0,0 +1,13 @@
#if NETSTANDARD2_0
namespace Spectre.Console
{
internal static class CancellationTokenHelpers
{
public static Task CancelAsync(this CancellationTokenSource cts)
{
cts.Cancel();
return Task.CompletedTask;
}
}
}
#endif

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks>
<IsPackable>true</IsPackable>
<PackageId>NetAdmin.Spectre.Console</PackageId>
<DefineConstants>$(DefineConstants)TRACE;WCWIDTH_VISIBILITY_INTERNAL</DefineConstants>
</PropertyGroup>
<PropertyGroup>
@ -19,7 +20,7 @@
</ItemGroup>
<ItemGroup Label="Dependencies">
<PackageReference Condition="'$(TargetFramework)' == 'netstandard2.0'" Include="System.Memory"/>
<PackageReference Include="System.Memory" />
<PackageReference Include="Wcwidth.Sources">
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View File

@ -33,11 +33,12 @@ internal static class ExceptionFormatter
{
var shortenTypes = (settings.Format & ExceptionFormats.ShortenTypes) != 0;
var exceptionType = ex.GetType();
var exceptionTypeFullName = exceptionType.FullName ?? exceptionType.Name;
var type = Emphasize(exceptionTypeFullName, new[] { '.' }, settings.Style.Exception, shortenTypes, settings);
var exceptionTypeName = TypeNameHelper.GetTypeDisplayName(exceptionType, fullName: !shortenTypes, includeSystemNamespace: true);
var type = new StringBuilder();
Emphasize(type, exceptionTypeName, new[] { '.' }, settings.Style.Exception, shortenTypes, settings, limit: '<');
var message = $"[{settings.Style.Message.ToMarkup()}]{ex.Message.EscapeMarkup()}[/]";
return new Markup(string.Concat(type, ": ", message));
return new Markup($"{type}: {message}");
}
private static Grid GetStackFrames(Exception ex, ExceptionSettings settings)
@ -64,7 +65,7 @@ internal static class ExceptionFormatter
var stackTrace = new StackTrace(ex, fNeedFileInfo: true);
var allFrames = stackTrace.GetFrames();
if (allFrames[0]?.GetMethod() == null)
if (allFrames.Length > 0 && allFrames[0]?.GetMethod() == null)
{
// if we can't easily get the method for the frame, then we are in AOT
// fallback to using ToString method of each frame.
@ -101,7 +102,7 @@ internal static class ExceptionFormatter
builder.Append(' ');
}
builder.Append(Emphasize(methodName, new[] { '.' }, styles.Method, shortenMethods, settings));
Emphasize(builder, methodName, new[] { '.' }, styles.Method, shortenMethods, settings);
builder.AppendWithStyle(styles.Parenthesis, "(");
AppendParameters(builder, method, settings);
builder.AppendWithStyle(styles.Parenthesis, ")");
@ -168,7 +169,7 @@ internal static class ExceptionFormatter
void AppendPath()
{
var shortenPaths = (settings.Format & ExceptionFormats.ShortenPaths) != 0;
builder.Append(Emphasize(path, new[] { '/', '\\' }, settings.Style.Path, shortenPaths, settings));
Emphasize(builder, path, new[] { '/', '\\' }, settings.Style.Path, shortenPaths, settings);
}
if ((settings.Format & ExceptionFormats.ShowLinks) != 0)
@ -192,32 +193,25 @@ internal static class ExceptionFormatter
}
}
private static string Emphasize(string input, char[] separators, Style color, bool compact,
ExceptionSettings settings)
private static void Emphasize(StringBuilder builder, string input, char[] separators, Style color, bool compact,
ExceptionSettings settings, char? limit = null)
{
var builder = new StringBuilder();
var limitIndex = limit.HasValue ? input.IndexOf(limit.Value) : -1;
var type = input;
var index = type.LastIndexOfAny(separators);
var index = limitIndex != -1 ? input[..limitIndex].LastIndexOfAny(separators) : input.LastIndexOfAny(separators);
if (index != -1)
{
if (!compact)
{
builder.AppendWithStyle(
settings.Style.NonEmphasized,
type.Substring(0, index + 1));
builder.AppendWithStyle(settings.Style.NonEmphasized, input[..(index + 1)]);
}
builder.AppendWithStyle(
color,
type.Substring(index + 1, type.Length - index - 1));
builder.AppendWithStyle(color, input[(index + 1)..]);
}
else
{
builder.Append(type.EscapeMarkup());
builder.AppendWithStyle(color, input);
}
return builder.ToString();
}
private static bool ShowInStackTrace(StackFrame frame)

View File

@ -39,11 +39,12 @@ internal static class TypeNameHelper
/// <param name="type">The <see cref="Type"/>.</param>
/// <param name="fullName"><c>true</c> to print a fully qualified name.</param>
/// <param name="includeGenericParameterNames"><c>true</c> to include generic parameter names.</param>
/// <param name="includeSystemNamespace"><c>true</c> to include the <c>System</c> namespace.</param>
/// <returns>The pretty printed type name.</returns>
public static string GetTypeDisplayName(Type type, bool fullName = false, bool includeGenericParameterNames = true)
public static string GetTypeDisplayName(Type type, bool fullName = false, bool includeGenericParameterNames = true, bool includeSystemNamespace = false)
{
var builder = new StringBuilder();
ProcessType(builder, type, new DisplayNameOptions(fullName, includeGenericParameterNames));
ProcessType(builder, type, new DisplayNameOptions(fullName, includeGenericParameterNames, includeSystemNamespace));
return builder.ToString();
}
@ -71,7 +72,7 @@ internal static class TypeNameHelper
{
builder.Append(builtInName);
}
else if (type.Namespace == nameof(System))
else if (type.Namespace == nameof(System) && !options.IncludeSystemNamespace)
{
builder.Append(type.Name);
}
@ -181,14 +182,17 @@ internal static class TypeNameHelper
private struct DisplayNameOptions
{
public DisplayNameOptions(bool fullName, bool includeGenericParameterNames)
public DisplayNameOptions(bool fullName, bool includeGenericParameterNames, bool includeSystemNamespace)
{
FullName = fullName;
IncludeGenericParameterNames = includeGenericParameterNames;
IncludeSystemNamespace = includeSystemNamespace;
}
public bool FullName { get; }
public bool IncludeGenericParameterNames { get; }
public bool IncludeSystemNamespace { get; }
}
}

View File

@ -20,7 +20,7 @@ public sealed class AsynchronousCommand : AsyncCommand<AsynchronousCommandSettin
}
else
{
_console.WriteLine($"Finished executing asynchronously");
_console.Write($"Finished executing asynchronously");
}
return 0;

View File

@ -3,5 +3,6 @@ namespace Spectre.Console.Tests.Data;
public sealed class VersionSettings : CommandSettings
{
[CommandOption("-v|--version")]
[Description("The command version")]
public string Version { get; set; }
}

View File

@ -1,5 +1,5 @@
DESCRIPTION:
Contains settings for a cat.
Contains settings for a cat
USAGE:
myapp cat [LEGS] [OPTIONS] <COMMAND>

View File

@ -1,5 +1,5 @@
DESCRIPTION:
Contains settings for a cat.
Contains settings for a cat
USAGE:
myapp cat [LEGS] [OPTIONS] <COMMAND>

View File

@ -1,5 +1,5 @@
DESCRIPTION:
The animal command.
The animal command
USAGE:
myapp animal [LEGS] [OPTIONS] <COMMAND>

View File

@ -1,5 +1,5 @@
DESCRIPTION:
Contains settings for a cat.
Contains settings for a cat
USAGE:
myapp cat [LEGS] [OPTIONS] <COMMAND>

View File

@ -1,5 +1,5 @@
DESCRIPTION:
The lion command.
The lion command
USAGE:
myapp <TEETH> [LEGS] [OPTIONS]

View File

@ -1,5 +1,5 @@
DESCRIPTION:
The dog command.
The dog command
USAGE:
myapp <AGE> [LEGS] [OPTIONS]

View File

@ -1,5 +1,5 @@
DESCRIPTION:
The lion command.
The lion command
USAGE:
myapp <TEETH> [LEGS] [OPTIONS]

View File

@ -1,5 +1,5 @@
BESCHREIBUNG:
The lion command.
The lion command
VERWENDUNG:
myapp <TEETH> [LEGS] [OPTIONEN] [KOMMANDO]
@ -14,6 +14,7 @@ ARGUMENTE:
OPTIONEN:
STANDARDWERT
-h, --help Zeigt Hilfe an
-v, --version Zeigt Versionsinformationen an
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100

View File

@ -1,5 +1,5 @@
DESCRIPTION:
The lion command.
The lion command
USAGE:
myapp <TEETH> [LEGS] [OPTIONS] [COMMAND]
@ -14,6 +14,7 @@ ARGUMENTS:
OPTIONS:
DEFAULT
-h, --help Prints help information
-v, --version Prints version information
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100

View File

@ -1,5 +1,5 @@
DESCRIPTION:
The lion command.
The lion command
UTILISATION:
myapp <TEETH> [LEGS] [OPTIONS] [COMMANDE]
@ -14,6 +14,7 @@ ARGUMENTS:
OPTIONS:
DÉFAUT
-h, --help Affiche l'aide
-v, --version Affiche la version
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100

View File

@ -0,0 +1,26 @@
DESCRIZIONE:
The lion command
USO:
myapp <TEETH> [LEGS] [OPZIONI] [COMANDO]
ESEMPI:
myapp 20 --alive
ARGOMENTI:
<TEETH> The number of teeth the lion has
[LEGS] The number of legs
OPZIONI:
PREDEFINITO
-h, --help Visualizza le informazioni di aiuto
-v, --version Visualizza le informazioni sulla versione
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting
-w, --weight [WEIGHT] The weight of the lion, in kgs
COMANDI:
giraffe <LENGTH> The giraffe command

View File

@ -0,0 +1,26 @@
説明:
The lion command
使用法:
myapp <TEETH> [LEGS] [オプション] [コマンド]
例:
myapp 20 --alive
引数:
<TEETH> The number of teeth the lion has
[LEGS] The number of legs
オプション:
デフォルト
-h, --help ヘルプ情報を表示
-v, --version バージョン情報を表示
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting
-w, --weight [WEIGHT] The weight of the lion, in kgs
コマンド群:
giraffe <LENGTH> The giraffe command

View File

@ -0,0 +1,26 @@
설명:
The lion command
사용법:
myapp <TEETH> [LEGS] [옵션] [명령]
예시:
myapp 20 --alive
인수:
<TEETH> The number of teeth the lion has
[LEGS] The number of legs
옵션:
기본값
-h, --help 도움말 정보를 출력
-v, --version 버전 정보를 출력
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting
-w, --weight [WEIGHT] The weight of the lion, in kgs
명령어:
giraffe <LENGTH> The giraffe command

View File

@ -0,0 +1,26 @@
DESCRIÇÃO:
The lion command
USO:
myapp <TEETH> [LEGS] [OPÇÕES] [COMANDO]
EXEMPLOS:
myapp 20 --alive
ARGUMENTOS:
<TEETH> The number of teeth the lion has
[LEGS] The number of legs
OPÇÕES:
PADRÃO
-h, --help Exibe informações de ajuda
-v, --version Exibe informações de versão
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting
-w, --weight [WEIGHT] The weight of the lion, in kgs
COMANDOS:
giraffe <LENGTH> The giraffe command

View File

@ -0,0 +1,26 @@
ОПИСАНИЕ:
The lion command
ИСПОЛЬЗОВАНИЕ:
myapp <TEETH> [LEGS] [ОПЦИИ] [КОМАНДА]
ПРИМЕРЫ:
myapp 20 --alive
АРГУМЕНТЫ:
<TEETH> The number of teeth the lion has
[LEGS] The number of legs
ОПЦИИ:
ПО УМОЛЧАНИЮ
-h, --help Выводит информацию о помощи
-v, --version Выводит информацию о версии
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting
-w, --weight [WEIGHT] The weight of the lion, in kgs
КОМАНДЫ:
giraffe <LENGTH> The giraffe command

View File

@ -1,5 +1,5 @@
BESKRIVNING:
The lion command.
The lion command
ANVÄNDING:
myapp <TEETH> [LEGS] [VAL] [KOMMANDO]
@ -14,6 +14,7 @@ ARGUMENT:
VAL:
STANDARD
-h, --help Skriver ut hjälpinformation
-v, --version Skriver ut versionsnummer
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100

View File

@ -0,0 +1,26 @@
描述:
The lion command
用法:
myapp <TEETH> [LEGS] [选项] [命令]
示例:
myapp 20 --alive
参数:
<TEETH> The number of teeth the lion has
[LEGS] The number of legs
选项:
默认
-h, --help 显示帮助信息
-v, --version 显示版本信息
-a, --alive Indicates whether or not the animal is alive
-n, --name <VALUE>
--agility <VALUE> 10 The agility between 0 and 100
-c <CHILDREN> The number of children the lion has
-d <DAY> Monday, Thursday The days the lion goes hunting
-w, --weight [WEIGHT] The weight of the lion, in kgs
命令列表:
giraffe <LENGTH> The giraffe command

View File

@ -1,5 +1,5 @@
[bold]DESCRIPTION:[/]
The lion command.
The lion command
[bold]USAGE:[/]
myapp []<TEETH>[/] [][[LEGS]][/] [][[OPTIONS]][/] [][[COMMAND]][/]

View File

@ -1,5 +1,5 @@
[yellow]DESCRIPTION:[/]
The lion command.
The lion command
[yellow]USAGE:[/]
myapp [aqua]<TEETH>[/] [silver][[LEGS]][/] [grey][[OPTIONS]][/] [aqua][[COMMAND]][/]

View File

@ -1,5 +1,5 @@
[]DESCRIPTION:[/]
The lion command.
The lion command
[]USAGE:[/]
myapp []<TEETH>[/] [][[LEGS]][/] [][[OPTIONS]][/] [][[COMMAND]][/]

View File

@ -1,9 +1,15 @@
USAGE:
myapp [OPTIONS] <COMMAND>
DESCRIPTION:
The dog command.
USAGE:
myapp dog <AGE> [LEGS] [OPTIONS]
ARGUMENTS:
<AGE>
[LEGS] The number of legs.
OPTIONS:
-h, --help Prints help information
COMMANDS:
dog <AGE> The dog command.
horse The horse command.
-h, --help Prints help information.
-a, --alive Indicates whether or not the animal is alive.
-n, --name <VALUE>
-g, --good-boy

View File

@ -1,5 +1,5 @@
DESCRIPTION:
The lion command.
The lion command
USAGE:
myapp cat [LEGS] lion <TEETH> [OPTIONS]

View File

@ -1,5 +1,5 @@
DESCRIPTION:
The horse command.
The horse command
USAGE:
myapp horse [LEGS] [OPTIONS]

View File

@ -1,5 +1,5 @@
DESCRIPTION:
The horse command.
The horse command
USAGE:
myapp horse [LEGS] [OPTIONS]

View File

@ -4,13 +4,16 @@ public sealed partial class CommandAppTests
{
public sealed class Branches
{
[Fact]
public void Should_Run_The_Default_Command_On_Branch()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Should_Run_The_Default_Command_On_Branch(bool strictParsing)
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.Settings.StrictParsing = strictParsing;
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
@ -29,13 +32,16 @@ public sealed partial class CommandAppTests
result.Settings.ShouldBeOfType<CatSettings>();
}
[Fact]
public void Should_Throw_When_No_Default_Command_On_Branch()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Should_Throw_When_No_Default_Command_On_Branch(bool strictParsing)
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.Settings.StrictParsing = strictParsing;
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal => { });
});
@ -56,10 +62,8 @@ public sealed partial class CommandAppTests
});
}
[SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:SingleLineCommentMustBePrecededByBlankLine", Justification = "Helps to illustrate the expected behaviour of this unit test.")]
[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1005:SingleLineCommentsMustBeginWithSingleSpace", Justification = "Helps to illustrate the expected behaviour of this unit test.")]
[Fact]
public void Should_Be_Unable_To_Parse_Default_Command_Arguments_Relaxed_Parsing()
public void Should_Parse_Branch_And_Default_Command_Arguments_Relaxed_Parsing()
{
// Given
var app = new CommandAppTester();
@ -75,25 +79,27 @@ public sealed partial class CommandAppTests
// When
var result = app.Run(new[]
{
// The CommandTreeParser should be unable to determine which command line
// arguments belong to the branch and which belong to the branch's
// default command (once inserted).
"animal", "4", "--name", "Kitty",
// The CommandTreeParser should determine which command line arguments
// belong to the branch and which belong to the branch's default command.
"animal", "4", "-a", "false", "--name", "Kitty", "--agility", "four", "--nick-name", "Felix"
});
// Then
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<CatSettings>().And(cat =>
{
cat.IsAlive.ShouldBeFalse();
cat.Legs.ShouldBe(4);
//cat.Name.ShouldBe("Kitty"); //<-- Should normally be correct, but instead name will be added to the remaining arguments (see below).
cat.Name.ShouldBe("Kitty");
cat.Agility.ShouldBe(4);
});
result.Context.Remaining.Parsed.Count.ShouldBe(1);
result.Context.ShouldHaveRemainingArgument("--name", values: new[] { "Kitty", });
result.Context.ShouldHaveRemainingArgument("--nick-name", values: new[] { "Felix" });
result.Context.Remaining.Raw.Count.ShouldBe(0);
}
[Fact]
public void Should_Be_Unable_To_Parse_Default_Command_Arguments_Strict_Parsing()
public void Should_Parse_Branch_And_Default_Command_Arguments_Strict_Parsing()
{
// Given
var app = new CommandAppTester();
@ -108,31 +114,37 @@ public sealed partial class CommandAppTests
});
// When
var result = Record.Exception(() =>
var result = app.Run(new[]
{
app.Run(new[]
{
// The CommandTreeParser should be unable to determine which command line
// arguments belong to the branch and which belong to the branch's
// default command (once inserted).
"animal", "4", "--name", "Kitty",
});
// The CommandTreeParser should determine which command line arguments
// belong to the branch and which belong to the branch's default command.
"animal", "4", "-a", "false", "--name", "Kitty", "--agility", "four", "--", "--nick-name", "Felix"
});
// Then
result.ShouldBeOfType<CommandParseException>().And(ex =>
result.ExitCode.ShouldBe(0);
result.Settings.ShouldBeOfType<CatSettings>().And(cat =>
{
ex.Message.ShouldBe("Unknown option 'name'.");
cat.IsAlive.ShouldBeFalse();
cat.Legs.ShouldBe(4);
cat.Name.ShouldBe("Kitty");
cat.Agility.ShouldBe(4);
});
result.Context.Remaining.Parsed.Count.ShouldBe(1);
result.Context.ShouldHaveRemainingArgument("--nick-name", values: new[] { "Felix" });
result.Context.Remaining.Raw.Count.ShouldBe(2);
}
[Fact]
public void Should_Run_The_Default_Command_On_Branch_On_Branch()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Should_Run_The_Default_Command_On_Branch_On_Branch(bool strictParsing)
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.Settings.StrictParsing = strictParsing;
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
@ -154,13 +166,16 @@ public sealed partial class CommandAppTests
result.Settings.ShouldBeOfType<CatSettings>();
}
[Fact]
public void Should_Run_The_Default_Command_On_Branch_On_Branch_With_Arguments()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Should_Run_The_Default_Command_On_Branch_On_Branch_With_Arguments(bool strictParsing)
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.Settings.StrictParsing = strictParsing;
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
@ -186,13 +201,16 @@ public sealed partial class CommandAppTests
});
}
[Fact]
public void Should_Run_The_Default_Command_Not_The_Named_Command_On_Branch()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Should_Run_The_Default_Command_Not_The_Named_Command_On_Branch(bool strictParsing)
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.Settings.StrictParsing = strictParsing;
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
@ -213,13 +231,16 @@ public sealed partial class CommandAppTests
result.Settings.ShouldBeOfType<CatSettings>();
}
[Fact]
public void Should_Run_The_Named_Command_Not_The_Default_Command_On_Branch()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Should_Run_The_Named_Command_Not_The_Default_Command_On_Branch(bool strictParsing)
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.Settings.StrictParsing = strictParsing;
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
@ -246,13 +267,16 @@ public sealed partial class CommandAppTests
});
}
[Fact]
public void Should_Allow_Multiple_Branches_Multiple_Commands()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Should_Allow_Multiple_Branches_Multiple_Commands(bool strictParsing)
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.Settings.StrictParsing = strictParsing;
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
@ -282,13 +306,16 @@ public sealed partial class CommandAppTests
});
}
[Fact]
public void Should_Allow_Single_Branch_Multiple_Commands()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Should_Allow_Single_Branch_Multiple_Commands(bool strictParsing)
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.Settings.StrictParsing = strictParsing;
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
@ -315,13 +342,16 @@ public sealed partial class CommandAppTests
});
}
[Fact]
public void Should_Allow_Single_Branch_Single_Command()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Should_Allow_Single_Branch_Single_Command(bool strictParsing)
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.Settings.StrictParsing = strictParsing;
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{

View File

@ -132,7 +132,7 @@ public sealed partial class CommandAppTests
});
// When
var result = fixture.Run("--help");
var result = fixture.Run("dog", "--help");
// Then
return Verifier.Verify(result.Output);
@ -303,6 +303,18 @@ public sealed partial class CommandAppTests
[InlineData("sv-SE", "SV")]
[InlineData("de", "DE")]
[InlineData("de-DE", "DE")]
[InlineData("it", "IT")]
[InlineData("it-IT", "IT")]
[InlineData("ja", "JA")]
[InlineData("ja-JP", "JA")]
[InlineData("ko", "KO")]
[InlineData("ko-KR", "KO")]
[InlineData("pt", "PT")]
[InlineData("pt-BR", "PT")]
[InlineData("ru", "RU")]
[InlineData("ru-RU", "RU")]
[InlineData("zh-Hans", "ZH-HANS")]
[InlineData("zh-Hans-CN", "ZH-HANS")]
[Expectation("Default_Without_Args_Additional")]
public Task Should_Output_Default_Command_And_Additional_Commands_When_Default_Command_Has_Required_Parameters_And_Is_Called_Without_Args_Localised(string culture, string expectationPrefix)
{
@ -314,6 +326,7 @@ public sealed partial class CommandAppTests
configurator.AddExample("20", "--alive");
configurator.SetApplicationCulture(string.IsNullOrEmpty(culture) ? null : new CultureInfo(culture));
configurator.SetApplicationName("myapp");
configurator.SetApplicationVersion("1.0.0");
configurator.AddCommand<GiraffeCommand>("giraffe");
});
@ -335,10 +348,10 @@ public sealed partial class CommandAppTests
fixture.SetDefaultCommand<LionCommand>();
fixture.Configure(configurator =>
{
configurator.SetApplicationName("myapp");
configurator.AddExample("20", "--alive");
configurator.AddCommand<GiraffeCommand>("giraffe");
configurator.SetHelpProvider(new RenderMarkupHelpProvider(configurator.Settings));
configurator.SetApplicationName("myapp")
.SetHelpProvider(new RenderMarkupHelpProvider(configurator.Settings))
.AddExample("20", "--alive")
.AddCommand<GiraffeCommand>("giraffe");
});
// When
@ -526,10 +539,9 @@ public sealed partial class CommandAppTests
fixture.Configure(configurator =>
{
// Configure the custom help provider type
configurator.SetHelpProvider<RedirectHelpProvider>();
configurator.SetApplicationName("myapp");
configurator.AddCommand<DogCommand>("dog");
configurator.SetHelpProvider<RedirectHelpProvider>()
.SetApplicationName("myapp")
.AddCommand<DogCommand>("dog");
});
// When
@ -939,12 +951,12 @@ public sealed partial class CommandAppTests
configurator.SetApplicationName("myapp");
// All root examples should be shown
configurator.AddExample("--name", "Rufus", "--age", "12", "--good-boy");
configurator.AddExample("--name", "Luna");
configurator.AddExample("--name", "Charlie");
configurator.AddExample("--name", "Bella");
configurator.AddExample("--name", "Daisy");
configurator.AddExample("--name", "Milo");
configurator.AddExample("--name", "Rufus", "--age", "12", "--good-boy")
.AddExample("--name", "Luna")
.AddExample("--name", "Charlie")
.AddExample("--name", "Bella")
.AddExample("--name", "Daisy")
.AddExample("--name", "Milo");
});
// When

View File

@ -4,6 +4,129 @@ public sealed partial class CommandAppTests
{
public sealed class Remaining
{
[Fact]
public void Should_Add_Unknown_Flags_To_Remaining_Arguments_Default_Command()
{
// Given
var app = new CommandAppTester();
app.SetDefaultCommand<EmptyCommand>();
// When
var result = app.Run("--felix");
// Then
result.Output.ShouldBe(string.Empty);
result.Context.ShouldHaveRemainingArgument("--felix", new[] { (string)null });
}
[Fact]
public void Should_Add_Unknown_Flags_To_Remaining_Arguments_Command()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.AddCommand<EmptyCommand>("empty");
});
// When
var result = app.Run("empty", "--felix");
// Then
result.Output.ShouldBe(string.Empty);
result.Context.ShouldHaveRemainingArgument("--felix", new[] { (string)null });
}
[Fact]
public void Should_Add_Unknown_Flags_To_Remaining_Arguments_Branch_Default_Command()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.AddBranch<EmptyCommandSettings>("branch", branch =>
{
branch.SetDefaultCommand<EmptyCommand>();
});
});
// When
var result = app.Run("branch", "--felix");
// Then
result.Output.ShouldBe(string.Empty);
result.Context.ShouldHaveRemainingArgument("--felix", new[] { (string)null });
}
[Fact]
public void Should_Add_Unknown_Flags_To_Remaining_Arguments_Branch_Command()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.AddBranch<EmptyCommandSettings>("branch", branch =>
{
branch.AddCommand<EmptyCommand>("hello");
});
});
// When
var result = app.Run("branch", "hello", "--felix");
// Then
result.Output.ShouldBe(string.Empty);
result.Context.ShouldHaveRemainingArgument("--felix", new[] { (string)null });
}
[Fact]
public void Should_Add_Unknown_Flags_To_Remaining_Arguments_Branch_Branch_Default_Command()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.AddBranch<EmptyCommandSettings>("branch", branch =>
{
branch.AddBranch<EmptyCommandSettings>("hello", hello =>
{
hello.SetDefaultCommand<EmptyCommand>();
});
});
});
// When
var result = app.Run("branch", "hello", "--felix");
// Then
result.Output.ShouldBe(string.Empty);
result.Context.ShouldHaveRemainingArgument("--felix", new[] { (string)null });
}
[Fact]
public void Should_Add_Unknown_Flags_To_Remaining_Arguments_Branch_Branch_Command()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.AddBranch<EmptyCommandSettings>("branch", branch =>
{
branch.AddBranch<EmptyCommandSettings>("hello", hello =>
{
hello.AddCommand<EmptyCommand>("world");
});
});
});
// When
var result = app.Run("branch", "hello", "world", "--felix");
// Then
result.Output.ShouldBe(string.Empty);
result.Context.ShouldHaveRemainingArgument("--felix", new[] { (string)null });
}
[Theory]
[InlineData("-a")]
[InlineData("--alive")]
@ -259,14 +382,14 @@ public sealed partial class CommandAppTests
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Should_Convert_Flags_To_Remaining_Arguments_If_Cannot_Be_Assigned(bool useStrictParsing)
public void Should_Convert_Flags_To_Remaining_Arguments_If_Cannot_Be_Assigned(bool strictParsing)
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.Settings.ConvertFlagsToRemainingArguments = true;
config.Settings.StrictParsing = useStrictParsing;
config.Settings.StrictParsing = strictParsing;
config.PropagateExceptions();
config.AddCommand<DogCommand>("dog");
});

View File

@ -0,0 +1,236 @@
namespace Spectre.Console.Tests.Unit.Cli;
public sealed partial class CommandAppTests
{
public sealed partial class Version
{
public sealed class Help
{
[Theory]
[InlineData("-?")]
[InlineData("-h")]
[InlineData("--help")]
public void Help_Should_Include_Application_Version_Flag_With_No_Command(string helpOption)
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationVersion("1.0");
});
// When
var result = fixture.Run(helpOption);
// Then
result.Output.ShouldContain("-v, --version Prints version information");
}
[Theory]
[InlineData("-?")]
[InlineData("-h")]
[InlineData("--help")]
public void Help_Should_Not_Include_Application_Version_Flag_If_Not_Specified(string helpOption)
{
// Given
var fixture = new CommandAppTester();
// When
var result = fixture.Run(helpOption);
// Then
result.Output.ShouldNotContain("-v, --version Prints version information");
}
[Theory]
[InlineData("-?")]
[InlineData("-h")]
[InlineData("--help")]
public void Help_Should_Include_Application_Version_Flag_For_Default_Command(string helpOption)
{
// Given
var fixture = new CommandAppTester();
fixture.SetDefaultCommand<EmptyCommand>();
fixture.Configure(configurator =>
{
configurator.SetApplicationVersion("1.0");
});
// When
var result = fixture.Run(helpOption);
// Then
result.Output.ShouldContain("-v, --version Prints version information");
}
[Theory]
[InlineData("-?")]
[InlineData("-h")]
[InlineData("--help")]
public void Help_Should_Not_Include_Application_Version_Flag_For_Command(string helpOption)
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationVersion("1.0");
configurator.AddCommand<EmptyCommand>("empty");
});
// When
var result = fixture.Run("empty", helpOption);
// Then
result.Output.ShouldNotContain("-v, --version Prints version information");
}
[Theory]
[InlineData("-?")]
[InlineData("-h")]
[InlineData("--help")]
public void Help_Should_Not_Include_Application_Version_Flag_For_Branch_Default_Command(string helpOption)
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationVersion("1.0");
configurator.AddBranch<EmptyCommandSettings>("branch", branch =>
{
branch.SetDefaultCommand<EmptyCommand>();
});
});
// When
var result = fixture.Run("branch", helpOption);
// Then
result.Output.ShouldNotContain("-v, --version Prints version information");
}
[Theory]
[InlineData("-?")]
[InlineData("-h")]
[InlineData("--help")]
public void Help_Should_Not_Include_Application_Version_Flag_For_Branch_Command(string helpOption)
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationVersion("1.0");
configurator.AddBranch<EmptyCommandSettings>("branch", branch =>
{
branch.AddCommand<EmptyCommand>("empty");
});
});
// When
var result = fixture.Run("branch", "empty", helpOption);
// Then
result.Output.ShouldNotContain("-v, --version Prints version information");
}
/// <summary>
/// When a command with a version flag in the settings is set as the application default command,
/// then override the in-built Application Version flag with the command version flag instead.
/// Rationale: This behaviour makes the most sense because the other flags for the default command
/// will be shown in the help output and the user can set any of these when executing the application.
/// </summary>
[Theory]
[InlineData("-?")]
[InlineData("-h")]
[InlineData("--help")]
public void Help_Should_Include_Command_Version_Flag_For_Default_Command(string helpOption)
{
// Given
var fixture = new CommandAppTester();
fixture.SetDefaultCommand<Spectre.Console.Tests.Data.VersionCommand>();
fixture.Configure(configurator =>
{
configurator.SetApplicationVersion("1.0");
});
// When
var result = fixture.Run(helpOption);
// Then
result.Output.ShouldContain("-v, --version The command version");
result.Output.ShouldNotContain("-v, --version Prints version information");
}
[Theory]
[InlineData("-?")]
[InlineData("-h")]
[InlineData("--help")]
public void Help_Should_Include_Command_Version_Flag_For_Command(string helpOption)
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationVersion("1.0");
configurator.AddCommand<Spectre.Console.Tests.Data.VersionCommand>("hello");
});
// When
var result = fixture.Run("hello", helpOption);
// Then
result.Output.ShouldContain("-v, --version The command version");
result.Output.ShouldNotContain("-v, --version Prints version information");
}
[Theory]
[InlineData("-?")]
[InlineData("-h")]
[InlineData("--help")]
public void Help_Should_Include_Command_Version_Flag_For_Branch_Default_Command(string helpOption)
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationVersion("1.0");
configurator.AddBranch<VersionSettings>("branch", branch =>
{
branch.SetDefaultCommand<Spectre.Console.Tests.Data.VersionCommand>();
});
});
// When
var result = fixture.Run("branch", helpOption);
// Then
result.Output.ShouldContain("-v, --version The command version");
result.Output.ShouldNotContain("-v, --version Prints version information");
}
[Theory]
[InlineData("-?")]
[InlineData("-h")]
[InlineData("--help")]
public void Help_Should_Include_Command_Version_Flag_For_Branch_Command(string helpOption)
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationVersion("1.0");
configurator.AddBranch<VersionSettings>("branch", branch =>
{
branch.AddCommand<Spectre.Console.Tests.Data.VersionCommand>("hello");
});
});
// When
var result = fixture.Run("branch", "hello", helpOption);
// Then
result.Output.ShouldContain("-v, --version The command version");
result.Output.ShouldNotContain("-v, --version Prints version information");
}
}
}
}

View File

@ -2,13 +2,19 @@ namespace Spectre.Console.Tests.Unit.Cli;
public sealed partial class CommandAppTests
{
public sealed class Version
public sealed partial class Version
{
[Fact]
public void Should_Output_CLI_Version_To_The_Console()
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Should_Output_CLI_Version_To_The_Console(bool strictParsing)
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.Settings.StrictParsing = strictParsing;
});
// When
var result = fixture.Run(Constants.VersionCommand);
@ -17,81 +23,124 @@ public sealed partial class CommandAppTests
result.Output.ShouldStartWith("Spectre.Cli version ");
}
[Fact]
public void Should_Output_Application_Version_To_The_Console_With_No_Command()
[Theory]
[InlineData("-v", false)]
[InlineData("-v", true)]
[InlineData("--version", false)]
[InlineData("--version", true)]
public void Should_Output_Application_Version_To_The_Console_With_No_Command(string versionOption, bool strictParsing)
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.Settings.StrictParsing = strictParsing;
configurator.SetApplicationVersion("1.0");
});
// When
var result = fixture.Run("--version");
var result = fixture.Run(versionOption);
// Then
result.Output.ShouldBe("1.0");
}
[Fact]
public void Should_Not_Display_Version_If_Not_Specified()
[Theory]
[InlineData("-v", false)]
[InlineData("-v", true)]
[InlineData("--version", false)]
[InlineData("--version", true)]
public void Should_Not_Display_Version_If_Not_Specified(string versionOption, bool strictParsing)
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.AddCommand<EmptyCommand>("empty");
configurator.Settings.StrictParsing = strictParsing;
});
// When
var result = fixture.Run("--version");
var result = fixture.Run(versionOption);
// Then
result.ExitCode.ShouldNotBe(0);
result.Output.ShouldStartWith("Error: Unexpected option 'version'");
result.Output.ShouldStartWith($"Error: Unexpected option '{versionOption.Replace("-", "")}'");
}
[Fact]
public void Should_Execute_Command_Not_Output_Application_Version_To_The_Console()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationVersion("1.0");
configurator.AddCommand<EmptyCommand>("empty");
});
// When
var result = fixture.Run("empty", "--version");
// Then
result.Output.ShouldBe(string.Empty);
result.Context.ShouldHaveRemainingArgument("--version", new[] { (string)null });
}
[Fact]
public void Should_Execute_Default_Command_Not_Output_Application_Version_To_The_Console()
[Theory]
[InlineData("-v", false)]
[InlineData("-v", true)]
[InlineData("--version", false)]
[InlineData("--version", true)]
public void Should_Output_Application_Version_To_The_Console_With_Default_Command(string versionOption, bool strictParsing)
{
// Given
var fixture = new CommandAppTester();
fixture.SetDefaultCommand<EmptyCommand>();
fixture.Configure(configurator =>
{
configurator.Settings.StrictParsing = strictParsing;
configurator.SetApplicationVersion("1.0");
});
// When
var result = fixture.Run("--version");
var result = fixture.Run(versionOption);
// Then
result.Output.ShouldBe("1.0");
}
[Theory]
[InlineData("-v")]
[InlineData("--version")]
public void Should_Execute_Command_Not_Output_Application_Version_To_The_Console_Relaxed_Parsing(string versionOption)
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationVersion("1.0");
configurator.AddCommand<EmptyCommand>("empty");
});
// When
var result = fixture.Run("empty", versionOption);
// Then
result.Output.ShouldBe(string.Empty);
result.Context.ShouldHaveRemainingArgument("--version", new[] { (string)null });
result.Context.ShouldHaveRemainingArgument(versionOption, new[] { (string)null });
}
[Fact]
public void Should_Output_Application_Version_To_The_Console_With_Branch_Default_Command()
[Theory]
[InlineData("-v", false)]
[InlineData("-v", true)]
[InlineData("--version", false)]
[InlineData("--version", true)]
public void Should_Output_Application_Version_To_The_Console_With_Branch_Default_Command(string versionOption, bool strictParsing)
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.Settings.StrictParsing = strictParsing;
configurator.SetApplicationVersion("1.0");
configurator.AddBranch<EmptyCommandSettings>("branch", branch =>
{
branch.SetDefaultCommand<EmptyCommand>();
});
});
// When
var result = fixture.Run(versionOption);
// Then
result.Output.ShouldBe("1.0");
}
[Theory]
[InlineData("-v")]
[InlineData("--version")]
public void Should_Execute_Branch_Default_Command_Not_Output_Application_Version_To_The_Console_Relaxed_Parsing(string versionOption)
{
// Given
var fixture = new CommandAppTester();
@ -105,10 +154,137 @@ public sealed partial class CommandAppTests
});
// When
var result = fixture.Run("--version");
var result = fixture.Run("branch", versionOption);
// Then
result.Output.ShouldBe("1.0");
result.Output.ShouldBe(string.Empty);
result.Context.ShouldHaveRemainingArgument(versionOption, new[] { (string)null });
}
[Theory]
[InlineData("-v")]
[InlineData("--version")]
public void Should_Execute_Branch_Command_Not_Output_Application_Version_To_The_Console_Relaxed_Parsing(string versionOption)
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.SetApplicationVersion("1.0");
configurator.AddBranch<EmptyCommandSettings>("branch", branch =>
{
branch.AddCommand<EmptyCommand>("empty");
});
});
// When
var result = fixture.Run("branch", "empty", versionOption);
// Then
result.Output.ShouldBe(string.Empty);
result.Context.ShouldHaveRemainingArgument(versionOption, new[] { (string)null });
}
/// <summary>
/// When a command with a version option in the settings is set as the application default command,
/// then execute this command instead of displaying the explicitly set Application Version.
/// </summary>
[Theory]
[InlineData("-v", false)]
[InlineData("-v", true)]
[InlineData("--version", false)]
[InlineData("--version", true)]
public void Should_Execute_Default_VersionCommand_Not_Output_Application_Version_To_The_Console(string versionOption, bool strictParsing)
{
// Given
var fixture = new CommandAppTester();
fixture.SetDefaultCommand<Spectre.Console.Tests.Data.VersionCommand>();
fixture.Configure(configurator =>
{
configurator.Settings.StrictParsing = strictParsing;
configurator.SetApplicationVersion("1.0");
});
// When
var result = fixture.Run(versionOption, "X.Y.Z");
// Then
result.Output.ShouldBe("VersionCommand ran, Version: X.Y.Z");
}
[Theory]
[InlineData("-v", false)]
[InlineData("-v", true)]
[InlineData("--version", false)]
[InlineData("--version", true)]
public void Should_Execute_VersionCommand_Not_Output_Application_Version_To_The_Console(string versionOption, bool strictParsing)
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.Settings.StrictParsing = strictParsing;
configurator.SetApplicationVersion("1.0");
configurator.AddCommand<Spectre.Console.Tests.Data.VersionCommand>("hello");
});
// When
var result = fixture.Run("hello", versionOption, "X.Y.Z");
// Then
result.Output.ShouldBe("VersionCommand ran, Version: X.Y.Z");
}
[Theory]
[InlineData("-v", false)]
[InlineData("-v", true)]
[InlineData("--version", false)]
[InlineData("--version", true)]
public void Should_Execute_Branch_Default_VersionCommand_Not_Output_Application_Version_To_The_Console(string versionOption, bool strictParsing)
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.Settings.StrictParsing = strictParsing;
configurator.SetApplicationVersion("1.0");
configurator.AddBranch<VersionSettings>("branch", branch =>
{
branch.SetDefaultCommand<Spectre.Console.Tests.Data.VersionCommand>();
});
});
// When
var result = fixture.Run("branch", versionOption, "X.Y.Z");
// Then
result.Output.ShouldBe("VersionCommand ran, Version: X.Y.Z");
}
[Theory]
[InlineData("-v", false)]
[InlineData("-v", true)]
[InlineData("--version", false)]
[InlineData("--version", true)]
public void Should_Execute_Branch_VersionCommand_Not_Output_Application_Version_To_The_Console(string versionOption, bool strictParsing)
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(configurator =>
{
configurator.Settings.StrictParsing = strictParsing;
configurator.SetApplicationVersion("1.0");
configurator.AddBranch<VersionSettings>("branch", branch =>
{
branch.AddCommand<Spectre.Console.Tests.Data.VersionCommand>("hello");
});
});
// When
var result = fixture.Run("branch", "hello", versionOption, "X.Y.Z");
// Then
result.Output.ShouldBe("VersionCommand ran, Version: X.Y.Z");
}
}
}

View File

@ -0,0 +1,83 @@
using System;
namespace Spectre.Console.Tests.Unit.Cli.Testing;
public sealed class CommandAppTesterTests
{
private class CommandAppTesterCommand : Command<OptionalArgumentWithDefaultValueSettings>
{
private readonly IAnsiConsole _console;
public CommandAppTesterCommand(IAnsiConsole console)
{
_console = console;
}
public override int Execute(CommandContext context, OptionalArgumentWithDefaultValueSettings settings)
{
_console.Write(settings.Greeting);
return 0;
}
}
[Theory]
[InlineData(false, " Hello ", " Hello ")]
[InlineData(true, " Hello ", "Hello")]
[InlineData(false, " Hello \n World ", " Hello \n World ")]
[InlineData(true, " Hello \n World ", "Hello\n World")]
public void Should_Respect_Trim_Setting(bool trim, string actual, string expected)
{
// Given
var settings = new CommandAppTesterSettings { TrimConsoleOutput = trim };
var app = new CommandAppTester(settings);
app.SetDefaultCommand<CommandAppTesterCommand>();
app.Configure(config =>
{
config.PropagateExceptions();
});
// When
var result = app.Run(actual);
// Then
result.Output.ShouldBe(expected);
}
[Fact]
public void DefaultCtor_WithoutParameters_CreatesDefaultConsole()
{
// Given, When
CommandAppTester app = new();
// Then
app.Console.ShouldNotBeNull();
app.Console.Profile.Width.ShouldBe(int.MaxValue);
}
[Fact]
public void DefaultCtor_WithCustomConsole_UsesProvidedInstance()
{
// Given
TestConsole console = new();
// When
CommandAppTester app = new(null, new CommandAppTesterSettings(), console);
// Then
app.Console.ShouldNotBeNull();
app.Console.ShouldBeSameAs(console);
}
[Fact]
public void Ctor_WithSettings_CreatesDefaultConsole()
{
// Given, When
CommandAppTester app = new(new CommandAppTesterSettings());
// Then
app.Console.ShouldNotBeNull();
app.Console.Profile.Width.ShouldBe(int.MaxValue);
}
}

View File

@ -0,0 +1,80 @@
namespace Spectre.Console.Cli.Tests.Unit.Testing;
public sealed class InteractiveCommandTests
{
private sealed class InteractiveCommand : Command
{
private readonly IAnsiConsole _console;
public InteractiveCommand(IAnsiConsole console)
{
_console = console;
}
public override int Execute(CommandContext context)
{
var fruits = _console.Prompt(
new MultiSelectionPrompt<string>()
.Title("What are your [green]favorite fruits[/]?")
.NotRequired() // Not required to have a favorite fruit
.PageSize(10)
.MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]")
.InstructionsText(
"[grey](Press [blue]<space>[/] to toggle a fruit, " +
"[green]<enter>[/] to accept)[/]")
.AddChoices(new[] {
"Apple", "Apricot", "Avocado",
"Banana", "Blackcurrant", "Blueberry",
"Cherry", "Cloudberry", "Coconut",
}));
var fruit = _console.Prompt(
new SelectionPrompt<string>()
.Title("What's your [green]favorite fruit[/]?")
.PageSize(10)
.MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]")
.AddChoices(new[] {
"Apple", "Apricot", "Avocado",
"Banana", "Blackcurrant", "Blueberry",
"Cherry", "Cloudberry", "Cocunut",
}));
var name = _console.Ask<string>("What's your name?");
_console.WriteLine($"[{string.Join(',', fruits)};{fruit};{name}]");
return 0;
}
}
[Fact]
public void InteractiveCommand_WithMockedUserInputs_ProducesExpectedOutput()
{
// Given
TestConsole console = new();
console.Interactive();
// Your mocked inputs must always end with "Enter" for each prompt!
// Multi selection prompt: Choose first option
console.Input.PushKey(ConsoleKey.Spacebar);
console.Input.PushKey(ConsoleKey.Enter);
// Selection prompt: Choose second option
console.Input.PushKey(ConsoleKey.DownArrow);
console.Input.PushKey(ConsoleKey.Enter);
// Ask text prompt: Enter name
console.Input.PushTextWithEnter("Spectre Console");
var app = new CommandAppTester(null, new CommandAppTesterSettings(), console);
app.SetDefaultCommand<InteractiveCommand>();
// When
var result = app.Run();
// Then
result.ExitCode.ShouldBe(0);
result.Output.EndsWith("[Apple;Apricot;Spectre Console]");
}
}

View File

@ -6,6 +6,8 @@ public static class TestExceptions
public static bool GenericMethodThatThrows<T0, T1, TRet>(int? number) => throw new InvalidOperationException("Throwing!");
public static bool MethodThatThrowsGenericException<T>() => throw new GenericException<T>("Throwing!", default);
public static void ThrowWithInnerException()
{
try
@ -42,3 +44,6 @@ public static class TestExceptions
return ("key", new List<T>());
}
}
#pragma warning disable CS9113 // Parameter is unread.
public class GenericException<T>(string message, T value) : Exception(message);

View File

@ -0,0 +1,4 @@
Spectre.Console.Tests.Data.GenericException<Spectre.Console.IAnsiConsole>: Throwing!
at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrowsGenericException<T>() in {ProjectDirectory}Data/Exceptions.cs:9
at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_GenericException>b__8_0() in {ProjectDirectory}Unit/ExceptionTests.cs:134
at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in {ProjectDirectory}Unit/ExceptionTests.cs:147

View File

@ -0,0 +1,4 @@
GenericException<IAnsiConsole>: Throwing!
at bool Spectre.Console.Tests.Data.TestExceptions.MethodThatThrowsGenericException<T>() in {ProjectDirectory}Data/Exceptions.cs:9
at void Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_GenericException>b__8_0() in {ProjectDirectory}Unit/ExceptionTests.cs:134
at Exception Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in {ProjectDirectory}Unit/ExceptionTests.cs:147

View File

@ -142,4 +142,21 @@ public partial class AnsiConsoleTests
.ShouldBe("Hello\nWorld\n");
}
}
public sealed class WriteException
{
[Fact]
public void Should_Not_Throw_If_Exception_Has_No_StackTrace()
{
// Given
var console = new TestConsole();
var exception = new InvalidOperationException("An exception.");
// When
void When() => console.WriteException(exception);
// Then
Should.NotThrow(When);
}
}
}

View File

@ -68,6 +68,23 @@ public sealed class ColorTests
color.ShouldBe(Color.Default);
}
[Theory]
[InlineData("ffffff")]
[InlineData("#ffffff")]
[InlineData("fff")]
[InlineData("#fff")]
public void Should_Parse_3_Digit_Hex_Colors_From_Hex(string color)
{
// Given
var expected = new Color(255, 255, 255);
// When
var result = Color.FromHex(color);
// Then
result.ShouldBe(expected);
}
[Fact]
public void Should_Consider_Color_And_Non_Color_Equal()
{

View File

@ -123,6 +123,23 @@ public sealed class ExceptionTests
return Verifier.Verify(result);
}
[Theory]
[InlineData(ExceptionFormats.Default)]
[InlineData(ExceptionFormats.ShortenTypes)]
[Expectation("GenericException")]
public Task Should_Write_GenericException(ExceptionFormats exceptionFormats)
{
// Given
var console = new TestConsole { EmitAnsiSequences = true }.Width(1024);
var dex = GetException(() => TestExceptions.MethodThatThrowsGenericException<IAnsiConsole>());
// When
var result = console.WriteNormalizedException(dex, exceptionFormats);
// Then
return Verifier.Verify(result).UseParameters(exceptionFormats);
}
public static Exception GetException(Action action)
{
try