Compare commits

...

29 Commits

Author SHA1 Message Date
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
67 changed files with 1777 additions and 447 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

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

View File

@@ -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

@@ -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)
{
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

@@ -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

@@ -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,10 +43,10 @@
</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="Scriban" Version="6.2.1" />
<PackageReference Include="Spectre.IO" Version="0.18.0" />
</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

@@ -1,27 +1,30 @@
<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.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" Version="8.0.0" />
<PackageVersion Include="MinVer" PrivateAssets="All" Version="6.0.0" />
<PackageVersion Include="PolySharp" Version="1.15.0" />
<PackageVersion Include="Roslynator.Analyzers" PrivateAssets="All" Version="4.13.1"/>
<PackageVersion Include="Roslynator.Analyzers" PrivateAssets="All" Version="4.14.0" />
<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.Console" Version="0.50.0" />
<PackageVersion Include="Spectre.Console.Cli" Version="0.50.0" />
<PackageVersion Include="Spectre.Console.Testing" Version="0.50.1-preview.0.20" />
<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="Verify.Xunit" Version="30.5.0" />
<PackageVersion Include="Wcwidth.Sources" Version="2.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.3">
<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

@@ -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

@@ -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

@@ -103,7 +103,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));

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

@@ -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

@@ -142,7 +142,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 +186,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

@@ -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

@@ -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;
@@ -39,7 +39,7 @@ internal abstract class CommandParameter : ICommandParameterInfo, ICommandParame
PairDeconstructor = deconstructor;
ValueProvider = valueProvider;
Validators = new List<ParameterValidationAttribute>(validators ?? Array.Empty<ParameterValidationAttribute>());
Required = required;
IsRequired = required;
IsHidden = isHidden;
}

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>
@@ -12,10 +12,6 @@
<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>
@@ -26,6 +22,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Spectre.Console" />
</ItemGroup>

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());
Run(args, Console, c => c.PropagateExceptions());
throw new InvalidOperationException("Expected an exception to be thrown, but there was none.");
}
catch (T ex)
{
if (ex is CommandAppException commandAppException && commandAppException.Pretty != null)
{
console.Write(commandAppException.Pretty);
Console.Write(commandAppException.Pretty);
}
else
{
console.WriteLine(ex.Message);
Console.WriteLine(ex.Message);
}
return new CommandAppFailure(ex, console.Output);
return new CommandAppFailure(ex, Console.Output);
}
catch (Exception ex)
{
@@ -120,8 +129,7 @@ public sealed class CommandAppTester
/// <returns>The result.</returns>
public CommandAppResult Run(params string[] args)
{
var console = new TestConsole().Width(int.MaxValue);
return Run(args, console);
return Run(args, Console);
}
private CommandAppResult Run(string[] args, TestConsole console, Action<IConfigurator>? config = null)
@@ -164,8 +172,7 @@ public sealed class CommandAppTester
/// <returns>The result.</returns>
public async Task<CommandAppResult> RunAsync(params string[] args)
{
var console = new TestConsole().Width(int.MaxValue);
return await RunAsync(args, console);
return await RunAsync(args, Console);
}
private async Task<CommandAppResult> RunAsync(string[] args, TestConsole console, Action<IConfigurator>? config = null)

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

@@ -0,0 +1,14 @@
namespace Spectre.Console.Tests.Data;
public class RequiredOptionsSettings : CommandSettings
{
[CommandOption("--foo <VALUE>", true)]
[Description("Foos the bars")]
public string Foo { get; set; }
}
public class RequiredOptionsWithoutDescriptionSettings : CommandSettings
{
[CommandOption("--foo <VALUE>", true)]
public string Foo { get; set; }
}

View File

@@ -0,0 +1,6 @@
USAGE:
myapp [OPTIONS]
OPTIONS:
-h, --help Prints help information
--foo <VALUE> Foos the bars. Required

View File

@@ -0,0 +1,6 @@
USAGE:
myapp [OPTIONS]
OPTIONS:
-h, --help Prints help information
--foo <VALUE> Required

View File

@@ -9,6 +9,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Shouldly" />
<PackageReference Include="Spectre.Console.Testing" />
<PackageReference Include="Spectre.Verify.Extensions" />
<PackageReference Include="Verify.Xunit" />
<PackageReference Include="xunit" />
@@ -20,8 +21,6 @@
<ItemGroup>
<ProjectReference Include="..\..\Spectre.Console.Cli\Spectre.Console.Cli.csproj" />
<ProjectReference Include="..\..\Spectre.Console.Testing\Spectre.Console.Testing.csproj"/>
<ProjectReference Include="..\..\Spectre.Console\Spectre.Console.csproj"/>
</ItemGroup>
</Project>

View File

@@ -1061,5 +1061,43 @@ public sealed partial class CommandAppTests
// Then
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Required_Options")]
public Task Should_Show_Required_Options()
{
// Given
var fixture = new CommandAppTester();
fixture.SetDefaultCommand<GenericCommand<RequiredOptionsSettings>>();
fixture.Configure(configurator =>
{
configurator.SetApplicationName("myapp");
});
// When
var result = fixture.Run("--help");
// Then
return Verifier.Verify(result.Output);
}
[Fact]
[Expectation("Required_Options_No_Description")]
public Task Should_Show_Required_Options_Without_Description()
{
// Given
var fixture = new CommandAppTester();
fixture.SetDefaultCommand<GenericCommand<RequiredOptionsWithoutDescriptionSettings>>();
fixture.Configure(configurator =>
{
configurator.SetApplicationName("myapp");
});
// When
var result = fixture.Run("--help");
// Then
return Verifier.Verify(result.Output);
}
}
}

