Compare commits

..

44 Commits

Author SHA1 Message Date
renovate[bot]
818fe36712 chore: Update actions/upload-artifact action to v5 2025-10-24 20:09:26 +00:00
Cédric Luthi
e51812237e Update the Spectre.Console.Cli documentation with CancellationToken
Now that #1911 is merged, all `Execute` methods of commands have a new CancellationToken parameter. Update the documentation to reflect that.

New documentation shall still be written with an example on how to pass the top-level CancellationToken to the app.Run(Async) method.
2025-10-16 22:50:52 +02:00
Patrik Svensson
749f0fded8 Update Verify to 31.0.1 2025-10-11 20:54:48 +02:00
Cédric Luthi
f5f61ca610 Add top-level CancellationToken support to Spectre.Console.Cli
Also raise CA2016 (forward the CancellationToken parameter to methods that take one) to warning

Fixes #701
2025-10-11 20:51:01 +02:00
Patrik Svensson
d90e94dbb3 Fix spacing in blog post 2025-10-10 21:27:37 +02:00
Patrik Svensson
169abca986 Add blog post for 0.52.0 2025-10-10 21:12:54 +02:00
Patrik Svensson
3c2156268c Remove global.json for docs project 2025-10-10 20:36:13 +02:00
Patrik Svensson
6fb81103f0 Update more dependencies 2025-10-10 20:32:48 +02:00
Patrik Svensson
880e83b27c Update Cake to 5.1.0 2025-10-10 20:20:50 +02:00
Patrik Svensson
0b270e1ccd Update dependencies 2025-10-10 20:13:21 +02:00
Mattias Karlsson
2d9e8069fd Fix OPENCLI_VISIBILITY_INTERNAL to DefineConstants concat 2025-10-06 23:19:10 +02:00
Patrik Svensson
b551bbd244 Add OpenCLI integration to Spectre.Console.Cli 2025-10-02 02:05:46 +02:00
Patrik Svensson
3a70fbec75 Update the blog post with info about 0.51.1 2025-09-07 01:04:05 +02:00
Patrik Svensson
c67b3df3ba Fix package references 2025-09-07 00:44:00 +02:00
Patrik Svensson
8e474f514c Add blog post for 0.51 release 2025-09-07 00:29:44 +02:00
Patrik Svensson
097f740bbd Revert back to using a single solution
Also migrates solution format from sln to slnx.
2025-09-06 23:59:53 +02:00
Patrik Svensson
ba7299adcf Build and package Spectre.Console.Cli as part of build script 2025-09-06 23:33:54 +02:00
mattfennerom
d84f9ae713 fix bug setting Header (#1890) 2025-09-05 15:42:18 +02:00
Mitch Denny
3a6d3e4520 Fix space triggering selection when items in selection list have a space. (#1881)
* Changes Search in SelectionPrompt to accept Space Key as text

---------

Co-authored-by: Philipp <30900810+DerReparator@users.noreply.github.com>
2025-08-19 20:09:12 +02:00
Tobias Tengler
a8b2f1f1e0 Support J and K for navigating list prompts (#1877) 2025-08-13 18:23:26 +02:00
FroggieFrog
0889c2f97c Update spinners (#1873)
Update spinners from https://github.com/sindresorhus/cli-spinners
2025-08-03 13:19:22 +02:00
Patrik Svensson
f4782d9916 Update dependencies 2025-08-03 13:13:12 +02:00
Frank Ray
8b59ddfd41 Separate Spectre.Console.Cli from Spectre.Console (#1850) 2025-07-23 23:11:07 +02:00
Mustafa Al-Janabi
6ad814cab0 Corrects comment for optional text prompt (#1857) 2025-07-16 00:00:29 +02:00
Daniel
f32f80dc57 Fix resizing of Live views with reduced size. (#1840) 2025-06-21 12:30:18 +02:00
Pannoniae
7f3ebe02c4 Reduce memory usage for rune width cache. (#1756) 2025-06-20 12:49:45 +02:00
renovate[bot]
d77bfb6391 chore: Update dependency AngleSharp to 1.3.0 (#1809)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 18:05:15 +02:00
renovate[bot]
7819f0693d chore: Update dependency SixLabors.ImageSharp to 3.1.10 (#1812)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 18:01:14 +02:00
renovate[bot]
465be9391b chore: Update dependency Microsoft.Playwright to 1.52.0 (#1816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 18:00:41 +02:00
renovate[bot]
7e5ddb1efe chore: Update dependency Verify.Xunit to v30 (#1818)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 17:57:36 +02:00
renovate[bot]
aabe8eeaf8 chore: Update dependency Scriban to 6.2.1 (#1808)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 17:56:33 +02:00
renovate[bot]
108b23fca8 chore: Update dependency xunit.runner.visualstudio to 3.1.1 (#1820)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 17:55:15 +02:00
renovate[bot]
7051bc9e2d chore: Update dependency Microsoft.NET.Test.Sdk to 17.14.1 (#1822)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 17:52:31 +02:00
Weihan Li
65bab890f2 Update System.Memory dependency 2025-06-05 10:34:24 +02:00
Elementttto
bd0e2d3e22 Fixed link not dispalyed in markup in Style.cs and added unit test cases (#1750) 2025-06-02 10:19:53 +02:00
Elemento
9efc426eb9 Added documentation for align widget 2025-06-01 13:54:15 +02:00
Patrik Svensson
2570202990 Fix build errors 2025-05-25 16:42:43 +02:00
Patrik Svensson
e4b5b56d93 Update help output for required options 2025-05-25 16:42:43 +02:00
Patrik Svensson
67c3909bbb Add support for required options 2025-05-25 16:42:43 +02: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
139 changed files with 2628 additions and 697 deletions

View File

@@ -8,7 +8,7 @@ indent_size = 4
insert_final_newline = false
trim_trailing_whitespace = true
[*.sln]
[*.{sln,slnx}]
indent_style = tab
[*.{csproj,vbproj,vcxproj,vcxproj.filters}]

View File

@@ -18,12 +18,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
@@ -37,7 +37,7 @@ jobs:
- name: Upload Verify Test Results
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: verify-test-results
path: |

View File

@@ -24,12 +24,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
@@ -53,17 +53,17 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: '16'
node-version: '22'
- name: Cache dependencies
uses: actions/cache@v4

View File

@@ -15,7 +15,8 @@ Task("Build")
.Does(context =>
{
Information("Compiling generator...");
DotNetBuild("./resources/scripts/Generator/Generator.sln", new DotNetBuildSettings {
DotNetBuild("./resources/scripts/Generator/Generator.slnx", new DotNetBuildSettings
{
Configuration = configuration,
Verbosity = DotNetVerbosity.Minimal,
NoLogo = true,
@@ -25,7 +26,8 @@ Task("Build")
});
Information("\nCompiling Spectre.Console...");
DotNetBuild("./src/Spectre.Console.sln", new DotNetBuildSettings {
DotNetBuild("./src/Spectre.Console.slnx", new DotNetBuildSettings
{
Configuration = configuration,
Verbosity = DotNetVerbosity.Minimal,
NoLogo = true,
@@ -60,7 +62,8 @@ Task("Package")
.IsDependentOn("Test")
.Does(context =>
{
context.DotNetPack($"./src/Spectre.Console.sln", new DotNetPackSettings {
context.DotNetPack($"./src/Spectre.Console.slnx", new DotNetPackSettings
{
Configuration = configuration,
Verbosity = DotNetVerbosity.Minimal,
NoLogo = true,
@@ -106,4 +109,4 @@ Task("Default")
////////////////////////////////////////////////////////////////
// Execution
RunTarget(target)
RunTarget(target);

View File

@@ -38,8 +38,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Playwright" Version="1.51.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.Playwright" Version="1.55.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Statiq.CodeAnalysis" Version="1.0.0-beta.72" />
<PackageReference Include="Statiq.Common" Version="1.0.0-beta.72" />
<PackageReference Include="Statiq.Web" Version="1.0.0-beta.60" />

View File

@@ -1,25 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30011.22
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docs", "Docs.csproj", "{C337F609-A890-4E52-BDA3-91658039B0E3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C337F609-A890-4E52-BDA3-91658039B0E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C337F609-A890-4E52-BDA3-91658039B0E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C337F609-A890-4E52-BDA3-91658039B0E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C337F609-A890-4E52-BDA3-91658039B0E3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2FB3922B-494A-45EB-A479-FC507B8E107C}
EndGlobalSection
EndGlobal

3
docs/Docs.slnx Normal file
View File

@@ -0,0 +1,3 @@
<Solution>
<Project Path="Docs.csproj" />
</Solution>

View File

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

View File

@@ -29,7 +29,7 @@ Optional: Embed an asciicast. The cast parameter should be the base name of the
one suffixed with -rich.cast and a second named -plain.cast. The cast attribute should be the name without
the suffix.
To generate a new cast file, open the \resources\scripts\Generator\Generator.sln project and add a new sample in the
To generate a new cast file, open the \resources\scripts\Generator\Generator.slnx project and add a new sample in the
Commands/AsciiCast/Samples/ folder. If the widget is static such as a tree or a table, try and animate the widget
using the Live widget to change the content or styling.

View File

@@ -0,0 +1,2 @@
{"version": 2, "width": 40, "height": 3, "timestamp": 1667342769, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}}
[0.0, "o", "\u001b[H\u001b[2B\u001b[38;5;9;48;5;0mSpectre!\u001b[0m"]

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

@@ -0,0 +1,43 @@
Title: Spectre.Console 0.51.1 released!
Description: Not a substitute for human interaction.
Published: 2025-09-07
Category: Release Notes
Excluded: false
---
Version `0.51.1` of Spectre.Console has been released!
_Note: Due to an issue discovered after the release of version 0.51.0, that version has now been unlisted. Lets all pretend it never existed 😅_
## What's Changed
* Fix IndexOutOfRangeException in ExceptionFormatter by [@martincostello](https://github.com/martincostello) in [#1800](https://github.com/spectreconsole/spectre.console/pull/1800)
* TestConsole can now be configured and accessed in CommandAppTester by [@magiino](https://github.com/magiino) in [#1803](https://github.com/spectreconsole/spectre.console/pull/1803)
* Add ShowRowSeparators in Table Widget docs by [@bartoginski](https://github.com/bartoginski) in [#1807](https://github.com/spectreconsole/spectre.console/pull/1807)
* Add support for required options by [@patriksvensson](https://github.com/patriksvensson) in [#1825](https://github.com/spectreconsole/spectre.console/pull/1825)
* Added documentation for align widget by [@Elementttto](https://github.com/Elementttto) in [#1746](https://github.com/spectreconsole/spectre.console/pull/1746)
* Fixed link not displayed in markup in Style.cs and added unit test cases by [@Elementttto](https://github.com/Elementttto) in [#1750](https://github.com/spectreconsole/spectre.console/pull/1750)
* Update System.Memory dependency by [@WeihanLi](https://github.com/WeihanLi) in [#1832](https://github.com/spectreconsole/spectre.console/pull/1832)
* Reduce memory usage for rune width cache. by [@Pannoniae](https://github.com/Pannoniae) in [#1756](https://github.com/spectreconsole/spectre.console/pull/1756)
* Fix resizing of Live views with reduced size. by [@belucha](https://github.com/belucha) in [#1840](https://github.com/spectreconsole/spectre.console/pull/1840)
* Corrects comment for optional text prompt by [@aljanabim](https://github.com/aljanabim) in [#1857](https://github.com/spectreconsole/spectre.console/pull/1857)
* Update spinners by [@FroggieFrog](https://github.com/FroggieFrog) in [#1873](https://github.com/spectreconsole/spectre.console/pull/1873)
* Support J and K for navigating list prompts by [@tobias-tengler](https://github.com/tobias-tengler) in [#1877](https://github.com/spectreconsole/spectre.console/pull/1877)
* Fix space triggering selection when items in the selection list have a space. by [@mitchdenny](https://github.com/mitchdenny) in [#1881](https://github.com/spectreconsole/spectre.console/pull/1881)
* Fix bug setting Header by [@mattfennerom](https://github.com/mattfennerom) in [#1890](https://github.com/spectreconsole/spectre.console/pull/1890)
## New Contributors
* [@magiino](https://github.com/magiino) made their first contribution in [#1803](https://github.com/spectreconsole/spectre.console/pull/1803)
* [@bartoginski](https://github.com/bartoginski) made their first contribution in [#1807](https://github.com/spectreconsole/spectre.console/pull/1807)
* [@Elementttto](https://github.com/Elementttto) made their first contribution in [#1746](https://github.com/spectreconsole/spectre.console/pull/1746)
* [@WeihanLi](https://github.com/WeihanLi) made their first contribution in [#1832](https://github.com/spectreconsole/spectre.console/pull/1832)
* [@Pannoniae](https://github.com/Pannoniae) made their first contribution in [#1756](https://github.com/spectreconsole/spectre.console/pull/1756)
* [@belucha](https://github.com/belucha) made their first contribution in [#1840](https://github.com/spectreconsole/spectre.console/pull/1840)
* [@aljanabim](https://github.com/aljanabim) made their first contribution in [#1857](https://github.com/spectreconsole/spectre.console/pull/1857)
* [@FroggieFrog](https://github.com/FroggieFrog) made their first contribution in [#1873](https://github.com/spectreconsole/spectre.console/pull/1873)
* [@tobias-tengler](https://github.com/tobias-tengler) made their first contribution in [#1877](https://github.com/spectreconsole/spectre.console/pull/1877)
* [@mitchdenny](https://github.com/mitchdenny) made their first contribution in [#1881](https://github.com/spectreconsole/spectre.console/pull/1881)
* [@mattfennerom](https://github.com/mattfennerom) made their first contribution in [#1890](https://github.com/spectreconsole/spectre.console/pull/1890)
**Full Changelog**: [0.50.0...0.51.0](https://github.com/spectreconsole/spectre.console/compare/0.50.0...0.51.1)

View File

@@ -0,0 +1,17 @@
Title: Spectre.Console 0.52.0 released!
Description: Don't eat (too much) glue.
Published: 2025-10-10
Category: Release Notes
Excluded: false
---
Version `0.52.0` of Spectre.Console has been released!
Exciting things are happening. Weve merged support for my love child, OpenCli, in this release. That means you can now pass the parameter `--help-dump-opencli` to your application to get an [OpenCli](https://opencli.org) description dumped to stdout.
## What's Changed
* Add OpenCLI integration to Spectre.Console.Cli by [@patriksvensson](https://github.com/patriksvensson) in [#1909](https://github.com/spectreconsole/spectre.console/pull/1909)
* Fix OPENCLI_VISIBILITY_INTERNAL to DefineConstants concat by [@devlead](https://github.com/devlead) in [#1912](https://github.com/spectreconsole/spectre.console/pull/1912)
**Full Changelog**: https://github.com/spectreconsole/spectre.console/compare/0.51.1...0.52.0

View File

@@ -18,7 +18,7 @@ public class HelloCommand : Command<HelloCommand.Settings>
}
public override int Execute(CommandContext context, Settings settings)
public override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken)
{
AnsiConsole.MarkupLine($"Hello, [blue]{settings.Name}[/]");
return 0;

View File

@@ -55,7 +55,7 @@ in the previous step.
```csharp
public class AddPackageCommand : Command<AddPackageSettings>
{
public override int Execute(CommandContext context, AddPackageSettings settings)
public override int Execute(CommandContext context, AddPackageSettings settings, CancellationToken cancellationToken)
{
// Omitted
return 0;
@@ -64,7 +64,7 @@ public class AddPackageCommand : Command<AddPackageSettings>
public class AddReferenceCommand : Command<AddReferenceSettings>
{
public override int Execute(CommandContext context, AddReferenceSettings settings)
public override int Execute(CommandContext context, AddReferenceSettings settings, CancellationToken cancellationToken)
{
// Omitted
return 0;

View File

@@ -33,7 +33,7 @@ internal sealed class FileSizeCommand : Command<FileSizeCommand.Settings>
public bool IncludeHidden { get; init; }
}
public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings)
public override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken)
{
var searchOptions = new EnumerationOptions
{

View File

@@ -61,7 +61,7 @@ in the previous step.
```csharp
public class AddPackageCommand : Command<AddPackageSettings>
{
public override int Execute(CommandContext context, AddPackageSettings settings)
public override int Execute(CommandContext context, AddPackageSettings settings, CancellationToken cancellationToken)
{
// Omitted
return 0;
@@ -70,7 +70,7 @@ public class AddPackageCommand : Command<AddPackageSettings>
public class AddReferenceCommand : Command<AddReferenceSettings>
{
public override int Execute(CommandContext context, AddReferenceSettings settings)
public override int Execute(CommandContext context, AddReferenceSettings settings, CancellationToken cancellationToken)
{
// Omitted
return 0;

22
docs/input/cli/opencli.md Normal file
View File

@@ -0,0 +1,22 @@
Title: OpenCLI Integration
Order: 15
Description: OpenCLI integration
Highlights:
- Generate OpenCLI descriptions
---
From version `0.52.0` and above, you will be able to generate [OpenCLI](https://opencli.org)
descriptions from your `Spectre.Console.Cli` applications.
Simply add the `--help-dump-opencli` option to your application, and an
OpenCLI description will be written to stdout.
```shell
$ ./myapp --help-dump-opencli
```
If you want to save it to disk, pipe it to a file.
```shell
$ ./myapp --help-dump-opencli > myapp.openapi.json
```

View File

@@ -40,7 +40,7 @@ The following example validates the exit code and terminal output of a `Spectre.
_console = console;
}
public override int Execute(CommandContext context)
public override int Execute(CommandContext context, CancellationToken cancellationToken)
{
_console.WriteLine("Hello world.");
return 0;
@@ -63,6 +63,90 @@ The following example validates the exit code and terminal output of a `Spectre.
}
```
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, CancellationToken cancellationToken)
{
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.

View File

@@ -197,7 +197,7 @@ Console.WriteLine($"Your password is {password}");
### Usage
```csharp
// Ask the user to enter the password
// Ask for the user's favorite color (optional)
var color = AnsiConsole.Prompt(
new TextPrompt<string>("[[Optional]] Favorite color?")
.AllowEmpty());

View File

@@ -0,0 +1,66 @@
Title: Align
Description: "Use **Align** to render and position widgets in the console."
Highlights:
- Custom colors
- Labels
- Use your own data with a converter.
Reference: T:Spectre.Console.Align
---
Use `Align` to render and position widgets in the console.
<?# AsciiCast cast="align" /?>
## Usage
### Basic usage
```csharp
// Render an item and align it in the bottom-left corner of the console
AnsiConsole.Write(new Align(
new Text("Spectre!"),
HorizontalAlignment.Left,
VerticalAlignment.Bottom
));
```
### Align items from an IEnumerable
```csharp
// Create a list of items
var alignItems = new List<Text>(){
new Text("Spectre"),
new Text("Console"),
new Text("Is Awesome!")
};
// Render the items in the middle-right of the console
AnsiConsole.Write(new Align(
alignItems,
HorizontalAlignment.Right,
VerticalAlignment.Middle
));
```
### Dynamically align with different widgets
```csharp
// Create a table
var table = new Table()
.AddColumn("ID")
.AddColumn("Methods")
.AddColumn("Purpose")
.AddRow("1", "Center()", "Initializes a new instance that is center aligned")
.AddRow("2", "Measure()", "Measures the renderable object")
.AddRow("3", "Right()", "Initializes a new instance that is right aligned.");
// Create a panel
var panel = new Panel(table)
.Header("Other Align Methods")
.Border(BoxBorder.Double);
// Renders the panel in the top-center of the console
AnsiConsole.Write(new Align(panel, HorizontalAlignment.Center, VerticalAlignment.Top));
```

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();
```

View File

@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"cake.tool": {
"version": "5.0.0",
"version": "5.1.0",
"commands": [
"dotnet-cake"
]

View File

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

View File

@@ -19,4 +19,4 @@ if(!$?) {
Pop-Location
# Copy the files to the correct location
Copy-Item (Join-Path "$Output" "Spinner.Generated.cs") -Destination "$Source/Widgets/Progress/Spinner.Generated.cs"
Copy-Item (Join-Path "$Output" "Spinner.Generated.cs") -Destination "$Source/Live/Progress/Spinner.Generated.cs"

View File

@@ -1,4 +1,5 @@
using System.IO;
using System.Threading;
using Generator.Models;
using Scriban;
using Spectre.Console.Cli;
@@ -21,7 +22,7 @@ namespace Generator.Commands
public string Input { get; set; }
}
public override int Execute(CommandContext context, Settings settings)
public override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken)
{
var templates = new FilePath[]
{

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using AngleSharp.Html.Parser;
using Generator.Models;
@@ -39,7 +40,7 @@ namespace Generator.Commands
_parser = new HtmlParser();
}
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken)
{
var output = new DirectoryPath(settings.Output);
if (!_fileSystem.Directory.Exists(settings.Output))

View File

@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using Generator.Commands.Samples;
using Spectre.Console;
using Spectre.Console.Cli;
@@ -38,7 +39,7 @@ namespace Generator.Commands
_console = new AsciiCastConsole(console);
}
public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings)
public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings, CancellationToken cancellationToken)
{
var samples = typeof(BaseSample).Assembly
.GetTypes()

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Generator.Models;
using Scriban;
using Spectre.Console.Cli;
@@ -16,7 +17,7 @@ namespace Generator.Commands
_fileSystem = new FileSystem();
}
public override int Execute(CommandContext context, GeneratorSettings settings)
public override int Execute(CommandContext context, GeneratorSettings settings, CancellationToken cancellationToken)
{
// Read the spinner model.
var spinners = new List<Spinner>();

View File

@@ -286,6 +286,38 @@
"⠀⡀"
]
},
"dots13": {
"interval": 80,
"unicode": true,
"frames": [
"⣼",
"⣹",
"⢻",
"⠿",
"⡟",
"⣏",
"⣧",
"⣶"
]
},
"dots14": {
"interval": 80,
"unicode": true,
"frames": [
"⠉⠉",
"⠈⠙",
"⠀⠹",
"⠀⢸",
"⠀⣰",
"⢀⣠",
"⣀⣀",
"⣄⡀",
"⣆⠀",
"⡇⠀",
"⠏⠀",
"⠋⠁"
]
},
"dots8Bit": {
"interval": 80,
"unicode": true,
@@ -548,6 +580,61 @@
"⣿"
]
},
"dotsCircle": {
"interval": 80,
"unicode": true,
"frames": [
"⢎ ",
"⠎⠁",
"⠊⠑",
"⠈⠱",
" ⡱",
"⢀⡰",
"⢄⡠",
"⢆⡀"
]
},
"sand": {
"interval": 80,
"unicode": true,
"frames": [
"⠁",
"⠂",
"⠄",
"⡀",
"⡈",
"⡐",
"⡠",
"⣀",
"⣁",
"⣂",
"⣄",
"⣌",
"⣔",
"⣤",
"⣥",
"⣦",
"⣮",
"⣶",
"⣷",
"⣿",
"⡿",
"⠿",
"⢟",
"⠟",
"⡛",
"⠛",
"⠫",
"⢋",
"⠋",
"⠍",
"⡉",
"⠉",
"⠑",
"⠡",
"⢁"
]
},
"line": {
"interval": 130,
"unicode": false,
@@ -763,6 +850,22 @@
"◥"
]
},
"binary": {
"interval": 80,
"unicode": false,
"frames": [
"010010",
"001100",
"100101",
"111010",
"111101",
"010111",
"101011",
"111000",
"110011",
"110101"
]
},
"arc": {
"interval": 100,
"unicode": true,
@@ -978,6 +1081,7 @@
"[= ]",
"[== ]",
"[=== ]",
"[====]",
"[ ===]",
"[ ==]",
"[ =]",
@@ -1351,6 +1455,135 @@
"ββββββρ"
]
},
"fingerDance": {
"interval": 160,
"unicode": true,
"frames": [
"🤘 ",
"🤟 ",
"🖖 ",
"✋ ",
"🤚 ",
"👆 "
]
},
"fistBump": {
"interval": 80,
"unicode": true,
"frames": [
"🤜\u3000\u3000\u3000\u3000🤛 ",
"🤜\u3000\u3000\u3000\u3000🤛 ",
"🤜\u3000\u3000\u3000\u3000🤛 ",
"\u3000🤜\u3000\u3000🤛\u3000 ",
"\u3000\u3000🤜🤛\u3000\u3000 ",
"\u3000🤜✨🤛\u3000\u3000 ",
"🤜\u3000✨\u3000🤛\u3000 "
]
},
"soccerHeader": {
"interval": 80,
"unicode": true,
"frames": [
" 🧑⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️ 🧑 "
]
},
"mindblown": {
"interval": 160,
"unicode": true,
"frames": [
"😐 ",
"😐 ",
"😮 ",
"😮 ",
"😦 ",
"😦 ",
"😧 ",
"😧 ",
"🤯 ",
"💥 ",
"✨ ",
"\u3000 ",
"\u3000 ",
"\u3000 "
]
},
"speaker": {
"interval": 160,
"unicode": true,
"frames": [
"🔈 ",
"🔉 ",
"🔊 ",
"🔉 "
]
},
"orangePulse": {
"interval": 100,
"unicode": true,
"frames": [
"🔸 ",
"🔶 ",
"🟠 ",
"🟠 ",
"🔶 "
]
},
"bluePulse": {
"interval": 100,
"unicode": true,
"frames": [
"🔹 ",
"🔷 ",
"🔵 ",
"🔵 ",
"🔷 "
]
},
"orangeBluePulse": {
"interval": 100,
"unicode": true,
"frames": [
"🔸 ",
"🔶 ",
"🟠 ",
"🟠 ",
"🔶 ",
"🔹 ",
"🔷 ",
"🔵 ",
"🔵 ",
"🔷 "
]
},
"timeTravel": {
"interval": 100,
"unicode": true,
"frames": [
"🕛 ",
"🕚 ",
"🕙 ",
"🕘 ",
"🕗 ",
"🕖 ",
"🕕 ",
"🕔 ",
"🕓 ",
"🕒 ",
"🕑 ",
"🕐 "
]
},
"aesthetic": {
"interval": 80,
"unicode": true,
@@ -1364,5 +1597,144 @@
"▰▰▰▰▰▰▰",
"▰▱▱▱▱▱▱"
]
},
"dwarfFortress": {
"interval": 80,
"unicode": true,
"frames": [
" ██████£££ ",
"☺██████£££ ",
"☺██████£££ ",
"☺▓█████£££ ",
"☺▓█████£££ ",
"☺▒█████£££ ",
"☺▒█████£££ ",
"☺░█████£££ ",
"☺░█████£££ ",
"☺ █████£££ ",
" ☺█████£££ ",
" ☺█████£££ ",
" ☺▓████£££ ",
" ☺▓████£££ ",
" ☺▒████£££ ",
" ☺▒████£££ ",
" ☺░████£££ ",
" ☺░████£££ ",
" ☺ ████£££ ",
" ☺████£££ ",
" ☺████£££ ",
" ☺▓███£££ ",
" ☺▓███£££ ",
" ☺▒███£££ ",
" ☺▒███£££ ",
" ☺░███£££ ",
" ☺░███£££ ",
" ☺ ███£££ ",
" ☺███£££ ",
" ☺███£££ ",
" ☺▓██£££ ",
" ☺▓██£££ ",
" ☺▒██£££ ",
" ☺▒██£££ ",
" ☺░██£££ ",
" ☺░██£££ ",
" ☺ ██£££ ",
" ☺██£££ ",
" ☺██£££ ",
" ☺▓█£££ ",
" ☺▓█£££ ",
" ☺▒█£££ ",
" ☺▒█£££ ",
" ☺░█£££ ",
" ☺░█£££ ",
" ☺ █£££ ",
" ☺█£££ ",
" ☺█£££ ",
" ☺▓£££ ",
" ☺▓£££ ",
" ☺▒£££ ",
" ☺▒£££ ",
" ☺░£££ ",
" ☺░£££ ",
" ☺ £££ ",
" ☺£££ ",
" ☺£££ ",
" ☺▓££ ",
" ☺▓££ ",
" ☺▒££ ",
" ☺▒££ ",
" ☺░££ ",
" ☺░££ ",
" ☺ ££ ",
" ☺££ ",
" ☺££ ",
" ☺▓£ ",
" ☺▓£ ",
" ☺▒£ ",
" ☺▒£ ",
" ☺░£ ",
" ☺░£ ",
" ☺ £ ",
" ☺£ ",
" ☺£ ",
" ☺▓ ",
" ☺▓ ",
" ☺▒ ",
" ☺▒ ",
" ☺░ ",
" ☺░ ",
" ☺ ",
" ☺ &",
" ☺ ☼&",
" ☺ ☼ &",
" ☺☼ &",
" ☺☼ & ",
" ‼ & ",
" ☺ & ",
" ‼ & ",
" ☺ & ",
" ‼ & ",
" ☺ & ",
"‼ & ",
" & ",
" & ",
" & ░ ",
" & ▒ ",
" & ▓ ",
" & £ ",
" & ░£ ",
" & ▒£ ",
" & ▓£ ",
" & ££ ",
" & ░££ ",
" & ▒££ ",
"& ▓££ ",
"& £££ ",
" ░£££ ",
" ▒£££ ",
" ▓£££ ",
" █£££ ",
" ░█£££ ",
" ▒█£££ ",
" ▓█£££ ",
" ██£££ ",
" ░██£££ ",
" ▒██£££ ",
" ▓██£££ ",
" ███£££ ",
" ░███£££ ",
" ▒███£££ ",
" ▓███£££ ",
" ████£££ ",
" ░████£££ ",
" ▒████£££ ",
" ▓████£££ ",
" █████£££ ",
" ░█████£££ ",
" ▒█████£££ ",
" ▓█████£££ ",
" ██████£££ ",
" ██████£££ "
]
}
}

View File

@@ -43,11 +43,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.2.0" />
<PackageReference Include="AngleSharp" Version="1.3.0" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Scriban" Version="6.2.0" />
<PackageReference Include="Spectre.IO" Version="0.18.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Scriban" Version="6.4.0" />
<PackageReference Include="Spectre.IO" Version="0.20.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,101 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32922.545
MinimumVisualStudioVersion = 15.0.26124.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generator", "Generator.csproj", "{5668D267-53E3-4B99-97AE-59AA597D22ED}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console", "..\..\..\src\Spectre.Console\Spectre.Console.csproj", "{F75B882A-06DB-426B-9580-A7302D32E684}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.ImageSharp", "..\..\..\src\Extensions\Spectre.Console.ImageSharp\Spectre.Console.ImageSharp.csproj", "{112A37CB-1EFE-4A90-BD5B-5437038BE276}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Library", "Library", "{CFE7445D-F971-429D-B6E6-9E68456AE00F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Cli", "..\..\..\src\Spectre.Console.Cli\Spectre.Console.Cli.csproj", "{18A3F32D-FECD-463B-A194-6EE74EA9E5EC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spectre.Console.Json", "..\..\..\src\Extensions\Spectre.Console.Json\Spectre.Console.Json.csproj", "{6C96C268-CEEE-478A-A36F-E1450AC33B73}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5668D267-53E3-4B99-97AE-59AA597D22ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5668D267-53E3-4B99-97AE-59AA597D22ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5668D267-53E3-4B99-97AE-59AA597D22ED}.Debug|x64.ActiveCfg = Debug|Any CPU
{5668D267-53E3-4B99-97AE-59AA597D22ED}.Debug|x64.Build.0 = Debug|Any CPU
{5668D267-53E3-4B99-97AE-59AA597D22ED}.Debug|x86.ActiveCfg = Debug|Any CPU
{5668D267-53E3-4B99-97AE-59AA597D22ED}.Debug|x86.Build.0 = Debug|Any CPU
{5668D267-53E3-4B99-97AE-59AA597D22ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5668D267-53E3-4B99-97AE-59AA597D22ED}.Release|Any CPU.Build.0 = Release|Any CPU
{5668D267-53E3-4B99-97AE-59AA597D22ED}.Release|x64.ActiveCfg = Release|Any CPU
{5668D267-53E3-4B99-97AE-59AA597D22ED}.Release|x64.Build.0 = Release|Any CPU
{5668D267-53E3-4B99-97AE-59AA597D22ED}.Release|x86.ActiveCfg = Release|Any CPU
{5668D267-53E3-4B99-97AE-59AA597D22ED}.Release|x86.Build.0 = Release|Any CPU
{F75B882A-06DB-426B-9580-A7302D32E684}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F75B882A-06DB-426B-9580-A7302D32E684}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F75B882A-06DB-426B-9580-A7302D32E684}.Debug|x64.ActiveCfg = Debug|Any CPU
{F75B882A-06DB-426B-9580-A7302D32E684}.Debug|x64.Build.0 = Debug|Any CPU
{F75B882A-06DB-426B-9580-A7302D32E684}.Debug|x86.ActiveCfg = Debug|Any CPU
{F75B882A-06DB-426B-9580-A7302D32E684}.Debug|x86.Build.0 = Debug|Any CPU
{F75B882A-06DB-426B-9580-A7302D32E684}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F75B882A-06DB-426B-9580-A7302D32E684}.Release|Any CPU.Build.0 = Release|Any CPU
{F75B882A-06DB-426B-9580-A7302D32E684}.Release|x64.ActiveCfg = Release|Any CPU
{F75B882A-06DB-426B-9580-A7302D32E684}.Release|x64.Build.0 = Release|Any CPU
{F75B882A-06DB-426B-9580-A7302D32E684}.Release|x86.ActiveCfg = Release|Any CPU
{F75B882A-06DB-426B-9580-A7302D32E684}.Release|x86.Build.0 = Release|Any CPU
{112A37CB-1EFE-4A90-BD5B-5437038BE276}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{112A37CB-1EFE-4A90-BD5B-5437038BE276}.Debug|Any CPU.Build.0 = Debug|Any CPU
{112A37CB-1EFE-4A90-BD5B-5437038BE276}.Debug|x64.ActiveCfg = Debug|Any CPU
{112A37CB-1EFE-4A90-BD5B-5437038BE276}.Debug|x64.Build.0 = Debug|Any CPU
{112A37CB-1EFE-4A90-BD5B-5437038BE276}.Debug|x86.ActiveCfg = Debug|Any CPU
{112A37CB-1EFE-4A90-BD5B-5437038BE276}.Debug|x86.Build.0 = Debug|Any CPU
{112A37CB-1EFE-4A90-BD5B-5437038BE276}.Release|Any CPU.ActiveCfg = Release|Any CPU
{112A37CB-1EFE-4A90-BD5B-5437038BE276}.Release|Any CPU.Build.0 = Release|Any CPU
{112A37CB-1EFE-4A90-BD5B-5437038BE276}.Release|x64.ActiveCfg = Release|Any CPU
{112A37CB-1EFE-4A90-BD5B-5437038BE276}.Release|x64.Build.0 = Release|Any CPU
{112A37CB-1EFE-4A90-BD5B-5437038BE276}.Release|x86.ActiveCfg = Release|Any CPU
{112A37CB-1EFE-4A90-BD5B-5437038BE276}.Release|x86.Build.0 = Release|Any CPU
{18A3F32D-FECD-463B-A194-6EE74EA9E5EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{18A3F32D-FECD-463B-A194-6EE74EA9E5EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{18A3F32D-FECD-463B-A194-6EE74EA9E5EC}.Debug|x64.ActiveCfg = Debug|Any CPU
{18A3F32D-FECD-463B-A194-6EE74EA9E5EC}.Debug|x64.Build.0 = Debug|Any CPU
{18A3F32D-FECD-463B-A194-6EE74EA9E5EC}.Debug|x86.ActiveCfg = Debug|Any CPU
{18A3F32D-FECD-463B-A194-6EE74EA9E5EC}.Debug|x86.Build.0 = Debug|Any CPU
{18A3F32D-FECD-463B-A194-6EE74EA9E5EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18A3F32D-FECD-463B-A194-6EE74EA9E5EC}.Release|Any CPU.Build.0 = Release|Any CPU
{18A3F32D-FECD-463B-A194-6EE74EA9E5EC}.Release|x64.ActiveCfg = Release|Any CPU
{18A3F32D-FECD-463B-A194-6EE74EA9E5EC}.Release|x64.Build.0 = Release|Any CPU
{18A3F32D-FECD-463B-A194-6EE74EA9E5EC}.Release|x86.ActiveCfg = Release|Any CPU
{18A3F32D-FECD-463B-A194-6EE74EA9E5EC}.Release|x86.Build.0 = Release|Any CPU
{6C96C268-CEEE-478A-A36F-E1450AC33B73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6C96C268-CEEE-478A-A36F-E1450AC33B73}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6C96C268-CEEE-478A-A36F-E1450AC33B73}.Debug|x64.ActiveCfg = Debug|Any CPU
{6C96C268-CEEE-478A-A36F-E1450AC33B73}.Debug|x64.Build.0 = Debug|Any CPU
{6C96C268-CEEE-478A-A36F-E1450AC33B73}.Debug|x86.ActiveCfg = Debug|Any CPU
{6C96C268-CEEE-478A-A36F-E1450AC33B73}.Debug|x86.Build.0 = Debug|Any CPU
{6C96C268-CEEE-478A-A36F-E1450AC33B73}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6C96C268-CEEE-478A-A36F-E1450AC33B73}.Release|Any CPU.Build.0 = Release|Any CPU
{6C96C268-CEEE-478A-A36F-E1450AC33B73}.Release|x64.ActiveCfg = Release|Any CPU
{6C96C268-CEEE-478A-A36F-E1450AC33B73}.Release|x64.Build.0 = Release|Any CPU
{6C96C268-CEEE-478A-A36F-E1450AC33B73}.Release|x86.ActiveCfg = Release|Any CPU
{6C96C268-CEEE-478A-A36F-E1450AC33B73}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F75B882A-06DB-426B-9580-A7302D32E684} = {CFE7445D-F971-429D-B6E6-9E68456AE00F}
{112A37CB-1EFE-4A90-BD5B-5437038BE276} = {CFE7445D-F971-429D-B6E6-9E68456AE00F}
{18A3F32D-FECD-463B-A194-6EE74EA9E5EC} = {CFE7445D-F971-429D-B6E6-9E68456AE00F}
{6C96C268-CEEE-478A-A36F-E1450AC33B73} = {CFE7445D-F971-429D-B6E6-9E68456AE00F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5F37FDE3-D591-4D43-8DDE-2ED6BAB0A7B4}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,14 @@
<Solution>
<Configurations>
<Platform Name="Any CPU" />
<Platform Name="x64" />
<Platform Name="x86" />
</Configurations>
<Folder Name="/Library/">
<Project Path="../../../src/Extensions/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj" />
<Project Path="../../../src/Extensions/Spectre.Console.Json/Spectre.Console.Json.csproj" />
<Project Path="../../../src/Spectre.Console.Cli/Spectre.Console.Cli.csproj" />
<Project Path="../../../src/Spectre.Console/Spectre.Console.csproj" />
</Folder>
<Project Path="Generator.csproj" />
</Solution>

View File

@@ -100,5 +100,8 @@ dotnet_diagnostic.RCS1047.severity = none
# RCS1090: Call 'ConfigureAwait(false)'.
dotnet_diagnostic.RCS1090.severity = warning
# The file header is missing or not located at the top of the file
# SA1633: The file header is missing or not located at the top of the file
dotnet_diagnostic.SA1633.severity = none
# CA2016: Forward the CancellationToken parameter to methods that take one
dotnet_diagnostic.CA2016.severity = warning

View File

@@ -1,27 +1,28 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="IsExternalInit" Version="1.0.3" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3"/>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" Version="8.0.0" />
<PackageVersion Include="MinVer" PrivateAssets="All" Version="6.0.0" />
<PackageVersion Include="OpenCli.Sources" Version="0.5.0" />
<PackageVersion Include="PolySharp" Version="1.15.0" />
<PackageVersion Include="Roslynator.Analyzers" PrivateAssets="All" Version="4.13.1"/>
<PackageVersion Include="Roslynator.Analyzers" PrivateAssets="All" Version="4.14.1" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7"/>
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageVersion Include="Spectre.Verify.Extensions" Version="28.16.0" />
<PackageVersion Include="StyleCop.Analyzers" PrivateAssets="All" Version="1.2.0-beta.556" />
<PackageVersion Include="System.Memory" Version="4.6.3" />
<PackageVersion Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" Version="1.0.0-alpha.160" />
<PackageVersion Include="Verify.Xunit" Version="29.2.0"/>
<PackageVersion Include="Wcwidth.Sources" Version="2.0.0"/>
<PackageVersion Include="Verify.Xunit" Version="31.0.1" />
<PackageVersion Include="Wcwidth.Sources" Version="3.0.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.2"/>
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
</ItemGroup>
</Project>

View File

@@ -45,6 +45,6 @@ public sealed class CommandArgumentAttribute : Attribute
// Assign the result.
Position = position;
ValueName = result.Value;
IsRequired = result.Required;
IsRequired = result.IsRequired;
}
}

View File

@@ -30,6 +30,11 @@ public sealed class CommandOptionAttribute : Attribute
/// </summary>
public bool ValueIsOptional { get; }
/// <summary>
/// Gets a value indicating whether the value is required.
/// </summary>
public bool IsRequired { get; }
/// <summary>
/// Gets or sets a value indicating whether this option is hidden from the help text.
/// </summary>
@@ -39,7 +44,8 @@ public sealed class CommandOptionAttribute : Attribute
/// Initializes a new instance of the <see cref="CommandOptionAttribute"/> class.
/// </summary>
/// <param name="template">The option template.</param>
public CommandOptionAttribute(string template)
/// <param name="isRequired">Indicates whether the option is required or not.</param>
public CommandOptionAttribute(string template, bool isRequired = false)
{
if (template == null)
{
@@ -54,6 +60,7 @@ public sealed class CommandOptionAttribute : Attribute
ShortNames = result.ShortNames;
ValueName = result.Value;
ValueIsOptional = result.ValueIsOptional;
IsRequired = isRequired;
}
internal bool IsMatch(string name)

View File

@@ -9,19 +9,20 @@ public abstract class AsyncCommand : ICommand<EmptyCommandSettings>
/// Executes the command.
/// </summary>
/// <param name="context">The command context.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to abort the command.</param>
/// <returns>An integer indicating whether or not the command executed successfully.</returns>
public abstract Task<int> ExecuteAsync(CommandContext context);
public abstract Task<int> ExecuteAsync(CommandContext context, CancellationToken cancellationToken);
/// <inheritdoc/>
Task<int> ICommand<EmptyCommandSettings>.Execute(CommandContext context, EmptyCommandSettings settings)
Task<int> ICommand<EmptyCommandSettings>.ExecuteAsync(CommandContext context, EmptyCommandSettings settings, CancellationToken cancellationToken)
{
return ExecuteAsync(context);
return ExecuteAsync(context, cancellationToken);
}
/// <inheritdoc/>
Task<int> ICommand.Execute(CommandContext context, CommandSettings settings)
Task<int> ICommand.ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken)
{
return ExecuteAsync(context);
return ExecuteAsync(context, cancellationToken);
}
/// <inheritdoc/>

View File

@@ -23,8 +23,9 @@ public abstract class AsyncCommand<TSettings> : ICommand<TSettings>
/// </summary>
/// <param name="context">The command context.</param>
/// <param name="settings">The settings.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to abort the command.</param>
/// <returns>An integer indicating whether or not the command executed successfully.</returns>
public abstract Task<int> ExecuteAsync(CommandContext context, TSettings settings);
public abstract Task<int> ExecuteAsync(CommandContext context, TSettings settings, CancellationToken cancellationToken);
/// <inheritdoc/>
ValidationResult ICommand.Validate(CommandContext context, CommandSettings settings)
@@ -33,15 +34,15 @@ public abstract class AsyncCommand<TSettings> : ICommand<TSettings>
}
/// <inheritdoc/>
Task<int> ICommand.Execute(CommandContext context, CommandSettings settings)
Task<int> ICommand.ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken)
{
Debug.Assert(settings is TSettings, "Command settings is of unexpected type.");
return ExecuteAsync(context, (TSettings)settings);
return ExecuteAsync(context, (TSettings)settings, cancellationToken);
}
/// <inheritdoc/>
Task<int> ICommand<TSettings>.Execute(CommandContext context, TSettings settings)
Task<int> ICommand<TSettings>.ExecuteAsync(CommandContext context, TSettings settings, CancellationToken cancellationToken)
{
return ExecuteAsync(context, settings);
return ExecuteAsync(context, settings, cancellationToken);
}
}

View File

@@ -10,19 +10,20 @@ public abstract class Command : ICommand<EmptyCommandSettings>
/// Executes the command.
/// </summary>
/// <param name="context">The command context.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to abort the command.</param>
/// <returns>An integer indicating whether or not the command executed successfully.</returns>
public abstract int Execute(CommandContext context);
public abstract int Execute(CommandContext context, CancellationToken cancellationToken);
/// <inheritdoc/>
Task<int> ICommand<EmptyCommandSettings>.Execute(CommandContext context, EmptyCommandSettings settings)
Task<int> ICommand<EmptyCommandSettings>.ExecuteAsync(CommandContext context, EmptyCommandSettings settings, CancellationToken cancellationToken)
{
return Task.FromResult(Execute(context));
return Task.FromResult(Execute(context, cancellationToken));
}
/// <inheritdoc/>
Task<int> ICommand.Execute(CommandContext context, CommandSettings settings)
Task<int> ICommand.ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken)
{
return Task.FromResult(Execute(context));
return Task.FromResult(Execute(context, cancellationToken));
}
/// <inheritdoc/>

View File

@@ -26,10 +26,7 @@ public sealed class CommandApp : ICommandApp
_executor = new CommandExecutor(registrar);
}
/// <summary>
/// Configures the command line application.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <inheritdoc/>
public void Configure(Action<IConfigurator> configuration)
{
if (configuration == null)
@@ -51,22 +48,14 @@ public sealed class CommandApp : ICommandApp
return new DefaultCommandConfigurator(GetConfigurator().SetDefaultCommand<TCommand>());
}
/// <summary>
/// Runs the command line application with specified arguments.
/// </summary>
/// <param name="args">The arguments.</param>
/// <returns>The exit code from the executed command.</returns>
public int Run(IEnumerable<string> args)
/// <inheritdoc/>
public int Run(IEnumerable<string> args, CancellationToken cancellationToken = default)
{
return RunAsync(args).GetAwaiter().GetResult();
return RunAsync(args, cancellationToken).GetAwaiter().GetResult();
}
/// <summary>
/// Runs the command line application with specified arguments.
/// </summary>
/// <param name="args">The arguments.</param>
/// <returns>The exit code from the executed command.</returns>
public async Task<int> RunAsync(IEnumerable<string> args)
/// <inheritdoc/>
public async Task<int> RunAsync(IEnumerable<string> args, CancellationToken cancellationToken = default)
{
try
{
@@ -79,13 +68,14 @@ public sealed class CommandApp : ICommandApp
cli.AddCommand<VersionCommand>(CliConstants.Commands.Version);
cli.AddCommand<XmlDocCommand>(CliConstants.Commands.XmlDoc);
cli.AddCommand<ExplainCommand>(CliConstants.Commands.Explain);
cli.AddCommand<OpenCliGeneratorCommand>(CliConstants.Commands.OpenCli);
});
_executed = true;
}
return await _executor
.Execute(_configurator, args)
.ExecuteAsync(_configurator, args, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception ex)
@@ -108,6 +98,11 @@ public sealed class CommandApp : ICommandApp
return _configurator.Settings.ExceptionHandler(ex, null);
}
if (ex is OperationCanceledException)
{
return _configurator.Settings.CancellationExitCode;
}
// Render the exception.
var pretty = GetRenderableErrorMessage(ex);
if (pretty != null)

View File

@@ -25,33 +25,22 @@ public sealed class CommandApp<TDefaultCommand> : ICommandApp
_defaultCommandConfigurator = _app.SetDefaultCommand<TDefaultCommand>();
}
/// <summary>
/// Configures the command line application.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <inheritdoc/>
public void Configure(Action<IConfigurator> configuration)
{
_app.Configure(configuration);
}
/// <summary>
/// Runs the command line application with specified arguments.
/// </summary>
/// <param name="args">The arguments.</param>
/// <returns>The exit code from the executed command.</returns>
public int Run(IEnumerable<string> args)
/// <inheritdoc/>
public int Run(IEnumerable<string> args, CancellationToken cancellationToken = default)
{
return _app.Run(args);
return _app.Run(args, cancellationToken);
}
/// <summary>
/// Runs the command line application with specified arguments.
/// </summary>
/// <param name="args">The arguments.</param>
/// <returns>The exit code from the executed command.</returns>
public Task<int> RunAsync(IEnumerable<string> args)
/// <inheritdoc/>
public Task<int> RunAsync(IEnumerable<string> args, CancellationToken cancellationToken = default)
{
return _app.RunAsync(args);
return _app.RunAsync(args, cancellationToken);
}
internal Configurator GetConfigurator()

View File

@@ -24,8 +24,9 @@ public abstract class Command<TSettings> : ICommand<TSettings>
/// </summary>
/// <param name="context">The command context.</param>
/// <param name="settings">The settings.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to abort the command.</param>
/// <returns>An integer indicating whether or not the command executed successfully.</returns>
public abstract int Execute(CommandContext context, TSettings settings);
public abstract int Execute(CommandContext context, TSettings settings, CancellationToken cancellationToken);
/// <inheritdoc/>
ValidationResult ICommand.Validate(CommandContext context, CommandSettings settings)
@@ -34,15 +35,15 @@ public abstract class Command<TSettings> : ICommand<TSettings>
}
/// <inheritdoc/>
Task<int> ICommand.Execute(CommandContext context, CommandSettings settings)
Task<int> ICommand.ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken)
{
Debug.Assert(settings is TSettings, "Command settings is of unexpected type.");
return Task.FromResult(Execute(context, (TSettings)settings));
return Task.FromResult(Execute(context, (TSettings)settings, cancellationToken));
}
/// <inheritdoc/>
Task<int> ICommand<TSettings>.Execute(CommandContext context, TSettings settings)
Task<int> ICommand<TSettings>.ExecuteAsync(CommandContext context, TSettings settings, CancellationToken cancellationToken)
{
return Task.FromResult(Execute(context, settings));
return Task.FromResult(Execute(context, settings, cancellationToken));
}
}

View File

@@ -37,6 +37,16 @@ public class CommandRuntimeException : CommandAppException
return new CommandRuntimeException($"Command '{node.Command.Name}' is missing required argument '{argument.Value}'.");
}
internal static CommandRuntimeException MissingRequiredOption(CommandTree node, CommandOption option)
{
if (node.Command.Name == CliConstants.DefaultCommandName)
{
return new CommandRuntimeException($"Missing required option '{option.GetOptionName()}'.");
}
return new CommandRuntimeException($"Command '{node.Command.Name}' is missing required argument '{option.GetOptionName()}'.");
}
internal static CommandRuntimeException NoConverterFound(CommandParameter parameter)
{
return new CommandRuntimeException($"Could not find converter for type '{parameter.ParameterType.FullName}'.");

View File

@@ -201,6 +201,24 @@ public static class ConfiguratorExtensions
return configurator;
}
/// <summary>
/// Tells the command line application to return the specified exit code when it's aborted through the <see cref="CancellationToken"/>.
/// The default cancellation exit code is 130.
/// </summary>
/// <param name="configurator">The configurator.</param>
/// <param name="exitCode">The exit code to return in case of cancellation.</param>
/// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator CancellationExitCode(this IConfigurator configurator, int exitCode)
{
if (configurator == null)
{
throw new ArgumentNullException(nameof(configurator));
}
configurator.Settings.CancellationExitCode = exitCode;
return configurator;
}
/// <summary>
/// Configures case sensitivity.
/// </summary>
@@ -304,14 +322,14 @@ public static class ConfiguratorExtensions
public static ICommandConfigurator AddDelegate(
this IConfigurator configurator,
string name,
Func<CommandContext, int> func)
Func<CommandContext, CancellationToken, int> func)
{
if (configurator == null)
{
throw new ArgumentNullException(nameof(configurator));
}
return configurator.AddDelegate<EmptyCommandSettings>(name, (c, _) => func(c));
return configurator.AddDelegate<EmptyCommandSettings>(name, (c, _, ct) => func(c, ct));
}
/// <summary>
@@ -324,14 +342,14 @@ public static class ConfiguratorExtensions
public static ICommandConfigurator AddAsyncDelegate(
this IConfigurator configurator,
string name,
Func<CommandContext, Task<int>> func)
Func<CommandContext, CancellationToken, Task<int>> func)
{
if (configurator == null)
{
throw new ArgumentNullException(nameof(configurator));
}
return configurator.AddAsyncDelegate<EmptyCommandSettings>(name, (c, _) => func(c));
return configurator.AddAsyncDelegate<EmptyCommandSettings>(name, (c, _, ct) => func(c, ct));
}
/// <summary>
@@ -345,7 +363,7 @@ public static class ConfiguratorExtensions
public static ICommandConfigurator AddDelegate<TSettings>(
this IConfigurator<TSettings>? configurator,
string name,
Func<CommandContext, int> func)
Func<CommandContext, CancellationToken, int> func)
where TSettings : CommandSettings
{
if (typeof(TSettings).IsAbstract)
@@ -358,7 +376,7 @@ public static class ConfiguratorExtensions
throw new ArgumentNullException(nameof(configurator));
}
return configurator.AddDelegate<TSettings>(name, (c, _) => func(c));
return configurator.AddDelegate<TSettings>(name, (c, _, ct) => func(c, ct));
}
/// <summary>
@@ -372,7 +390,7 @@ public static class ConfiguratorExtensions
public static ICommandConfigurator AddAsyncDelegate<TSettings>(
this IConfigurator<TSettings> configurator,
string name,
Func<CommandContext, Task<int>> func)
Func<CommandContext, CancellationToken, Task<int>> func)
where TSettings : CommandSettings
{
if (configurator == null)
@@ -380,7 +398,7 @@ public static class ConfiguratorExtensions
throw new ArgumentNullException(nameof(configurator));
}
return configurator.AddAsyncDelegate<TSettings>(name, (c, _) => func(c));
return configurator.AddAsyncDelegate<TSettings>(name, (c, _, ct) => func(c, ct));
}
/// <summary>

View File

@@ -53,7 +53,7 @@ public class HelpProvider : IHelpProvider
{
var arguments = new List<HelpArgument>();
arguments.AddRange(command?.Parameters?.OfType<ICommandArgument>()?.Select(
x => new HelpArgument(x.Value, x.Position, x.Required, x.Description))
x => new HelpArgument(x.Value, x.Position, x.IsRequired, x.Description))
?? Array.Empty<HelpArgument>());
return arguments;
}
@@ -65,15 +65,20 @@ public class HelpProvider : IHelpProvider
public string? Long { get; }
public string? Value { get; }
public bool? ValueIsOptional { get; }
public bool IsRequired { get; }
public string? Description { get; }
public object? DefaultValue { get; }
private HelpOption(string? @short, string? @long, string? @value, bool? valueIsOptional, string? description, object? defaultValue)
private HelpOption(
string? @short, string? @long, string? @value,
bool? valueIsOptional, bool isRequired,
string? description, object? defaultValue)
{
Short = @short;
Long = @long;
Value = value;
ValueIsOptional = valueIsOptional;
IsRequired = isRequired;
Description = description;
DefaultValue = defaultValue;
}
@@ -85,7 +90,8 @@ public class HelpProvider : IHelpProvider
{
var parameters = new List<HelpOption>
{
new HelpOption("h", "help", null, null, resources.PrintHelpDescription, null),
new HelpOption("h", "help", null, null, false,
resources.PrintHelpDescription, null),
};
// Version information applies to the entire CLI application.
@@ -107,7 +113,8 @@ public class HelpProvider : IHelpProvider
// 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.Add(new HelpOption("v", "version", null, null, false,
resources.PrintVersionDescription, null));
}
}
}
@@ -115,7 +122,7 @@ public class HelpProvider : IHelpProvider
parameters.AddRange(command?.Parameters.OfType<ICommandOption>().Where(o => !o.IsHidden).Select(o =>
new HelpOption(
o.ShortNames.FirstOrDefault(), o.LongNames.FirstOrDefault(),
o.ValueName, o.ValueIsOptional, o.Description,
o.ValueName, o.ValueIsOptional, o.IsRequired, o.Description,
o.IsFlag && o.DefaultValue?.Value is false ? null : o.DefaultValue?.Value))
?? Array.Empty<HelpOption>());
return parameters;
@@ -215,7 +222,9 @@ public class HelpProvider : IHelpProvider
{
if (isCurrent)
{
parameters.Add(NewComposer().Style(helpStyles?.Usage?.CurrentCommand ?? Style.Plain, $"{current.Name}"));
parameters.Add(NewComposer().Style(
helpStyles?.Usage?.CurrentCommand ?? Style.Plain,
$"{current.Name}"));
}
else
{
@@ -228,38 +237,46 @@ public class HelpProvider : IHelpProvider
if (isCurrent)
{
foreach (var argument in current.Parameters.OfType<ICommandArgument>()
.Where(a => a.Required).OrderBy(a => a.Position).ToArray())
.Where(a => a.IsRequired).OrderBy(a => a.Position).ToArray())
{
parameters.Add(NewComposer().Style(helpStyles?.Usage?.RequiredArgument ?? Style.Plain, $"<{argument.Value}>"));
parameters.Add(NewComposer().Style(
helpStyles?.Usage?.RequiredArgument ?? Style.Plain,
$"<{argument.Value}>"));
}
}
var optionalArguments = current.Parameters.OfType<ICommandArgument>().Where(x => !x.Required).ToArray();
var optionalArguments = current.Parameters.OfType<ICommandArgument>().Where(x => !x.IsRequired)
.ToArray();
if (optionalArguments.Length > 0 || !isCurrent)
{
foreach (var optionalArgument in optionalArguments)
{
parameters.Add(NewComposer().Style(helpStyles?.Usage?.OptionalArgument ?? Style.Plain, $"[{optionalArgument.Value}]"));
parameters.Add(NewComposer().Style(
helpStyles?.Usage?.OptionalArgument ?? Style.Plain,
$"[{optionalArgument.Value}]"));
}
}
}
if (isCurrent)
{
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Options ?? Style.Plain, $"[{resources.Options}]"));
parameters.Add(NewComposer()
.Style(helpStyles?.Usage?.Options ?? Style.Plain, $"[{resources.Options}]"));
}
}
if (command.IsBranch && command.DefaultCommand == null)
{
// The user must specify the command
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"<{resources.Command}>"));
parameters.Add(NewComposer()
.Style(helpStyles?.Usage?.Command ?? Style.Plain, $"<{resources.Command}>"));
}
else if (command.IsBranch && command.DefaultCommand != null && command.Commands.Count > 0)
{
// We are on a branch with a default command
// The user can optionally specify the command
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]"));
parameters.Add(NewComposer()
.Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]"));
}
else if (command.IsDefaultCommand)
{
@@ -269,7 +286,8 @@ public class HelpProvider : IHelpProvider
{
// Commands other than the default are present
// So make these optional in the usage statement
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]"));
parameters.Add(NewComposer()
.Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]"));
}
}
}
@@ -338,7 +356,8 @@ public class HelpProvider : IHelpProvider
for (var index = 0; index < Math.Min(maxExamples, examples.Count); index++)
{
var args = string.Join(" ", examples[index]);
composer.Tab().Text(model.ApplicationName).Space().Style(helpStyles?.Examples?.Arguments ?? Style.Plain, args);
composer.Tab().Text(model.ApplicationName).Space()
.Style(helpStyles?.Examples?.Arguments ?? Style.Plain, args);
composer.LineBreak();
}
@@ -364,7 +383,8 @@ public class HelpProvider : IHelpProvider
var result = new List<IRenderable>
{
NewComposer().LineBreak().Style(helpStyles?.Arguments?.Header ?? Style.Plain, $"{resources.Arguments}:").LineBreak(),
NewComposer().LineBreak().Style(helpStyles?.Arguments?.Header ?? Style.Plain, $"{resources.Arguments}:")
.LineBreak(),
};
var grid = new Grid();
@@ -407,7 +427,8 @@ public class HelpProvider : IHelpProvider
var result = new List<IRenderable>
{
NewComposer().LineBreak().Style(helpStyles?.Options?.Header ?? Style.Plain, $"{resources.Options}:").LineBreak(),
NewComposer().LineBreak().Style(helpStyles?.Options?.Header ?? Style.Plain, $"{resources.Options}:")
.LineBreak(),
};
var helpOptions = parameters.ToArray();
@@ -439,7 +460,15 @@ public class HelpProvider : IHelpProvider
columns.Add(GetDefaultValueForOption(option.DefaultValue));
}
columns.Add(NewComposer().Text(NormalizeDescription(option.Description)));
var description = option.Description;
if (option.IsRequired)
{
description = string.IsNullOrWhiteSpace(description)
? "[i]Required[/]"
: description.TrimEnd('.') + ". [i]Required[/]";
}
columns.Add(NewComposer().Text(NormalizeDescription(description)));
grid.AddRow(columns.ToArray());
}
@@ -470,7 +499,8 @@ public class HelpProvider : IHelpProvider
var result = new List<IRenderable>
{
NewComposer().LineBreak().Style(helpStyles?.Commands?.Header ?? Style.Plain, $"{resources.Commands}:").LineBreak(),
NewComposer().LineBreak().Style(helpStyles?.Commands?.Header ?? Style.Plain, $"{resources.Commands}:")
.LineBreak(),
};
var grid = new Grid();
@@ -546,11 +576,11 @@ public class HelpProvider : IHelpProvider
composer.Text(" ");
if (option.ValueIsOptional ?? false)
{
composer.Style(helpStyles?.Options?.OptionalOption ?? Style.Plain, $"[{option.Value}]");
composer.Style(helpStyles?.Options?.OptionalOptionValue ?? Style.Plain, $"[{option.Value}]");
}
else
{
composer.Style(helpStyles?.Options?.RequiredOption ?? Style.Plain, $"<{option.Value}>");
composer.Style(helpStyles?.Options?.RequiredOptionValue ?? Style.Plain, $"<{option.Value}>");
}
}
@@ -564,8 +594,15 @@ public class HelpProvider : IHelpProvider
null => NewComposer().Text(" "),
"" => NewComposer().Text(" "),
Array { Length: 0 } => NewComposer().Text(" "),
Array array => NewComposer().Join(", ", array.Cast<object>().Select(o => NewComposer().Style(helpStyles?.Options?.DefaultValue ?? Style.Plain, o.ToString() ?? string.Empty))),
_ => NewComposer().Style(helpStyles?.Options?.DefaultValue ?? Style.Plain, defaultValue?.ToString() ?? string.Empty),
Array array => NewComposer().Join(
", ",
array.Cast<object>().Select(o =>
NewComposer().Style(
helpStyles?.Options?.DefaultValue ?? Style.Plain,
o.ToString() ?? string.Empty))),
_ => NewComposer().Style(
helpStyles?.Options?.DefaultValue ?? Style.Plain,
defaultValue?.ToString() ?? string.Empty),
};
}

View File

@@ -76,8 +76,8 @@ public sealed class HelpProviderStyle
Header = "yellow",
DefaultValueHeader = "lime",
DefaultValue = "bold",
RequiredOption = "silver",
OptionalOption = "grey",
RequiredOptionValue = "silver",
OptionalOptionValue = "grey",
},
};
}
@@ -212,8 +212,13 @@ public sealed class OptionStyle
/// </summary>
public Style? RequiredOption { get; set; }
/// <summary>
/// Gets or sets the style for required option values.
/// </summary>
public Style? RequiredOptionValue { get; set; }
/// <summary>
/// Gets or sets the style for optional options.
/// </summary>
public Style? OptionalOption { get; set; }
public Style? OptionalOptionValue { get; set; }
}

View File

@@ -13,7 +13,7 @@ public interface ICommandParameter
/// <summary>
/// Gets a value indicating whether the parameter is required.
/// </summary>
bool Required { get; }
bool IsRequired { get; }
/// <summary>
/// Gets the description of the parameter.

View File

@@ -18,6 +18,7 @@ public interface ICommand
/// </summary>
/// <param name="context">The command context.</param>
/// <param name="settings">The settings.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to abort the command.</param>
/// <returns>The validation result.</returns>
Task<int> Execute(CommandContext context, CommandSettings settings);
Task<int> ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken);
}

View File

@@ -15,13 +15,15 @@ public interface ICommandApp
/// Runs the command line application with specified arguments.
/// </summary>
/// <param name="args">The arguments.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to abort the application.</param>
/// <returns>The exit code from the executed command.</returns>
int Run(IEnumerable<string> args);
int Run(IEnumerable<string> args, CancellationToken cancellationToken = default);
/// <summary>
/// Runs the command line application with specified arguments.
/// </summary>
/// <param name="args">The arguments.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to abort the application.</param>
/// <returns>The exit code from the executed command.</returns>
Task<int> RunAsync(IEnumerable<string> args);
Task<int> RunAsync(IEnumerable<string> args, CancellationToken cancellationToken = default);
}

View File

@@ -88,6 +88,12 @@ public interface ICommandAppSettings
/// </summary>
bool PropagateExceptions { get; set; }
/// <summary>
/// Gets or sets the value used as the application exit code when it's aborted through the <see cref="CancellationToken"/>.
/// The default cancellation exit code is 130.
/// </summary>
int CancellationExitCode { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not examples should be validated.
/// </summary>

View File

@@ -12,6 +12,7 @@ public interface ICommand<TSettings> : ICommandLimiter<TSettings>
/// </summary>
/// <param name="context">The command context.</param>
/// <param name="settings">The settings.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to abort the command.</param>
/// <returns>An integer indicating whether or not the command executed successfully.</returns>
Task<int> Execute(CommandContext context, TSettings settings);
Task<int> ExecuteAsync(CommandContext context, TSettings settings, CancellationToken cancellationToken);
}

View File

@@ -48,7 +48,7 @@ public interface IConfigurator
/// <param name="name">The name of the command.</param>
/// <param name="func">The delegate to execute as part of command execution.</param>
/// <returns>A command configurator that can be used to configure the command further.</returns>
ICommandConfigurator AddDelegate<TSettings>(string name, Func<CommandContext, TSettings, int> func)
ICommandConfigurator AddDelegate<TSettings>(string name, Func<CommandContext, TSettings, CancellationToken, int> func)
where TSettings : CommandSettings;
/// <summary>
@@ -58,7 +58,7 @@ public interface IConfigurator
/// <param name="name">The name of the command.</param>
/// <param name="func">The delegate to execute as part of command execution.</param>
/// <returns>A command configurator that can be used to configure the command further.</returns>
ICommandConfigurator AddAsyncDelegate<TSettings>(string name, Func<CommandContext, TSettings, Task<int>> func)
ICommandConfigurator AddAsyncDelegate<TSettings>(string name, Func<CommandContext, TSettings, CancellationToken, Task<int>> func)
where TSettings : CommandSettings;
/// <summary>

View File

@@ -54,7 +54,7 @@ public interface IConfigurator<in TSettings>
/// <param name="name">The name of the command.</param>
/// <param name="func">The delegate to execute as part of command execution.</param>
/// <returns>A command configurator that can be used to configure the command further.</returns>
ICommandConfigurator AddDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, int> func)
ICommandConfigurator AddDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, CancellationToken, int> func)
where TDerivedSettings : TSettings;
/// <summary>
@@ -64,7 +64,7 @@ public interface IConfigurator<in TSettings>
/// <param name="name">The name of the command.</param>
/// <param name="func">The delegate to execute as part of command execution.</param>
/// <returns>A command configurator that can be used to configure the command further.</returns>
ICommandConfigurator AddAsyncDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, Task<int>> func)
ICommandConfigurator AddAsyncDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, CancellationToken, Task<int>> func)
where TDerivedSettings : TSettings;
/// <summary>

View File

@@ -12,7 +12,7 @@ internal sealed class CommandExecutor
_registrar.Register(typeof(DefaultPairDeconstructor), typeof(DefaultPairDeconstructor));
}
public async Task<int> Execute(IConfiguration configuration, IEnumerable<string> args)
public async Task<int> ExecuteAsync(IConfiguration configuration, IEnumerable<string> args, CancellationToken cancellationToken)
{
CommandTreeParserResult parsedResult;
@@ -68,6 +68,13 @@ internal sealed class CommandExecutor
}
}
}
// OpenCLI?
if (firstArgument.Equals(CliConstants.DumpHelpOpenCliOption, StringComparison.OrdinalIgnoreCase))
{
// Replace all arguments with the opencligen command
arguments = ["cli", "opencli"];
}
}
// Parse and map the model against the arguments.
@@ -103,7 +110,7 @@ internal sealed class CommandExecutor
}
// Is this the default and is it called without arguments when there are required arguments?
if (leaf.Command.IsDefaultCommand && arguments.Count == 0 && leaf.Command.Parameters.Any(p => p.Required))
if (leaf.Command.IsDefaultCommand && arguments.Count == 0 && leaf.Command.Parameters.Any(p => p.IsRequired))
{
// Display help for default command.
configuration.Settings.Console.SafeRender(helpProvider.Write(model, leaf.Command));
@@ -118,7 +125,7 @@ internal sealed class CommandExecutor
leaf.Command.Data);
// Execute the command tree.
return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false);
return await ExecuteAsync(leaf, parsedResult.Tree, context, resolver, configuration, cancellationToken).ConfigureAwait(false);
}
}
@@ -215,12 +222,13 @@ internal sealed class CommandExecutor
return (parsedResult, tokenizerResult);
}
private static async Task<int> Execute(
private static async Task<int> ExecuteAsync(
CommandTree leaf,
CommandTree tree,
CommandContext context,
ITypeResolver resolver,
IConfiguration configuration)
IConfiguration configuration,
CancellationToken cancellationToken)
{
try
{
@@ -249,7 +257,7 @@ internal sealed class CommandExecutor
}
// Execute the command.
var result = await command.Execute(context, settings);
var result = await command.ExecuteAsync(context, settings, cancellationToken);
foreach (var interceptor in interceptors)
{
interceptor.InterceptResult(context, settings, ref result);

View File

@@ -9,12 +9,14 @@ internal static class CommandValidator
{
foreach (var parameter in node.Unmapped)
{
if (parameter.Required)
if (parameter.IsRequired)
{
switch (parameter)
{
case CommandArgument argument:
throw CommandRuntimeException.MissingRequiredArgument(node, argument);
case CommandOption option:
throw CommandRuntimeException.MissingRequiredOption(node, option);
}
}
}

View File

@@ -2,7 +2,7 @@ namespace Spectre.Console.Cli;
[Description("Displays diagnostics about CLI configurations")]
[SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")]
internal sealed class ExplainCommand : Command<ExplainCommand.Settings>
internal sealed class ExplainCommand : Command<ExplainCommand.Settings>, IBuiltInCommand
{
private readonly CommandModel _commandModel;
private readonly IAnsiConsole _writer;
@@ -27,7 +27,7 @@ internal sealed class ExplainCommand : Command<ExplainCommand.Settings>
public bool IncludeHidden { get; set; }
}
public override int Execute(CommandContext context, Settings settings)
public override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken)
{
var tree = new Tree("CLI Configuration");
tree.AddNode(ValueMarkup("Application Name", _commandModel.ApplicationName, "no application name"));
@@ -212,7 +212,7 @@ internal sealed class ExplainCommand : Command<ExplainCommand.Settings>
parameterNode.AddNode(ValueMarkup("Value", commandArgumentParameter.Value));
}
parameterNode.AddNode(ValueMarkup("Required", parameter.Required.ToString()));
parameterNode.AddNode(ValueMarkup("Required", parameter.IsRequired.ToString()));
if (parameter.Converter != null)
{

View File

@@ -0,0 +1,9 @@
namespace Spectre.Console.Cli;
/// <summary>
/// Represents a built-in command.
/// Used as a marker interface.
/// </summary>
internal interface IBuiltInCommand : ICommand
{
}

View File

@@ -0,0 +1,224 @@
using OpenCli;
namespace Spectre.Console.Cli;
internal sealed class OpenCliGeneratorCommand : Command, IBuiltInCommand
{
private readonly IConfiguration _configuration;
private readonly CommandModel _model;
public OpenCliGeneratorCommand(IConfiguration configuration, CommandModel model)
{
_configuration = configuration;
_model = model ?? throw new ArgumentNullException(nameof(model));
}
public override int Execute(CommandContext context, CancellationToken cancellationToken)
{
var document = new OpenCliDocument
{
OpenCli = "0.1-draft",
Info = new OpenCliInfo
{
Title = ((ICommandModel)_model).ApplicationName, Version = _model.ApplicationVersion ?? "1.0",
},
Commands = CreateCommands(_model.Commands),
Arguments = CreateArguments(_model.DefaultCommand?.GetArguments()),
Options = CreateOptions(_model.DefaultCommand?.GetOptions()),
};
var writer = _configuration.Settings.Console.GetConsole();
writer.WriteLine(document.Write());
return 0;
}
private List<OpenCliCommand> CreateCommands(IList<CommandInfo> commands)
{
var result = new List<OpenCliCommand>();
foreach (var command in commands.OrderBy(o => o.Name, StringComparer.OrdinalIgnoreCase))
{
if (typeof(IBuiltInCommand).IsAssignableFrom(command.CommandType))
{
continue;
}
var openCliCommand = new OpenCliCommand
{
Name = command.Name,
Aliases =
[
..command.Aliases.OrderBy(str => str)
],
Commands = CreateCommands(command.Children),
Arguments = CreateArguments(command.GetArguments()),
Options = CreateOptions(command.GetOptions()),
Description = command.Description,
Hidden = command.IsHidden,
Examples = [..command.Examples.Select(example => string.Join(" ", example))],
};
// Skip branches without commands
if (command.IsBranch && openCliCommand.Commands.Count == 0)
{
continue;
}
result.Add(openCliCommand);
}
return result;
}
private List<OpenCliArgument> CreateArguments(IEnumerable<CommandArgument>? arguments)
{
var result = new List<OpenCliArgument>();
if (arguments == null)
{
return result;
}
foreach (var argument in arguments.OrderBy(x => x.Position))
{
var metadata = default(List<OpenCliMetadata>);
if (argument.ParameterType != typeof(void) &&
argument.ParameterType != typeof(bool))
{
metadata =
[
new OpenCliMetadata { Name = "ClrType", Value = argument.ParameterType.ToCliTypeString(), },
];
}
result.Add(new OpenCliArgument
{
Name = argument.Value,
Required = argument.IsRequired,
Arity = new OpenCliArity
{
// TODO: Look into this
Minimum = 1,
Maximum = 1,
},
Description = argument.Description,
Hidden = argument.IsHidden,
Metadata = metadata,
AcceptedValues = null,
Group = null,
});
}
return result;
}
private List<OpenCliOption> CreateOptions(IEnumerable<CommandOption>? options)
{
var result = new List<OpenCliOption>();
if (options == null)
{
return result;
}
foreach (var option in options.OrderBy(o => o.GetOptionName(), StringComparer.OrdinalIgnoreCase))
{
var arguments = new List<OpenCliArgument>();
if (option.ParameterType != typeof(void) &&
option.ParameterType != typeof(bool))
{
arguments.Add(new OpenCliArgument
{
Name = option.ValueName ?? "VALUE",
Required = !option.ValueIsOptional,
Arity = new OpenCliArity
{
// TODO: Look into this
Minimum = option.ValueIsOptional
? 0
: 1,
Maximum = 1,
},
AcceptedValues = null,
Group = null,
Hidden = null,
Metadata =
[
new OpenCliMetadata
{
Name = "ClrType",
Value = option.ParameterType.ToCliTypeString(),
},
],
});
}
var optionMetadata = default(List<OpenCliMetadata>);
if (arguments.Count == 0 && option.ParameterType != typeof(void) &&
option.ParameterType != typeof(bool))
{
optionMetadata =
[
new OpenCliMetadata { Name = "ClrType", Value = option.ParameterType.ToCliTypeString(), },
];
}
var (optionName, optionAliases) = GetOptionNames(option);
result.Add(new OpenCliOption
{
Name = optionName,
Required = option.IsRequired,
Aliases = [..optionAliases.OrderBy(str => str)],
Arguments = arguments,
Description = option.Description,
Group = null,
Hidden = option.IsHidden,
Recursive = option.IsShadowed, // TODO: Is this correct?
Metadata = optionMetadata,
});
}
return result;
}
private static (string Name, HashSet<string> Aliases) GetOptionNames(CommandOption option)
{
var name = GetOptionName(option);
var aliases = new HashSet<string>();
if (option.LongNames.Count > 0)
{
foreach (var alias in option.LongNames.Skip(1))
{
aliases.Add("--" + alias);
}
foreach (var alias in option.ShortNames)
{
aliases.Add("-" + alias);
}
}
else
{
foreach (var alias in option.LongNames)
{
aliases.Add("--" + alias);
}
foreach (var alias in option.ShortNames.Skip(1))
{
aliases.Add("-" + alias);
}
}
return (name, aliases);
}
private static string GetOptionName(CommandOption option)
{
return option.LongNames.Count > 0
? "--" + option.LongNames[0]
: "-" + option.ShortNames[0];
}
}

View File

@@ -2,7 +2,7 @@ namespace Spectre.Console.Cli;
[Description("Displays the CLI library version")]
[SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")]
internal sealed class VersionCommand : Command<VersionCommand.Settings>
internal sealed class VersionCommand : Command, IBuiltInCommand
{
private readonly IAnsiConsole _writer;
@@ -11,11 +11,7 @@ internal sealed class VersionCommand : Command<VersionCommand.Settings>
_writer = configuration.Settings.Console.GetConsole();
}
public sealed class Settings : CommandSettings
{
}
public override int Execute(CommandContext context, Settings settings)
public override int Execute(CommandContext context, CancellationToken cancellationToken)
{
_writer.MarkupLine(
"[yellow]Spectre.Cli[/] version [aqua]{0}[/]",

View File

@@ -2,7 +2,7 @@ namespace Spectre.Console.Cli;
[Description("Generates an XML representation of the CLI configuration.")]
[SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")]
internal sealed class XmlDocCommand : Command<XmlDocCommand.Settings>
internal sealed class XmlDocCommand : Command, IBuiltInCommand
{
private readonly CommandModel _model;
private readonly IAnsiConsole _writer;
@@ -13,11 +13,7 @@ internal sealed class XmlDocCommand : Command<XmlDocCommand.Settings>
_writer = configuration.Settings.Console.GetConsole();
}
public sealed class Settings : CommandSettings
{
}
public override int Execute(CommandContext context, Settings settings)
public override int Execute(CommandContext context, CancellationToken cancellationToken)
{
_writer.Write(Serialize(_model), Style.Plain);
return 0;
@@ -142,7 +138,7 @@ internal sealed class XmlDocCommand : Command<XmlDocCommand.Settings>
var node = document.CreateElement("Argument");
node.SetNullableAttribute("Name", argument.Value);
node.SetAttribute("Position", argument.Position.ToString(CultureInfo.InvariantCulture));
node.SetBooleanAttribute("Required", argument.Required);
node.SetBooleanAttribute("Required", argument.IsRequired);
node.SetEnumAttribute("Kind", argument.ParameterKind);
node.SetNullableAttribute("ClrType", argument.ParameterType?.FullName);
@@ -186,7 +182,7 @@ internal sealed class XmlDocCommand : Command<XmlDocCommand.Settings>
node.SetNullableAttribute("Short", option.ShortNames);
node.SetNullableAttribute("Long", option.LongNames);
node.SetNullableAttribute("Value", option.ValueName);
node.SetBooleanAttribute("Required", option.Required);
node.SetBooleanAttribute("Required", option.IsRequired);
node.SetEnumAttribute("Kind", option.ParameterKind);
node.SetNullableAttribute("ClrType", option.ParameterType?.FullName);

View File

@@ -18,6 +18,7 @@ internal sealed class CommandAppSettings : ICommandAppSettings
public HelpProviderStyle? HelpProviderStyles { get; set; }
public bool StrictParsing { get; set; }
public bool ConvertFlagsToRemainingArguments { get; set; }
public int CancellationExitCode { get; set; }
public ParsingMode ParsingMode =>
StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed;
@@ -33,6 +34,7 @@ internal sealed class CommandAppSettings : ICommandAppSettings
TrimTrailingPeriod = true;
HelpProviderStyles = HelpProviderStyle.Default;
ConvertFlagsToRemainingArguments = false;
CancellationExitCode = 130;
}
public bool IsTrue(Func<CommandAppSettings, bool> func, string environmentVariableName)

View File

@@ -56,19 +56,19 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig
return new CommandConfigurator(command);
}
public ICommandConfigurator AddDelegate<TSettings>(string name, Func<CommandContext, TSettings, int> func)
public ICommandConfigurator AddDelegate<TSettings>(string name, Func<CommandContext, TSettings, CancellationToken, int> func)
where TSettings : CommandSettings
{
var command = Commands.AddAndReturn(ConfiguredCommand.FromDelegate<TSettings>(
name, (context, settings) => Task.FromResult(func(context, (TSettings)settings))));
name, (context, settings, cancellationToken) => Task.FromResult(func(context, (TSettings)settings, cancellationToken))));
return new CommandConfigurator(command);
}
public ICommandConfigurator AddAsyncDelegate<TSettings>(string name, Func<CommandContext, TSettings, Task<int>> func)
public ICommandConfigurator AddAsyncDelegate<TSettings>(string name, Func<CommandContext, TSettings, CancellationToken, Task<int>> func)
where TSettings : CommandSettings
{
var command = Commands.AddAndReturn(ConfiguredCommand.FromDelegate<TSettings>(
name, (context, settings) => func(context, (TSettings)settings)));
name, (context, settings, cancellationToken) => func(context, (TSettings)settings, cancellationToken)));
return new CommandConfigurator(command);
}

View File

@@ -46,21 +46,21 @@ internal sealed class Configurator<TSettings> : IUnsafeBranchConfigurator, IConf
return configurator;
}
public ICommandConfigurator AddDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, int> func)
public ICommandConfigurator AddDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, CancellationToken, int> func)
where TDerivedSettings : TSettings
{
var command = ConfiguredCommand.FromDelegate<TDerivedSettings>(
name, (context, settings) => Task.FromResult(func(context, (TDerivedSettings)settings)));
name, (context, settings, cancellationToken) => Task.FromResult(func(context, (TDerivedSettings)settings, cancellationToken)));
_command.Children.Add(command);
return new CommandConfigurator(command);
}
public ICommandConfigurator AddAsyncDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, Task<int>> func)
public ICommandConfigurator AddAsyncDelegate<TDerivedSettings>(string name, Func<CommandContext, TDerivedSettings, CancellationToken, Task<int>> func)
where TDerivedSettings : TSettings
{
var command = ConfiguredCommand.FromDelegate<TDerivedSettings>(
name, (context, settings) => func(context, (TDerivedSettings)settings));
name, (context, settings, cancellationToken) => func(context, (TDerivedSettings)settings, cancellationToken));
_command.Children.Add(command);
return new CommandConfigurator(command);

View File

@@ -8,7 +8,7 @@ internal sealed class ConfiguredCommand
public object? Data { get; set; }
public Type? CommandType { get; }
public Type SettingsType { get; }
public Func<CommandContext, CommandSettings, Task<int>>? Delegate { get; }
public Func<CommandContext, CommandSettings, CancellationToken, Task<int>>? Delegate { get; }
public bool IsDefaultCommand { get; }
public bool IsHidden { get; set; }
@@ -19,7 +19,7 @@ internal sealed class ConfiguredCommand
string name,
Type? commandType,
Type settingsType,
Func<CommandContext, CommandSettings, Task<int>>? @delegate,
Func<CommandContext, CommandSettings, CancellationToken, Task<int>>? @delegate,
bool isDefaultCommand)
{
Name = name;
@@ -60,7 +60,7 @@ internal sealed class ConfiguredCommand
}
public static ConfiguredCommand FromDelegate<TSettings>(
string name, Func<CommandContext, CommandSettings, Task<int>>? @delegate = null)
string name, Func<CommandContext, CommandSettings, CancellationToken, Task<int>>? @delegate = null)
where TSettings : CommandSettings
{
return new ConfiguredCommand(name, null, typeof(TSettings), @delegate, false);

View File

@@ -5,12 +5,12 @@ internal static class TemplateParser
public sealed class ArgumentResult
{
public string Value { get; set; }
public bool Required { get; set; }
public bool IsRequired { get; set; }
public ArgumentResult(string value, bool required)
public ArgumentResult(string value, bool isRequired)
{
Value = value;
Required = required;
IsRequired = isRequired;
}
}

View File

@@ -5,6 +5,7 @@ internal static class CliConstants
public const string DefaultCommandName = "__default_command";
public const string True = "true";
public const string False = "false";
public const string DumpHelpOpenCliOption = "--help-dump-opencli";
public static string[] AcceptedBooleanValues { get; } = new string[]
{
@@ -18,5 +19,6 @@ internal static class CliConstants
public const string Version = "version";
public const string XmlDoc = "xmldoc";
public const string Explain = "explain";
public const string OpenCli = "opencli";
}
}

View File

@@ -2,16 +2,16 @@ namespace Spectre.Console.Cli;
internal sealed class DelegateCommand : ICommand
{
private readonly Func<CommandContext, CommandSettings, Task<int>> _func;
private readonly Func<CommandContext, CommandSettings, CancellationToken, Task<int>> _func;
public DelegateCommand(Func<CommandContext, CommandSettings, Task<int>> func)
public DelegateCommand(Func<CommandContext, CommandSettings, CancellationToken, Task<int>> func)
{
_func = func;
}
public Task<int> Execute(CommandContext context, CommandSettings settings)
public Task<int> ExecuteAsync(CommandContext context, CommandSettings settings, CancellationToken cancellationToken)
{
return _func(context, settings);
return _func(context, settings, cancellationToken);
}
public ValidationResult Validate(CommandContext context, CommandSettings settings)

View File

@@ -0,0 +1,13 @@
#if NETSTANDARD2_0
namespace System.IO;
// Polyfills needed for OpenCli.
// This can be removed once me migrate over to the Polyfill library.
internal static class OpenCliExtensions
{
public static Task<string> ReadToEndAsync(this StreamReader reader, CancellationToken cancellationToken)
{
return reader.ReadToEndAsync();
}
}
#endif

View File

@@ -16,4 +16,19 @@ internal static class TypeExtensions
return false;
}
// Taken from https://github.com/dotnet/sdk/blob/main/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/TypeExtensions.cs#L15
// Licensed under MIT
public static string ToCliTypeString(this Type type)
{
var typeName = type.FullName ?? string.Empty;
if (!type.IsGenericType)
{
return typeName;
}
var genericTypeName = typeName.Substring(0, typeName.IndexOf('`'));
var genericTypes = string.Join(", ", type.GenericTypeArguments.Select(generic => generic.ToCliTypeString()));
return $"{genericTypeName}<{genericTypes}>";
}
}

View File

@@ -8,7 +8,7 @@ internal sealed class CommandInfo : ICommandContainer, ICommandInfo
public object? Data { get; }
public Type? CommandType { get; }
public Type SettingsType { get; }
public Func<CommandContext, CommandSettings, Task<int>>? Delegate { get; }
public Func<CommandContext, CommandSettings, CancellationToken, Task<int>>? Delegate { get; }
public bool IsDefaultCommand { get; }
public CommandInfo? Parent { get; }
public IList<CommandInfo> Children { get; }

View File

@@ -2,6 +2,16 @@ namespace Spectre.Console.Cli;
internal static class CommandInfoExtensions
{
public static IEnumerable<CommandArgument>? GetArguments(this CommandInfo? command)
{
return command?.Parameters.OfType<CommandArgument>();
}
public static IEnumerable<CommandOption>? GetOptions(this CommandInfo? command)
{
return command?.Parameters.OfType<CommandOption>();
}
public static bool HaveParentWithOption(this CommandInfo command, CommandOption option)
{
var parent = command?.Parent;

View File

@@ -86,7 +86,7 @@ internal static class CommandModelValidator
// Arguments
foreach (var argument in arguments)
{
if (argument.Required && argument.DefaultValue != null)
if (argument.IsRequired && argument.DefaultValue != null)
{
throw CommandConfigurationException.RequiredArgumentsCannotHaveDefaultValue(argument);
}

View File

@@ -15,7 +15,8 @@ internal sealed class CommandOption : CommandParameter, ICommandOption
IEnumerable<ParameterValidationAttribute> validators,
DefaultValueAttribute? defaultValue, bool valueIsOptional)
: base(parameterType, parameterKind, property, description, converter,
defaultValue, deconstructor, valueProvider, validators, false, optionAttribute.IsHidden)
defaultValue, deconstructor, valueProvider, validators,
optionAttribute.IsRequired, optionAttribute.IsHidden)
{
LongNames = optionAttribute.LongNames;
ShortNames = optionAttribute.ShortNames;

View File

@@ -12,7 +12,7 @@ internal abstract class CommandParameter : ICommandParameterInfo, ICommandParame
public PairDeconstructorAttribute? PairDeconstructor { get; }
public List<ParameterValidationAttribute> Validators { get; }
public ParameterValueProviderAttribute? ValueProvider { get; }
public bool Required { get; set; }
public bool IsRequired { get; set; }
public bool IsHidden { get; }
public string PropertyName => Property.Name;
@@ -38,8 +38,8 @@ internal abstract class CommandParameter : ICommandParameterInfo, ICommandParame
DefaultValue = defaultValue;
PairDeconstructor = deconstructor;
ValueProvider = valueProvider;
Validators = new List<ParameterValidationAttribute>(validators ?? Array.Empty<ParameterValidationAttribute>());
Required = required;
Validators = new List<ParameterValidationAttribute>(validators ?? []);
IsRequired = required;
IsHidden = isHidden;
}

View File

@@ -9,6 +9,7 @@ global using System.IO;
global using System.Linq;
global using System.Reflection;
global using System.Text;
global using System.Threading;
global using System.Threading.Tasks;
global using System.Xml;
global using Spectre.Console.Cli.Help;

View File

@@ -1,34 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks>
<IsPackable>true</IsPackable>
</PropertyGroup>
<PropertyGroup>
<IsAotCompatible Condition="'$(TargetFramework)' != 'netstandard2.0'">false</IsAotCompatible>
<IsTrimmable>false</IsTrimmable>
<DefineConstants>$(DefineConstants);OPENCLI_VISIBILITY_INTERNAL</DefineConstants>
</PropertyGroup>
<ItemGroup Label="REMOVE THIS">
<InternalsVisibleTo Include="Spectre.Console.Cli.Tests"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<AnnotatedReferenceAssemblyVersion>3.0.0</AnnotatedReferenceAssemblyVersion>
<GenerateNullableAttributes>False</GenerateNullableAttributes>
</PropertyGroup>
<ItemGroup Label="Dependencies">
<PackageReference Include="OpenCli.Sources" />
<PackageReference Include="PolySharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" PrivateAssets="all"/>
<PackageDownload Include="Microsoft.NETCore.App.Ref" Version="[$(AnnotatedReferenceAssemblyVersion)]"/>
@@ -49,4 +45,8 @@
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>
</Project>

View File

@@ -8,6 +8,11 @@ public sealed class CommandAppTester
private Action<CommandApp>? _appConfiguration;
private Action<IConfigurator>? _configuration;
/// <summary>
/// Gets the test console used by both the CommandAppTester and CommandApp.
/// </summary>
public TestConsole Console { get; }
/// <summary>
/// Gets or sets the Registrar to use in the CommandApp.
/// </summary>
@@ -23,10 +28,15 @@ public sealed class CommandAppTester
/// </summary>
/// <param name="registrar">The registrar.</param>
/// <param name="settings">The settings.</param>
public CommandAppTester(ITypeRegistrar? registrar = null, CommandAppTesterSettings? settings = null)
/// <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>
@@ -36,6 +46,7 @@ public sealed class CommandAppTester
public CommandAppTester(CommandAppTesterSettings settings)
{
TestSettings = settings;
Console = new TestConsole().Width(int.MaxValue);
}
/// <summary>
@@ -85,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());
RunAsync(args, Console, c => c.PropagateExceptions()).GetAwaiter().GetResult();
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)
{
@@ -120,55 +129,21 @@ 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);
}
private CommandAppResult Run(string[] args, TestConsole console, Action<IConfigurator>? config = null)
{
CommandContext? context = null;
CommandSettings? settings = null;
var app = new CommandApp(Registrar);
_appConfiguration?.Invoke(app);
if (_configuration != null)
{
app.Configure(_configuration);
}
if (config != null)
{
app.Configure(config);
}
app.Configure(c => c.ConfigureConsole(console));
app.Configure(c => c.SetInterceptor(new CallbackCommandInterceptor((ctx, s) =>
{
context = ctx;
settings = s;
})));
var result = app.Run(args);
var output = console.Output.NormalizeLineEndings();
output = TestSettings.TrimConsoleOutput ? output.TrimLines().Trim() : output;
return new CommandAppResult(result, output, context, settings);
return RunAsync(args, Console).GetAwaiter().GetResult();
}
/// <summary>
/// Runs the command application asynchronously.
/// </summary>
/// <param name="args">The arguments.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The result.</returns>
public async Task<CommandAppResult> RunAsync(params string[] args)
public async Task<CommandAppResult> RunAsync(string[]? args = null, CancellationToken cancellationToken = default)
{
var console = new TestConsole().Width(int.MaxValue);
return await RunAsync(args, console);
return await RunAsync(args ?? [], Console, cancellationToken: cancellationToken);
}
private async Task<CommandAppResult> RunAsync(string[] args, TestConsole console, Action<IConfigurator>? config = null)
private async Task<CommandAppResult> RunAsync(string[] args, TestConsole console, Action<IConfigurator>? config = null, CancellationToken cancellationToken = default)
{
CommandContext? context = null;
CommandSettings? settings = null;
@@ -193,7 +168,7 @@ public sealed class CommandAppTester
settings = s;
})));
var result = await app.RunAsync(args);
var result = await app.RunAsync(args, cancellationToken);
var output = console.Output.NormalizeLineEndings();
output = TestSettings.TrimConsoleOutput ? output.TrimLines().Trim() : output;

View File

@@ -1,7 +1,20 @@
namespace Spectre.Console;
namespace Spectre.Console.Testing;
internal static class ShouldlyExtensions
/// <summary>
/// Provides extensions for testing using the Shouldly-style fluent assertions.
/// </summary>
public static class ShouldlyExtensions
{
/// <summary>
/// Performs the specified action on the given object and then returns the object.
/// Useful for fluent testing patterns where additional assertions or operations
/// are chained together in a readable manner.
/// </summary>
/// <typeparam name="T">The type of the object.</typeparam>
/// <param name="item">The object to operate on.</param>
/// <param name="action">An action to perform on the object.</param>
/// <returns>The original object, to allow further chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="action"/> is null.</exception>
[DebuggerStepThrough]
public static T And<T>(this T item, Action<T> action)
{

View File

@@ -0,0 +1,59 @@
namespace Spectre.Console.Testing;
/// <summary>
/// Provides extension methods for working with <see cref="TestConsole"/> in a testing context,
/// including stack trace normalization for consistent and deterministic test output.
/// </summary>
public static partial class TestConsoleExtensions
{
private static readonly Regex _lineNumberRegex = new Regex(":\\d+", RegexOptions.Singleline);
private static readonly Regex _filenameRegex = new Regex("\\sin\\s.*cs:nn", RegexOptions.Multiline);
/// <summary>
/// Writes the given exception to the <see cref="TestConsole"/> and returns a normalized string
/// representation of the exception, with file paths and line numbers sanitized.
/// </summary>
/// <param name="console">The <see cref="TestConsole"/> to write to.</param>
/// <param name="ex">The exception to write and normalize.</param>
/// <param name="formats">Optional formatting options for exception output.</param>
/// <returns>A normalized string of the exception's output, safe for snapshot testing.</returns>
/// <exception cref="InvalidOperationException">
/// Thrown if the console's output buffer is not empty before writing the exception.
/// </exception>
public static string WriteNormalizedException(this TestConsole console, Exception ex, ExceptionFormats formats = ExceptionFormats.Default)
{
if (!string.IsNullOrWhiteSpace(console.Output))
{
throw new InvalidOperationException("Output buffer is not empty.");
}
console.WriteException(ex, formats);
return string.Join("\n", NormalizeStackTrace(console.Output)
.NormalizeLineEndings()
.Split(new char[] { '\n' })
.Select(line => line.TrimEnd()));
}
/// <summary>
/// Normalizes a stack trace string by replacing line numbers with ":nn"
/// and converting full file paths to a fixed placeholder path ("/xyz/filename.cs").
/// </summary>
/// <param name="text">The stack trace text to normalize.</param>
/// <returns>A sanitized stack trace suitable for stable testing output.</returns>
public static string NormalizeStackTrace(string text)
{
text = _lineNumberRegex.Replace(text, match =>
{
return ":nn";
});
return _filenameRegex.Replace(text, match =>
{
var value = match.Value;
var index = value.LastIndexOfAny(new[] { '\\', '/' });
var filename = value.Substring(index + 1, value.Length - index - 1);
return $" in /xyz/{filename}";
});
}
}

View File

@@ -3,7 +3,7 @@ namespace Spectre.Console.Testing;
/// <summary>
/// Contains extensions for <see cref="TestConsole"/>.
/// </summary>
public static class TestConsoleExtensions
public static partial class TestConsoleExtensions
{
/// <summary>
/// Sets the console's color system.

View File

@@ -3,6 +3,7 @@ global using System.Collections.Generic;
global using System.Diagnostics;
global using System.IO;
global using System.Linq;
global using System.Text.RegularExpressions;
global using System.Threading;
global using System.Threading.Tasks;
global using Spectre.Console.Cli;

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;netstandard2.0</TargetFrameworks>
@@ -7,12 +7,7 @@
<Description>Contains testing utilities for Spectre.Console.</Description>
</PropertyGroup>
<ItemGroup Label="REMOVE THIS">
<InternalsVisibleTo Include="Spectre.Console.Tests" />
<InternalsVisibleTo Include="Spectre.Console.Cli.Tests" />
</ItemGroup>
<ItemGroup Label="Project References">
<ItemGroup>
<ProjectReference Include="..\Spectre.Console.Cli\Spectre.Console.Cli.csproj" />
<ProjectReference Include="..\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>

View File

@@ -1,153 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32414.318
MinimumVisualStudioVersion = 15.0.26124.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console", "Spectre.Console\Spectre.Console.csproj", "{80DCBEF3-99D6-46C0-9C5B-42B4534D9113}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{20595AD4-8D75-4AF8-B6BC-9C38C160423F}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets
..\dotnet-tools.json = ..\dotnet-tools.json
..\global.json = ..\global.json
stylecop.json = stylecop.json
Directory.Packages.props = Directory.Packages.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{C3E2CB5C-1517-4C75-B59A-93D4E22BEC8D}"
ProjectSection(SolutionItems) = preProject
..\.github\workflows\ci.yaml = ..\.github\workflows\ci.yaml
..\.github\workflows\publish.yaml = ..\.github\workflows\publish.yaml
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.ImageSharp", "Extensions\Spectre.Console.ImageSharp\Spectre.Console.ImageSharp.csproj", "{0EFE694D-0770-4E71-BF4E-EC2B41362F79}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Testing", "Spectre.Console.Testing\Spectre.Console.Testing.csproj", "{7D5F6704-8249-46DD-906C-9E66419F215F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{E0E45070-123C-4A4D-AA98-2A780308876C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Tests", "Tests\Spectre.Console.Tests\Spectre.Console.Tests.csproj", "{60A4CADD-2B3D-48ED-89C0-1637A1F111AE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Cli", "Spectre.Console.Cli\Spectre.Console.Cli.csproj", "{1B67B74F-1243-4381-9A2B-86EA66D135C5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Cli.Tests", "Tests\Spectre.Console.Cli.Tests\Spectre.Console.Cli.Tests.csproj", "{E07C46D2-714F-4116-BADD-FEE09617A9C4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Json", "Extensions\Spectre.Console.Json\Spectre.Console.Json.csproj", "{579E6E31-1E2F-4FE1-8F8C-9770878993E9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{F34EFD87-6CEA-453F-858B-094EA413578C}"
ProjectSection(SolutionItems) = preProject
Tests\Directory.Build.props = Tests\Directory.Build.props
Tests\.editorconfig = Tests\.editorconfig
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{80DCBEF3-99D6-46C0-9C5B-42B4534D9113}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{80DCBEF3-99D6-46C0-9C5B-42B4534D9113}.Debug|Any CPU.Build.0 = Debug|Any CPU
{80DCBEF3-99D6-46C0-9C5B-42B4534D9113}.Debug|x64.ActiveCfg = Debug|Any CPU
{80DCBEF3-99D6-46C0-9C5B-42B4534D9113}.Debug|x64.Build.0 = Debug|Any CPU
{80DCBEF3-99D6-46C0-9C5B-42B4534D9113}.Debug|x86.ActiveCfg = Debug|Any CPU
{80DCBEF3-99D6-46C0-9C5B-42B4534D9113}.Debug|x86.Build.0 = Debug|Any CPU
{80DCBEF3-99D6-46C0-9C5B-42B4534D9113}.Release|Any CPU.ActiveCfg = Release|Any CPU
{80DCBEF3-99D6-46C0-9C5B-42B4534D9113}.Release|Any CPU.Build.0 = Release|Any CPU
{80DCBEF3-99D6-46C0-9C5B-42B4534D9113}.Release|x64.ActiveCfg = Release|Any CPU
{80DCBEF3-99D6-46C0-9C5B-42B4534D9113}.Release|x64.Build.0 = Release|Any CPU
{80DCBEF3-99D6-46C0-9C5B-42B4534D9113}.Release|x86.ActiveCfg = Release|Any CPU
{80DCBEF3-99D6-46C0-9C5B-42B4534D9113}.Release|x86.Build.0 = Release|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|x64.ActiveCfg = Debug|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|x64.Build.0 = Debug|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|x86.ActiveCfg = Debug|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|x86.Build.0 = Debug|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|Any CPU.Build.0 = Release|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x64.ActiveCfg = Release|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x64.Build.0 = Release|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x86.ActiveCfg = Release|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x86.Build.0 = Release|Any CPU
{7D5F6704-8249-46DD-906C-9E66419F215F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D5F6704-8249-46DD-906C-9E66419F215F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D5F6704-8249-46DD-906C-9E66419F215F}.Debug|x64.ActiveCfg = Debug|Any CPU
{7D5F6704-8249-46DD-906C-9E66419F215F}.Debug|x64.Build.0 = Debug|Any CPU
{7D5F6704-8249-46DD-906C-9E66419F215F}.Debug|x86.ActiveCfg = Debug|Any CPU
{7D5F6704-8249-46DD-906C-9E66419F215F}.Debug|x86.Build.0 = Debug|Any CPU
{7D5F6704-8249-46DD-906C-9E66419F215F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D5F6704-8249-46DD-906C-9E66419F215F}.Release|Any CPU.Build.0 = Release|Any CPU
{7D5F6704-8249-46DD-906C-9E66419F215F}.Release|x64.ActiveCfg = Release|Any CPU
{7D5F6704-8249-46DD-906C-9E66419F215F}.Release|x64.Build.0 = Release|Any CPU
{7D5F6704-8249-46DD-906C-9E66419F215F}.Release|x86.ActiveCfg = Release|Any CPU
{7D5F6704-8249-46DD-906C-9E66419F215F}.Release|x86.Build.0 = Release|Any CPU
{60A4CADD-2B3D-48ED-89C0-1637A1F111AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{60A4CADD-2B3D-48ED-89C0-1637A1F111AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{60A4CADD-2B3D-48ED-89C0-1637A1F111AE}.Debug|x64.ActiveCfg = Debug|Any CPU
{60A4CADD-2B3D-48ED-89C0-1637A1F111AE}.Debug|x64.Build.0 = Debug|Any CPU
{60A4CADD-2B3D-48ED-89C0-1637A1F111AE}.Debug|x86.ActiveCfg = Debug|Any CPU
{60A4CADD-2B3D-48ED-89C0-1637A1F111AE}.Debug|x86.Build.0 = Debug|Any CPU
{60A4CADD-2B3D-48ED-89C0-1637A1F111AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{60A4CADD-2B3D-48ED-89C0-1637A1F111AE}.Release|Any CPU.Build.0 = Release|Any CPU
{60A4CADD-2B3D-48ED-89C0-1637A1F111AE}.Release|x64.ActiveCfg = Release|Any CPU
{60A4CADD-2B3D-48ED-89C0-1637A1F111AE}.Release|x64.Build.0 = Release|Any CPU
{60A4CADD-2B3D-48ED-89C0-1637A1F111AE}.Release|x86.ActiveCfg = Release|Any CPU
{60A4CADD-2B3D-48ED-89C0-1637A1F111AE}.Release|x86.Build.0 = Release|Any CPU
{1B67B74F-1243-4381-9A2B-86EA66D135C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1B67B74F-1243-4381-9A2B-86EA66D135C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B67B74F-1243-4381-9A2B-86EA66D135C5}.Debug|x64.ActiveCfg = Debug|Any CPU
{1B67B74F-1243-4381-9A2B-86EA66D135C5}.Debug|x64.Build.0 = Debug|Any CPU
{1B67B74F-1243-4381-9A2B-86EA66D135C5}.Debug|x86.ActiveCfg = Debug|Any CPU
{1B67B74F-1243-4381-9A2B-86EA66D135C5}.Debug|x86.Build.0 = Debug|Any CPU
{1B67B74F-1243-4381-9A2B-86EA66D135C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1B67B74F-1243-4381-9A2B-86EA66D135C5}.Release|Any CPU.Build.0 = Release|Any CPU
{1B67B74F-1243-4381-9A2B-86EA66D135C5}.Release|x64.ActiveCfg = Release|Any CPU
{1B67B74F-1243-4381-9A2B-86EA66D135C5}.Release|x64.Build.0 = Release|Any CPU
{1B67B74F-1243-4381-9A2B-86EA66D135C5}.Release|x86.ActiveCfg = Release|Any CPU
{1B67B74F-1243-4381-9A2B-86EA66D135C5}.Release|x86.Build.0 = Release|Any CPU
{E07C46D2-714F-4116-BADD-FEE09617A9C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E07C46D2-714F-4116-BADD-FEE09617A9C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E07C46D2-714F-4116-BADD-FEE09617A9C4}.Debug|x64.ActiveCfg = Debug|Any CPU
{E07C46D2-714F-4116-BADD-FEE09617A9C4}.Debug|x64.Build.0 = Debug|Any CPU
{E07C46D2-714F-4116-BADD-FEE09617A9C4}.Debug|x86.ActiveCfg = Debug|Any CPU
{E07C46D2-714F-4116-BADD-FEE09617A9C4}.Debug|x86.Build.0 = Debug|Any CPU
{E07C46D2-714F-4116-BADD-FEE09617A9C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E07C46D2-714F-4116-BADD-FEE09617A9C4}.Release|Any CPU.Build.0 = Release|Any CPU
{E07C46D2-714F-4116-BADD-FEE09617A9C4}.Release|x64.ActiveCfg = Release|Any CPU
{E07C46D2-714F-4116-BADD-FEE09617A9C4}.Release|x64.Build.0 = Release|Any CPU
{E07C46D2-714F-4116-BADD-FEE09617A9C4}.Release|x86.ActiveCfg = Release|Any CPU
{E07C46D2-714F-4116-BADD-FEE09617A9C4}.Release|x86.Build.0 = Release|Any CPU
{579E6E31-1E2F-4FE1-8F8C-9770878993E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{579E6E31-1E2F-4FE1-8F8C-9770878993E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{579E6E31-1E2F-4FE1-8F8C-9770878993E9}.Debug|x64.ActiveCfg = Debug|Any CPU
{579E6E31-1E2F-4FE1-8F8C-9770878993E9}.Debug|x64.Build.0 = Debug|Any CPU
{579E6E31-1E2F-4FE1-8F8C-9770878993E9}.Debug|x86.ActiveCfg = Debug|Any CPU
{579E6E31-1E2F-4FE1-8F8C-9770878993E9}.Debug|x86.Build.0 = Debug|Any CPU
{579E6E31-1E2F-4FE1-8F8C-9770878993E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{579E6E31-1E2F-4FE1-8F8C-9770878993E9}.Release|Any CPU.Build.0 = Release|Any CPU
{579E6E31-1E2F-4FE1-8F8C-9770878993E9}.Release|x64.ActiveCfg = Release|Any CPU
{579E6E31-1E2F-4FE1-8F8C-9770878993E9}.Release|x64.Build.0 = Release|Any CPU
{579E6E31-1E2F-4FE1-8F8C-9770878993E9}.Release|x86.ActiveCfg = Release|Any CPU
{579E6E31-1E2F-4FE1-8F8C-9770878993E9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{C3E2CB5C-1517-4C75-B59A-93D4E22BEC8D} = {20595AD4-8D75-4AF8-B6BC-9C38C160423F}
{0EFE694D-0770-4E71-BF4E-EC2B41362F79} = {E0E45070-123C-4A4D-AA98-2A780308876C}
{579E6E31-1E2F-4FE1-8F8C-9770878993E9} = {E0E45070-123C-4A4D-AA98-2A780308876C}
{60A4CADD-2B3D-48ED-89C0-1637A1F111AE} = {F34EFD87-6CEA-453F-858B-094EA413578C}
{E07C46D2-714F-4116-BADD-FEE09617A9C4} = {F34EFD87-6CEA-453F-858B-094EA413578C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C}
EndGlobalSection
EndGlobal

31
src/Spectre.Console.slnx Normal file
View File

@@ -0,0 +1,31 @@
<Solution>
<Configurations>
<Platform Name="Any CPU" />
<Platform Name="x64" />
<Platform Name="x86" />
</Configurations>
<Folder Name="/Build/">
<File Path="../dotnet-tools.json" />
<File Path="../global.json" />
<File Path=".editorconfig" />
<File Path="Directory.Build.props" />
<File Path="Directory.Build.targets" />
<File Path="Directory.Packages.props" />
<File Path="stylecop.json" />
</Folder>
<Folder Name="/Build/GitHub/">
<File Path="../.github/workflows/ci.yaml" />
<File Path="../.github/workflows/publish.yaml" />
</Folder>
<Folder Name="/CLI/">
<Project Path="Spectre.Console.Cli/Spectre.Console.Cli.csproj" />
<Project Path="Tests/Spectre.Console.Cli.Tests/Spectre.Console.Cli.Tests.csproj" />
</Folder>
<Folder Name="/Extensions/">
<Project Path="Extensions/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj" />
<Project Path="Extensions/Spectre.Console.Json/Spectre.Console.Json.csproj" />
</Folder>
<Project Path="Spectre.Console.Testing/Spectre.Console.Testing.csproj" />
<Project Path="Spectre.Console/Spectre.Console.csproj" />
<Project Path="Tests/Spectre.Console.Tests/Spectre.Console.Tests.csproj" />
</Solution>

View File

@@ -45,7 +45,7 @@ public static class TableColumnExtensions
throw new ArgumentNullException(nameof(header));
}
column.Footer = header;
column.Header = header;
return column;
}

View File

@@ -2,7 +2,26 @@ namespace Spectre.Console;
internal static class Cell
{
private static readonly int?[] _runeWidthCache = new int?[char.MaxValue];
private const sbyte Sentinel = -2;
/// <summary>
/// UnicodeCalculator.GetWidth documents the width as (-1, 0, 1, 2). We only need space for these values and a sentinel for uninitialized values.
/// This is only five values in total so we are storing one byte per value. We could store 2 per byte but that would add more logic to the retrieval.
/// We should add one to char.MaxValue because the total number of characters includes \0 too so there are 65536 valid chars.
/// </summary>
private static readonly sbyte[] _runeWidthCache = new sbyte[char.MaxValue + 1];
static Cell()
{
#if !NETSTANDARD2_0
Array.Fill(_runeWidthCache, Sentinel);
#else
for (var i = 0; i < _runeWidthCache.Length; i++)
{
_runeWidthCache[i] = Sentinel;
}
#endif
}
public static int GetCellLength(string text)
{
@@ -29,6 +48,13 @@ internal static class Cell
return 1;
}
return _runeWidthCache[rune] ??= UnicodeCalculator.GetWidth(rune);
var width = _runeWidthCache[rune];
if (width == Sentinel)
{
_runeWidthCache[rune] = (sbyte)UnicodeCalculator.GetWidth(rune);
return _runeWidthCache[rune];
}
return _runeWidthCache[rune];
}
}

View File

@@ -4,7 +4,6 @@ internal sealed class LiveDisplayRenderer : IRenderHook
{
private readonly IAnsiConsole _console;
private readonly LiveDisplayContext _context;
public LiveDisplayRenderer(IAnsiConsole console, LiveDisplayContext context)
{
_console = console;
@@ -45,7 +44,7 @@ internal sealed class LiveDisplayRenderer : IRenderHook
{
lock (_context.Lock)
{
yield return _context.Live.PositionCursor();
yield return _context.Live.PositionCursor(options);
foreach (var renderable in renderables)
{

View File

@@ -39,7 +39,7 @@ internal sealed class LiveRenderable : Renderable
}
}
public IRenderable PositionCursor()
public IRenderable PositionCursor(RenderOptions options)
{
lock (_lock)
{
@@ -48,6 +48,14 @@ internal sealed class LiveRenderable : Renderable
return new ControlCode(string.Empty);
}
// Check if the size have been reduced
if (_shape.Value.Height > options.ConsoleSize.Height || _shape.Value.Width > options.ConsoleSize.Width)
{
// Important reset shape, so the size can shrink
_shape = null;
return new ControlCode(ED(2) + ED(3) + CUP(1, 1));
}
var linesToMoveUp = _shape.Value.Height - 1;
return new ControlCode("\r" + CUU(linesToMoveUp));
}

View File

@@ -118,7 +118,7 @@ internal sealed class DefaultProgressRenderer : ProgressRenderer
{
lock (_lock)
{
yield return _live.PositionCursor();
yield return _live.PositionCursor(options);
foreach (var renderable in renderables)
{

View File

@@ -360,6 +360,42 @@ namespace Spectre.Console
"⠀⡀",
};
}
private sealed class Dots13Spinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(80);
public override bool IsUnicode => true;
public override IReadOnlyList<string> Frames => new List<string>
{
"⣼",
"⣹",
"⢻",
"⠿",
"⡟",
"⣏",
"⣧",
"⣶",
};
}
private sealed class Dots14Spinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(80);
public override bool IsUnicode => true;
public override IReadOnlyList<string> Frames => new List<string>
{
"⠉⠉",
"⠈⠙",
"⠀⠹",
"⠀⢸",
"⠀⣰",
"⢀⣠",
"⣀⣀",
"⣄⡀",
"⣆⠀",
"⡇⠀",
"⠏⠀",
"⠋⠁",
};
}
private sealed class Dots8BitSpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(80);
@@ -624,6 +660,65 @@ namespace Spectre.Console
"⣿",
};
}
private sealed class DotsCircleSpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(80);
public override bool IsUnicode => true;
public override IReadOnlyList<string> Frames => new List<string>
{
"⢎ ",
"⠎⠁",
"⠊⠑",
"⠈⠱",
" ⡱",
"⢀⡰",
"⢄⡠",
"⢆⡀",
};
}
private sealed class SandSpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(80);
public override bool IsUnicode => true;
public override IReadOnlyList<string> Frames => new List<string>
{
"⠁",
"⠂",
"⠄",
"⡀",
"⡈",
"⡐",
"⡠",
"⣀",
"⣁",
"⣂",
"⣄",
"⣌",
"⣔",
"⣤",
"⣥",
"⣦",
"⣮",
"⣶",
"⣷",
"⣿",
"⡿",
"⠿",
"⢟",
"⠟",
"⡛",
"⠛",
"⠫",
"⢋",
"⠋",
"⠍",
"⡉",
"⠉",
"⠑",
"⠡",
"⢁",
};
}
private sealed class LineSpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(130);
@@ -875,6 +970,24 @@ namespace Spectre.Console
"◥",
};
}
private sealed class BinarySpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(80);
public override bool IsUnicode => false;
public override IReadOnlyList<string> Frames => new List<string>
{
"010010",
"001100",
"100101",
"111010",
"111101",
"010111",
"101011",
"111000",
"110011",
"110101",
};
}
private sealed class ArcSpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(100);
@@ -1136,6 +1249,7 @@ namespace Spectre.Console
"[= ]",
"[== ]",
"[=== ]",
"[====]",
"[ ===]",
"[ ==]",
"[ =]",
@@ -1545,6 +1659,153 @@ namespace Spectre.Console
"ββββββρ",
};
}
private sealed class FingerDanceSpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(160);
public override bool IsUnicode => true;
public override IReadOnlyList<string> Frames => new List<string>
{
"🤘 ",
"🤟 ",
"🖖 ",
"✋ ",
"🤚 ",
"👆 ",
};
}
private sealed class FistBumpSpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(80);
public override bool IsUnicode => true;
public override IReadOnlyList<string> Frames => new List<string>
{
"🤜    🤛 ",
"🤜    🤛 ",
"🤜    🤛 ",
" 🤜  🤛  ",
"  🤜🤛   ",
" 🤜✨🤛   ",
"🤜 ✨ 🤛  ",
};
}
private sealed class SoccerHeaderSpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(80);
public override bool IsUnicode => true;
public override IReadOnlyList<string> Frames => new List<string>
{
" 🧑⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
"🧑 ⚽️ 🧑 ",
};
}
private sealed class MindblownSpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(160);
public override bool IsUnicode => true;
public override IReadOnlyList<string> Frames => new List<string>
{
"😐 ",
"😐 ",
"😮 ",
"😮 ",
"😦 ",
"😦 ",
"😧 ",
"😧 ",
"🤯 ",
"💥 ",
"✨ ",
"  ",
"  ",
"  ",
};
}
private sealed class SpeakerSpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(160);
public override bool IsUnicode => true;
public override IReadOnlyList<string> Frames => new List<string>
{
"🔈 ",
"🔉 ",
"🔊 ",
"🔉 ",
};
}
private sealed class OrangePulseSpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(100);
public override bool IsUnicode => true;
public override IReadOnlyList<string> Frames => new List<string>
{
"🔸 ",
"🔶 ",
"🟠 ",
"🟠 ",
"🔶 ",
};
}
private sealed class BluePulseSpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(100);
public override bool IsUnicode => true;
public override IReadOnlyList<string> Frames => new List<string>
{
"🔹 ",
"🔷 ",
"🔵 ",
"🔵 ",
"🔷 ",
};
}
private sealed class OrangeBluePulseSpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(100);
public override bool IsUnicode => true;
public override IReadOnlyList<string> Frames => new List<string>
{
"🔸 ",
"🔶 ",
"🟠 ",
"🟠 ",
"🔶 ",
"🔹 ",
"🔷 ",
"🔵 ",
"🔵 ",
"🔷 ",
};
}
private sealed class TimeTravelSpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(100);
public override bool IsUnicode => true;
public override IReadOnlyList<string> Frames => new List<string>
{
"🕛 ",
"🕚 ",
"🕙 ",
"🕘 ",
"🕗 ",
"🕖 ",
"🕕 ",
"🕔 ",
"🕓 ",
"🕒 ",
"🕑 ",
"🕐 ",
};
}
private sealed class AestheticSpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(80);
@@ -1561,6 +1822,147 @@ namespace Spectre.Console
"▰▱▱▱▱▱▱",
};
}
private sealed class DwarfFortressSpinner : Spinner
{
public override TimeSpan Interval => TimeSpan.FromMilliseconds(80);
public override bool IsUnicode => true;
public override IReadOnlyList<string> Frames => new List<string>
{
" ██████£££ ",
"☺██████£££ ",
"☺██████£££ ",
"☺▓█████£££ ",
"☺▓█████£££ ",
"☺▒█████£££ ",
"☺▒█████£££ ",
"☺░█████£££ ",
"☺░█████£££ ",
"☺ █████£££ ",
" ☺█████£££ ",
" ☺█████£££ ",
" ☺▓████£££ ",
" ☺▓████£££ ",
" ☺▒████£££ ",
" ☺▒████£££ ",
" ☺░████£££ ",
" ☺░████£££ ",
" ☺ ████£££ ",
" ☺████£££ ",
" ☺████£££ ",
" ☺▓███£££ ",
" ☺▓███£££ ",
" ☺▒███£££ ",
" ☺▒███£££ ",
" ☺░███£££ ",
" ☺░███£££ ",
" ☺ ███£££ ",
" ☺███£££ ",
" ☺███£££ ",
" ☺▓██£££ ",
" ☺▓██£££ ",
" ☺▒██£££ ",
" ☺▒██£££ ",
" ☺░██£££ ",
" ☺░██£££ ",
" ☺ ██£££ ",
" ☺██£££ ",
" ☺██£££ ",
" ☺▓█£££ ",
" ☺▓█£££ ",
" ☺▒█£££ ",
" ☺▒█£££ ",
" ☺░█£££ ",
" ☺░█£££ ",
" ☺ █£££ ",
" ☺█£££ ",
" ☺█£££ ",
" ☺▓£££ ",
" ☺▓£££ ",
" ☺▒£££ ",
" ☺▒£££ ",
" ☺░£££ ",
" ☺░£££ ",
" ☺ £££ ",
" ☺£££ ",
" ☺£££ ",
" ☺▓££ ",
" ☺▓££ ",
" ☺▒££ ",
" ☺▒££ ",
" ☺░££ ",
" ☺░££ ",
" ☺ ££ ",
" ☺££ ",
" ☺££ ",
" ☺▓£ ",
" ☺▓£ ",
" ☺▒£ ",
" ☺▒£ ",
" ☺░£ ",
" ☺░£ ",
" ☺ £ ",
" ☺£ ",
" ☺£ ",
" ☺▓ ",
" ☺▓ ",
" ☺▒ ",
" ☺▒ ",
" ☺░ ",
" ☺░ ",
" ☺ ",
" ☺ &",
" ☺ ☼&",
" ☺ ☼ &",
" ☺☼ &",
" ☺☼ & ",
" ‼ & ",
" ☺ & ",
" ‼ & ",
" ☺ & ",
" ‼ & ",
" ☺ & ",
"‼ & ",
" & ",
" & ",
" & ░ ",
" & ▒ ",
" & ▓ ",
" & £ ",
" & ░£ ",
" & ▒£ ",
" & ▓£ ",
" & ££ ",
" & ░££ ",
" & ▒££ ",
"& ▓££ ",
"& £££ ",
" ░£££ ",
" ▒£££ ",
" ▓£££ ",
" █£££ ",
" ░█£££ ",
" ▒█£££ ",
" ▓█£££ ",
" ██£££ ",
" ░██£££ ",
" ▒██£££ ",
" ▓██£££ ",
" ███£££ ",
" ░███£££ ",
" ▒███£££ ",
" ▓███£££ ",
" ████£££ ",
" ░████£££ ",
" ▒████£££ ",
" ▓████£££ ",
" █████£££ ",
" ░█████£££ ",
" ▒█████£££ ",
" ▓█████£££ ",
" ██████£££ ",
" ██████£££ ",
};
}
/// <summary>
/// Contains all predefined spinners.
@@ -1624,10 +2026,26 @@ namespace Spectre.Console
/// </summary>
public static Spinner Dots12 { get; } = new Dots12Spinner();
/// <summary>
/// Gets the "dots13" spinner.
/// </summary>
public static Spinner Dots13 { get; } = new Dots13Spinner();
/// <summary>
/// Gets the "dots14" spinner.
/// </summary>
public static Spinner Dots14 { get; } = new Dots14Spinner();
/// <summary>
/// Gets the "dots8Bit" spinner.
/// </summary>
public static Spinner Dots8Bit { get; } = new Dots8BitSpinner();
/// <summary>
/// Gets the "dotsCircle" spinner.
/// </summary>
public static Spinner DotsCircle { get; } = new DotsCircleSpinner();
/// <summary>
/// Gets the "sand" spinner.
/// </summary>
public static Spinner Sand { get; } = new SandSpinner();
/// <summary>
/// Gets the "line" spinner.
/// </summary>
public static Spinner Line { get; } = new LineSpinner();
@@ -1700,6 +2118,10 @@ namespace Spectre.Console
/// </summary>
public static Spinner Triangle { get; } = new TriangleSpinner();
/// <summary>
/// Gets the "binary" spinner.
/// </summary>
public static Spinner Binary { get; } = new BinarySpinner();
/// <summary>
/// Gets the "arc" spinner.
/// </summary>
public static Spinner Arc { get; } = new ArcSpinner();
@@ -1864,9 +2286,49 @@ namespace Spectre.Console
/// </summary>
public static Spinner BetaWave { get; } = new BetaWaveSpinner();
/// <summary>
/// Gets the "fingerDance" spinner.
/// </summary>
public static Spinner FingerDance { get; } = new FingerDanceSpinner();
/// <summary>
/// Gets the "fistBump" spinner.
/// </summary>
public static Spinner FistBump { get; } = new FistBumpSpinner();
/// <summary>
/// Gets the "soccerHeader" spinner.
/// </summary>
public static Spinner SoccerHeader { get; } = new SoccerHeaderSpinner();
/// <summary>
/// Gets the "mindblown" spinner.
/// </summary>
public static Spinner Mindblown { get; } = new MindblownSpinner();
/// <summary>
/// Gets the "speaker" spinner.
/// </summary>
public static Spinner Speaker { get; } = new SpeakerSpinner();
/// <summary>
/// Gets the "orangePulse" spinner.
/// </summary>
public static Spinner OrangePulse { get; } = new OrangePulseSpinner();
/// <summary>
/// Gets the "bluePulse" spinner.
/// </summary>
public static Spinner BluePulse { get; } = new BluePulseSpinner();
/// <summary>
/// Gets the "orangeBluePulse" spinner.
/// </summary>
public static Spinner OrangeBluePulse { get; } = new OrangeBluePulseSpinner();
/// <summary>
/// Gets the "timeTravel" spinner.
/// </summary>
public static Spinner TimeTravel { get; } = new TimeTravelSpinner();
/// <summary>
/// Gets the "aesthetic" spinner.
/// </summary>
public static Spinner Aesthetic { get; } = new AestheticSpinner();
/// <summary>
/// Gets the "dwarfFortress" spinner.
/// </summary>
public static Spinner DwarfFortress { get; } = new DwarfFortressSpinner();
}
}
}

View File

@@ -42,7 +42,7 @@ internal sealed class ListPromptRenderHook<T> : IRenderHook
_dirty = false;
}
yield return _live.PositionCursor();
yield return _live.PositionCursor(options);
foreach (var renderable in renderables)
{

View File

@@ -63,6 +63,7 @@ internal sealed class ListPromptState<T>
switch (keyInfo.Key)
{
case ConsoleKey.UpArrow:
case ConsoleKey.K:
if (currentLeafIndex > 0)
{
index = _leafIndexes[currentLeafIndex - 1];
@@ -75,6 +76,7 @@ internal sealed class ListPromptState<T>
break;
case ConsoleKey.DownArrow:
case ConsoleKey.J:
if (currentLeafIndex < _leafIndexes.Count - 1)
{
index = _leafIndexes[currentLeafIndex + 1];
@@ -117,8 +119,8 @@ internal sealed class ListPromptState<T>
{
index = keyInfo.Key switch
{
ConsoleKey.UpArrow => Index - 1,
ConsoleKey.DownArrow => Index + 1,
ConsoleKey.UpArrow or ConsoleKey.K => Index - 1,
ConsoleKey.DownArrow or ConsoleKey.J => Index + 1,
ConsoleKey.Home => 0,
ConsoleKey.End => ItemCount - 1,
ConsoleKey.PageUp => Index - PageSize,

View File

@@ -109,7 +109,9 @@ public sealed class SelectionPrompt<T> : IPrompt<T>, IListPromptStrategy<T>
/// <inheritdoc/>
ListPromptInputResult IListPromptStrategy<T>.HandleInput(ConsoleKeyInfo key, ListPromptState<T> state)
{
if (key.Key == ConsoleKey.Enter || key.Key == ConsoleKey.Spacebar || key.Key == ConsoleKey.Packet)
if (key.Key == ConsoleKey.Enter
|| key.Key == ConsoleKey.Packet
|| (!state.SearchEnabled && key.Key == ConsoleKey.Spacebar))
{
// Selecting a non leaf in "leaf mode" is not allowed
if (state.Current.IsGroup && Mode == SelectionMode.Leaf)

View File

@@ -19,7 +19,6 @@
</ItemGroup>
<ItemGroup Label="Dependencies">
<PackageReference Include="System.Memory" />
<PackageReference Include="Wcwidth.Sources">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@@ -34,6 +33,7 @@
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.Memory" />
<PackageReference Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" PrivateAssets="all"/>
<PackageDownload Include="Microsoft.NETCore.App.Ref" Version="[$(AnnotatedReferenceAssemblyVersion)]"/>
</ItemGroup>

View File

@@ -226,6 +226,11 @@ public sealed class Style : IEquatable<Style>
builder.Add("on " + Background.ToMarkup());
}
if (Link != null)
{
builder.Add($"link={Link}");
}
return string.Join(" ", builder);
}

View File

@@ -1,7 +1,7 @@
namespace Spectre.Console;
/// <summary>
/// Represents a prompt validation result.
/// Represents a validation result.
/// </summary>
public sealed class ValidationResult
{

View File

@@ -65,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.

View File

@@ -3,16 +3,17 @@ namespace Spectre.Console.Tests;
public static class Constants
{
public static string[] VersionCommand { get; } =
new[]
{
[
CliConstants.Commands.Branch,
CliConstants.Commands.Version,
};
CliConstants.Commands.Version
];
public static string[] XmlDocCommand { get; } =
new[]
{
[
CliConstants.Commands.Branch,
CliConstants.Commands.XmlDoc,
};
CliConstants.Commands.XmlDoc
];
public static string[] OpenCliOption { get; } =
[CliConstants.DumpHelpOpenCliOption];
}

View File

@@ -9,10 +9,10 @@ public sealed class AsynchronousCommand : AsyncCommand<AsynchronousCommandSettin
_console = console;
}
public async override Task<int> ExecuteAsync(CommandContext context, AsynchronousCommandSettings settings)
public async override Task<int> ExecuteAsync(CommandContext context, AsynchronousCommandSettings settings, CancellationToken cancellationToken)
{
// Simulate a long running asynchronous task
await Task.Delay(200);
await Task.Delay(200, cancellationToken);
if (settings.ThrowException)
{

Some files were not shown because too many files have changed in this diff Show More