Compare commits

..

50 Commits

Author SHA1 Message Date
Patrik Svensson
5d4b2c88e5 Add extension method to use automatic application version 2024-04-25 20:28:13 +02:00
Patrik Svensson
3acc90e47c Make -v|--version opt-in
We added an automatic version option in 0.49. We did this with good
intentions, but forgot that people might already use --version
as an option for a root command.

This commit makes -v|--version completely opt-in.
2024-04-25 20:28:13 +02:00
Patrik Svensson
88515b7d7f Add Verify.Tool dotnet tool 2024-04-25 20:28:13 +02:00
Patrik Svensson
c5e11626b5 Add blog post for 0.49 release 2024-04-23 15:18:52 +02:00
Patrik Svensson
2ead177404 Update social card to show .NET 8.0 2024-04-23 15:18:42 +02:00
Patrik Svensson
71f762f646 Add token representation to remaining arguments
Before, when adding parsed information to the IRemainingArguments.Parsed,
we used the name of the parsed option ('foo') instead of it's
representation ('--foo'). This commit fixes that.
2024-04-23 14:10:04 +02:00
Patrik Svensson
95bff47b85 Expose raw arguments on the command context 2024-04-20 22:17:31 +02:00
renovate[bot]
de04619f88 chore: Update dependency Roslynator.Analyzers to v4.12.1 2024-04-17 09:53:53 +02:00
Frank Ray
ecdfdd4b85 Please upvote 👍 if you are interested in it (#1491) 2024-04-16 08:43:29 +01:00
Frank Ray
a893a9601e Top Issues GitHub Action (#1488) 2024-04-14 12:47:55 +01:00
BlazeFace
ff7282ecb8 Adding test and update render to fix issue with rendering separator when headers are hidden fixing issue 1391. 2024-04-14 02:05:13 +02:00
BlazeFace
eb38f76a6a Fixing #1507 AddDelegate uses an abstract type when used in a branch (#1509) 2024-04-13 13:40:12 +01:00
Phil Scott
fc0b553a4a Revert "Fixed render issue where writeline inside status caused corrupt output #415 #694"
This reverts commit 71a5d83067.

The commit introduced major flickering when working with LiveRenderables

Fixes #1466
2024-04-01 19:47:20 +02:00
Yennefer
1a3249cdae Improve XmlDoc output (#1503)
* Add command description and examples in XML Output

Closes #1115
2024-03-29 20:30:59 +01:00
Frank Ray
43f9ae92ad Pipe character for listing options - Fully implemented and tested. 2024-03-18 16:31:55 +01:00
Gerardo Grignoli
e66d3aab2e Added NoStackTrace to ExceptionFormats (#1489) 2024-03-08 15:26:45 +00:00
Tom Longhurst
d921ac6f02 Positioned Progress Tasks - Before or After Other Tasks (#1250) 2024-03-07 08:38:59 +00:00
renovate[bot]
5acd83a3ef chore: Update dependency SixLabors.ImageSharp to v3.1.3 2024-03-05 08:05:38 +01:00
Stuart Lang
397b742bec Add selection orompt Search (#1289)
* Add selection prompt search as you type

* Fix small bug

* Simplify

* Simplify

* Remove spacebar as a selection prompt submit key

* Trigger CI

* Update src/Spectre.Console/Prompts/SelectionPrompt.cs

Co-authored-by: Martin Costello <martin@martincostello.com>

* Simplifty Mask method

* Handle multi-selection prompt better

* Update API naming

* Address feedback

* Add some tests

* Remove whitespace

* Improve search and highlighting

* Add test case for previous issue

* Add extra test case

* Make prompt searchable

---------

Co-authored-by: Martin Costello <martin@martincostello.com>
Co-authored-by: Patrik Svensson <patrik@patriksvensson.se>
2024-02-25 12:57:27 +01:00
Martin Costello
d30b08201d Remove [DebuggerDisplay] from Paragraph
The `_text` field no longer exists, so hovering over instances in the debugger just shows an error message.
2024-02-23 14:25:32 +01:00
renovate[bot]
8da05bcc17 chore: Update dependency Roslynator.Analyzers to v4.11.0 2024-02-19 18:31:54 +01:00
renovate[bot]
badcd642ec chore: Update dependency dotnet-example to v3.1.0 2024-02-18 21:20:34 +01:00
Patrik Svensson
fde9ee04cf Automatically register command settings
Closes #428
2024-02-15 21:33:51 +01:00
renovate[bot]
b6e0b2389a chore: Update dependency Verify.Xunit to v23.2.0 2024-02-15 07:45:54 +01:00
renovate[bot]
3f5e8aabf5 chore: Update dependency dotnet-sdk to v8.0.200 2024-02-14 10:54:08 +01:00
renovate[bot]
ead874e6b2 chore: Update dependency dotnet-example to v3 2024-02-13 10:07:55 +01:00
Patrik Svensson
e13410861d Update dependencies to latest version 2024-02-13 09:34:00 +01:00
Daniel Weber
bf3b91a535 Don't erase the prompt text when backspacing on a secret prompt with a null mask. 2024-02-13 09:01:24 +01:00
Daniel Weber
72704529c5 Delete [UsesVerify] which has become obsolete through the latest update. 2024-02-13 08:53:16 +01:00
Patrik Svensson
b21e07ea94 Update dependencies
* Add support for C# 12
* Run all tests on all major .NET SDKs
* Only build on Ubuntu
* Do not build docs for pull requests
* Add Cédric Luthi, and Frank Ray to authors
* Drop netstandard2.0 for ImageSharp plugin
2024-01-31 20:51:49 +01:00
Jonathan Sheely
703d653ec5 Adds ValueFormatter to ProgressBar 2024-01-31 17:37:05 +01:00
Caelan Sayler
71631b248a Fix potential deadlock when cancelling prompts (#1439) 2024-01-31 12:02:01 +01:00
Tony Knight
63c22575ea Direct contributors to the current CONTRIBUTING.md (#1435) 2024-01-22 23:15:04 +01:00
Frank Ray
9cc888e5ad Make HelpProvider colors configurable (#1408) 2024-01-18 15:31:28 +01:00
Frank Ray
d5b4621233 Fully addresses review comments 2024-01-15 19:22:34 +01:00
Frank Ray
cee97821d6 Fix stylecop warning 2024-01-15 19:22:34 +01:00
Frank Ray
55c763a5c2 Completed 2024-01-15 19:22:34 +01:00
Frank Ray
d03c10623c Unit test to reproduce the -v/--version bug 2024-01-15 19:22:34 +01:00
Donovan Brown
5a52c1f277 Updated typo in commandApp.md
There was a sentence where the word `commits` should have been `commands`.
2024-01-13 18:58:32 +01:00
Nils Andresen
544e6a92df Added the ITypeResolver to the ExceptionHandler (#1411) 2024-01-06 23:34:02 +00:00
Nils Andresen
a94bc15746 Add the possibility to register multiple interceptors (#1412)
Having the interceptors registered with the ITypeRegistrar also enables the usage of ITypeResolver in interceptors.
2024-01-06 22:28:20 +00:00
Chet Husk
e7ce6a69b7 Allow specifying a property to ignore the use of build-time packages for versioning and analysis 2024-01-03 21:45:16 +01:00
Patrik Svensson
7cf7e84dd8 Remove mandelbrot example due to conflicting license
Closes #1423
Closes #1424
2024-01-03 21:31:16 +01:00
Nils Andresen
6f1f29967d (#1363) fix rendering of ListPrompt for odd pageSizes 2024-01-01 10:19:09 +01:00
Nils Andresen
006da0f9ea (#971) added spectre.console.cli to quick-start. 2024-01-01 10:17:59 +01:00
Nils Andresen
c62f79eded Bump CodeAnalyses references
So we can benefit from roslyn issue 69309
and have consistent EOL sequences, even on
auto-generated using statements.
2023-11-28 00:14:54 +01:00
Nils Andresen
1d19079913 add last commit to .git-blame-ignore-revs 2023-11-28 00:14:54 +01:00
Nils Andresen
44300c871f fixed line-endings 2023-11-28 00:14:54 +01:00
Nils Andresen
989c0b9904 added a .gitattributes file
to force lf checkouts on windows
2023-11-28 00:14:54 +01:00
Patrik Svensson
cb52eb63ce Add blog post for 0.48 2023-11-22 22:13:48 +01:00
250 changed files with 5574 additions and 3682 deletions

View File

@@ -1,2 +1,5 @@
# Use file scoped namespace declarations
7b2da0a4f63bf3ceab99d2c88535e74155f2b99c
# fix line-endings
e2ad4b1ea5555e701cda4fd400bb6592e318e1ff

4
.gitattributes vendored Normal file
View File

@@ -0,0 +1,4 @@
* text=auto
*.cs text eol=lf
*.md text eol=lf

View File

@@ -26,3 +26,6 @@ If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.
---
Please upvote :+1: this issue if you are interested in it.

View File

@@ -18,3 +18,6 @@ A clear and concise description of any alternative solutions or features you've
**Additional context**
Add any other context or screenshots about the feature request here.
---
Please upvote :+1: this issue if you are interested in it.

View File

@@ -7,7 +7,7 @@ fixes #
<!-- formalities. These are not optional. -->
- [ ] I have read the [Contribution Guidelines](../CONTRIBUTING.md)
- [ ] I have read the [Contribution Guidelines](https://github.com/spectreconsole/spectre.console/blob/main/CONTRIBUTING.md)
- [ ] I have commented on the issue above and discussed the intended changes
- [ ] A maintainer has signed off on the changes and the issue was assigned to me
- [ ] All newly added code is adequately covered by tests
@@ -17,3 +17,6 @@ fixes #
## Changes
<!-- describe the changes you made. -->
---
Please upvote :+1: this pull request if you are interested in it.

View File

@@ -8,48 +8,6 @@ env:
jobs:
###################################################
# DOCS
###################################################
docs:
name: Documentation
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
- name: Setup .NET SDK
uses: actions/setup-dotnet@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: npm-
- name: Build
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cd docs
dotnet tool restore
dotnet run --configuration Release
- name: Archive doc generation
uses: actions/upload-artifact@v3
with:
name: documentation-output
path: docs/output/
retention-days: 5
###################################################
# BUILD
###################################################
@@ -57,25 +15,15 @@ jobs:
build:
name: Build
if: "!contains(github.event.head_commit.message, 'skip-ci')"
strategy:
matrix:
kind: ['linux', 'windows', 'macOS']
include:
- kind: linux
os: ubuntu-latest
- kind: windows
os: windows-latest
- kind: macOS
os: macos-latest
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET SDK
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
@@ -90,7 +38,7 @@ jobs:
- name: Upload Verify Test Results
if: failure()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: verify-test-results
path: |

View File

@@ -15,40 +15,34 @@ env:
jobs:
###################################################
# BUILD
# PUBLISH
###################################################
build:
name: Build
if: |
(!startsWith(github.event.head_commit.message, 'skip-ci')
&& !startsWith(github.event.head_commit.message, 'chore:'))
|| startsWith(github.ref, 'refs/tags/')
strategy:
matrix:
kind: ['linux', 'windows', 'macOS']
include:
- kind: linux
os: ubuntu-latest
- kind: windows
os: windows-latest
- kind: macOS
os: macos-latest
runs-on: ${{ matrix.os }}
name: Publish NuGet Packages
if: "!contains(github.event.head_commit.message, 'skip-ci') || startsWith(github.ref, 'refs/tags/')"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET SDK
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
7.0.x
8.0.x
- name: Build
- name: Publish
shell: bash
run: |
dotnet tool restore
dotnet cake
dotnet cake --target="publish" \
--nuget-key="${{secrets.NUGET_API_KEY}}" \
--github-key="${{secrets.GITHUB_TOKEN}}"
###################################################
# DOCS
@@ -60,20 +54,20 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET SDK
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '16'
- name: Cache dependencies
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
@@ -89,34 +83,3 @@ jobs:
cd docs
dotnet tool restore
dotnet run --configuration Release -- deploy
###################################################
# PUBLISH
###################################################
publish:
name: Publish NuGet Packages
needs: [build]
if: "!contains(github.event.head_commit.message, 'skip-ci') || startsWith(github.ref, 'refs/tags/')"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup .NET SDK
uses: actions/setup-dotnet@v3
with:
dotnet-version: |
6.0.x
7.0.x
8.0.x
- name: Publish
shell: bash
run: |
dotnet tool restore
dotnet cake --target="publish" \
--nuget-key="${{secrets.NUGET_API_KEY}}" \
--github-key="${{secrets.GITHUB_TOKEN}}"

View File

@@ -0,0 +1,24 @@
name: Top issues action.
on:
schedule:
- cron: '0 0 */1 * *'
jobs:
ShowAndLabelTopIssues:
name: Display and label top issues.
runs-on: ubuntu-latest
steps:
- name: Top Issues action
uses: rickstaa/top-issues-action@v1.3.99
env:
github_token: ${{ secrets.GITHUB_TOKEN }}
with:
top_list_size: 10
label: true
dashboard: true
dashboard_show_total_reactions: true
top_issues: true
top_bugs: true
top_features: true
feature_label: feature
top_pull_requests: true

View File

@@ -3,8 +3,7 @@
_[![Spectre.Console NuGet Version](https://img.shields.io/nuget/v/spectre.console.svg?style=flat&label=NuGet%3A%20Spectre.Console)](https://www.nuget.org/packages/spectre.console)_ _[![Spectre.Console CLI NuGet Version](https://img.shields.io/nuget/v/spectre.console.cli.svg?style=flat&label=NuGet%3A%20Spectre.Console.Cli)](https://www.nuget.org/packages/spectre.console.cli)_ [![Netlify Status](https://api.netlify.com/api/v1/badges/1eaf215a-eb9c-45e4-8c64-c90b62963149/deploy-status)](https://app.netlify.com/sites/spectreconsole/deploys)
A .NET library that makes it easier to create beautiful, cross platform, console applications.
It is heavily inspired by the excellent [Rich library](https://github.com/willmcgugan/rich)
for Python. For detailed usage instructions, [please refer to the documentation at https://spectreconsole.net/.](https://spectreconsole.net/)
It is heavily inspired by the excellent Python library, [Rich](https://github.com/willmcgugan/rich). Detailed instructions for using `Spectre.Console` are located on the project website, https://spectreconsole.net
## Table of Contents
@@ -19,18 +18,22 @@ for Python. For detailed usage instructions, [please refer to the documentation
## Features
* Written with unit testing in mind.
* Supports tables, grids, panels, and a [rich](https://github.com/willmcgugan/rich) inspired markup language.
* Supports tables, grids, panels, and a [Rich](https://github.com/willmcgugan/rich) inspired markup language.
* Supports the most common SRG parameters when it comes to text
styling such as bold, dim, italic, underline, strikethrough,
and blinking text.
* Supports 3/4/8/24-bit colors in the terminal.
The library will detect the capabilities of the current terminal
and downgrade colors as needed.
* Written with unit testing in mind.
![Example](docs/input/assets/images/example.png)
## Important Notices
> [!IMPORTANT]\
> We use the [Top Issues Dashboard](https://github.com/spectreconsole/spectre.console/issues/1517) for tracking community demand. Please upvote :+1: the issues and pull requests you are interested in.
## Installing
The fastest way of getting started using `Spectre.Console` is to install the NuGet package.
@@ -42,7 +45,7 @@ dotnet add package Spectre.Console
## Documentation
The documentation for `Spectre.Console` can be found at
https://spectreconsole.net/
https://spectreconsole.net
## Examples
@@ -69,7 +72,7 @@ And to run an example:
## Sponsors
The following people are [sponsoring](https://github.com/sponsors/patriksvensson)
Spectre.Console to show their support and to ensure the longevity of the project.
`Spectre.Console` to show their support and to ensure the longevity of the project.
* [Rodney Littles II](https://github.com/RLittlesII)
* [Martin Björkström](https://github.com/bjorkstromm)
@@ -99,6 +102,6 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org)
Copyright © Patrik Svensson, Phil Scott, Nils Andresen
Spectre.Console is provided as-is under the MIT license. For more information see LICENSE.
`Spectre.Console` is provided as-is under the MIT license. For more information see LICENSE.
* SixLabors.ImageSharp, a library which Spectre.Console relies upon, is licensed under Apache 2.0 when distributed as part of Spectre.Console. The Six Labors Split License covers all other usage, see: https://github.com/SixLabors/ImageSharp/blob/master/LICENSE
* SixLabors.ImageSharp, a library which `Spectre.Console` relies upon, is licensed under Apache 2.0 when distributed as part of `Spectre.Console`. The Six Labors Split License covers all other usage, see: https://github.com/SixLabors/ImageSharp/blob/master/LICENSE

View File

@@ -0,0 +1,63 @@
Title: Spectre.Console 0.48 released!
Description: .NET 8, custom help providers, and more!
Published: 2023-11-22
Category: Release Notes
Excluded: false
---
Version 0.48 of Spectre.Console has been released!
Several rendering issues have been addressed, such as fixing problems related to rendering inside status causing corrupt output, avoiding exceptions on Rows with no children, as well as addressing rendering bugs in TextPath.
New features have been added, such as the ability to show separators between table rows. Other notable additions include progress bar header and footer support, customizable (and localizable) help providers, and the option to style text and confirmation prompts.
# New Contributors
* [@icalvo](https://github.com/icalvo) made their first contribution in [#1215](https://github.com/spectreconsole/spectre.console/pull/1215)
* [@fredrikbentzen](https://github.com/fredrikbentzen) made their first contribution in [#1132](https://github.com/spectreconsole/spectre.console/pull/1132)
* [@jeppevammenkristensen](https://github.com/jeppevammenkristensen) made their first contribution in [#1241](https://github.com/spectreconsole/spectre.console/pull/1241)
* [@tomaszprasolek](https://github.com/tomaszprasolek) made their first contribution in [#1257](https://github.com/spectreconsole/spectre.console/pull/1257)
* [@olabacker](https://github.com/olabacker) made their first contribution in [#1302](https://github.com/spectreconsole/spectre.console/pull/1302)
* [@AndrewRathbun](https://github.com/AndrewRathbun) made their first contribution in [#1315](https://github.com/spectreconsole/spectre.console/pull/1315)
# What's Changed
## Rendering
* Add .NET 8 support by [@patriksvensson](https://github.com/patriksvensson) in [#1367](https://github.com/spectreconsole/spectre.console/pull/1367)
* Fixed render issue where writeline inside status caused corrupt output #415 #694 by [@fredrikbentzen](https://github.com/fredrikbentzen) in [#1132]([#1132](https://github.com/spectreconsole/spectre.console/pull/1132))
* Relax the SDK requirements by rolling forward to the latest feature by [@0xced](https://github.com/0xced) in [#1237]([#1237](https://github.com/spectreconsole/spectre.console/pull/1237))
* Add fix to avoid exception on rows with no children by [@jeppevammenkristensen](https://github.com/jeppevammenkristensen) in [#1241]([#1241](https://github.com/spectreconsole/spectre.console/pull/1241))
* Set `end_of_line` to `LF` instead of `CRLF` by [@0xced](https://github.com/0xced) in [#1256](https://github.com/spectreconsole/spectre.console/pull/1256)
* Fix `Rule` widget docs by [@tomaszprasolek](https://github.com/tomaszprasolek) in [#1257](https://github.com/spectreconsole/spectre.console/pull/1257)
* Added the missing columns-cast by [@nils](https://github.com/nils)-a in [#1294](https://github.com/spectreconsole/spectre.console/pull/1294)
* Render tables with zero-width columns by [@Frassle](https://github.com/Frassle) in [#1197](https://github.com/spectreconsole/spectre.console/pull/1197)
* Fix figlet centering possibly throwing due to negative size by [@olabacker](https://github.com/olabacker) in [#1302](https://github.com/spectreconsole/spectre.console/pull/1302)
* Add option to show separator between table rows by [@patriksvensson](https://github.com/patriksvensson) in [#1304](https://github.com/spectreconsole/spectre.console/pull/1304)
* Enable setting the color of the values in a `BreakdownChart` by [@nils](https://github.com/nils)-a in [#1303](https://github.com/spectreconsole/spectre.console/pull/1303)
* Progress bar header and footer by [@phil](https://github.com/phil)-scott-78 in [#1262](https://github.com/spectreconsole/spectre.console/pull/1262)
* Add an example showing the decorations off by [@Frassle](https://github.com/Frassle) in [#1191](https://github.com/spectreconsole/spectre.console/pull/1191)
* Fixes `TextPath` rendering bugs by [@patriksvensson](https://github.com/patriksvensson) in [#1308](https://github.com/spectreconsole/spectre.console/pull/1308)
* Fix greedy row measure by [@nils](https://github.com/nils)-a in [#1338](https://github.com/spectreconsole/spectre.console/pull/1338)
* Fix `AnsiConsoleOutput` safe height by [@0xced](https://github.com/0xced) in [#1358](https://github.com/spectreconsole/spectre.console/pull/1358)
* Allow passing a nullable style in `DefaultValueStyle()` and `ChoicesStyle()` by [@0xced](https://github.com/0xced) in [#1359](https://github.com/spectreconsole/spectre.console/pull/1359)
* Allow `ConfirmationPrompt` Styling by [@wbaldoumas](https://github.com/wbaldoumas) in [#1210](https://github.com/spectreconsole/spectre.console/pull/1210)
## CLI
* Add async command unit tests by [@FrankRay78](https://github.com/FrankRay78) in [#1228](https://github.com/spectreconsole/spectre.console/pull/1228)
* Add support for async delegate by [@icalvo](https://github.com/icalvo) in [#1215]([#1215](https://github.com/spectreconsole/spectre.console/pull/1215))
* Remove unnecessary `[NotNull]` attributes by [@0xced](https://github.com/0xced) in [#1255](https://github.com/spectreconsole/spectre.console/pull/1255)
* Allow custom help providers by [@FrankRay78](https://github.com/FrankRay78) in [#1259](https://github.com/spectreconsole/spectre.console/pull/1259)
* Specified details for settings for the argument vector by [@nils](https://github.com/nils)-a in [#1301](https://github.com/spectreconsole/spectre.console/pull/1301)
* Add support for localisation in help provider by [@FrankRay78](https://github.com/FrankRay78) in [#1349](https://github.com/spectreconsole/spectre.console/pull/1349)
* Fix DefaultValue for `FileInfo` and `DirectoryInfo` by [@0xced](https://github.com/0xced) in [#1238](https://github.com/spectreconsole/spectre.console/pull/1238)
## Documentation & Samples
* Added a minimal PR template by [@nils](https://github.com/nils)-a in [#1318](https://github.com/spectreconsole/spectre.console/pull/1318)
* Fix typo in `showcase` sample by [@AndrewRathbun](https://github.com/AndrewRathbun) in [#1315](https://github.com/spectreconsole/spectre.console/pull/1315)
* Update `columns` sample to showcase nicer data by [@nils](https://github.com/nils)-a in [#1295](https://github.com/spectreconsole/spectre.console/pull/1295)
* Change all `SetErrorHandler` to `SetExceptionHandler` by [@nils](https://github.com/nils)-a in [#1298](https://github.com/spectreconsole/spectre.console/pull/1298)
## Other stuff
* Ensure the `Generator` project compiles by [@patriksvensson](https://github.com/patriksvensson) in [#1371](https://github.com/spectreconsole/spectre.console/pull/1371)

View File

@@ -0,0 +1,55 @@
Title: Spectre.Console 0.49 released!
Description: Bug fixes, bug fixes, bug fixes
Published: 2024-04-23
Category: Release Notes
Excluded: false
---
Version 0.49 of Spectre.Console has been released!
## New Contributors
* @baronfel made their first contribution in https://github.com/spectreconsole/spectre.console/pull/1425
* @DarqueWarrior made their first contribution in https://github.com/spectreconsole/spectre.console/pull/1431
* @tonycknight made their first contribution in https://github.com/spectreconsole/spectre.console/pull/1435
* @caesay made their first contribution in https://github.com/spectreconsole/spectre.console/pull/1439
* @jsheely made their first contribution in https://github.com/spectreconsole/spectre.console/pull/1414
* @danielcweber made their first contribution in https://github.com/spectreconsole/spectre.console/pull/1456
* @martincostello made their first contribution in https://github.com/spectreconsole/spectre.console/pull/1477
* @slang25 made their first contribution in https://github.com/spectreconsole/spectre.console/pull/1289
* @thomhurst made their first contribution in https://github.com/spectreconsole/spectre.console/pull/1250
* @gerardog made their first contribution in https://github.com/spectreconsole/spectre.console/pull/1489
* @yenneferofvengerberg made their first contribution in https://github.com/spectreconsole/spectre.console/pull/1503
* @BlazeFace made their first contribution in https://github.com/spectreconsole/spectre.console/pull/1509
## Changes
* Cleanup line endings by @nils-a in https://github.com/spectreconsole/spectre.console/pull/1381
* Added Spectre.Console.Cli to quick-start. by @nils-a in https://github.com/spectreconsole/spectre.console/pull/1413
* Fix rendering of ListPrompt for odd pageSizes by @nils-a in https://github.com/spectreconsole/spectre.console/pull/1365
* Remove mandelbrot example due to conflicting license by @patriksvensson in https://github.com/spectreconsole/spectre.console/pull/1426
* Allow specifying a property to ignore the use of build-time packages for versioning and analysis by @baronfel in https://github.com/spectreconsole/spectre.console/pull/1425
* Add the possibility to register multiple interceptors by @nils-a in https://github.com/spectreconsole/spectre.console/pull/1412
* Added the ITypeResolver to the ExceptionHandler by @nils-a in https://github.com/spectreconsole/spectre.console/pull/1411
* Updated typo in commandApp.md by @DarqueWarrior in https://github.com/spectreconsole/spectre.console/pull/1431
* Command with -v displays app version instead of executing the command by @FrankRay78 in https://github.com/spectreconsole/spectre.console/pull/1427
* HelpProvider colors should be configurable by @FrankRay78 in https://github.com/spectreconsole/spectre.console/pull/1408
* Direct contributors to the current CONTRIBUTING.md by @tonycknight in https://github.com/spectreconsole/spectre.console/pull/1435
* Fix deadlock when cancelling prompts by @caesay in https://github.com/spectreconsole/spectre.console/pull/1439
* Add progress bar value formatter by @jsheely in https://github.com/spectreconsole/spectre.console/pull/1414
* Update dependencies and do some clean-up by @patriksvensson in https://github.com/spectreconsole/spectre.console/pull/1440
* Delete [UsesVerify], which has become obsolete through the latest update. by @danielcweber in https://github.com/spectreconsole/spectre.console/pull/1456
* Don't erase secret prompt text upon backspace when mask is null by @danielcweber in https://github.com/spectreconsole/spectre.console/pull/1458
* Update dependencies to the latest version by @patriksvensson in https://github.com/spectreconsole/spectre.console/pull/1459
* Automatically register command settings by @patriksvensson in https://github.com/spectreconsole/spectre.console/pull/1463
* Remove [DebuggerDisplay] from Paragraph by @martincostello in https://github.com/spectreconsole/spectre.console/pull/1477
* Selection Prompt Search by @slang25 in https://github.com/spectreconsole/spectre.console/pull/1289
* Update dependency SixLabors.ImageSharp to v3.1.3 by @renovate in https://github.com/spectreconsole/spectre.console/pull/1486
* Positioned Progress Tasks - Before or After Other Tasks by @thomhurst in https://github.com/spectreconsole/spectre.console/pull/1250
* Added NoStackTrace to ExceptionFormats by @gerardog in https://github.com/spectreconsole/spectre.console/pull/1489
* Pipe character for listing options (issue 1434) by @FrankRay78 in https://github.com/spectreconsole/spectre.console/pull/1498
* Improve XmlDoc output by @yenneferofvengerberg in https://github.com/spectreconsole/spectre.console/pull/1503
* Revert 71a5d830 to undo flickering regression by @phil-scott-78 in https://github.com/spectreconsole/spectre.console/pull/1504
* AddDelegate uses an abstract type when used in a branch by @BlazeFace in https://github.com/spectreconsole/spectre.console/pull/1509
* Missing Separator When Headers are Hidden by @BlazeFace in https://github.com/spectreconsole/spectre.console/pull/1513
* Expose raw arguments on the command context by @patriksvensson in https://github.com/spectreconsole/spectre.console/pull/1523
* Add token representation to remaining arguments by @patriksvensson in https://github.com/spectreconsole/spectre.console/pull/1525

View File

@@ -15,6 +15,34 @@ The help is also context aware and tailored depending on what has been specified
`HelpProvider` is the `Spectre.Console` class responsible for determining context and preparing the help text to write to the console. It is an implementation of the public interface `IHelpProvider`.
## Styling the help text
Basic styling is applied to the generated help text by default, however this is configurable.
`HelpProviderStyle` is the `Spectre.Console` class that holds the style information for the help text.
The default theme shipped with Spectre.Console is provided by a factory method, `HelpProviderStyle.Default`.
However, you can explicitly set a custom theme when configuring a CommandApp, for example:
```csharp
config.Settings.HelpProviderStyles = new HelpProviderStyle()
{
Description = new DescriptionStyle()
{
Header = "bold",
},
};
```
Removing all styling from help text is also possible, a good choice for ensuring maximum accessibility. This is configured by clearing the style provider entirely:
```csharp
config.Settings.HelpProviderStyles = null;
```
See [Markup](../markup) for information about the use of markup in Spectre.Console, and [Styles](xref:styles) for a listing of supported styles.
## Custom help providers
Whilst it shouldn't be common place to implement your own help provider, it is however possible.

View File

@@ -23,7 +23,7 @@ app.Configure(config =>
## Multiple Commands
In the previous example we have a single command that is configured. For complex command line applications, it is common for them to have multiple commands (or verbs) defined. Examples of applications like this are `git`, `dotnet` and `gh`. For example, git would have a `commit` command and along with other commits like `add` or `rebase`. Each with their own settings and validation. With `Spectre.Console.Cli` we use the `Configure` method to add these commands.
In the previous example we have a single command that is configured. For complex command line applications, it is common for them to have multiple commands (or verbs) defined. Examples of applications like this are `git`, `dotnet` and `gh`. For example, git would have a `commit` command and along with other commands like `add` or `rebase`. Each with their own settings and validation. With `Spectre.Console.Cli` we use the `Configure` method to add these commands.
For example, to add three different commands to the application:
@@ -81,9 +81,12 @@ Hint: If you do write your own implementation of `TypeRegistrar` and `TypeResolv
there is a utility `TypeRegistrarBaseTests` available that can be used to ensure your implementations adhere to the required implementation. Simply call `TypeRegistrarBaseTests.RunAllTests()` and expect no `TypeRegistrarBaseTests.TestFailedException` to be thrown.
## Interception
Interceptors can be registered with the `TypeRegistrar` (or with a custom DI-Container). Alternatively, `CommandApp` also provides a `SetInterceptor` configuration.
`CommandApp` also provides a `SetInterceptor` configuration. An interceptor is run before all commands are executed. This is typically used for configuring logging or other infrastructure concerns.
All interceptors must implement `ICommandInterceptor`. Upon execution of a command, The `Intercept`-Method of an instance of your interceptor will be called with the parsed settings. This provides an opportunity for configuring any infrastructure or modifying the settings.
When the command has been run, the `InterceptResult`-Method of the same instance is called with the result of the command.
This provides an opportunity to modify the result and also to tear down any infrastructure in use.
All interceptors must implement `ICommandInterceptor`. Upon execution of a command, an instance of your interceptor will be called with the parsed settings. This provides an opportunity for configuring any infrastructure or modifying the settings.
The `Intercept`-Method of each interceptor is run before the command is executed and the `InterceptResult`-Method is run after it. These are typically used for configuring logging or other infrastructure concerns.
For an example of using the interceptor to configure logging, see the [Serilog demo](https://github.com/spectreconsole/spectre.console/tree/main/examples/Cli/Logging).

View File

@@ -53,7 +53,11 @@ Using the `SetExceptionHandler()` during configuration it is possible to handle
This method comes in two flavours: One that uses the default exitCode (or `return` value) of `-1` and one
where the exitCode needs to be supplied.
### Using `SetExceptionHandler(Func<Exception, int> handler)`
The `ITypeResolver?` parameter will be null, when the exception occurs while no `ITypeResolver` is available.
(Basically the `ITypeResolver` will be set, when the exception occurs during a command execution, but not
during the parsing phase and construction of the command.)
### Using `SetExceptionHandler(Func<Exception, ITypeResolver?, int> handler)`
Using this method exceptions can be handled in a custom way. The return value of the handler is used as
the exitCode for the application.
@@ -71,7 +75,7 @@ namespace MyApp
app.Configure(config =>
{
config.SetExceptionHandler(ex =>
config.SetExceptionHandler((ex, resolver) =>
{
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything);
return -99;
@@ -84,9 +88,9 @@ namespace MyApp
}
```
### Using `SetExceptionHandler(Action<Exception> handler)`
### Using `SetExceptionHandler(Action<Exception, ITypeResolver?> handler)`
Using this method exceptions can be handled in a custom way, much the same as with the `SetExceptionHandler(Func<Exception, int> handler)`.
Using this method exceptions can be handled in a custom way, much the same as with the `SetExceptionHandler(Func<Exception, ITypeResolver?, int> handler)`.
Using the `Action` as the handler however, it is not possible (or required) to supply a return value.
The exitCode for the application will be `-1`.
@@ -103,7 +107,7 @@ namespace MyApp
app.Configure(config =>
{
config.SetExceptionHandler(ex =>
config.SetExceptionHandler((ex, resolver) =>
{
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything);
});

View File

@@ -8,9 +8,10 @@ to install the NuGet package.
```text
> dotnet add package Spectre.Console
> dotnet add package Spectre.Console.Cli
```
After that you will need to reference the `Spectre.Console` namespace.
After that you will need to reference the `Spectre.Console` and `Spectre.Console.Cli` namespaces.
Once that is done, you can start using all the available features.
```csharp

View File

@@ -132,3 +132,13 @@ AnsiConsole.Write(new BreakdownChart()
.AddItem(new Fruit("Mango", 3, Color.Orange4))
.AddItems(items));
```
### Add value formatter to chart numbers
```csharp
var chart = new BreakdownChart();
chart.UseValueFormater(value => value.ToString("N0"));
// This can be simplified as extension methods are chainable.
var chart = new BreakdownChart().UseValueFormatter(v => v.ToString("N0"));
```

View File

@@ -1,4 +1,4 @@
Title: Calendar
Title: Calendar
Order: 40
RedirectFrom: calendar
Description: "The **Calendar** is used to render a calendar to the terminal."

View File

@@ -1,4 +1,4 @@
using Statiq.App;
using Statiq.App;
using Statiq.Common;
using Statiq.Web;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;

View File

@@ -1,4 +1,4 @@
namespace Docs.Extensions
namespace Docs.Extensions
{
public static class StringExtensions
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
namespace Docs.Models
{

View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using System.Net;
using Docs.Utilities;
using Microsoft.Extensions.DependencyInjection;

View File

@@ -1,4 +1,4 @@
using Statiq.Common;
using Statiq.Common;
using Statiq.Web.GitHub;
using Statiq.Web.Netlify;

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Docs.Extensions;

View File

@@ -15,7 +15,7 @@
<div id="container">
<div id="console">
<div class="line"><span style="color:var(--brightBlack)">╭─</span><span style="color:var(--folder)">&#xe0b2;</span><span style="background-color:var(--folder);color:var(--black)"> ~/spectre.console</span><span style="color:var(--folder);background-color:var(--dotnet)">&#xe0b0;</span><span style="background-color:var(--blue)"> .NET 7.0 </span><span style="color:var(--dotnet);background-color:var(--git)">&#xe0b0;</span><span style="background-color:var(--git);color:var(--background)"> &#xe0a0; main </span><span style="color:var(--git)">&#xe0b4;</span></div>
<div class="line"><span style="color:var(--brightBlack)">╭─</span><span style="color:var(--folder)">&#xe0b2;</span><span style="background-color:var(--folder);color:var(--black)"> ~/spectre.console</span><span style="color:var(--folder);background-color:var(--dotnet)">&#xe0b0;</span><span style="background-color:var(--blue)"> .NET 8.0 </span><span style="color:var(--dotnet);background-color:var(--git)">&#xe0b0;</span><span style="background-color:var(--git);color:var(--background)"> &#xe0a0; main </span><span style="color:var(--git)">&#xe0b4;</span></div>
<div class="line"><span style="color:var(--brightBlack)">╰─</span> dotnet run</div>
<div class="line"></div>
<div class="line">╭────────────────────────────────────────────────────────╮</div>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

View File

@@ -9,10 +9,16 @@
]
},
"dotnet-example": {
"version": "2.0.0",
"version": "3.1.0",
"commands": [
"dotnet-example"
]
},
"verify.tool": {
"version": "0.6.0",
"commands": [
"dotnet-verify"
]
}
}
}

View File

@@ -1,4 +1,5 @@
using Spectre.Console.Cli;
using Spectre.Console.Cli.Help;
namespace Help;
@@ -12,6 +13,9 @@ public static class Program
{
// Register the custom help provider
config.SetHelpProvider(new CustomHelpProvider(config.Settings));
// Render an unstyled help text for maximum accessibility
config.Settings.HelpProviderStyles = null;
});
return app.Run(args);

View File

@@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -12,9 +12,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Map" Version="1.0.2" />
</ItemGroup>

View File

@@ -1,86 +0,0 @@
/*
Ported from: https://rosettacode.org/wiki/Mandelbrot_set#C.23
Licensed under GNU Free Documentation License 1.2
*/
using System;
using Spectre.Console;
namespace Canvas;
public static class Mandelbrot
{
private const double MaxValueExtent = 2.0;
private struct ComplexNumber
{
public double Real { get; }
public double Imaginary { get; }
public ComplexNumber(double real, double imaginary)
{
Real = real;
Imaginary = imaginary;
}
public static ComplexNumber operator +(ComplexNumber x, ComplexNumber y)
{
return new ComplexNumber(x.Real + y.Real, x.Imaginary + y.Imaginary);
}
public static ComplexNumber operator *(ComplexNumber x, ComplexNumber y)
{
return new ComplexNumber(x.Real * y.Real - x.Imaginary * y.Imaginary,
x.Real * y.Imaginary + x.Imaginary * y.Real);
}
public double Abs()
{
return Real * Real + Imaginary * Imaginary;
}
}
public static Spectre.Console.Canvas Generate(int width, int height)
{
var canvas = new Spectre.Console.Canvas(width, height);
var scale = 2 * MaxValueExtent / Math.Min(canvas.Width, canvas.Height);
for (var i = 0; i < canvas.Height; i++)
{
var y = (canvas.Height / 2 - i) * scale;
for (var j = 0; j < canvas.Width; j++)
{
var x = (j - canvas.Width / 2) * scale;
var value = Calculate(new ComplexNumber(x, y));
canvas.SetPixel(j, i, GetColor(value));
}
}
return canvas;
}
private static double Calculate(ComplexNumber c)
{
const int MaxIterations = 1000;
const double MaxNorm = MaxValueExtent * MaxValueExtent;
var iteration = 0;
var z = new ComplexNumber();
do
{
z = z * z + c;
iteration++;
} while (z.Abs() < MaxNorm && iteration < MaxIterations);
return iteration < MaxIterations
? (double)iteration / MaxIterations
: 0;
}
private static Color GetColor(double value)
{
const double MaxColor = 256;
const double ContrastValue = 0.2;
return new Color(0, 0, (byte)(MaxColor * Math.Pow(value, ContrastValue)));
}
}

View File

@@ -10,10 +10,6 @@ public static class Program
{
public static void Main()
{
// Draw a mandelbrot set using a Canvas
var mandelbrot = Mandelbrot.Generate(32, 32);
Render(mandelbrot, "Mandelbrot");
// Draw an image using CanvasImage powered by ImageSharp.
// This requires the "Spectre.Console.ImageSharp" NuGet package.
var image = new CanvasImage("cake.png");
@@ -21,7 +17,7 @@ public static class Program
image.MaxWidth(16);
Render(image, "Image from file (16 wide)");
// Draw image again, but without render width
// Draw image again, but without max width
image.NoMaxWidth();
image.Mutate(ctx => ctx.Grayscale().Rotate(-45).EntropyCrop());
Render(image, "Image from file (fit, greyscale, rotated)");

View File

@@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>

View File

@@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>

View File

@@ -110,6 +110,7 @@ namespace Prompt
{
fruit = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.EnableSearch()
.Title("Ok, but if you could only choose [green]one[/]?")
.MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]")
.AddChoices(favorites));

View File

@@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>

View File

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

View File

@@ -1,4 +1,4 @@
using Spectre.Console;
using Spectre.Console;
namespace Generator.Commands
{

View File

@@ -1,7 +1,7 @@
<Project>
<PropertyGroup Label="Settings">
<Deterministic>true</Deterministic>
<LangVersion>10</LangVersion>
<LangVersion>12</LangVersion>
<DebugSymbols>true</DebugSymbols>
<DebugType>embedded</DebugType>
<MinVerSkip Condition="'$(Configuration)' == 'Debug'">true</MinVerSkip>
@@ -15,8 +15,8 @@
<PropertyGroup Label="Package Information">
<Description>A library that makes it easier to create beautiful console applications.</Description>
<Copyright>Patrik Svensson, Phil Scott, Nils Andresen</Copyright>
<Authors>Patrik Svensson, Phil Scott, Nils Andresen</Authors>
<Copyright>Patrik Svensson, Phil Scott, Nils Andresen, Cédric Luthi, Frank Ray</Copyright>
<Authors>Patrik Svensson, Phil Scott, Nils Andresen, Cédric Luthi, Frank Ray</Authors>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/spectreconsole/spectre.console</RepositoryUrl>
<PackageIcon>small-logo.png</PackageIcon>
@@ -31,13 +31,14 @@
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="MinVer" PrivateAssets="All" Version="4.2.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" Version="1.1.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
<!-- Allow folks to build with minimal dependencies (though they will need to provide their own Version data) -->
<ItemGroup Label="Build Tools Package References" Condition="'$(UseBuildTimeTools)' != 'false'">
<PackageReference Include="MinVer" PrivateAssets="All" Version="4.3.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" Version="8.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<PackageReference Include="Roslynator.Analyzers" Version="4.1.2">
<PackageReference Include="Roslynator.Analyzers" Version="4.12.1">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>

View File

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

View File

@@ -8,6 +8,7 @@
<IncludeBuildOutput>false</IncludeBuildOutput>
<Nullable>enable</Nullable>
<NoPackageAnalysis>true</NoPackageAnalysis>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
@@ -16,10 +17,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>

View File

@@ -102,7 +102,7 @@ public sealed class CommandApp : ICommandApp
if (_configurator.Settings.ExceptionHandler != null)
{
return _configurator.Settings.ExceptionHandler(ex);
return _configurator.Settings.ExceptionHandler(ex, null);
}
// Render the exception.

View File

@@ -13,6 +13,11 @@ public sealed class CommandContext
/// </value>
public IRemainingArguments Remaining { get; }
/// <summary>
/// Gets all the arguments that were passed to the applicaton.
/// </summary>
public IReadOnlyList<string> Arguments { get; }
/// <summary>
/// Gets the name of the command.
/// </summary>
@@ -32,11 +37,17 @@ public sealed class CommandContext
/// <summary>
/// Initializes a new instance of the <see cref="CommandContext"/> class.
/// </summary>
/// <param name="arguments">All arguments that were passed to the application.</param>
/// <param name="remaining">The remaining arguments.</param>
/// <param name="name">The command name.</param>
/// <param name="data">The command data.</param>
public CommandContext(IRemainingArguments remaining, string name, object? data)
public CommandContext(
IEnumerable<string> arguments,
IRemainingArguments remaining,
string name,
object? data)
{
Arguments = arguments.ToSafeReadOnlyList();
Remaining = remaining ?? throw new System.ArgumentNullException(nameof(remaining));
Name = name ?? throw new System.ArgumentNullException(nameof(name));
Data = data;

View File

@@ -82,7 +82,7 @@ public static class ConfiguratorExtensions
}
/// <summary>
/// Overrides the auto-detected version of the application.
/// Sets the version of the application.
/// </summary>
/// <param name="configurator">The configurator.</param>
/// <param name="version">The version of application.</param>
@@ -98,6 +98,25 @@ public static class ConfiguratorExtensions
return configurator;
}
/// <summary>
/// Uses the version retrieved from the <see cref="AssemblyInformationalVersionAttribute"/>
/// as the application's version.
/// </summary>
/// <param name="configurator">The configurator.</param>
/// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator UseAssemblyInformationalVersion(this IConfigurator configurator)
{
if (configurator == null)
{
throw new ArgumentNullException(nameof(configurator));
}
configurator.Settings.ApplicationVersion =
VersionHelper.GetVersion(Assembly.GetEntryAssembly());
return configurator;
}
/// <summary>
/// Hides the <c>DEFAULT</c> column that lists default values coming from the
/// <see cref="DefaultValueAttribute"/> in the options help text.
@@ -229,7 +248,7 @@ public static class ConfiguratorExtensions
throw new ArgumentNullException(nameof(configurator));
}
configurator.Settings.Interceptor = interceptor;
configurator.Settings.Registrar.RegisterInstance<ICommandInterceptor>(interceptor);
return configurator;
}
@@ -324,11 +343,16 @@ public static class ConfiguratorExtensions
/// <param name="func">The delegate to execute as part of command execution.</param>
/// <returns>A command configurator that can be used to configure the command further.</returns>
public static ICommandConfigurator AddDelegate<TSettings>(
this IConfigurator<TSettings> configurator,
this IConfigurator<TSettings>? configurator,
string name,
Func<CommandContext, int> func)
where TSettings : CommandSettings
{
if (typeof(TSettings).IsAbstract)
{
AddDelegate(configurator as IConfigurator<EmptyCommandSettings>, name, func);
}
if (configurator == null)
{
throw new ArgumentNullException(nameof(configurator));
@@ -367,11 +391,11 @@ public static class ConfiguratorExtensions
/// <param name="configurator">The configurator.</param>
/// <param name="exceptionHandler">The Action that handles the exception.</param>
/// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator SetExceptionHandler(this IConfigurator configurator, Action<Exception> exceptionHandler)
public static IConfigurator SetExceptionHandler(this IConfigurator configurator, Action<Exception, ITypeResolver?> exceptionHandler)
{
return configurator.SetExceptionHandler(ex =>
return configurator.SetExceptionHandler((ex, resolver) =>
{
exceptionHandler(ex);
exceptionHandler(ex, resolver);
return -1;
});
}
@@ -382,7 +406,7 @@ public static class ConfiguratorExtensions
/// <param name="configurator">The configurator.</param>
/// <param name="exceptionHandler">The Action that handles the exception.</param>
/// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator SetExceptionHandler(this IConfigurator configurator, Func<Exception, int>? exceptionHandler)
public static IConfigurator SetExceptionHandler(this IConfigurator configurator, Func<Exception, ITypeResolver?, int>? exceptionHandler)
{
if (configurator == null)
{

View File

@@ -1,5 +1,3 @@
using Spectre.Console.Cli.Resources;
namespace Spectre.Console.Cli.Help;
/// <summary>
@@ -10,7 +8,8 @@ namespace Spectre.Console.Cli.Help;
/// </remarks>
public class HelpProvider : IHelpProvider
{
private HelpProviderResources resources;
private readonly HelpProviderResources resources;
private readonly HelpProviderStyle? helpStyles;
/// <summary>
/// Gets a value indicating how many examples from direct children to show in the help text.
@@ -27,6 +26,14 @@ public class HelpProvider : IHelpProvider
/// </summary>
protected virtual bool TrimTrailingPeriod { get; }
/// <summary>
/// Gets a value indicating whether to emit the markup styles, inline, when rendering the help text.
/// </summary>
/// <remarks>
/// Useful for unit testing different styling of the same help text.
/// </remarks>
protected virtual bool RenderMarkupInline { get; } = false;
private sealed class HelpArgument
{
public string Name { get; }
@@ -34,7 +41,7 @@ public class HelpProvider : IHelpProvider
public bool Required { get; }
public string? Description { get; }
public HelpArgument(string name, int position, bool required, string? description)
private HelpArgument(string name, int position, bool required, string? description)
{
Name = name;
Position = position;
@@ -61,7 +68,7 @@ public class HelpProvider : IHelpProvider
public string? Description { get; }
public object? DefaultValue { get; }
public HelpOption(string? @short, string? @long, string? @value, bool? valueIsOptional, string? description, object? defaultValue)
private HelpOption(string? @short, string? @long, string? @value, bool? valueIsOptional, string? description, object? defaultValue)
{
Short = @short;
Long = @long;
@@ -71,18 +78,28 @@ public class HelpProvider : IHelpProvider
DefaultValue = defaultValue;
}
public static IReadOnlyList<HelpOption> Get(ICommandInfo? command, HelpProviderResources resources)
public static IReadOnlyList<HelpOption> Get(
ICommandModel model,
ICommandInfo? command,
HelpProviderResources resources)
{
var parameters = new List<HelpOption>();
parameters.Add(new HelpOption("h", "help", null, null, resources.PrintHelpDescription, null));
var parameters = new List<HelpOption>
{
new HelpOption("h", "help", null, null, resources.PrintHelpDescription, null),
};
// Version information applies to the entire application
// Include the "-v" option in the help when at the root of the command line application
// Don't allow the "-v" option if users have specified one or more sub-commands
if ((command == null || command?.Parent == null) && !(command?.IsBranch ?? false))
if ((command?.Parent == null) && !(command?.IsBranch ?? false))
{
// Only show the version command if there is an
// application version set.
if (model.ApplicationVersion != null)
{
parameters.Add(new HelpOption("v", "version", null, null, resources.PrintVersionDescription, null));
}
}
parameters.AddRange(command?.Parameters.OfType<ICommandOption>().Where(o => !o.IsHidden).Select(o =>
new HelpOption(
@@ -104,6 +121,10 @@ public class HelpProvider : IHelpProvider
this.MaximumIndirectExamples = settings.MaximumIndirectExamples;
this.TrimTrailingPeriod = settings.TrimTrailingPeriod;
// Don't provide a default style if HelpProviderStyles is null,
// as the user will have explicitly done this to output unstyled help text
this.helpStyles = settings.HelpProviderStyles;
resources = new HelpProviderResources(settings.Culture);
}
@@ -148,8 +169,8 @@ public class HelpProvider : IHelpProvider
yield break;
}
var composer = new Composer();
composer.Style("yellow", $"{resources.Description}:").LineBreak();
var composer = NewComposer();
composer.Style(helpStyles?.Description?.Header ?? Style.Plain, $"{resources.Description}:").LineBreak();
composer.Text(command.Description).LineBreak();
yield return composer.LineBreak();
}
@@ -162,16 +183,16 @@ public class HelpProvider : IHelpProvider
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetUsage(ICommandModel model, ICommandInfo? command)
{
var composer = new Composer();
composer.Style("yellow", $"{resources.Usage}:").LineBreak();
var composer = NewComposer();
composer.Style(helpStyles?.Usage?.Header ?? Style.Plain, $"{resources.Usage}:").LineBreak();
composer.Tab().Text(model.ApplicationName);
var parameters = new List<string>();
var parameters = new List<Composer>();
if (command == null)
{
parameters.Add($"[grey][[{resources.Options}]][/]");
parameters.Add($"[aqua]<{resources.Command}>[/]");
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Options ?? Style.Plain, $"[{resources.Options}]"));
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"<{resources.Command}>"));
}
else
{
@@ -183,11 +204,11 @@ public class HelpProvider : IHelpProvider
{
if (isCurrent)
{
parameters.Add($"[underline]{current.Name.EscapeMarkup()}[/]");
parameters.Add(NewComposer().Style(helpStyles?.Usage?.CurrentCommand ?? Style.Plain, $"{current.Name}"));
}
else
{
parameters.Add($"{current.Name.EscapeMarkup()}");
parameters.Add(NewComposer().Text(current.Name));
}
}
@@ -198,7 +219,7 @@ public class HelpProvider : IHelpProvider
foreach (var argument in current.Parameters.OfType<ICommandArgument>()
.Where(a => a.Required).OrderBy(a => a.Position).ToArray())
{
parameters.Add($"[aqua]<{argument.Value.EscapeMarkup()}>[/]");
parameters.Add(NewComposer().Style(helpStyles?.Usage?.RequiredArgument ?? Style.Plain, $"<{argument.Value}>"));
}
}
@@ -207,27 +228,27 @@ public class HelpProvider : IHelpProvider
{
foreach (var optionalArgument in optionalArguments)
{
parameters.Add($"[silver][[{optionalArgument.Value.EscapeMarkup()}]][/]");
parameters.Add(NewComposer().Style(helpStyles?.Usage?.OptionalArgument ?? Style.Plain, $"[{optionalArgument.Value}]"));
}
}
}
if (isCurrent)
{
parameters.Add($"[grey][[{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($"[aqua]<{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($"[aqua][[{resources.Command}]][/]");
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]"));
}
else if (command.IsDefaultCommand)
{
@@ -237,7 +258,7 @@ public class HelpProvider : IHelpProvider
{
// Commands other than the default are present
// So make these optional in the usage statement
parameters.Add($"[aqua][[{resources.Command}]][/]");
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]"));
}
}
}
@@ -245,10 +266,7 @@ public class HelpProvider : IHelpProvider
composer.Join(" ", parameters);
composer.LineBreak();
return new[]
{
composer,
};
return new[] { composer };
}
/// <summary>
@@ -302,14 +320,14 @@ public class HelpProvider : IHelpProvider
if (Math.Min(maxExamples, examples.Count) > 0)
{
var composer = new Composer();
var composer = NewComposer();
composer.LineBreak();
composer.Style("yellow", $"{resources.Examples}:").LineBreak();
composer.Style(helpStyles?.Examples?.Header ?? Style.Plain, $"{resources.Examples}:").LineBreak();
for (var index = 0; index < Math.Min(maxExamples, examples.Count); index++)
{
var args = string.Join(" ", examples[index]);
composer.Tab().Text(model.ApplicationName).Space().Style("grey", args);
composer.Tab().Text(model.ApplicationName).Space().Style(helpStyles?.Examples?.Arguments ?? Style.Plain, args);
composer.LineBreak();
}
@@ -335,9 +353,7 @@ public class HelpProvider : IHelpProvider
var result = new List<IRenderable>
{
new Markup(Environment.NewLine),
new Markup($"[yellow]{resources.Arguments}:[/]"),
new Markup(Environment.NewLine),
NewComposer().LineBreak().Style(helpStyles?.Arguments?.Header ?? Style.Plain, $"{resources.Arguments}:").LineBreak(),
};
var grid = new Grid();
@@ -347,15 +363,15 @@ public class HelpProvider : IHelpProvider
foreach (var argument in arguments.Where(x => x.Required).OrderBy(x => x.Position))
{
grid.AddRow(
$"[silver]<{argument.Name.EscapeMarkup()}>[/]",
argument.Description?.TrimEnd('.') ?? " ");
NewComposer().Style(helpStyles?.Arguments?.RequiredArgument ?? Style.Plain, $"<{argument.Name}>"),
NewComposer().Text(argument.Description?.TrimEnd('.') ?? " "));
}
foreach (var argument in arguments.Where(x => !x.Required).OrderBy(x => x.Position))
{
grid.AddRow(
$"[grey][[{argument.Name.EscapeMarkup()}]][/]",
argument.Description?.TrimEnd('.') ?? " ");
NewComposer().Style(helpStyles?.Arguments?.OptionalArgument ?? Style.Plain, $"[{argument.Name}]"),
NewComposer().Text(argument.Description?.TrimEnd('.') ?? " "));
}
result.Add(grid);
@@ -372,7 +388,7 @@ public class HelpProvider : IHelpProvider
public virtual IEnumerable<IRenderable> GetOptions(ICommandModel model, ICommandInfo? command)
{
// Collect all options into a single structure.
var parameters = HelpOption.Get(command, resources);
var parameters = HelpOption.Get(model, command, resources);
if (parameters.Count == 0)
{
return Array.Empty<IRenderable>();
@@ -380,9 +396,7 @@ public class HelpProvider : IHelpProvider
var result = new List<IRenderable>
{
new Markup(Environment.NewLine),
new Markup($"[yellow]{resources.Options}:[/]"),
new Markup(Environment.NewLine),
NewComposer().LineBreak().Style(helpStyles?.Options?.Header ?? Style.Plain, $"{resources.Options}:").LineBreak(),
};
var helpOptions = parameters.ToArray();
@@ -397,71 +411,24 @@ public class HelpProvider : IHelpProvider
grid.AddColumn(new GridColumn { Padding = new Padding(0, 0) });
static string GetOptionParts(HelpOption option)
{
var builder = new StringBuilder();
if (option.Short != null)
{
builder.Append('-').Append(option.Short.EscapeMarkup());
if (option.Long != null)
{
builder.Append(", ");
}
}
else
{
builder.Append(" ");
if (option.Long != null)
{
builder.Append(" ");
}
}
if (option.Long != null)
{
builder.Append("--").Append(option.Long.EscapeMarkup());
}
if (option.Value != null)
{
builder.Append(' ');
if (option.ValueIsOptional ?? false)
{
builder.Append("[grey][[").Append(option.Value.EscapeMarkup()).Append("]][/]");
}
else
{
builder.Append("[silver]<").Append(option.Value.EscapeMarkup()).Append(">[/]");
}
}
return builder.ToString();
}
if (defaultValueColumn)
{
grid.AddRow(" ", $"[lime]{resources.Default}[/]", " ");
grid.AddRow(
NewComposer().Space(),
NewComposer().Style(helpStyles?.Options?.DefaultValueHeader ?? Style.Plain, resources.Default),
NewComposer().Space());
}
foreach (var option in helpOptions)
{
var columns = new List<string> { GetOptionParts(option) };
var columns = new List<IRenderable>() { GetOptionParts(option) };
if (defaultValueColumn)
{
static string Bold(object obj) => $"[bold]{obj.ToString().EscapeMarkup()}[/]";
var defaultValue = option.DefaultValue switch
{
null => " ",
"" => " ",
Array { Length: 0 } => " ",
Array array => string.Join(", ", array.Cast<object>().Select(Bold)),
_ => Bold(option.DefaultValue),
};
columns.Add(defaultValue);
columns.Add(GetDefaultValueForOption(option.DefaultValue));
}
columns.Add(option.Description?.TrimEnd('.') ?? " ");
columns.Add(NewComposer().Text(option.Description?.TrimEnd('.') ?? " "));
grid.AddRow(columns.ToArray());
}
@@ -492,9 +459,7 @@ public class HelpProvider : IHelpProvider
var result = new List<IRenderable>
{
new Markup(Environment.NewLine),
new Markup($"[yellow]{resources.Commands}:[/]"),
new Markup(Environment.NewLine),
NewComposer().LineBreak().Style(helpStyles?.Commands?.Header ?? Style.Plain, $"{resources.Commands}:").LineBreak(),
};
var grid = new Grid();
@@ -503,27 +468,27 @@ public class HelpProvider : IHelpProvider
foreach (var child in commands)
{
var arguments = new Composer();
arguments.Style("silver", child.Name.EscapeMarkup());
var arguments = NewComposer();
arguments.Style(helpStyles?.Commands?.ChildCommand ?? Style.Plain, child.Name);
arguments.Space();
foreach (var argument in HelpArgument.Get(child).Where(a => a.Required))
{
arguments.Style("silver", $"<{argument.Name.EscapeMarkup()}>");
arguments.Style(helpStyles?.Commands?.RequiredArgument ?? Style.Plain, $"<{argument.Name}>");
arguments.Space();
}
if (TrimTrailingPeriod)
{
grid.AddRow(
arguments.ToString().TrimEnd(),
child.Description?.TrimEnd('.') ?? " ");
NewComposer().Text(arguments.ToString().TrimEnd()),
NewComposer().Text(child.Description?.TrimEnd('.') ?? " "));
}
else
{
grid.AddRow(
arguments.ToString().TrimEnd(),
child.Description ?? " ");
NewComposer().Text(arguments.ToString().TrimEnd()),
NewComposer().Text(child.Description ?? " "));
}
}
@@ -542,4 +507,63 @@ public class HelpProvider : IHelpProvider
{
yield break;
}
private Composer NewComposer()
{
return new Composer(RenderMarkupInline);
}
private IRenderable GetOptionParts(HelpOption option)
{
var composer = NewComposer();
if (option.Short != null)
{
composer.Text("-").Text(option.Short);
if (option.Long != null)
{
composer.Text(", ");
}
}
else
{
composer.Text(" ");
if (option.Long != null)
{
composer.Text(" ");
}
}
if (option.Long != null)
{
composer.Text("--").Text(option.Long);
}
if (option.Value != null)
{
composer.Text(" ");
if (option.ValueIsOptional ?? false)
{
composer.Style(helpStyles?.Options?.OptionalOption ?? Style.Plain, $"[{option.Value}]");
}
else
{
composer.Style(helpStyles?.Options?.RequiredOption ?? Style.Plain, $"<{option.Value}>");
}
}
return composer;
}
private Composer GetDefaultValueForOption(object? defaultValue)
{
return defaultValue switch
{
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),
};
}
}

View File

@@ -0,0 +1,219 @@
namespace Spectre.Console.Cli.Help;
/// <summary>
/// Styles for the HelpProvider to use when rendering help text.
/// </summary>
public sealed class HelpProviderStyle
{
/// <summary>
/// Gets or sets the style for describing the purpose or details of a command.
/// </summary>
public DescriptionStyle? Description { get; set; }
/// <summary>
/// Gets or sets the style for specifying the usage format of a command.
/// </summary>
public UsageStyle? Usage { get; set; }
/// <summary>
/// Gets or sets the style for providing examples of command usage.
/// </summary>
public ExampleStyle? Examples { get; set; }
/// <summary>
/// Gets or sets the style for specifying arguments in a command.
/// </summary>
public ArgumentStyle? Arguments { get; set; }
/// <summary>
/// Gets or sets the style for specifying options or flags in a command.
/// </summary>
public OptionStyle? Options { get; set; }
/// <summary>
/// Gets or sets the style for specifying subcommands or nested commands.
/// </summary>
public CommandStyle? Commands { get; set; }
/// <summary>
/// Gets the default HelpProvider styles.
/// </summary>
public static HelpProviderStyle Default { get; } =
new HelpProviderStyle()
{
Description = new DescriptionStyle()
{
Header = "yellow",
},
Usage = new UsageStyle()
{
Header = "yellow",
CurrentCommand = "underline",
Command = "aqua",
Options = "grey",
RequiredArgument = "aqua",
OptionalArgument = "silver",
},
Examples = new ExampleStyle()
{
Header = "yellow",
Arguments = "grey",
},
Arguments = new ArgumentStyle()
{
Header = "yellow",
RequiredArgument = "silver",
OptionalArgument = "silver",
},
Commands = new CommandStyle()
{
Header = "yellow",
ChildCommand = "silver",
RequiredArgument = "silver",
},
Options = new OptionStyle()
{
Header = "yellow",
DefaultValueHeader = "lime",
DefaultValue = "bold",
RequiredOption = "silver",
OptionalOption = "grey",
},
};
}
/// <summary>
/// Defines styles for describing the purpose or details of a command.
/// </summary>
public sealed class DescriptionStyle
{
/// <summary>
/// Gets or sets the style for the header in the description.
/// </summary>
public Style? Header { get; set; }
}
/// <summary>
/// Defines styles for specifying the usage format of a command.
/// </summary>
public sealed class UsageStyle
{
/// <summary>
/// Gets or sets the style for the header in the usage.
/// </summary>
public Style? Header { get; set; }
/// <summary>
/// Gets or sets the style for the current command in the usage.
/// </summary>
public Style? CurrentCommand { get; set; }
/// <summary>
/// Gets or sets the style for general commands in the usage.
/// </summary>
public Style? Command { get; set; }
/// <summary>
/// Gets or sets the style for options in the usage.
/// </summary>
public Style? Options { get; set; }
/// <summary>
/// Gets or sets the style for required arguments in the usage.
/// </summary>
public Style? RequiredArgument { get; set; }
/// <summary>
/// Gets or sets the style for optional arguments in the usage.
/// </summary>
public Style? OptionalArgument { get; set; }
}
/// <summary>
/// Defines styles for providing examples of command usage.
/// </summary>
public sealed class ExampleStyle
{
/// <summary>
/// Gets or sets the style for the header in the examples.
/// </summary>
public Style? Header { get; set; }
/// <summary>
/// Gets or sets the style for arguments in the examples.
/// </summary>
public Style? Arguments { get; set; }
}
/// <summary>
/// Defines styles for specifying arguments in a command.
/// </summary>
public sealed class ArgumentStyle
{
/// <summary>
/// Gets or sets the style for the header in the arguments.
/// </summary>
public Style? Header { get; set; }
/// <summary>
/// Gets or sets the style for required arguments.
/// </summary>
public Style? RequiredArgument { get; set; }
/// <summary>
/// Gets or sets the style for optional arguments.
/// </summary>
public Style? OptionalArgument { get; set; }
}
/// <summary>
/// Defines styles for specifying subcommands or nested commands.
/// </summary>
public sealed class CommandStyle
{
/// <summary>
/// Gets or sets the style for the header in the command section.
/// </summary>
public Style? Header { get; set; }
/// <summary>
/// Gets or sets the style for child commands in the command section.
/// </summary>
public Style? ChildCommand { get; set; }
/// <summary>
/// Gets or sets the style for required arguments in the command section.
/// </summary>
public Style? RequiredArgument { get; set; }
}
/// <summary>
/// Defines styles for specifying options or flags in a command.
/// </summary>
public sealed class OptionStyle
{
/// <summary>
/// Gets or sets the style for the header in the options.
/// </summary>
public Style? Header { get; set; }
/// <summary>
/// Gets or sets the style for the header of default values in the options.
/// </summary>
public Style? DefaultValueHeader { get; set; }
/// <summary>
/// Gets or sets the style for default values in the options.
/// </summary>
public Style? DefaultValue { get; set; }
/// <summary>
/// Gets or sets the style for required options.
/// </summary>
public Style? RequiredOption { get; set; }
/// <summary>
/// Gets or sets the style for optional options.
/// </summary>
public Style? OptionalOption { get; set; }
}

View File

@@ -9,4 +9,9 @@ public interface ICommandModel : ICommandContainer
/// Gets the name of the application.
/// </summary>
string ApplicationName { get; }
/// <summary>
/// Gets the version of the application.
/// </summary>
string? ApplicationVersion { get; }
}

View File

@@ -41,6 +41,11 @@ public interface ICommandAppSettings
/// </summary>
bool TrimTrailingPeriod { get; set; }
/// <summary>
/// Gets or sets the styles to used when rendering the help text.
/// </summary>
HelpProviderStyle? HelpProviderStyles { get; set; }
/// <summary>
/// Gets or sets the <see cref="IAnsiConsole"/>.
/// </summary>
@@ -50,6 +55,7 @@ public interface ICommandAppSettings
/// Gets or sets the <see cref="ICommandInterceptor"/> used
/// to intercept settings before it's being sent to the command.
/// </summary>
[Obsolete("Register the interceptor with the ITypeRegistrar.")]
ICommandInterceptor? Interceptor { get; set; }
/// <summary>
@@ -90,6 +96,8 @@ public interface ICommandAppSettings
/// <summary>
/// Gets or sets a handler for Exceptions.
/// <para>This handler will not be called, if <see cref="PropagateExceptions"/> is set to <c>true</c>.</para>
/// The <see cref="ITypeResolver"/> argument will only be not-null, when the exception occurs during execution of
/// a command. I.e. only when the resolver is available.
/// </summary>
public Func<Exception, int>? ExceptionHandler { get; set; }
public Func<Exception, ITypeResolver?, int>? ExceptionHandler { get; set; }
}

View File

@@ -12,5 +12,25 @@ public interface ICommandInterceptor
/// </summary>
/// <param name="context">The intercepted <see cref="CommandContext"/>.</param>
/// <param name="settings">The intercepted <see cref="CommandSettings"/>.</param>
void Intercept(CommandContext context, CommandSettings settings);
void Intercept(CommandContext context, CommandSettings settings)
#if NETSTANDARD2_0
;
#else
{
}
#endif
/// <summary>
/// Intercepts a command result before it's passed as the result.
/// </summary>
/// <param name="context">The intercepted <see cref="CommandContext"/>.</param>
/// <param name="settings">The intercepted <see cref="CommandSettings"/>.</param>
/// <param name="result">The result from the command execution.</param>
void InterceptResult(CommandContext context, CommandSettings settings, ref int result)
#if NETSTANDARD2_0
;
#else
{
}
#endif
}

View File

@@ -12,6 +12,7 @@ public interface IRemainingArguments
/// <summary>
/// Gets the raw, non-parsed remaining arguments.
/// This is normally everything after the `--` delimiter.
/// </summary>
IReadOnlyList<string> Raw { get; }
}

View File

@@ -17,7 +17,7 @@ internal sealed class CommandExecutor
throw new ArgumentNullException(nameof(configuration));
}
args ??= new List<string>();
var arguments = args.ToSafeReadOnlyList();
_registrar.RegisterInstance(typeof(IConfiguration), configuration);
_registrar.RegisterLazy(typeof(IAnsiConsole), () => configuration.Settings.Console.GetConsole());
@@ -27,17 +27,30 @@ internal sealed class CommandExecutor
_registrar.RegisterInstance(typeof(CommandModel), model);
_registrar.RegisterDependencies(model);
// No default command?
if (model.DefaultCommand == null)
{
// Got at least one argument?
var firstArgument = arguments.FirstOrDefault();
if (firstArgument != null)
{
// Asking for version? Kind of a hack, but it's alright.
// We should probably make this a bit better in the future.
if (args.Contains("-v") || args.Contains("--version"))
if (firstArgument.Equals("--version", StringComparison.OrdinalIgnoreCase) ||
firstArgument.Equals("-v", StringComparison.OrdinalIgnoreCase))
{
if (configuration.Settings.ApplicationVersion != null)
{
var console = configuration.Settings.Console.GetConsole();
console.WriteLine(ResolveApplicationVersion(configuration));
console.MarkupLine(configuration.Settings.ApplicationVersion);
return 0;
}
}
}
}
// Parse and map the model against the arguments.
var parsedResult = ParseCommandLineArguments(model, configuration.Settings, args);
var parsedResult = ParseCommandLineArguments(model, configuration.Settings, arguments);
// Register the arguments with the container.
_registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult);
@@ -69,7 +82,7 @@ internal sealed class CommandExecutor
}
// Is this the default and is it called without arguments when there are required arguments?
if (leaf.Command.IsDefaultCommand && args.Count() == 0 && leaf.Command.Parameters.Any(p => p.Required))
if (leaf.Command.IsDefaultCommand && arguments.Count == 0 && leaf.Command.Parameters.Any(p => p.Required))
{
// Display help for default command.
configuration.Settings.Console.SafeRender(helpProvider.Write(model, leaf.Command));
@@ -77,15 +90,18 @@ internal sealed class CommandExecutor
}
// Create the content.
var context = new CommandContext(parsedResult.Remaining, leaf.Command.Name, leaf.Command.Data);
var context = new CommandContext(
arguments,
parsedResult.Remaining,
leaf.Command.Name,
leaf.Command.Data);
// Execute the command tree.
return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false);
}
}
#pragma warning disable CS8603 // Possible null reference return.
private CommandTreeParserResult ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IEnumerable<string> args)
private CommandTreeParserResult ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IReadOnlyList<string> args)
{
var parser = new CommandTreeParser(model, settings.CaseSensitivity, settings.ParsingMode, settings.ConvertFlagsToRemainingArguments);
@@ -93,7 +109,7 @@ internal sealed class CommandExecutor
var tokenizerResult = CommandTreeTokenizer.Tokenize(args);
var parsedResult = parser.Parse(parserContext, tokenizerResult);
var lastParsedLeaf = parsedResult?.Tree?.GetLeafCommand();
var lastParsedLeaf = parsedResult.Tree?.GetLeafCommand();
var lastParsedCommand = lastParsedLeaf?.Command;
if (lastParsedLeaf != null && lastParsedCommand != null &&
lastParsedCommand.IsBranch && !lastParsedLeaf.ShowHelp &&
@@ -112,25 +128,31 @@ internal sealed class CommandExecutor
return parsedResult;
}
#pragma warning restore CS8603 // Possible null reference return.
private static string ResolveApplicationVersion(IConfiguration configuration)
{
return
configuration.Settings.ApplicationVersion ?? // potential override
VersionHelper.GetVersion(Assembly.GetEntryAssembly());
}
private static Task<int> Execute(
private static async Task<int> Execute(
CommandTree leaf,
CommandTree tree,
CommandContext context,
ITypeResolver resolver,
IConfiguration configuration)
{
try
{
// Bind the command tree against the settings.
var settings = CommandBinder.Bind(tree, leaf.Command.SettingsType, resolver);
configuration.Settings.Interceptor?.Intercept(context, settings);
var interceptors =
((IEnumerable<ICommandInterceptor>?)resolver.Resolve(typeof(IEnumerable<ICommandInterceptor>))
?? Array.Empty<ICommandInterceptor>()).ToList();
#pragma warning disable CS0618 // Type or member is obsolete
if (configuration.Settings.Interceptor != null)
{
interceptors.Add(configuration.Settings.Interceptor);
}
#pragma warning restore CS0618 // Type or member is obsolete
foreach (var interceptor in interceptors)
{
interceptor.Intercept(context, settings);
}
// Create and validate the command.
var command = leaf.CreateCommand(resolver);
@@ -141,6 +163,17 @@ internal sealed class CommandExecutor
}
// Execute the command.
return command.Execute(context, settings);
var result = await command.Execute(context, settings);
foreach (var interceptor in interceptors)
{
interceptor.InterceptResult(context, settings, ref result);
}
return result;
}
catch (Exception ex) when (configuration.Settings is { ExceptionHandler: not null, PropagateExceptions: false })
{
return configuration.Settings.ExceptionHandler(ex, resolver);
}
}
}

View File

@@ -84,6 +84,13 @@ internal sealed class XmlDocCommand : Command<XmlDocCommand.Settings>
node.SetNullableAttribute("Settings", command.SettingsType?.FullName);
if (!string.IsNullOrWhiteSpace(command.Description))
{
var descriptionNode = doc.CreateElement("Description");
descriptionNode.InnerText = command.Description;
node.AppendChild(descriptionNode);
}
// Parameters
if (command.Parameters.Count > 0)
{
@@ -103,6 +110,27 @@ internal sealed class XmlDocCommand : Command<XmlDocCommand.Settings>
node.AppendChild(CreateCommandNode(doc, childCommand));
}
// Examples
if (command.Examples.Count > 0)
{
var exampleRootNode = doc.CreateElement("Examples");
foreach (var example in command.Examples.SelectMany(static x => x))
{
var exampleNode = CreateExampleNode(doc, example);
exampleRootNode.AppendChild(exampleNode);
}
node.AppendChild(exampleRootNode);
}
return node;
}
private static XmlNode CreateExampleNode(XmlDocument document, string example)
{
var node = document.CreateElement("Example");
node.SetAttribute("commandLine", example);
return node;
}

View File

@@ -4,22 +4,43 @@ internal sealed class Composer : IRenderable
{
private readonly StringBuilder _content;
/// <summary>
/// Whether to emit the markup styles, inline, when rendering the content.
/// </summary>
private readonly bool _renderMarkup = false;
public Composer()
{
_content = new StringBuilder();
}
public Composer(bool renderMarkup)
: this()
{
_renderMarkup = renderMarkup;
}
public Composer Text(string text)
{
_content.Append(text);
return this;
}
public Composer Style(Style style, string text)
{
_content.Append('[').Append(style.ToMarkup()).Append(']');
_content.Append(text.EscapeMarkup());
_content.Append("[/]");
return this;
}
public Composer Style(string style, string text)
{
_content.Append('[').Append(style).Append(']');
_content.Append(text.EscapeMarkup());
_content.Append("[/]");
return this;
}
@@ -28,6 +49,7 @@ internal sealed class Composer : IRenderable
_content.Append('[').Append(style).Append(']');
action(this);
_content.Append("[/]");
return this;
}
@@ -72,26 +94,47 @@ internal sealed class Composer : IRenderable
return this;
}
public Composer Join(string separator, IEnumerable<string> composers)
public Composer Join(string separator, IEnumerable<Composer> composers)
{
if (composers != null)
{
Space();
Text(string.Join(separator, composers));
foreach (var composer in composers)
{
if (_content.ToString().Length > 0)
{
Text(separator);
}
Text(composer.ToString());
}
}
return this;
}
public Measurement Measure(RenderOptions options, int maxWidth)
{
if (_renderMarkup)
{
return ((IRenderable)new Paragraph(_content.ToString())).Measure(options, maxWidth);
}
else
{
return ((IRenderable)new Markup(_content.ToString())).Measure(options, maxWidth);
}
}
public IEnumerable<Segment> Render(RenderOptions options, int maxWidth)
{
if (_renderMarkup)
{
return ((IRenderable)new Paragraph(_content.ToString())).Render(options, maxWidth);
}
else
{
return ((IRenderable)new Markup(_content.ToString())).Render(options, maxWidth);
}
}
public override string ToString()
{

View File

@@ -1,4 +1,4 @@
namespace Spectre.Console.Cli;
namespace Spectre.Console.Cli;
internal sealed class BranchConfigurator : IBranchConfigurator
{

View File

@@ -8,19 +8,21 @@ internal sealed class CommandAppSettings : ICommandAppSettings
public int MaximumIndirectExamples { get; set; }
public bool ShowOptionDefaultValues { get; set; }
public IAnsiConsole? Console { get; set; }
[Obsolete("Register the interceptor with the ITypeRegistrar.")]
public ICommandInterceptor? Interceptor { get; set; }
public ITypeRegistrarFrontend Registrar { get; set; }
public CaseSensitivity CaseSensitivity { get; set; }
public bool PropagateExceptions { get; set; }
public bool ValidateExamples { get; set; }
public bool TrimTrailingPeriod { get; set; } = true;
public bool TrimTrailingPeriod { get; set; }
public HelpProviderStyle? HelpProviderStyles { get; set; }
public bool StrictParsing { get; set; }
public bool ConvertFlagsToRemainingArguments { get; set; } = false;
public bool ConvertFlagsToRemainingArguments { get; set; }
public ParsingMode ParsingMode =>
StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed;
public Func<Exception, int>? ExceptionHandler { get; set; }
public Func<Exception, ITypeResolver?, int>? ExceptionHandler { get; set; }
public CommandAppSettings(ITypeRegistrar registrar)
{
@@ -28,6 +30,9 @@ internal sealed class CommandAppSettings : ICommandAppSettings
CaseSensitivity = CaseSensitivity.All;
ShowOptionDefaultValues = true;
MaximumIndirectExamples = 5;
TrimTrailingPeriod = true;
HelpProviderStyles = HelpProviderStyle.Default;
ConvertFlagsToRemainingArguments = false;
}
public bool IsTrue(Func<CommandAppSettings, bool> func, string environmentVariableName)

View File

@@ -124,7 +124,7 @@ internal static class TemplateParser
foreach (var character in token.Value)
{
if (!char.IsLetterOrDigit(character) &&
character != '=' && character != '-' && character != '_')
character != '=' && character != '-' && character != '_' && character != '|')
{
throw CommandTemplateException.InvalidCharacterInValueName(template, token, character);
}

View File

@@ -0,0 +1,14 @@
namespace Spectre.Console.Cli;
internal static class EnumerableExtensions
{
public static IReadOnlyList<T> ToSafeReadOnlyList<T>(this IEnumerable<T> source)
{
return source switch
{
null => new List<T>(),
IReadOnlyList<T> list => list,
_ => source.ToList(),
};
}
}

View File

@@ -17,6 +17,12 @@ internal static class TypeRegistrarExtensions
throw new InvalidOperationException("Command setting type cannot be null.");
}
if (command.SettingsType is { IsAbstract: false, IsClass: true })
{
// Register the settings type
registrar?.Register(command.SettingsType, command.SettingsType);
}
if (command.CommandType != null)
{
registrar?.Register(command.CommandType, command.CommandType);

View File

@@ -3,6 +3,7 @@ namespace Spectre.Console.Cli;
internal sealed class CommandModel : ICommandContainer, ICommandModel
{
public string? ApplicationName { get; }
public string? ApplicationVersion { get; }
public ParsingMode ParsingMode { get; }
public IList<CommandInfo> Commands { get; }
public IList<string[]> Examples { get; }
@@ -20,9 +21,10 @@ internal sealed class CommandModel : ICommandContainer, ICommandModel
IEnumerable<string[]> examples)
{
ApplicationName = settings.ApplicationName;
ApplicationVersion = settings.ApplicationVersion;
ParsingMode = settings.ParsingMode;
Commands = new List<CommandInfo>(commands ?? Array.Empty<CommandInfo>());
Examples = new List<string[]>(examples ?? Array.Empty<string[]>());
Commands = new List<CommandInfo>(commands);
Examples = new List<string[]>(examples);
}
/// <summary>

View File

@@ -302,11 +302,7 @@ internal class CommandTreeParser
var valueToken = stream.Peek();
if (valueToken?.TokenKind == CommandTreeToken.Kind.String)
{
var parseValue = true;
if (token.TokenKind == CommandTreeToken.Kind.ShortOption && token.IsGrouped)
{
parseValue = false;
}
bool parseValue = token is not { TokenKind: CommandTreeToken.Kind.ShortOption, IsGrouped: true };
if (context.State == State.Normal && parseValue)
{
@@ -333,7 +329,7 @@ internal class CommandTreeParser
{
value = stream.Consume(CommandTreeToken.Kind.String)?.Value;
context.AddRemainingArgument(token.Value, value);
context.AddRemainingArgument(token.Representation, value);
// Prevent the option and it's non-boolean value from being added to
// mapped parameters (otherwise an exception will be thrown later
@@ -364,14 +360,14 @@ internal class CommandTreeParser
// In relaxed parsing mode?
if (context.ParsingMode == ParsingMode.Relaxed)
{
context.AddRemainingArgument(token.Value, value);
context.AddRemainingArgument(token.Representation, value);
}
}
}
}
else
{
context.AddRemainingArgument(token.Value, parseValue ? valueToken.Value : null);
context.AddRemainingArgument(token.Representation, parseValue ? valueToken.Value : null);
}
}
else
@@ -379,7 +375,7 @@ internal class CommandTreeParser
if (parameter == null && // Only add tokens which have not been matched to a command parameter
(context.State == State.Remaining || context.ParsingMode == ParsingMode.Relaxed))
{
context.AddRemainingArgument(token.Value, null);
context.AddRemainingArgument(token.Representation, null);
}
}

View File

@@ -171,7 +171,7 @@ internal static class CommandTreeTokenizer
}
// Encountered a separator?
if (current == '=' || current == ':')
if (current is '=' or ':')
{
break;
}
@@ -184,7 +184,7 @@ internal static class CommandTreeTokenizer
var value = current.ToString(CultureInfo.InvariantCulture);
result.Add(result.Count == 0
? new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position, value, $"-{value}")
: new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position + result.Count, value, value));
: new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position + result.Count, value, $"-{value}"));
}
else if (result.Count == 0 && char.IsDigit(current))
{

View File

@@ -1,4 +1,4 @@
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net7.0;net6.0;netstandard2.0</TargetFrameworks>
<TargetFrameworks>net8.0;net7.0;net6.0</TargetFrameworks>
<Nullable>enable</Nullable>
<IsPackable>true</IsPackable>
<Description>A library that extends Spectre.Console with ImageSharp superpowers.</Description>
@@ -13,7 +13,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
</ItemGroup>
<ItemGroup>

View File

@@ -21,4 +21,11 @@ public sealed class CallbackCommandInterceptor : ICommandInterceptor
{
_callback(context, settings);
}
#if NETSTANDARD2_0
/// <inheritdoc/>
public void InterceptResult(CommandContext context, CommandSettings settings, ref int result)
{
}
#endif
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net7.0;net6.0;netstandard2.0</TargetFrameworks>
<TargetFrameworks>net8.0;net7.0;net6.0</TargetFrameworks>
<IsTestProject>false</IsTestProject>
<Nullable>enable</Nullable>
<IsPackable>true</IsPackable>

View File

@@ -19,6 +19,7 @@ public static partial class AnsiConsoleExtensions
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
var rawKey = await console.Input.ReadKeyAsync(true, cancellationToken).ConfigureAwait(false);
if (rawKey == null)
{
@@ -52,8 +53,12 @@ public static partial class AnsiConsoleExtensions
if (text.Length > 0)
{
text = text.Substring(0, text.Length - 1);
if (mask != null)
{
console.Write("\b \b");
}
}
continue;
}

View File

@@ -116,6 +116,43 @@ public static class BarChartExtensions
return chart;
}
/// <summary>
/// Sets the value formatter for the bar chart using culture info.
/// </summary>
/// <param name="chart">The bar chart.</param>
/// <param name="func">The value formatter function with culture info.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static BarChart UseValueFormatter(this BarChart chart, Func<double, CultureInfo, string>? func)
{
if (chart is null)
{
throw new ArgumentNullException(nameof(chart));
}
chart.ValueFormatter = func;
return chart;
}
/// <summary>
/// Sets the value formatter for the bar chart.
/// </summary>
/// <param name="chart">The bar chart.</param>
/// <param name="func">The value formatter to use.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static BarChart UseValueFormatter(this BarChart chart, Func<double, string>? func)
{
if (chart is null)
{
throw new ArgumentNullException(nameof(chart));
}
chart.ValueFormatter = func != null
? (value, _) => func(value)
: null;
return chart;
}
/// <summary>
/// Sets the width of the bar chart.
/// </summary>

View File

@@ -187,6 +187,13 @@ public static class StringExtensions
#endif
}
#if NETSTANDARD2_0
internal static bool Contains(this string target, string value, System.StringComparison comparisonType)
{
return target.IndexOf(value, comparisonType) != -1;
}
#endif
/// <summary>
/// "Masks" every character in a string.
/// </summary>
@@ -195,18 +202,105 @@ public static class StringExtensions
/// <returns>Masked string.</returns>
public static string Mask(this string value, char? mask)
{
var output = string.Empty;
if (mask is null)
{
return output;
return string.Empty;
}
foreach (var c in value)
return new string(mask.Value, value.Length);
}
/// <summary>
/// Highlights the first text match in provided value.
/// </summary>
/// <param name="value">Input value.</param>
/// <param name="searchText">Text to search for.</param>
/// <param name="highlightStyle">The style to apply to the matched text.</param>
/// <returns>Markup of input with the first matched text highlighted.</returns>
internal static string Highlight(this string value, string searchText, Style? highlightStyle)
{
output += mask;
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
return output;
if (searchText is null)
{
throw new ArgumentNullException(nameof(searchText));
}
if (highlightStyle is null)
{
throw new ArgumentNullException(nameof(highlightStyle));
}
if (searchText.Length == 0)
{
return value;
}
var foundSearchPattern = false;
var builder = new StringBuilder();
using var tokenizer = new MarkupTokenizer(value);
while (tokenizer.MoveNext())
{
var token = tokenizer.Current!;
switch (token.Kind)
{
case MarkupTokenKind.Text:
{
var tokenValue = token.Value;
if (tokenValue.Length == 0)
{
break;
}
if (foundSearchPattern)
{
builder.Append(tokenValue);
break;
}
var index = tokenValue.IndexOf(searchText, StringComparison.OrdinalIgnoreCase);
if (index == -1)
{
builder.Append(tokenValue);
break;
}
foundSearchPattern = true;
var before = tokenValue.Substring(0, index);
var match = tokenValue.Substring(index, searchText.Length);
var after = tokenValue.Substring(index + searchText.Length);
builder
.Append(before)
.AppendWithStyle(highlightStyle, match)
.Append(after);
break;
}
case MarkupTokenKind.Open:
{
builder.Append("[" + token.Value + "]");
break;
}
case MarkupTokenKind.Close:
{
builder.Append("[/]");
break;
}
default:
{
throw new InvalidOperationException("Unknown markup token kind.");
}
}
}
return builder.ToString();
}
}

View File

@@ -1,4 +1,4 @@
namespace Spectre.Console;
namespace Spectre.Console;
/// <summary>
/// Represents horizontal alignment.

View File

@@ -31,12 +31,11 @@ internal static class EnumerableExtensions
}
public static int IndexOf<T>(this IEnumerable<T> source, T item)
where T : class
{
var index = 0;
foreach (var candidate in source)
{
if (candidate == item)
if (Equals(candidate, item))
{
return index;
}

View File

@@ -26,7 +26,7 @@ internal static class MarkupParser
if (token.Kind == MarkupTokenKind.Open)
{
var parsedStyle = StyleParser.Parse(token.Value);
var parsedStyle = string.IsNullOrEmpty(token.Value) ? Style.Plain : StyleParser.Parse(token.Value);
stack.Push(parsedStyle);
}
else if (token.Kind == MarkupTokenKind.Close)

View File

@@ -49,7 +49,7 @@ internal sealed class LiveRenderable : Renderable
}
var linesToMoveUp = _shape.Value.Height - 1;
return new ControlCode("\r" + (EL(2) + CUU(1)).Repeat(linesToMoveUp));
return new ControlCode("\r" + CUU(linesToMoveUp));
}
}

View File

@@ -14,7 +14,16 @@ public sealed class ProgressContext
/// <summary>
/// Gets a value indicating whether or not all started tasks have completed.
/// </summary>
public bool IsFinished => _tasks.Where(x => x.IsStarted).All(task => task.IsFinished);
public bool IsFinished
{
get
{
lock (_taskLock)
{
return _tasks.Where(x => x.IsStarted).All(task => task.IsFinished);
}
}
}
internal ProgressContext(IAnsiConsole console, ProgressRenderer renderer)
{
@@ -33,11 +42,68 @@ public sealed class ProgressContext
/// <returns>The newly created task.</returns>
public ProgressTask AddTask(string description, bool autoStart = true, double maxValue = 100)
{
return AddTask(description, new ProgressTaskSettings
lock (_taskLock)
{
AutoStart = autoStart,
MaxValue = maxValue,
});
var settings = new ProgressTaskSettings { AutoStart = autoStart, MaxValue = maxValue, };
return AddTaskAtInternal(description, settings, _tasks.Count);
}
}
/// <summary>
/// Adds a task.
/// </summary>
/// <param name="description">The task description.</param>
/// <param name="index">The index at which the task should be inserted.</param>
/// <param name="autoStart">Whether or not the task should start immediately.</param>
/// <param name="maxValue">The task's max value.</param>
/// <returns>The newly created task.</returns>
public ProgressTask AddTaskAt(string description, int index, bool autoStart = true, double maxValue = 100)
{
lock (_taskLock)
{
var settings = new ProgressTaskSettings { AutoStart = autoStart, MaxValue = maxValue, };
return AddTaskAtInternal(description, settings, index);
}
}
/// <summary>
/// Adds a task before the reference task.
/// </summary>
/// <param name="description">The task description.</param>
/// <param name="referenceProgressTask">The reference task to add before.</param>
/// <param name="autoStart">Whether or not the task should start immediately.</param>
/// <param name="maxValue">The task's max value.</param>
/// <returns>The newly created task.</returns>
public ProgressTask AddTaskBefore(string description, ProgressTask referenceProgressTask, bool autoStart = true, double maxValue = 100)
{
lock (_taskLock)
{
var settings = new ProgressTaskSettings { AutoStart = autoStart, MaxValue = maxValue, };
var indexOfReference = _tasks.IndexOf(referenceProgressTask);
return AddTaskAtInternal(description, settings, indexOfReference);
}
}
/// <summary>
/// Adds a task after the reference task.
/// </summary>
/// <param name="description">The task description.</param>
/// <param name="referenceProgressTask">The reference task to add after.</param>
/// <param name="autoStart">Whether or not the task should start immediately.</param>
/// <param name="maxValue">The task's max value.</param>
/// <returns>The newly created task.</returns>
public ProgressTask AddTaskAfter(string description, ProgressTask referenceProgressTask, bool autoStart = true, double maxValue = 100)
{
lock (_taskLock)
{
var settings = new ProgressTaskSettings { AutoStart = autoStart, MaxValue = maxValue, };
var indexOfReference = _tasks.IndexOf(referenceProgressTask);
return AddTaskAtInternal(description, settings, indexOfReference + 1);
}
}
/// <summary>
@@ -48,18 +114,58 @@ public sealed class ProgressContext
/// <returns>The newly created task.</returns>
public ProgressTask AddTask(string description, ProgressTaskSettings settings)
{
if (settings is null)
{
throw new ArgumentNullException(nameof(settings));
}
lock (_taskLock)
{
var task = new ProgressTask(_taskId++, description, settings.MaxValue, settings.AutoStart);
return AddTaskAtInternal(description, settings, _tasks.Count);
}
}
_tasks.Add(task);
/// <summary>
/// Adds a task at the specified index.
/// </summary>
/// <param name="description">The task description.</param>
/// <param name="settings">The task settings.</param>
/// <param name="index">The index at which the task should be inserted.</param>
/// <returns>The newly created task.</returns>
public ProgressTask AddTaskAt(string description, ProgressTaskSettings settings, int index)
{
lock (_taskLock)
{
return AddTaskAtInternal(description, settings, index);
}
}
return task;
/// <summary>
/// Adds a task before the reference task.
/// </summary>
/// <param name="description">The task description.</param>
/// <param name="settings">The task settings.</param>
/// <param name="referenceProgressTask">The reference task to add before.</param>
/// <returns>The newly created task.</returns>
public ProgressTask AddTaskBefore(string description, ProgressTaskSettings settings, ProgressTask referenceProgressTask)
{
lock (_taskLock)
{
var indexOfReference = _tasks.IndexOf(referenceProgressTask);
return AddTaskAtInternal(description, settings, indexOfReference);
}
}
/// <summary>
/// Adds a task after the reference task.
/// </summary>
/// <param name="description">The task description.</param>
/// <param name="settings">The task settings.</param>
/// <param name="referenceProgressTask">The reference task to add after.</param>
/// <returns>The newly created task.</returns>
public ProgressTask AddTaskAfter(string description, ProgressTaskSettings settings, ProgressTask referenceProgressTask)
{
lock (_taskLock)
{
var indexOfReference = _tasks.IndexOf(referenceProgressTask);
return AddTaskAtInternal(description, settings, indexOfReference + 1);
}
}
@@ -72,6 +178,20 @@ public sealed class ProgressContext
_console.Write(new ControlCode(string.Empty));
}
private ProgressTask AddTaskAtInternal(string description, ProgressTaskSettings settings, int position)
{
if (settings is null)
{
throw new ArgumentNullException(nameof(settings));
}
var task = new ProgressTask(_taskId++, description, settings.MaxValue, settings.AutoStart);
_tasks.Insert(position, task);
return task;
}
internal IReadOnlyList<ProgressTask> GetTasks()
{
lock (_taskLock)

View File

@@ -31,6 +31,9 @@ internal interface IListPromptStrategy<T>
/// <param name="scrollable">Whether or not the list is scrollable.</param>
/// <param name="cursorIndex">The cursor index.</param>
/// <param name="items">The visible items.</param>
/// <param name="skipUnselectableItems">A value indicating whether or not the prompt should skip unselectable items.</param>
/// <param name="searchText">The search text.</param>
/// <returns>A <see cref="IRenderable"/> representing the items.</returns>
public IRenderable Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem<T> Node)> items);
public IRenderable Render(IAnsiConsole console, bool scrollable, int cursorIndex,
IEnumerable<(int Index, ListPromptItem<T> Node)> items, bool skipUnselectableItems, string searchText);
}

View File

@@ -14,9 +14,12 @@ internal sealed class ListPrompt<T>
public async Task<ListPromptState<T>> Show(
ListPromptTree<T> tree,
CancellationToken cancellationToken,
int requestedPageSize = 15,
bool wrapAround = false)
SelectionMode selectionMode,
bool skipUnselectableItems,
bool searchEnabled,
int requestedPageSize,
bool wrapAround,
CancellationToken cancellationToken = default)
{
if (tree is null)
{
@@ -38,7 +41,7 @@ internal sealed class ListPrompt<T>
}
var nodes = tree.Traverse().ToList();
var state = new ListPromptState<T>(nodes, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize), wrapAround);
var state = new ListPromptState<T>(nodes, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize), wrapAround, selectionMode, skipUnselectableItems, searchEnabled);
var hook = new ListPromptRenderHook<T>(_console, () => BuildRenderable(state));
using (new RenderHookScope(_console, hook))
@@ -48,6 +51,7 @@ internal sealed class ListPrompt<T>
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
var rawKey = await _console.Input.ReadKeyAsync(true, cancellationToken).ConfigureAwait(false);
if (rawKey == null)
{
@@ -61,7 +65,7 @@ internal sealed class ListPrompt<T>
break;
}
if (state.Update(key.Key) || result == ListPromptInputResult.Refresh)
if (state.Update(key) || result == ListPromptInputResult.Refresh)
{
hook.Refresh();
}
@@ -89,10 +93,10 @@ internal sealed class ListPrompt<T>
skip = Math.Max(0, state.Index - middleOfList);
take = Math.Min(pageSize, state.ItemCount - skip);
if (state.ItemCount - state.Index < middleOfList)
if (take < pageSize)
{
// Pointer should be below the end of the list
var diff = middleOfList - (state.ItemCount - state.Index);
// Pointer should be below the middle of the (visual) list
var diff = pageSize - take;
skip -= diff;
take += diff;
cursorIndex = middleOfList + diff;
@@ -109,6 +113,8 @@ internal sealed class ListPrompt<T>
_console,
scrollable, cursorIndex,
state.Items.Skip(skip).Take(take)
.Select((node, index) => (index, node)));
.Select((node, index) => (index, node)),
state.SkipUnselectableItems,
state.SearchText);
}
}

View File

@@ -8,4 +8,5 @@ internal sealed class ListPromptConstants
public const string GroupSelectedCheckbox = "[[[grey]X[/]]]";
public const string InstructionsMarkup = "[grey](Press <space> to select, <enter> to accept)[/]";
public const string MoreChoicesMarkup = "[grey](Move up and down to reveal more choices)[/]";
public const string SearchPlaceholderMarkup = "[grey](Type to search)[/]";
}

View File

@@ -7,21 +7,106 @@ internal sealed class ListPromptState<T>
public int ItemCount => Items.Count;
public int PageSize { get; }
public bool WrapAround { get; }
public SelectionMode Mode { get; }
public bool SkipUnselectableItems { get; private set; }
public bool SearchEnabled { get; }
public IReadOnlyList<ListPromptItem<T>> Items { get; }
private readonly IReadOnlyList<int>? _leafIndexes;
public ListPromptItem<T> Current => Items[Index];
public string SearchText { get; private set; }
public ListPromptState(IReadOnlyList<ListPromptItem<T>> items, int pageSize, bool wrapAround)
public ListPromptState(IReadOnlyList<ListPromptItem<T>> items, int pageSize, bool wrapAround, SelectionMode mode, bool skipUnselectableItems, bool searchEnabled)
{
Index = 0;
Items = items;
PageSize = pageSize;
WrapAround = wrapAround;
Mode = mode;
SkipUnselectableItems = skipUnselectableItems;
SearchEnabled = searchEnabled;
SearchText = string.Empty;
if (SkipUnselectableItems && mode == SelectionMode.Leaf)
{
_leafIndexes =
Items
.Select((item, index) => new { item, index })
.Where(x => !x.item.IsGroup)
.Select(x => x.index)
.ToList()
.AsReadOnly();
Index = _leafIndexes.FirstOrDefault();
}
else
{
Index = 0;
}
}
public bool Update(ConsoleKey key)
public bool Update(ConsoleKeyInfo keyInfo)
{
var index = key switch
var index = Index;
if (SkipUnselectableItems && Mode == SelectionMode.Leaf)
{
Debug.Assert(_leafIndexes != null, nameof(_leafIndexes) + " != null");
var currentLeafIndex = _leafIndexes.IndexOf(index);
switch (keyInfo.Key)
{
case ConsoleKey.UpArrow:
if (currentLeafIndex > 0)
{
index = _leafIndexes[currentLeafIndex - 1];
}
else if (WrapAround)
{
index = _leafIndexes.LastOrDefault();
}
break;
case ConsoleKey.DownArrow:
if (currentLeafIndex < _leafIndexes.Count - 1)
{
index = _leafIndexes[currentLeafIndex + 1];
}
else if (WrapAround)
{
index = _leafIndexes.FirstOrDefault();
}
break;
case ConsoleKey.Home:
index = _leafIndexes.FirstOrDefault();
break;
case ConsoleKey.End:
index = _leafIndexes.LastOrDefault();
break;
case ConsoleKey.PageUp:
index = Math.Max(currentLeafIndex - PageSize, 0);
if (index < _leafIndexes.Count)
{
index = _leafIndexes[index];
}
break;
case ConsoleKey.PageDown:
index = Math.Min(currentLeafIndex + PageSize, _leafIndexes.Count - 1);
if (index < _leafIndexes.Count)
{
index = _leafIndexes[index];
}
break;
}
}
else
{
index = keyInfo.Key switch
{
ConsoleKey.UpArrow => Index - 1,
ConsoleKey.DownArrow => Index + 1,
@@ -31,13 +116,46 @@ internal sealed class ListPromptState<T>
ConsoleKey.PageDown => Index + PageSize,
_ => Index,
};
}
var search = SearchText;
if (SearchEnabled)
{
// If is text input, append to search filter
if (!char.IsControl(keyInfo.KeyChar))
{
search = SearchText + keyInfo.KeyChar;
var item = Items.FirstOrDefault(x => x.Data.ToString()?.Contains(search, StringComparison.OrdinalIgnoreCase) == true && (!x.IsGroup || Mode != SelectionMode.Leaf));
if (item != null)
{
index = Items.IndexOf(item);
}
}
if (keyInfo.Key == ConsoleKey.Backspace)
{
if (search.Length > 0)
{
search = search.Substring(0, search.Length - 1);
}
var item = Items.FirstOrDefault(x => x.Data.ToString()?.Contains(search, StringComparison.OrdinalIgnoreCase) == true && (!x.IsGroup || Mode != SelectionMode.Leaf));
if (item != null)
{
index = Items.IndexOf(item);
}
}
}
index = WrapAround
? (ItemCount + (index % ItemCount)) % ItemCount
: index.Clamp(0, ItemCount - 1);
if (index != Index)
if (index != Index || SearchText != search)
{
Index = index;
SearchText = search;
return true;
}

View File

@@ -94,7 +94,7 @@ public sealed class MultiSelectionPrompt<T> : IPrompt<List<T>>, IListPromptStrat
{
// Create the list prompt
var prompt = new ListPrompt<T>(console, this);
var result = await prompt.Show(Tree, cancellationToken, PageSize, WrapAround).ConfigureAwait(false);
var result = await prompt.Show(Tree, Mode, false, false, PageSize, WrapAround, cancellationToken).ConfigureAwait(false);
if (Mode == SelectionMode.Leaf)
{
@@ -222,7 +222,8 @@ public sealed class MultiSelectionPrompt<T> : IPrompt<List<T>>, IListPromptStrat
}
/// <inheritdoc/>
IRenderable IListPromptStrategy<T>.Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem<T> Node)> items)
IRenderable IListPromptStrategy<T>.Render(IAnsiConsole console, bool scrollable, int cursorIndex,
IEnumerable<(int Index, ListPromptItem<T> Node)> items, bool skipUnselectableItems, string searchText)
{
var list = new List<IRenderable>();
var highlightStyle = HighlightStyle ?? Color.Blue;

View File

@@ -36,6 +36,16 @@ public sealed class SelectionPrompt<T> : IPrompt<T>, IListPromptStrategy<T>
/// </summary>
public Style? DisabledStyle { get; set; }
/// <summary>
/// Gets or sets the style of highlighted search matches.
/// </summary>
public Style? SearchHighlightStyle { get; set; }
/// <summary>
/// Gets or sets the text that will be displayed when no search text has been entered.
/// </summary>
public string? SearchPlaceholderText { get; set; }
/// <summary>
/// Gets or sets the converter to get the display string for a choice. By default
/// the corresponding <see cref="TypeConverter"/> is used.
@@ -53,6 +63,11 @@ public sealed class SelectionPrompt<T> : IPrompt<T>, IListPromptStrategy<T>
/// </summary>
public SelectionMode Mode { get; set; } = SelectionMode.Leaf;
/// <summary>
/// Gets or sets a value indicating whether or not search is enabled.
/// </summary>
public bool SearchEnabled { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="SelectionPrompt{T}"/> class.
/// </summary>
@@ -84,7 +99,7 @@ public sealed class SelectionPrompt<T> : IPrompt<T>, IListPromptStrategy<T>
{
// Create the list prompt
var prompt = new ListPrompt<T>(console, this);
var result = await prompt.Show(_tree, cancellationToken, PageSize, WrapAround).ConfigureAwait(false);
var result = await prompt.Show(_tree, Mode, true, SearchEnabled, PageSize, WrapAround, cancellationToken).ConfigureAwait(false);
// Return the selected item
return result.Items[result.Index].Data;
@@ -118,11 +133,20 @@ public sealed class SelectionPrompt<T> : IPrompt<T>, IListPromptStrategy<T>
extra += 2;
}
// Scrolling?
if (totalItemCount > requestedPageSize)
var scrollable = totalItemCount > requestedPageSize;
if (SearchEnabled || scrollable)
{
// The scrolling instructions takes up two rows
extra += 2;
extra += 1;
}
if (SearchEnabled)
{
extra += 1;
}
if (scrollable)
{
extra += 1;
}
if (requestedPageSize > console.Profile.Height - extra)
@@ -134,11 +158,13 @@ public sealed class SelectionPrompt<T> : IPrompt<T>, IListPromptStrategy<T>
}
/// <inheritdoc/>
IRenderable IListPromptStrategy<T>.Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem<T> Node)> items)
IRenderable IListPromptStrategy<T>.Render(IAnsiConsole console, bool scrollable, int cursorIndex,
IEnumerable<(int Index, ListPromptItem<T> Node)> items, bool skipUnselectableItems, string searchText)
{
var list = new List<IRenderable>();
var disabledStyle = DisabledStyle ?? Color.Grey;
var highlightStyle = HighlightStyle ?? Color.Blue;
var searchHighlightStyle = SearchHighlightStyle ?? new Style(foreground: Color.Default, background: Color.Yellow, Decoration.Bold);
if (Title != null)
{
@@ -169,15 +195,31 @@ public sealed class SelectionPrompt<T> : IPrompt<T>, IListPromptStrategy<T>
text = text.RemoveMarkup().EscapeMarkup();
}
if (searchText.Length > 0 && !(item.Node.IsGroup && Mode == SelectionMode.Leaf))
{
text = text.Highlight(searchText, searchHighlightStyle);
}
grid.AddRow(new Markup(indent + prompt + " " + text, style));
}
list.Add(grid);
if (SearchEnabled || scrollable)
{
// Add padding
list.Add(Text.Empty);
}
if (SearchEnabled)
{
list.Add(new Markup(
searchText.Length > 0 ? searchText.EscapeMarkup() : SearchPlaceholderText ?? ListPromptConstants.SearchPlaceholderMarkup));
}
if (scrollable)
{
// (Move up and down to reveal more choices)
list.Add(Text.Empty);
list.Add(new Markup(MoreChoicesText ?? ListPromptConstants.MoreChoicesMarkup));
}

View File

@@ -182,6 +182,61 @@ public static class SelectionPromptExtensions
return obj;
}
/// <summary>
/// Enables search for the prompt.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static SelectionPrompt<T> EnableSearch<T>(this SelectionPrompt<T> obj)
where T : notnull
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.SearchEnabled = true;
return obj;
}
/// <summary>
/// Disables search for the prompt.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static SelectionPrompt<T> DisableSearch<T>(this SelectionPrompt<T> obj)
where T : notnull
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.SearchEnabled = false;
return obj;
}
/// <summary>
/// Sets the text that will be displayed when no search text has been entered.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <param name="text">The text to display.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static SelectionPrompt<T> SearchPlaceholderText<T>(this SelectionPrompt<T> obj, string? text)
where T : notnull
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.SearchPlaceholderText = text;
return obj;
}
/// <summary>
/// Sets the highlight style of the selected choice.
/// </summary>

View File

@@ -16,8 +16,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Memory" Version="4.5.5" />
<PackageReference Include="Wcwidth.Sources" Version="1.0.0">
<PackageReference Condition="'$(TargetFramework)' == 'netstandard2.0'" Include="System.Memory" Version="4.5.5" />
<PackageReference Include="Wcwidth.Sources" Version="2.0.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

View File

@@ -1,4 +1,4 @@
namespace Spectre.Console;
namespace Spectre.Console;
/// <summary>
/// Represents vertical alignment.

View File

@@ -43,6 +43,11 @@ public sealed class BarChart : Renderable, IHasCulture
/// <remarks>Defaults to null, which corresponds to largest value in chart.</remarks>
public double? MaxValue { get; set; }
/// <summary>
/// Gets or sets the function used to format the values of the bar chart.
/// </summary>
public Func<double, CultureInfo, string>? ValueFormatter { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="BarChart"/> class.
/// </summary>
@@ -90,6 +95,7 @@ public sealed class BarChart : Renderable, IHasCulture
AsciiBar = '█',
ShowValue = ShowValues,
Culture = Culture,
ValueFormatter = ValueFormatter,
});
}

View File

@@ -35,4 +35,9 @@ public enum ExceptionFormats
/// Shortens everything that can be shortened.
/// </summary>
ShortenEverything = ShortenMethods | ShortenTypes | ShortenPaths,
/// <summary>
/// Whether or not to show the exception stack trace.
/// </summary>
NoStackTrace = 16,
}

View File

@@ -50,6 +50,11 @@ internal static class ExceptionFormatter
}
// Stack frames
if ((settings.Format & ExceptionFormats.NoStackTrace) != 0)
{
return grid;
}
var stackTrace = new StackTrace(ex, fNeedFileInfo: true);
var frames = stackTrace
.GetFrames()

View File

@@ -1,4 +1,4 @@
namespace Spectre.Console;
namespace Spectre.Console;
internal static class TypeNameHelper
{

View File

@@ -1,4 +1,4 @@
namespace Spectre.Console;
namespace Spectre.Console;
[DebuggerDisplay("{Region,nq}")]
internal sealed class LayoutRender

View File

@@ -4,7 +4,6 @@ namespace Spectre.Console;
/// A paragraph of text where different parts
/// of the paragraph can have individual styling.
/// </summary>
[DebuggerDisplay("{_text,nq}")]
public sealed class Paragraph : Renderable, IHasJustification, IOverflowable
{
private readonly List<SegmentLine> _lines;

View File

@@ -15,6 +15,7 @@ internal sealed class ProgressBar : Renderable, IHasCulture
public bool ShowValue { get; set; }
public bool IsIndeterminate { get; set; }
public CultureInfo? Culture { get; set; }
public Func<double, CultureInfo, string>? ValueFormatter { get; set; }
public Style CompletedStyle { get; set; } = Color.Yellow;
public Style FinishedStyle { get; set; } = Color.Green;
@@ -50,7 +51,7 @@ internal sealed class ProgressBar : Renderable, IHasCulture
var barCount = Math.Max(0, (int)(width * (completedBarCount / MaxValue)));
// Show value?
var value = completedBarCount.ToString(Culture ?? CultureInfo.InvariantCulture);
var value = ValueFormatter != null ? ValueFormatter(completedBarCount, Culture ?? CultureInfo.InvariantCulture) : completedBarCount.ToString(Culture ?? CultureInfo.InvariantCulture);
if (ShowValue)
{
barCount = barCount - value.Length - 1;

View File

@@ -150,9 +150,9 @@ internal static class TableRenderer
result.Add(Segment.LineBreak);
}
// Show row separator?
// Show row separator, if headers are hidden show separator after the first row
if (context.Border.SupportsRowSeparator && context.ShowRowSeparators
&& !isFirstRow && !isLastRow)
&& (!isFirstRow || (isFirstRow && !context.ShowHeaders)) && !isLastRow)
{
var hasVisibleFootes = context is { ShowFooters: true, HasFooters: true };
var isNextLastLine = index == context.Rows.Count - 2;

View File

@@ -1,15 +1,15 @@
<Project>
<PropertyGroup Label="Settings">
<LangVersion>10</LangVersion>
<LangVersion>12</LangVersion>
<IsPackable>false</IsPackable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<PackageReference Include="Roslynator.Analyzers" Version="4.1.2">
<PackageReference Include="Roslynator.Analyzers" Version="4.12.1">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>

View File

@@ -12,10 +12,10 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" Version="1.1.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" Version="1.1.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View File

@@ -24,8 +24,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<NoConcurrentLiveRenderablesAnalyzer>
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(10, 13))
.ConfigureAwait(false);
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(10, 13));
}
[Fact]
@@ -48,8 +47,7 @@ class Child
}";
await SpectreAnalyzerVerifier<NoConcurrentLiveRenderablesAnalyzer>
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(12, 13))
.ConfigureAwait(false);
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(12, 13));
}
[Fact]
@@ -69,7 +67,6 @@ class Program
}";
await SpectreAnalyzerVerifier<NoConcurrentLiveRenderablesAnalyzer>
.VerifyAnalyzerAsync(Source)
.ConfigureAwait(false);
.VerifyAnalyzerAsync(Source);
}
}

View File

@@ -21,8 +21,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<NoPromptsDuringLiveRenderablesAnalyzer>
.VerifyAnalyzerAsync(Source)
.ConfigureAwait(false);
.VerifyAnalyzerAsync(Source);
}
[Fact]
@@ -45,8 +44,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<NoPromptsDuringLiveRenderablesAnalyzer>
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(12, 26))
.ConfigureAwait(false);
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(12, 26));
}
[Fact]
@@ -67,8 +65,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<NoPromptsDuringLiveRenderablesAnalyzer>
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(10, 13))
.ConfigureAwait(false);
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(10, 13));
}
[Fact]
@@ -92,7 +89,6 @@ class Program
}";
await SpectreAnalyzerVerifier<NoPromptsDuringLiveRenderablesAnalyzer>
.VerifyAnalyzerAsync(Source)
.ConfigureAwait(false);
.VerifyAnalyzerAsync(Source);
}
}