View File

@@ -0,0 +1,27 @@
namespace Spectre.Console.Tests.Unit.Cli;
public sealed partial class CommandAppTests
{
public sealed class Options
{
[Fact]
public void Should_Throw_If_Required_Option_Is_Missing()
{
// Given
var fixture = new CommandAppTester();
fixture.Configure(config =>
{
config.AddCommand<GenericCommand<RequiredOptionsSettings>>("test");
config.PropagateExceptions();
});
// When
var result = Record.Exception(() => fixture.Run("test"));
// Then
result.ShouldBeOfType<CommandRuntimeException>()
.And(ex =>
ex.Message.ShouldBe("Command 'test' is missing required argument 'foo'."));
}
}
}

View File

@@ -1,6 +1,6 @@
namespace Spectre.Console.Tests.Unit.Cli;
public sealed partial class CommandApptests
public sealed partial class CommandAppTests
{
[Fact]
public void Should_Treat_Commands_As_Case_Sensitive_If_Specified()

View File

@@ -44,4 +44,40 @@ public sealed class CommandAppTesterTests
// Then
result.Output.ShouldBe(expected);
}
[Fact]
public void DefaultCtor_WithoutParameters_CreatesDefaultConsole()
{
// Given, When
CommandAppTester app = new();
// Then
app.Console.ShouldNotBeNull();
app.Console.Profile.Width.ShouldBe(int.MaxValue);
}
[Fact]
public void DefaultCtor_WithCustomConsole_UsesProvidedInstance()
{
// Given
TestConsole console = new();
// When
CommandAppTester app = new(null, new CommandAppTesterSettings(), console);
// Then
app.Console.ShouldNotBeNull();
app.Console.ShouldBeSameAs(console);
}
[Fact]
public void Ctor_WithSettings_CreatesDefaultConsole()
{
// Given, When
CommandAppTester app = new(new CommandAppTesterSettings());
// Then
app.Console.ShouldNotBeNull();
app.Console.Profile.Width.ShouldBe(int.MaxValue);
}
}

View File

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

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
@@ -26,7 +26,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Spectre.Console.Cli\Spectre.Console.Cli.csproj" />
<ProjectReference Include="..\..\Spectre.Console.Testing\Spectre.Console.Testing.csproj" />
<ProjectReference Include="..\..\Spectre.Console\Spectre.Console.csproj" />
<ProjectReference Include="..\..\Extensions\Spectre.Console.Json\Spectre.Console.Json.csproj" />

View File

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

View File