View File

@@ -24,8 +24,7 @@ internal sealed class Foo
";
await SpectreAnalyzerVerifier<FavorInstanceAnsiConsoleOverStaticAnalyzer>
.VerifyAnalyzerAsync(Source)
.ConfigureAwait(false);
.VerifyAnalyzerAsync(Source);
}
[Fact]
@@ -45,8 +44,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<FavorInstanceAnsiConsoleOverStaticAnalyzer>
.VerifyAnalyzerAsync(Source)
.ConfigureAwait(false);
.VerifyAnalyzerAsync(Source);
}
[Fact]
@@ -64,8 +62,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<FavorInstanceAnsiConsoleOverStaticAnalyzer>
.VerifyAnalyzerAsync(Source)
.ConfigureAwait(false);
.VerifyAnalyzerAsync(Source);
}
[Fact]
@@ -86,7 +83,6 @@ class TestClass
}";
await SpectreAnalyzerVerifier<FavorInstanceAnsiConsoleOverStaticAnalyzer>
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(11, 9))
.ConfigureAwait(false);
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(11, 9));
}
}

View File

@@ -20,8 +20,7 @@ class TestClass {
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyAnalyzerAsync(Source)
.ConfigureAwait(false);
.VerifyAnalyzerAsync(Source);
}
[Fact]
@@ -38,8 +37,7 @@ class TestClass {
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(7, 9))
.ConfigureAwait(false);
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(7, 9));
}
[Fact]
@@ -56,7 +54,6 @@ class TestClass
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(7, 9))
.ConfigureAwait(false);
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(7, 9));
}
}