@@ -22,16 +22,35 @@ public sealed class ListPromptStateTests
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Should_Increase_Index(bool wrap)
[InlineData(ConsoleKey.UpArrow)]
[InlineData(ConsoleKey.K)]
public void Should_Decrease_Index(ConsoleKey key)
{
// Given
var state = CreateListPromptState(100, 10, false, false);
state.Update(ConsoleKey.End.ToConsoleKeyInfo());
var index = state.Index;
// When
state.Update(key.ToConsoleKeyInfo());
// Then
state.Index.ShouldBe(index - 1);
}
[Theory]
[InlineData(ConsoleKey.DownArrow, true)]
[InlineData(ConsoleKey.DownArrow, false)]
[InlineData(ConsoleKey.J, true)]
[InlineData(ConsoleKey.J, false)]
public void Should_Increase_Index(ConsoleKey key, bool wrap)
{
// Given
var state = CreateListPromptState(100, 10, wrap, false);
var index = state.Index;
// When
state.Update(ConsoleKey.DownArrow.ToConsoleKeyInfo());
state.Update(key.ToConsoleKeyInfo());
// Then
state.Index.ShouldBe(index + 1);
@@ -52,42 +71,48 @@ public sealed class ListPromptStateTests
state.Index.ShouldBe(99);
}
[Fact]
public void Should_Clamp_Index_If_No_Wrap()
[Theory]
[InlineData(ConsoleKey.DownArrow)]
[InlineData(ConsoleKey.J)]
public void Should_Clamp_Index_If_No_Wrap(ConsoleKey key)
{
// Given
var state = CreateListPromptState(100, 10, false, false);
state.Update(ConsoleKey.End.ToConsoleKeyInfo());
// When
state.Update(ConsoleKey.DownArrow.ToConsoleKeyInfo());
state.Update(key.ToConsoleKeyInfo());
// Then
state.Index.ShouldBe(99);
}
[Fact]
public void Should_Wrap_Index_If_Wrap()
[Theory]
[InlineData(ConsoleKey.DownArrow)]
[InlineData(ConsoleKey.J)]
public void Should_Wrap_Index_If_Wrap(ConsoleKey key)
{
// Given
var state = CreateListPromptState(100, 10, true, false);
state.Update(ConsoleKey.End.ToConsoleKeyInfo());
// When
state.Update(ConsoleKey.DownArrow.ToConsoleKeyInfo());
state.Update(key.ToConsoleKeyInfo());
// Then
state.Index.ShouldBe(0);
}
[Fact]
public void Should_Wrap_Index_If_Wrap_And_Down()
[Theory]
[InlineData(ConsoleKey.UpArrow)]
[InlineData(ConsoleKey.K)]
public void Should_Wrap_Index_If_Wrap_And_Down(ConsoleKey key)
{
// Given
var state = CreateListPromptState(100, 10, true, false);
// When
state.Update(ConsoleKey.UpArrow.ToConsoleKeyInfo());
state.Update(key.ToConsoleKeyInfo());
// Then
state.Index.ShouldBe(99);
@@ -106,13 +131,15 @@ public sealed class ListPromptStateTests
state.Index.ShouldBe(0);
}
[Fact]
public void Should_Wrap_Index_If_Wrap_And_Offset_And_Page_Down()
[Theory]
[InlineData(ConsoleKey.UpArrow)]
[InlineData(ConsoleKey.K)]
public void Should_Wrap_Index_If_Wrap_And_Offset_And_Page_Down(ConsoleKey key)
{
// Given
var state = CreateListPromptState(10, 100, true, false);
state.Update(ConsoleKey.End.ToConsoleKeyInfo());
state.Update(ConsoleKey.UpArrow.ToConsoleKeyInfo());
state.Update(key.ToConsoleKeyInfo());
// When
state.Update(ConsoleKey.PageDown.ToConsoleKeyInfo());

View File

@@ -115,7 +115,8 @@ public sealed class SelectionPromptTests
selection.ShouldBe(choices[1]);
}
[Fact] public void Should_Throw_Meaningful_Exception_For_Empty_Prompt()
[Fact]
public void Should_Throw_Meaningful_Exception_For_Empty_Prompt()
{
// Given
var console = new TestConsole();
@@ -130,6 +131,30 @@ public sealed class SelectionPromptTests
var exception = action.ShouldThrow<InvalidOperationException>();
exception.Message.ShouldBe("Cannot show an empty selection prompt. Please call the AddChoice() method to configure the prompt.");
}
[Fact]
public void Should_Append_Space_To_Search_If_Search_Is_Enabled()
{
// Given
var console = new TestConsole();
console.Profile.Capabilities.Interactive = true;
console.EmitAnsiSequences();
console.Input.PushText("Item");
console.Input.PushKey(ConsoleKey.Spacebar);
console.Input.PushKey(ConsoleKey.Enter);
// When
var prompt = new SelectionPrompt<string>()
.Title("Search for something with space")
.EnableSearch()
.AddChoices("Item1")
.AddChoices("Item 2");
string result = prompt.Show(console);
// Then
result.ShouldBe("Item 2");
console.Output.ShouldContain($"{ESC}[38;5;12m> {ESC}[0m{ESC}[1;38;5;12;48;5;11mItem {ESC}[0m{ESC}[38;5;12m2{ESC}[0m ");
}
}
file sealed class CustomSelectionItem

View File

@@ -405,5 +405,31 @@ public sealed class StyleTests
// Then
result.ShouldBe("default on green");
}
[Fact]
public void Should_Return_Expected_Markup_For_Style_With_Only_Link()
{
// Given
var style = new Style(link:"https://spectreconsole.net/");
// When
var result = style.ToMarkup();
// Then
result.ShouldBe("link=https://spectreconsole.net/");
}
[Fact]
public void Should_Return_Expected_Markup_For_Style_With_Background_And_Link()
{
// Given
var style = new Style(background: Color.Blue, link: "https://spectreconsole.net/");
// When
var result = style.ToMarkup();
// Then
result.ShouldBe("default on blue link=https://spectreconsole.net/");
}
}
}

View File

@@ -1,38 +0,0 @@
namespace Spectre.Console.Tests;
public static 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);
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()));
}
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}";
});
}
}