View File

@@ -38,8 +38,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<FavorInstanceAnsiConsoleOverStaticAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource)
.ConfigureAwait(false);
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource);
}
[Fact]
@@ -78,8 +77,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<FavorInstanceAnsiConsoleOverStaticAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(12, 9), FixedSource)
.ConfigureAwait(false);
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(12, 9), FixedSource);
}
[Fact]
@@ -108,8 +106,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<FavorInstanceAnsiConsoleOverStaticAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(8, 9), FixedSource)
.ConfigureAwait(false);
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(8, 9), FixedSource);
}
[Fact]
@@ -144,7 +141,6 @@ class TestClass
}";
await SpectreAnalyzerVerifier<FavorInstanceAnsiConsoleOverStaticAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource)
.ConfigureAwait(false);
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource);
}
}

View File

@@ -33,8 +33,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(8, 9), FixedSource)
.ConfigureAwait(false);
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(8, 9), FixedSource);
}
[Fact]
@@ -64,8 +63,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(8, 9), FixedSource)
.ConfigureAwait(false);
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(8, 9), FixedSource);
}
[Fact]
@@ -100,8 +98,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource)
.ConfigureAwait(false);
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource);
}
[Fact]
@@ -134,8 +131,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(10, 9), FixedSource)
.ConfigureAwait(false);
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(10, 9), FixedSource);
}
[Fact]
@@ -168,8 +164,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(9, 9), FixedSource)
.ConfigureAwait(false);
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(9, 9), FixedSource);
}
[Fact]
@@ -204,8 +199,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource)
.ConfigureAwait(false);
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource);
}
[Fact]
@@ -240,8 +234,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource)
.ConfigureAwait(false);
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource);
}
[Fact]
@@ -272,8 +265,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(9, 64), FixedSource)
.ConfigureAwait(false);
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(9, 64), FixedSource);
}
[Fact]
@@ -306,8 +298,7 @@ class TestClass
}";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(10, 40), FixedSource)
.ConfigureAwait(false);
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(10, 40), FixedSource);
}
[Fact]
@@ -327,7 +318,7 @@ AnsiConsole.WriteLine(""Hello, World"");
";
await SpectreAnalyzerVerifier<UseSpectreInsteadOfSystemConsoleAnalyzer>
.VerifyCodeFixAsync(Source, OutputKind.ConsoleApplication, _expectedDiagnostic.WithLocation(4, 1), FixedSource)
.ConfigureAwait(false);
.VerifyCodeFixAsync(Source, OutputKind.ConsoleApplication, _expectedDiagnostic.WithLocation(4, 1),
FixedSource);
}
}

View File

@@ -5,41 +5,4 @@ namespace Spectre.Console.Tests.Data;
public abstract class AnimalCommand<TSettings> : Command<TSettings>
where TSettings : CommandSettings
{
protected void DumpSettings(CommandContext context, TSettings settings)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (settings == null)
{
throw new ArgumentNullException(nameof(settings));
}
var properties = settings.GetType().GetProperties();
foreach (var group in properties.GroupBy(x => x.DeclaringType).Reverse())
{
SystemConsole.WriteLine();
SystemConsole.ForegroundColor = ConsoleColor.Yellow;
SystemConsole.WriteLine(group.Key.FullName);
SystemConsole.ResetColor();
foreach (var property in group)
{
SystemConsole.WriteLine($" {property.Name} = {property.GetValue(settings)}");
}
}
if (context.Remaining.Raw.Count > 0)
{
SystemConsole.WriteLine();
SystemConsole.ForegroundColor = ConsoleColor.Yellow;
SystemConsole.WriteLine("Remaining:");
SystemConsole.ResetColor();
SystemConsole.WriteLine(string.Join(", ", context.Remaining));
}
SystemConsole.WriteLine();
}
}

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