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 # Use file scoped namespace declarations
7b2da0a4f63bf3ceab99d2c88535e74155f2b99c 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** **Additional context**
Add any other context about the problem here. 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** **Additional context**
Add any other context or screenshots about the feature request here. 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. --> <!-- 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 - [ ] 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 - [ ] A maintainer has signed off on the changes and the issue was assigned to me
- [ ] All newly added code is adequately covered by tests - [ ] All newly added code is adequately covered by tests
@@ -17,3 +17,6 @@ fixes #
## Changes ## Changes
<!-- describe the changes you made. --> <!-- 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: 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 # BUILD
################################################### ###################################################
@@ -57,25 +15,15 @@ jobs:
build: build:
name: Build name: Build
if: "!contains(github.event.head_commit.message, 'skip-ci')" if: "!contains(github.event.head_commit.message, 'skip-ci')"
strategy: runs-on: ubuntu-latest
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 }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup .NET SDK - name: Setup .NET SDK
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: | dotnet-version: |
6.0.x 6.0.x
@@ -90,7 +38,7 @@ jobs:
- name: Upload Verify Test Results - name: Upload Verify Test Results
if: failure() if: failure()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: verify-test-results name: verify-test-results
path: | path: |

View File

@@ -15,40 +15,34 @@ env:
jobs: jobs:
################################################### ###################################################
# BUILD # PUBLISH
################################################### ###################################################
build: build:
name: Build name: Publish NuGet Packages
if: | if: "!contains(github.event.head_commit.message, 'skip-ci') || startsWith(github.ref, 'refs/tags/')"
(!startsWith(github.event.head_commit.message, 'skip-ci') runs-on: ubuntu-latest
&& !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 }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup .NET SDK - 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 shell: bash
run: | run: |
dotnet tool restore dotnet tool restore
dotnet cake dotnet cake --target="publish" \
--nuget-key="${{secrets.NUGET_API_KEY}}" \
--github-key="${{secrets.GITHUB_TOKEN}}"
################################################### ###################################################
# DOCS # DOCS
@@ -60,20 +54,20 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup .NET SDK - name: Setup .NET SDK
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: '16' node-version: '16'
- name: Cache dependencies - name: Cache dependencies
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ~/.npm path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }} key: npm-${{ hashFiles('package-lock.json') }}
@@ -88,35 +82,4 @@ jobs:
run: | run: |
cd docs cd docs
dotnet tool restore dotnet tool restore
dotnet run --configuration Release -- deploy 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) _[![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. 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) 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
for Python. For detailed usage instructions, [please refer to the documentation at https://spectreconsole.net/.](https://spectreconsole.net/)
## Table of Contents ## Table of Contents
@@ -19,18 +18,22 @@ for Python. For detailed usage instructions, [please refer to the documentation
## Features ## 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 * Supports the most common SRG parameters when it comes to text
styling such as bold, dim, italic, underline, strikethrough, styling such as bold, dim, italic, underline, strikethrough,
and blinking text. and blinking text.
* Supports 3/4/8/24-bit colors in the terminal. * Supports 3/4/8/24-bit colors in the terminal.
The library will detect the capabilities of the current terminal The library will detect the capabilities of the current terminal
and downgrade colors as needed. and downgrade colors as needed.
* Written with unit testing in mind.
![Example](docs/input/assets/images/example.png) ![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 ## Installing
The fastest way of getting started using `Spectre.Console` is to install the NuGet package. 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 ## Documentation
The documentation for `Spectre.Console` can be found at The documentation for `Spectre.Console` can be found at
https://spectreconsole.net/ https://spectreconsole.net
## Examples ## Examples
@@ -69,7 +72,7 @@ And to run an example:
## Sponsors ## Sponsors
The following people are [sponsoring](https://github.com/sponsors/patriksvensson) 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) * [Rodney Littles II](https://github.com/RLittlesII)
* [Martin Björkström](https://github.com/bjorkstromm) * [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 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

@@ -1,77 +1,77 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Docs.Extensions; using Docs.Extensions;
using Docs.Shortcodes; using Docs.Shortcodes;
using Docs.Utilities; using Docs.Utilities;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Statiq.App; using Statiq.App;
using Statiq.Common; using Statiq.Common;
using Statiq.Core; using Statiq.Core;
using Statiq.Web; using Statiq.Web;
namespace Docs namespace Docs
{ {
public static class Program public static class Program
{ {
public static async Task<int> Main(string[] args) => public static async Task<int> Main(string[] args) =>
await Bootstrapper.Factory await Bootstrapper.Factory
.CreateWeb(args) .CreateWeb(args)
.AddSetting(Keys.Host, "spectreconsole.net") .AddSetting(Keys.Host, "spectreconsole.net")
.AddSetting(Keys.LinksUseHttps, true) .AddSetting(Keys.LinksUseHttps, true)
.AddSetting(Constants.EditLink, ConfigureEditLink()) .AddSetting(Constants.EditLink, ConfigureEditLink())
.AddSetting(Constants.SourceFiles, new List<string> .AddSetting(Constants.SourceFiles, new List<string>
{ {
"../../src/Spectre.Console/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs", "../../src/Spectre.Console/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs",
"../../src/Spectre.Console.Cli/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs", "../../src/Spectre.Console.Cli/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs",
"../../src/Spectre.Console.ImageSharp/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs", "../../src/Spectre.Console.ImageSharp/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs",
"../../src/Spectre.Console.Json/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs" "../../src/Spectre.Console.Json/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs"
}) })
.AddSetting(Constants.ExampleSourceFiles, new List<string> .AddSetting(Constants.ExampleSourceFiles, new List<string>
{ {
"../../examples/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs", "../../examples/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs",
} }
) )
.ConfigureServices(i => .ConfigureServices(i =>
{ {
i.AddSingleton(new TypeNameLinks()); i.AddSingleton(new TypeNameLinks());
}) })
.ConfigureSite("spectreconsole", "spectre.console", "main") .ConfigureSite("spectreconsole", "spectre.console", "main")
.AddShortcode("Children", typeof(ChildrenShortcode)) .AddShortcode("Children", typeof(ChildrenShortcode))
.AddShortcode("ColorTable", typeof(ColorTableShortcode)) .AddShortcode("ColorTable", typeof(ColorTableShortcode))
.AddShortcode("EmojiTable", typeof(EmojiTableShortcode)) .AddShortcode("EmojiTable", typeof(EmojiTableShortcode))
.AddShortcode("Alert", typeof(AlertShortcode)) .AddShortcode("Alert", typeof(AlertShortcode))
.AddShortcode("Info", typeof(InfoShortcode)) .AddShortcode("Info", typeof(InfoShortcode))
.AddShortcode("AsciiCast", typeof(AsciiCastShortcode)) .AddShortcode("AsciiCast", typeof(AsciiCastShortcode))
.AddShortcode("Example", typeof(ExampleSnippet)) .AddShortcode("Example", typeof(ExampleSnippet))
.AddPipelines() .AddPipelines()
.BuildPipeline( .BuildPipeline(
"Bootstrap", "Bootstrap",
builder => builder builder => builder
.WithInputReadFiles("../node_modules/asciinema-player/dist/bundle/asciinema-player.js") .WithInputReadFiles("../node_modules/asciinema-player/dist/bundle/asciinema-player.js")
.WithProcessModules(new SetDestination(Config.FromDocument(doc => new NormalizedPath($"./assets/{doc.Source.FileName}")), true)) .WithProcessModules(new SetDestination(Config.FromDocument(doc => new NormalizedPath($"./assets/{doc.Source.FileName}")), true))
.WithOutputWriteFiles() .WithOutputWriteFiles()
) )
.AddProcess(ProcessTiming.Initialization, _ => new ProcessLauncher("npm", "install --audit false --fund false --progress false") .AddProcess(ProcessTiming.Initialization, _ => new ProcessLauncher("npm", "install --audit false --fund false --progress false")
{ {
LogErrors = false LogErrors = false
}) })
.AddProcess(ProcessTiming.Initialization, _ => new ProcessLauncher("dotnet", "playwright install chromium")) .AddProcess(ProcessTiming.Initialization, _ => new ProcessLauncher("dotnet", "playwright install chromium"))
.AddProcess(ProcessTiming.BeforeDeployment, _ => new ProcessLauncher("npm", "run build:tailwind") .AddProcess(ProcessTiming.BeforeDeployment, _ => new ProcessLauncher("npm", "run build:tailwind")
{ {
LogErrors = false LogErrors = false
}) })
.RunAsync(); .RunAsync();
private static Config<string> ConfigureEditLink() private static Config<string> ConfigureEditLink()
{ {
return Config.FromDocument((doc, ctx) => return Config.FromDocument((doc, ctx) =>
{ {
return string.Format("https://github.com/{0}/{1}/edit/{2}/docs/input/{3}", return string.Format("https://github.com/{0}/{1}/edit/{2}/docs/input/{3}",
ctx.GetString(Constants.Site.Owner), ctx.GetString(Constants.Site.Owner),
ctx.GetString(Constants.Site.Repository), ctx.GetString(Constants.Site.Repository),
ctx.GetString(Constants.Site.Branch), ctx.GetString(Constants.Site.Branch),
doc.Source.GetRelativeInputPath()); doc.Source.GetRelativeInputPath());
}); });
} }
} }
} }

View File

@@ -4,7 +4,7 @@ Description: "*Spectre.Console* makes it easy to write text with different style
Highlights: Highlights:
- Bold, Italic, Underline, strikethrough - Bold, Italic, Underline, strikethrough
- Dim, Invert - Dim, Invert
- Conceal, slowblink, rapidblink - Conceal, slowblink, rapidblink
- Links - Links
--- ---
@@ -46,9 +46,9 @@ Note that what styles that can be used is defined by the system or your terminal
<tr> <tr>
<td><code>strikethrough</code></td> <td><code>strikethrough</code></td>
<td>Shows text with a horizontal line through the center</td> <td>Shows text with a horizontal line through the center</td>
</tr> </tr>
<tr> <tr>
<td><code>link</link></td> <td><code>link</link></td>
<td>Creates a clickable link within text</td> <td>Creates a clickable link within text</td>
</tr> </tr>
</table> </table>

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

@@ -1,47 +1,75 @@
Title: Command Help Title: Command Help
Order: 13 Order: 13
Description: "Console applications built with *Spectre.Console.Cli* include automatically generated help command line help." Description: "Console applications built with *Spectre.Console.Cli* include automatically generated help command line help."
--- ---
Console applications built with `Spectre.Console.Cli` include automatically generated help which is displayed when `-h` or `--help` has been specified on the command line. Console applications built with `Spectre.Console.Cli` include automatically generated help which is displayed when `-h` or `--help` has been specified on the command line.
The automatically generated help is derived from the configured commands and their command settings. The automatically generated help is derived from the configured commands and their command settings.
The help is also context aware and tailored depending on what has been specified on the command line before it. For example, The help is also context aware and tailored depending on what has been specified on the command line before it. For example,
1. When `-h` or `--help` appears immediately after the application name (eg. `application.exe --help`), then the help displayed is a high-level summary of the application, including any command line examples and a listing of all possible commands the user can execute. 1. When `-h` or `--help` appears immediately after the application name (eg. `application.exe --help`), then the help displayed is a high-level summary of the application, including any command line examples and a listing of all possible commands the user can execute.
2. When `-h` or `--help` appears immediately after a command has been specified (eg. `application.exe command --help`), then the help displayed is specific to the command and includes information about command specific switches and any default values. 2. When `-h` or `--help` appears immediately after a command has been specified (eg. `application.exe command --help`), then the help displayed is specific to the command and includes information about command specific switches and any default values.
`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`. `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`.
## Custom help providers ## Styling the help text
Whilst it shouldn't be common place to implement your own help provider, it is however possible. Basic styling is applied to the generated help text by default, however this is configurable.
You are able to implement your own `IHelpProvider` and configure a `CommandApp` to use that instead of the Spectre.Console help provider. `HelpProviderStyle` is the `Spectre.Console` class that holds the style information for the help text.
```csharp The default theme shipped with Spectre.Console is provided by a factory method, `HelpProviderStyle.Default`.
using Spectre.Console.Cli;
However, you can explicitly set a custom theme when configuring a CommandApp, for example:
namespace Help;
```csharp
public static class Program config.Settings.HelpProviderStyles = new HelpProviderStyle()
{ {
public static int Main(string[] args) Description = new DescriptionStyle()
{ {
var app = new CommandApp<DefaultCommand>(); Header = "bold",
},
app.Configure(config => };
{ ```
// Register the custom help provider
config.SetHelpProvider(new CustomHelpProvider(config.Settings)); 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
return app.Run(args); 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.
There is a working [example of a custom help provider](https://github.com/spectreconsole/spectre.console/tree/main/examples/Cli/Help) demonstrating this. ## Custom help providers
Whilst it shouldn't be common place to implement your own help provider, it is however possible.
You are able to implement your own `IHelpProvider` and configure a `CommandApp` to use that instead of the Spectre.Console help provider.
```csharp
using Spectre.Console.Cli;
namespace Help;
public static class Program
{
public static int Main(string[] args)
{
var app = new CommandApp<DefaultCommand>();
app.Configure(config =>
{
// Register the custom help provider
config.SetHelpProvider(new CustomHelpProvider(config.Settings));
});
return app.Run(args);
}
}
```
There is a working [example of a custom help provider](https://github.com/spectreconsole/spectre.console/tree/main/examples/Cli/Help) demonstrating this.

View File

@@ -23,7 +23,7 @@ app.Configure(config =>
## Multiple Commands ## 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: 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. 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 ## 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). 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 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. 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 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. the exitCode for the application.
@@ -71,7 +75,7 @@ namespace MyApp
app.Configure(config => app.Configure(config =>
{ {
config.SetExceptionHandler(ex => config.SetExceptionHandler((ex, resolver) =>
{ {
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything); AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything);
return -99; 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. 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`. The exitCode for the application will be `-1`.
@@ -103,7 +107,7 @@ namespace MyApp
app.Configure(config => app.Configure(config =>
{ {
config.SetExceptionHandler(ex => config.SetExceptionHandler((ex, resolver) =>
{ {
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything); AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything);
}); });

View File

@@ -1,119 +1,119 @@
Title: Markup Title: Markup
Order: 30 Order: 30
Description: The Markup class allows you to output rich text to the console. Description: The Markup class allows you to output rich text to the console.
Highlights: Highlights:
- Easily add *color*. - Easily add *color*.
- Add hyperlinks to for supported terminals. - Add hyperlinks to for supported terminals.
- Emoji 🚀 parsing. - Emoji 🚀 parsing.
Reference: Reference:
- M:Spectre.Console.AnsiConsole.Markup(System.String) - M:Spectre.Console.AnsiConsole.Markup(System.String)
- M:Spectre.Console.AnsiConsole.MarkupLine(System.String) - M:Spectre.Console.AnsiConsole.MarkupLine(System.String)
- T:Spectre.Console.Markup - T:Spectre.Console.Markup
--- ---
The `Markup` class allows you to output rich text to the console. The `Markup` class allows you to output rich text to the console.
## Syntax ## Syntax
Console markup uses a syntax inspired by bbcode. If you write the style (see [Styles](xref:styles)) Console markup uses a syntax inspired by bbcode. If you write the style (see [Styles](xref:styles))
in square brackets, e.g. `[bold red]`, that style will apply until it is closed with a `[/]`. in square brackets, e.g. `[bold red]`, that style will apply until it is closed with a `[/]`.
```csharp ```csharp
AnsiConsole.Write(new Markup("[bold yellow]Hello[/] [red]World![/]")); AnsiConsole.Write(new Markup("[bold yellow]Hello[/] [red]World![/]"));
``` ```
The `Markup` class implements `IRenderable` which means that you The `Markup` class implements `IRenderable` which means that you
can use this in tables, grids, and panels. Most classes that support can use this in tables, grids, and panels. Most classes that support
rendering of `IRenderable` also have overloads for rendering rich text. rendering of `IRenderable` also have overloads for rendering rich text.
```csharp ```csharp
var table = new Table(); var table = new Table();
table.AddColumn(new TableColumn(new Markup("[yellow]Foo[/]"))); table.AddColumn(new TableColumn(new Markup("[yellow]Foo[/]")));
table.AddColumn(new TableColumn("[blue]Bar[/]")); table.AddColumn(new TableColumn("[blue]Bar[/]"));
AnsiConsole.Write(table); AnsiConsole.Write(table);
``` ```
## Convenience methods ## Convenience methods
There are also convenience methods on `AnsiConsole` that can be used There are also convenience methods on `AnsiConsole` that can be used
to write markup text to the console without instantiating a new `Markup` to write markup text to the console without instantiating a new `Markup`
instance. instance.
```csharp ```csharp
AnsiConsole.Markup("[underline green]Hello[/] "); AnsiConsole.Markup("[underline green]Hello[/] ");
AnsiConsole.MarkupLine("[bold]World[/]"); AnsiConsole.MarkupLine("[bold]World[/]");
``` ```
## Escaping format characters ## Escaping format characters
To output a `[` you use `[[`, and to output a `]` you use `]]`. To output a `[` you use `[[`, and to output a `]` you use `]]`.
```csharp ```csharp
AnsiConsole.Markup("[[Hello]] "); // [Hello] AnsiConsole.Markup("[[Hello]] "); // [Hello]
AnsiConsole.Markup("[red][[World]][/]"); // [World] AnsiConsole.Markup("[red][[World]][/]"); // [World]
``` ```
You can also use the `EscapeMarkup` extension method. You can also use the `EscapeMarkup` extension method.
```csharp ```csharp
AnsiConsole.Markup("[red]{0}[/]", "Hello [World]".EscapeMarkup()); AnsiConsole.Markup("[red]{0}[/]", "Hello [World]".EscapeMarkup());
``` ```
You can also use the `Markup.Escape` method. You can also use the `Markup.Escape` method.
```csharp ```csharp
AnsiConsole.Markup("[red]{0}[/]", Markup.Escape("Hello [World]")); AnsiConsole.Markup("[red]{0}[/]", Markup.Escape("Hello [World]"));
``` ```
## Escaping Interpolated Strings ## Escaping Interpolated Strings
When working with interpolated strings, you can use the `MarkupInterpolated` and `MarkupLineInterpolated` methods to automatically escape the values in the interpolated string "holes". When working with interpolated strings, you can use the `MarkupInterpolated` and `MarkupLineInterpolated` methods to automatically escape the values in the interpolated string "holes".
```csharp ```csharp
string hello = "Hello [World]"; string hello = "Hello [World]";
AnsiConsole.MarkupInterpolated($"[red]{hello}[/]"); AnsiConsole.MarkupInterpolated($"[red]{hello}[/]");
``` ```
## Setting background color ## Setting background color
You can set the background color in markup by prefixing the color with `on`. You can set the background color in markup by prefixing the color with `on`.
```csharp ```csharp
AnsiConsole.Markup("[bold yellow on blue]Hello[/]"); AnsiConsole.Markup("[bold yellow on blue]Hello[/]");
AnsiConsole.Markup("[default on blue]World[/]"); AnsiConsole.Markup("[default on blue]World[/]");
``` ```
## Rendering emojis ## Rendering emojis
To output an emoji as part of markup, you can use emoji shortcodes. To output an emoji as part of markup, you can use emoji shortcodes.
```csharp ```csharp
AnsiConsole.Markup("Hello :globe_showing_europe_africa:!"); AnsiConsole.Markup("Hello :globe_showing_europe_africa:!");
``` ```
For a list of emoji, see the [Emojis](xref:emojis) appendix section. For a list of emoji, see the [Emojis](xref:emojis) appendix section.
## Colors ## Colors
In the examples above, all colors were referenced by their name, In the examples above, all colors were referenced by their name,
but you can also use the hex or rgb representation for colors in markdown. but you can also use the hex or rgb representation for colors in markdown.
```csharp ```csharp
AnsiConsole.Markup("[red]Foo[/] "); AnsiConsole.Markup("[red]Foo[/] ");
AnsiConsole.Markup("[#ff0000]Bar[/] "); AnsiConsole.Markup("[#ff0000]Bar[/] ");
AnsiConsole.Markup("[rgb(255,0,0)]Baz[/] "); AnsiConsole.Markup("[rgb(255,0,0)]Baz[/] ");
``` ```
For a list of colors, see the [Colors](xref:colors) appendix section. For a list of colors, see the [Colors](xref:colors) appendix section.
## Links ## Links
To output a clickable link, you can use the `[link]` style. To output a clickable link, you can use the `[link]` style.
```csharp ```csharp
AnsiConsole.Markup("[link]https://spectreconsole.net[/]"); AnsiConsole.Markup("[link]https://spectreconsole.net[/]");
AnsiConsole.Markup("[link=https://spectreconsole.net]Spectre Console Documentation[/]"); AnsiConsole.Markup("[link=https://spectreconsole.net]Spectre Console Documentation[/]");
``` ```
## Styles ## Styles
For a list of styles, see the [Styles](xref:styles) appendix section. For a list of styles, see the [Styles](xref:styles) appendix section.

View File

@@ -63,22 +63,22 @@ What's the secret number? _
```text ```text
Enter password: ************_ Enter password: ************_
``` ```
## Masks ## Masks
<?# Example symbol="M:Prompt.Program.AskPasswordWithCustomMask" project="Prompt" /?> <?# Example symbol="M:Prompt.Program.AskPasswordWithCustomMask" project="Prompt" /?>
```text ```text
Enter password: ------------_ Enter password: ------------_
``` ```
You can utilize a null character to completely hide input. You can utilize a null character to completely hide input.
<?# Example symbol="M:Prompt.Program.AskPasswordWithNullMask" project="Prompt" /?> <?# Example symbol="M:Prompt.Program.AskPasswordWithNullMask" project="Prompt" /?>
```text ```text
Enter password: _ Enter password: _
``` ```

View File

@@ -8,9 +8,10 @@ to install the NuGet package.
```text ```text
> dotnet add package Spectre.Console > 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. Once that is done, you can start using all the available features.
```csharp ```csharp

View File

@@ -132,3 +132,13 @@ AnsiConsole.Write(new BreakdownChart()
.AddItem(new Fruit("Mango", 3, Color.Orange4)) .AddItem(new Fruit("Mango", 3, Color.Orange4))
.AddItems(items)); .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 Order: 40
RedirectFrom: calendar RedirectFrom: calendar
Description: "The **Calendar** is used to render a calendar to the terminal." 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.Common;
using Statiq.Web; using Statiq.Web;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,119 +1,119 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Playwright; using Microsoft.Playwright;
using Statiq.Common; using Statiq.Common;
using Statiq.Core; using Statiq.Core;
using Statiq.Web; using Statiq.Web;
using Statiq.Web.Modules; using Statiq.Web.Modules;
using Statiq.Web.Pipelines; using Statiq.Web.Pipelines;
namespace Docs.Pipelines namespace Docs.Pipelines
{ {
public class SocialImages : Pipeline public class SocialImages : Pipeline
{ {
public SocialImages() public SocialImages()
{ {
Dependencies.AddRange(nameof(Inputs)); Dependencies.AddRange(nameof(Inputs));
ProcessModules = new ModuleList ProcessModules = new ModuleList
{ {
new GetPipelineDocuments(ContentType.Content), new GetPipelineDocuments(ContentType.Content),
// Filter to non-archive content // Filter to non-archive content
new FilterDocuments(Config.FromDocument(doc => !Archives.IsArchive(doc))), new FilterDocuments(Config.FromDocument(doc => !Archives.IsArchive(doc))),
// Process the content // Process the content
new CacheDocuments new CacheDocuments
{ {
new AddTitle(), new AddTitle(),
new SetDestination(true), new SetDestination(true),
new ExecuteIf(Config.FromSetting(WebKeys.OptimizeContentFileNames, true)) new ExecuteIf(Config.FromSetting(WebKeys.OptimizeContentFileNames, true))
{ {
new OptimizeFileName() new OptimizeFileName()
}, },
new GenerateSocialImage(), new GenerateSocialImage(),
} }
}; };
OutputModules = new ModuleList { new WriteFiles() }; OutputModules = new ModuleList { new WriteFiles() };
} }
} }
class GenerateSocialImage : ParallelModule class GenerateSocialImage : ParallelModule
{ {
private IPlaywright _playwright; private IPlaywright _playwright;
private IBrowser _browser; private IBrowser _browser;
private WebApplication _app; private WebApplication _app;
private IBrowserContext _context; private IBrowserContext _context;
protected override async Task BeforeExecutionAsync(IExecutionContext context) protected override async Task BeforeExecutionAsync(IExecutionContext context)
{ {
var builder = WebApplication.CreateBuilder(); var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders(); builder.Logging.ClearProviders();
builder.Services builder.Services
.AddRazorPages() .AddRazorPages()
.WithRazorPagesRoot("/src/SocialCards/"); .WithRazorPagesRoot("/src/SocialCards/");
_app = builder.Build(); _app = builder.Build();
_app.MapRazorPages(); _app.MapRazorPages();
_app.UseStaticFiles(new StaticFileOptions _app.UseStaticFiles(new StaticFileOptions
{ {
FileProvider = new PhysicalFileProvider( FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "src/SocialCards")), Path.Combine(builder.Environment.ContentRootPath, "src/SocialCards")),
RequestPath = "/static" RequestPath = "/static"
}); });
await _app.StartAsync().ConfigureAwait(false); await _app.StartAsync().ConfigureAwait(false);
_playwright = await Playwright.CreateAsync().ConfigureAwait(false); _playwright = await Playwright.CreateAsync().ConfigureAwait(false);
_browser = await _playwright.Chromium.LaunchAsync().ConfigureAwait(false); _browser = await _playwright.Chromium.LaunchAsync().ConfigureAwait(false);
_context = await _browser.NewContextAsync(new BrowserNewContextOptions { _context = await _browser.NewContextAsync(new BrowserNewContextOptions {
ViewportSize = new ViewportSize { Width = 1200, Height = 618 }, ViewportSize = new ViewportSize { Width = 1200, Height = 618 },
}).ConfigureAwait(false); }).ConfigureAwait(false);
} }
protected override async Task FinallyAsync(IExecutionContext context) protected override async Task FinallyAsync(IExecutionContext context)
{ {
await _context.DisposeAsync().ConfigureAwait(false); await _context.DisposeAsync().ConfigureAwait(false);
await _browser.DisposeAsync().ConfigureAwait(false); await _browser.DisposeAsync().ConfigureAwait(false);
_playwright.Dispose(); _playwright.Dispose();
await _app.DisposeAsync().ConfigureAwait(false); await _app.DisposeAsync().ConfigureAwait(false);
await base.FinallyAsync(context); await base.FinallyAsync(context);
} }
protected override async Task<IEnumerable<IDocument>> ExecuteInputAsync(IDocument input, IExecutionContext context) protected override async Task<IEnumerable<IDocument>> ExecuteInputAsync(IDocument input, IExecutionContext context)
{ {
var url = _app.Urls.FirstOrDefault(u => u.StartsWith("http://")); var url = _app.Urls.FirstOrDefault(u => u.StartsWith("http://"));
var page = await _context.NewPageAsync().ConfigureAwait(false); var page = await _context.NewPageAsync().ConfigureAwait(false);
var title = input.GetString("Title"); var title = input.GetString("Title");
var description = input.GetString("Description"); var description = input.GetString("Description");
var highlights = input.GetList<string>("Highlights") ?? Array.Empty<string>(); var highlights = input.GetList<string>("Highlights") ?? Array.Empty<string>();
await page.GotoAsync($"{url}/?title={title}&desc={description}&highlights={string.Join("||", highlights)}"); await page.GotoAsync($"{url}/?title={title}&desc={description}&highlights={string.Join("||", highlights)}");
// This will not just wait for the page to load over the network, but it'll also give // This will not just wait for the page to load over the network, but it'll also give
// chrome a chance to complete rendering of the fonts while the wait timeout completes. // chrome a chance to complete rendering of the fonts while the wait timeout completes.
await page.WaitForLoadStateAsync(LoadState.NetworkIdle).ConfigureAwait(false); await page.WaitForLoadStateAsync(LoadState.NetworkIdle).ConfigureAwait(false);
var bytes = await page.ScreenshotAsync().ConfigureAwait(false); var bytes = await page.ScreenshotAsync().ConfigureAwait(false);
await page.CloseAsync().ConfigureAwait(false); await page.CloseAsync().ConfigureAwait(false);
var destination = input.Destination.InsertSuffix("-social").ChangeExtension("png"); var destination = input.Destination.InsertSuffix("-social").ChangeExtension("png");
var doc = context.CreateDocument( var doc = context.CreateDocument(
input.Source, input.Source,
destination, destination,
new MetadataItems { { "DocId", input.Id }}, new MetadataItems { { "DocId", input.Id }},
context.GetContentProvider(bytes)); context.GetContentProvider(bytes));
return new[] { doc }; return new[] { doc };
} }
} }
} }

View File

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

View File

@@ -15,7 +15,7 @@
<div id="container"> <div id="container">
<div id="console"> <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"><span style="color:var(--brightBlack)">╰─</span> dotnet run</div>
<div class="line"></div> <div class="line"></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.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;

View File

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

View File

@@ -15,22 +15,22 @@ public static class Program
{ {
config.SetApplicationName("fake-dotnet"); config.SetApplicationName("fake-dotnet");
config.ValidateExamples(); config.ValidateExamples();
config.AddExample("run", "--no-build"); config.AddExample("run", "--no-build");
// Run // Run
config.AddCommand<RunCommand>("run"); config.AddCommand<RunCommand>("run");
// Add // Add
config.AddBranch<AddSettings>("add", add => config.AddBranch<AddSettings>("add", add =>
{ {
add.SetDescription("Add a package or reference to a .NET project"); add.SetDescription("Add a package or reference to a .NET project");
add.AddCommand<AddPackageCommand>("package"); add.AddCommand<AddPackageCommand>("package");
add.AddCommand<AddReferenceCommand>("reference"); add.AddCommand<AddReferenceCommand>("reference");
}); });
// Serve // Serve
config.AddCommand<ServeCommand>("serve") config.AddCommand<ServeCommand>("serve")
.WithExample("serve", "-o", "firefox") .WithExample("serve", "-o", "firefox")
.WithExample("serve", "--port", "80", "-o", "firefox"); .WithExample("serve", "--port", "80", "-o", "firefox");
}); });

View File

@@ -1,30 +1,30 @@
using System.Linq; using System.Linq;
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Cli; using Spectre.Console.Cli;
using Spectre.Console.Cli.Help; using Spectre.Console.Cli.Help;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
namespace Help; namespace Help;
/// <summary> /// <summary>
/// Example showing how to extend the built-in Spectre.Console help provider /// Example showing how to extend the built-in Spectre.Console help provider
/// by rendering a custom banner at the top of the help information /// by rendering a custom banner at the top of the help information
/// </summary> /// </summary>
internal class CustomHelpProvider : HelpProvider internal class CustomHelpProvider : HelpProvider
{ {
public CustomHelpProvider(ICommandAppSettings settings) public CustomHelpProvider(ICommandAppSettings settings)
: base(settings) : base(settings)
{ {
} }
public override IEnumerable<IRenderable> GetHeader(ICommandModel model, ICommandInfo? command) public override IEnumerable<IRenderable> GetHeader(ICommandModel model, ICommandInfo? command)
{ {
return new[] return new[]
{ {
new Text("--------------------------------------"), Text.NewLine, new Text("--------------------------------------"), Text.NewLine,
new Text("--- CUSTOM HELP PROVIDER ---"), Text.NewLine, new Text("--- CUSTOM HELP PROVIDER ---"), Text.NewLine,
new Text("--------------------------------------"), Text.NewLine, new Text("--------------------------------------"), Text.NewLine,
Text.NewLine, Text.NewLine,
}; };
} }
} }

View File

@@ -1,20 +1,20 @@
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Cli; using Spectre.Console.Cli;
namespace Help; namespace Help;
public sealed class DefaultCommand : Command public sealed class DefaultCommand : Command
{ {
private IAnsiConsole _console; private IAnsiConsole _console;
public DefaultCommand(IAnsiConsole console) public DefaultCommand(IAnsiConsole console)
{ {
_console = console; _console = console;
} }
public override int Execute(CommandContext context) public override int Execute(CommandContext context)
{ {
_console.WriteLine("Hello world"); _console.WriteLine("Hello world");
return 0; return 0;
} }
} }

View File

@@ -1,19 +1,23 @@
using Spectre.Console.Cli; using Spectre.Console.Cli;
using Spectre.Console.Cli.Help;
namespace Help;
namespace Help;
public static class Program
{ public static class Program
public static int Main(string[] args) {
{ public static int Main(string[] args)
var app = new CommandApp<DefaultCommand>(); {
var app = new CommandApp<DefaultCommand>();
app.Configure(config =>
{ app.Configure(config =>
// Register the custom help provider {
config.SetHelpProvider(new CustomHelpProvider(config.Settings)); // Register the custom help provider
}); config.SetHelpProvider(new CustomHelpProvider(config.Settings));
return app.Run(args); // Render an unstyled help text for maximum accessibility
} config.Settings.HelpProviderStyles = null;
} });
return app.Run(args);
}
}

View File

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

View File

@@ -12,9 +12,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Serilog" Version="2.11.0" /> <PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" /> <PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Map" Version="1.0.2" /> <PackageReference Include="Serilog.Sinks.Map" Version="1.0.2" />
</ItemGroup> </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() 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. // Draw an image using CanvasImage powered by ImageSharp.
// This requires the "Spectre.Console.ImageSharp" NuGet package. // This requires the "Spectre.Console.ImageSharp" NuGet package.
var image = new CanvasImage("cake.png"); var image = new CanvasImage("cake.png");
@@ -21,7 +17,7 @@ public static class Program
image.MaxWidth(16); image.MaxWidth(16);
Render(image, "Image from file (16 wide)"); Render(image, "Image from file (16 wide)");
// Draw image again, but without render width // Draw image again, but without max width
image.NoMaxWidth(); image.NoMaxWidth();
image.Mutate(ctx => ctx.Grayscale().Rotate(-45).EntropyCrop()); image.Mutate(ctx => ctx.Grayscale().Rotate(-45).EntropyCrop());
Render(image, "Image from file (fit, greyscale, rotated)"); Render(image, "Image from file (fit, greyscale, rotated)");

View File

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

View File

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

View File

@@ -33,11 +33,11 @@ namespace Prompt
var age = AskAge(); var age = AskAge();
WriteDivider("Secrets"); WriteDivider("Secrets");
var password = AskPassword(); var password = AskPassword();
WriteDivider("Mask"); WriteDivider("Mask");
var mask = AskPasswordWithCustomMask(); var mask = AskPasswordWithCustomMask();
WriteDivider("Null Mask"); WriteDivider("Null Mask");
var nullMask = AskPasswordWithNullMask(); var nullMask = AskPasswordWithNullMask();
@@ -54,8 +54,8 @@ namespace Prompt
.AddRow("[grey]Favorite fruit[/]", fruit) .AddRow("[grey]Favorite fruit[/]", fruit)
.AddRow("[grey]Favorite sport[/]", sport) .AddRow("[grey]Favorite sport[/]", sport)
.AddRow("[grey]Age[/]", age.ToString()) .AddRow("[grey]Age[/]", age.ToString())
.AddRow("[grey]Password[/]", password) .AddRow("[grey]Password[/]", password)
.AddRow("[grey]Mask[/]", mask) .AddRow("[grey]Mask[/]", mask)
.AddRow("[grey]Null Mask[/]", nullMask) .AddRow("[grey]Null Mask[/]", nullMask)
.AddRow("[grey]Favorite color[/]", string.IsNullOrEmpty(color) ? "Unknown" : color)); .AddRow("[grey]Favorite color[/]", string.IsNullOrEmpty(color) ? "Unknown" : color));
} }
@@ -110,6 +110,7 @@ namespace Prompt
{ {
fruit = AnsiConsole.Prompt( fruit = AnsiConsole.Prompt(
new SelectionPrompt<string>() new SelectionPrompt<string>()
.EnableSearch()
.Title("Ok, but if you could only choose [green]one[/]?") .Title("Ok, but if you could only choose [green]one[/]?")
.MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]") .MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]")
.AddChoices(favorites)); .AddChoices(favorites));
@@ -153,22 +154,22 @@ namespace Prompt
new TextPrompt<string>("Enter [green]password[/]?") new TextPrompt<string>("Enter [green]password[/]?")
.PromptStyle("red") .PromptStyle("red")
.Secret()); .Secret());
} }
public static string AskPasswordWithCustomMask() public static string AskPasswordWithCustomMask()
{ {
return AnsiConsole.Prompt( return AnsiConsole.Prompt(
new TextPrompt<string>("Enter [green]password[/]?") new TextPrompt<string>("Enter [green]password[/]?")
.PromptStyle("red") .PromptStyle("red")
.Secret('-')); .Secret('-'));
} }
public static string AskPasswordWithNullMask() public static string AskPasswordWithNullMask()
{ {
return AnsiConsole.Prompt( return AnsiConsole.Prompt(
new TextPrompt<string>("Enter [green]password[/]?") new TextPrompt<string>("Enter [green]password[/]?")
.PromptStyle("red") .PromptStyle("red")
.Secret(null)); .Secret(null));
} }
public static string AskColor() public static string AskColor()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Simplification; using Microsoft.CodeAnalysis.Simplification;
namespace Spectre.Console.Analyzer.CodeActions; namespace Spectre.Console.Analyzer.CodeActions;
@@ -32,171 +32,171 @@ public class SwitchToAnsiConsoleAction : CodeAction
/// <inheritdoc /> /// <inheritdoc />
protected override async Task<Document> GetChangedDocumentAsync(CancellationToken cancellationToken) protected override async Task<Document> GetChangedDocumentAsync(CancellationToken cancellationToken)
{ {
var editor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false); var editor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false);
var compilation = editor.SemanticModel.Compilation; var compilation = editor.SemanticModel.Compilation;
var operation = editor.SemanticModel.GetOperation(_originalInvocation, cancellationToken) as IInvocationOperation; var operation = editor.SemanticModel.GetOperation(_originalInvocation, cancellationToken) as IInvocationOperation;
if (operation == null) if (operation == null)
{ {
return _document; return _document;
} }
// If there is an IAnsiConsole passed into the method then we'll use it. // If there is an IAnsiConsole passed into the method then we'll use it.
// otherwise we'll check for a field level instance. // otherwise we'll check for a field level instance.
// if neither of those exist we'll fall back to the static param. // if neither of those exist we'll fall back to the static param.
var spectreConsoleSymbol = compilation.GetTypeByMetadataName("Spectre.Console.AnsiConsole"); var spectreConsoleSymbol = compilation.GetTypeByMetadataName("Spectre.Console.AnsiConsole");
var iansiConsoleSymbol = compilation.GetTypeByMetadataName("Spectre.Console.IAnsiConsole"); var iansiConsoleSymbol = compilation.GetTypeByMetadataName("Spectre.Console.IAnsiConsole");
ISymbol? accessibleConsoleSymbol = spectreConsoleSymbol; ISymbol? accessibleConsoleSymbol = spectreConsoleSymbol;
if (iansiConsoleSymbol != null) if (iansiConsoleSymbol != null)
{ {
var isInStaticContext = IsInStaticContext(operation, cancellationToken, out var parentStaticMemberStartPosition); var isInStaticContext = IsInStaticContext(operation, cancellationToken, out var parentStaticMemberStartPosition);
foreach (var symbol in editor.SemanticModel.LookupSymbols(operation.Syntax.GetLocation().SourceSpan.Start)) foreach (var symbol in editor.SemanticModel.LookupSymbols(operation.Syntax.GetLocation().SourceSpan.Start))
{ {
// LookupSymbols check the accessibility of the symbol, but it can // LookupSymbols check the accessibility of the symbol, but it can
// suggest instance members when the current context is static. // suggest instance members when the current context is static.
var symbolType = symbol switch var symbolType = symbol switch
{ {
IParameterSymbol parameter => parameter.Type, IParameterSymbol parameter => parameter.Type,
IFieldSymbol field when !isInStaticContext || field.IsStatic => field.Type, IFieldSymbol field when !isInStaticContext || field.IsStatic => field.Type,
IPropertySymbol { GetMethod: not null } property when !isInStaticContext || property.IsStatic => property.Type, IPropertySymbol { GetMethod: not null } property when !isInStaticContext || property.IsStatic => property.Type,
ILocalSymbol local => local.Type, ILocalSymbol local => local.Type,
_ => null, _ => null,
}; };
// Locals can be returned even if there are not valid in the current context. For instance, // Locals can be returned even if there are not valid in the current context. For instance,
// it can return locals declared after the current location. Or it can return locals that // it can return locals declared after the current location. Or it can return locals that
// should not be accessible in a static local function. // should not be accessible in a static local function.
// //
// void Sample() // void Sample()
// { // {
// int local = 0; // int local = 0;
// static void LocalFunction() => local; <-- local is invalid here but LookupSymbols suggests it // static void LocalFunction() => local; <-- local is invalid here but LookupSymbols suggests it
// } // }
// //
// Parameters from the ancestor methods or local functions are also returned even if the operation is in a static local function. // Parameters from the ancestor methods or local functions are also returned even if the operation is in a static local function.
if (symbol.Kind is SymbolKind.Local or SymbolKind.Parameter) if (symbol.Kind is SymbolKind.Local or SymbolKind.Parameter)
{ {
var localPosition = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken).GetLocation().SourceSpan.Start; var localPosition = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(cancellationToken).GetLocation().SourceSpan.Start;
// The local is not part of the source tree // The local is not part of the source tree
if (localPosition == null) if (localPosition == null)
{ {
break; break;
} }
// The local is declared after the current expression // The local is declared after the current expression
if (localPosition > _originalInvocation.Span.Start) if (localPosition > _originalInvocation.Span.Start)
{ {
break; break;
} }
// The local is declared outside the static local function // The local is declared outside the static local function
if (isInStaticContext && localPosition < parentStaticMemberStartPosition) if (isInStaticContext && localPosition < parentStaticMemberStartPosition)
{ {
break; break;
} }
} }
if (IsOrImplementSymbol(symbolType, iansiConsoleSymbol)) if (IsOrImplementSymbol(symbolType, iansiConsoleSymbol))
{ {
accessibleConsoleSymbol = symbol; accessibleConsoleSymbol = symbol;
break; break;
} }
} }
} }
if (accessibleConsoleSymbol == null) if (accessibleConsoleSymbol == null)
{ {
return _document; return _document;
} }
// Replace the original invocation // Replace the original invocation
var generator = editor.Generator; var generator = editor.Generator;
var consoleExpression = accessibleConsoleSymbol switch var consoleExpression = accessibleConsoleSymbol switch
{ {
ITypeSymbol typeSymbol => generator.TypeExpression(typeSymbol, addImport: true).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation), ITypeSymbol typeSymbol => generator.TypeExpression(typeSymbol, addImport: true).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation),
_ => generator.IdentifierName(accessibleConsoleSymbol.Name), _ => generator.IdentifierName(accessibleConsoleSymbol.Name),
}; };
var newExpression = generator.InvocationExpression(generator.MemberAccessExpression(consoleExpression, operation.TargetMethod.Name), _originalInvocation.ArgumentList.Arguments) var newExpression = generator.InvocationExpression(generator.MemberAccessExpression(consoleExpression, operation.TargetMethod.Name), _originalInvocation.ArgumentList.Arguments)
.WithLeadingTrivia(_originalInvocation.GetLeadingTrivia()) .WithLeadingTrivia(_originalInvocation.GetLeadingTrivia())
.WithTrailingTrivia(_originalInvocation.GetTrailingTrivia()); .WithTrailingTrivia(_originalInvocation.GetTrailingTrivia());
editor.ReplaceNode(_originalInvocation, newExpression); editor.ReplaceNode(_originalInvocation, newExpression);
return editor.GetChangedDocument(); return editor.GetChangedDocument();
} }
private static bool IsOrImplementSymbol(ITypeSymbol? symbol, ITypeSymbol interfaceSymbol) private static bool IsOrImplementSymbol(ITypeSymbol? symbol, ITypeSymbol interfaceSymbol)
{ {
if (symbol == null) if (symbol == null)
{ {
return false; return false;
} }
if (SymbolEqualityComparer.Default.Equals(symbol, interfaceSymbol)) if (SymbolEqualityComparer.Default.Equals(symbol, interfaceSymbol))
{ {
return true; return true;
} }
foreach (var iface in symbol.AllInterfaces) foreach (var iface in symbol.AllInterfaces)
{ {
if (SymbolEqualityComparer.Default.Equals(iface, interfaceSymbol)) if (SymbolEqualityComparer.Default.Equals(iface, interfaceSymbol))
{ {
return true; return true;
} }
} }
return false; return false;
} }
private static bool IsInStaticContext(IOperation operation, CancellationToken cancellationToken, out int parentStaticMemberStartPosition) private static bool IsInStaticContext(IOperation operation, CancellationToken cancellationToken, out int parentStaticMemberStartPosition)
{ {
// Local functions can be nested, and an instance local function can be declared // Local functions can be nested, and an instance local function can be declared
// in a static local function. So, you need to continue to check ancestors when a // in a static local function. So, you need to continue to check ancestors when a
// local function is not static. // local function is not static.
foreach (var member in operation.Syntax.Ancestors()) foreach (var member in operation.Syntax.Ancestors())
{ {
if (member is LocalFunctionStatementSyntax localFunction) if (member is LocalFunctionStatementSyntax localFunction)
{ {
var symbol = operation.SemanticModel!.GetDeclaredSymbol(localFunction, cancellationToken); var symbol = operation.SemanticModel!.GetDeclaredSymbol(localFunction, cancellationToken);
if (symbol != null && symbol.IsStatic) if (symbol != null && symbol.IsStatic)
{ {
parentStaticMemberStartPosition = localFunction.GetLocation().SourceSpan.Start; parentStaticMemberStartPosition = localFunction.GetLocation().SourceSpan.Start;
return true; return true;
} }
} }
else if (member is LambdaExpressionSyntax lambdaExpression) else if (member is LambdaExpressionSyntax lambdaExpression)
{ {
var symbol = operation.SemanticModel!.GetSymbolInfo(lambdaExpression, cancellationToken).Symbol; var symbol = operation.SemanticModel!.GetSymbolInfo(lambdaExpression, cancellationToken).Symbol;
if (symbol != null && symbol.IsStatic) if (symbol != null && symbol.IsStatic)
{ {
parentStaticMemberStartPosition = lambdaExpression.GetLocation().SourceSpan.Start; parentStaticMemberStartPosition = lambdaExpression.GetLocation().SourceSpan.Start;
return true; return true;
} }
} }
else if (member is AnonymousMethodExpressionSyntax anonymousMethod) else if (member is AnonymousMethodExpressionSyntax anonymousMethod)
{ {
var symbol = operation.SemanticModel!.GetSymbolInfo(anonymousMethod, cancellationToken).Symbol; var symbol = operation.SemanticModel!.GetSymbolInfo(anonymousMethod, cancellationToken).Symbol;
if (symbol != null && symbol.IsStatic) if (symbol != null && symbol.IsStatic)
{ {
parentStaticMemberStartPosition = anonymousMethod.GetLocation().SourceSpan.Start; parentStaticMemberStartPosition = anonymousMethod.GetLocation().SourceSpan.Start;
return true; return true;
} }
} }
else if (member is MethodDeclarationSyntax methodDeclaration) else if (member is MethodDeclarationSyntax methodDeclaration)
{ {
parentStaticMemberStartPosition = methodDeclaration.GetLocation().SourceSpan.Start; parentStaticMemberStartPosition = methodDeclaration.GetLocation().SourceSpan.Start;
var symbol = operation.SemanticModel!.GetDeclaredSymbol(methodDeclaration, cancellationToken); var symbol = operation.SemanticModel!.GetDeclaredSymbol(methodDeclaration, cancellationToken);
return symbol != null && symbol.IsStatic; return symbol != null && symbol.IsStatic;
} }
} }
parentStaticMemberStartPosition = -1; parentStaticMemberStartPosition = -1;
return false; return false;
} }
} }

View File

@@ -8,20 +8,21 @@
<IncludeBuildOutput>false</IncludeBuildOutput> <IncludeBuildOutput>false</IncludeBuildOutput>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<NoPackageAnalysis>true</NoPackageAnalysis> <NoPackageAnalysis>true</NoPackageAnalysis>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="..\stylecop.json" Link="Properties/stylecop.json" /> <AdditionalFiles Include="..\stylecop.json" Link="Properties/stylecop.json" />
<None Include="../../resources/gfx/small-logo.png" Pack="true" PackagePath="\" Link="Properties/small-logo.png" /> <None Include="../../resources/gfx/small-logo.png" Pack="true" PackagePath="\" Link="Properties/small-logo.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.2.0" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis" Version="4.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.2.0" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" PrivateAssets="all" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="bin\Debug\netstandard2.0\\Spectre.Console.Analyzer.dll" /> <None Remove="bin\Debug\netstandard2.0\\Spectre.Console.Analyzer.dll" />
</ItemGroup> </ItemGroup>

View File

@@ -1,5 +1,5 @@
using Spectre.Console.Cli.Internal.Configuration; using Spectre.Console.Cli.Internal.Configuration;
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
/// <summary> /// <summary>
@@ -102,7 +102,7 @@ public sealed class CommandApp : ICommandApp
if (_configurator.Settings.ExceptionHandler != null) if (_configurator.Settings.ExceptionHandler != null)
{ {
return _configurator.Settings.ExceptionHandler(ex); return _configurator.Settings.ExceptionHandler(ex, null);
} }
// Render the exception. // Render the exception.

View File

@@ -1,5 +1,5 @@
using Spectre.Console.Cli.Internal.Configuration; using Spectre.Console.Cli.Internal.Configuration;
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
/// <summary> /// <summary>
@@ -49,8 +49,8 @@ public sealed class CommandApp<TDefaultCommand> : ICommandApp
public Task<int> RunAsync(IEnumerable<string> args) public Task<int> RunAsync(IEnumerable<string> args)
{ {
return _app.RunAsync(args); return _app.RunAsync(args);
} }
internal Configurator GetConfigurator() internal Configurator GetConfigurator()
{ {
return _app.GetConfigurator(); return _app.GetConfigurator();

View File

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

View File

@@ -5,40 +5,40 @@ namespace Spectre.Console.Cli;
/// and <see cref="IConfigurator{TSettings}"/>. /// and <see cref="IConfigurator{TSettings}"/>.
/// </summary> /// </summary>
public static class ConfiguratorExtensions public static class ConfiguratorExtensions
{ {
/// <summary> /// <summary>
/// Sets the help provider for the application. /// Sets the help provider for the application.
/// </summary> /// </summary>
/// <param name="configurator">The configurator.</param> /// <param name="configurator">The configurator.</param>
/// <param name="helpProvider">The help provider to use.</param> /// <param name="helpProvider">The help provider to use.</param>
/// <returns>A configurator that can be used to configure the application further.</returns> /// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator SetHelpProvider(this IConfigurator configurator, IHelpProvider helpProvider) public static IConfigurator SetHelpProvider(this IConfigurator configurator, IHelpProvider helpProvider)
{ {
if (configurator == null) if (configurator == null)
{ {
throw new ArgumentNullException(nameof(configurator)); throw new ArgumentNullException(nameof(configurator));
} }
configurator.SetHelpProvider(helpProvider); configurator.SetHelpProvider(helpProvider);
return configurator; return configurator;
} }
/// <summary> /// <summary>
/// Sets the help provider for the application. /// Sets the help provider for the application.
/// </summary> /// </summary>
/// <param name="configurator">The configurator.</param> /// <param name="configurator">The configurator.</param>
/// <typeparam name="T">The type of the help provider to instantiate at runtime and use.</typeparam> /// <typeparam name="T">The type of the help provider to instantiate at runtime and use.</typeparam>
/// <returns>A configurator that can be used to configure the application further.</returns> /// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator SetHelpProvider<T>(this IConfigurator configurator) public static IConfigurator SetHelpProvider<T>(this IConfigurator configurator)
where T : IHelpProvider where T : IHelpProvider
{ {
if (configurator == null) if (configurator == null)
{ {
throw new ArgumentNullException(nameof(configurator)); throw new ArgumentNullException(nameof(configurator));
} }
configurator.SetHelpProvider<T>(); configurator.SetHelpProvider<T>();
return configurator; return configurator;
} }
/// <summary> /// <summary>
@@ -82,7 +82,7 @@ public static class ConfiguratorExtensions
} }
/// <summary> /// <summary>
/// Overrides the auto-detected version of the application. /// Sets the version of the application.
/// </summary> /// </summary>
/// <param name="configurator">The configurator.</param> /// <param name="configurator">The configurator.</param>
/// <param name="version">The version of application.</param> /// <param name="version">The version of application.</param>
@@ -98,6 +98,25 @@ public static class ConfiguratorExtensions
return configurator; 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> /// <summary>
/// Hides the <c>DEFAULT</c> column that lists default values coming from the /// Hides the <c>DEFAULT</c> column that lists default values coming from the
/// <see cref="DefaultValueAttribute"/> in the options help text. /// <see cref="DefaultValueAttribute"/> in the options help text.
@@ -229,7 +248,7 @@ public static class ConfiguratorExtensions
throw new ArgumentNullException(nameof(configurator)); throw new ArgumentNullException(nameof(configurator));
} }
configurator.Settings.Interceptor = interceptor; configurator.Settings.Registrar.RegisterInstance<ICommandInterceptor>(interceptor);
return configurator; return configurator;
} }
@@ -324,11 +343,16 @@ public static class ConfiguratorExtensions
/// <param name="func">The delegate to execute as part of command execution.</param> /// <param name="func">The delegate to execute as part of command execution.</param>
/// <returns>A command configurator that can be used to configure the command further.</returns> /// <returns>A command configurator that can be used to configure the command further.</returns>
public static ICommandConfigurator AddDelegate<TSettings>( public static ICommandConfigurator AddDelegate<TSettings>(
this IConfigurator<TSettings> configurator, this IConfigurator<TSettings>? configurator,
string name, string name,
Func<CommandContext, int> func) Func<CommandContext, int> func)
where TSettings : CommandSettings where TSettings : CommandSettings
{ {
if (typeof(TSettings).IsAbstract)
{
AddDelegate(configurator as IConfigurator<EmptyCommandSettings>, name, func);
}
if (configurator == null) if (configurator == null)
{ {
throw new ArgumentNullException(nameof(configurator)); throw new ArgumentNullException(nameof(configurator));
@@ -367,11 +391,11 @@ public static class ConfiguratorExtensions
/// <param name="configurator">The configurator.</param> /// <param name="configurator">The configurator.</param>
/// <param name="exceptionHandler">The Action that handles the exception.</param> /// <param name="exceptionHandler">The Action that handles the exception.</param>
/// <returns>A configurator that can be used to configure the application further.</returns> /// <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; return -1;
}); });
} }
@@ -382,7 +406,7 @@ public static class ConfiguratorExtensions
/// <param name="configurator">The configurator.</param> /// <param name="configurator">The configurator.</param>
/// <param name="exceptionHandler">The Action that handles the exception.</param> /// <param name="exceptionHandler">The Action that handles the exception.</param>
/// <returns>A configurator that can be used to configure the application further.</returns> /// <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) if (configurator == null)
{ {

View File

@@ -1,5 +1,3 @@
using Spectre.Console.Cli.Resources;
namespace Spectre.Console.Cli.Help; namespace Spectre.Console.Cli.Help;
/// <summary> /// <summary>
@@ -10,23 +8,32 @@ namespace Spectre.Console.Cli.Help;
/// </remarks> /// </remarks>
public class HelpProvider : IHelpProvider 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. /// <summary>
/// </summary> /// Gets a value indicating how many examples from direct children to show in the help text.
protected virtual int MaximumIndirectExamples { get; } /// </summary>
protected virtual int MaximumIndirectExamples { get; }
/// <summary>
/// Gets a value indicating whether any default values for command options are shown in the help text. /// <summary>
/// </summary> /// Gets a value indicating whether any default values for command options are shown in the help text.
protected virtual bool ShowOptionDefaultValues { get; } /// </summary>
protected virtual bool ShowOptionDefaultValues { get; }
/// <summary>
/// Gets a value indicating whether a trailing period of a command description is trimmed in the help text. /// <summary>
/// </summary> /// Gets a value indicating whether a trailing period of a command description is trimmed in the help text.
protected virtual bool TrimTrailingPeriod { get; } /// </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 private sealed class HelpArgument
{ {
public string Name { get; } public string Name { get; }
@@ -34,7 +41,7 @@ public class HelpProvider : IHelpProvider
public bool Required { get; } public bool Required { get; }
public string? Description { 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; Name = name;
Position = position; Position = position;
@@ -61,7 +68,7 @@ public class HelpProvider : IHelpProvider
public string? Description { get; } public string? Description { get; }
public object? DefaultValue { 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; Short = @short;
Long = @long; Long = @long;
@@ -71,17 +78,27 @@ public class HelpProvider : IHelpProvider
DefaultValue = defaultValue; 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>(); var parameters = new List<HelpOption>
parameters.Add(new HelpOption("h", "help", null, null, resources.PrintHelpDescription, null)); {
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 // Version information applies to the entire application
if ((command == null || command?.Parent == null) && !(command?.IsBranch ?? false)) // 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
parameters.Add(new HelpOption("v", "version", null, null, resources.PrintVersionDescription, null)); 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 => parameters.AddRange(command?.Parameters.OfType<ICommandOption>().Where(o => !o.IsHidden).Select(o =>
@@ -92,55 +109,59 @@ public class HelpProvider : IHelpProvider
?? Array.Empty<HelpOption>()); ?? Array.Empty<HelpOption>());
return parameters; return parameters;
} }
} }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HelpProvider"/> class. /// Initializes a new instance of the <see cref="HelpProvider"/> class.
/// </summary> /// </summary>
/// <param name="settings">The command line application settings used for configuration.</param> /// <param name="settings">The command line application settings used for configuration.</param>
public HelpProvider(ICommandAppSettings settings) public HelpProvider(ICommandAppSettings settings)
{ {
this.ShowOptionDefaultValues = settings.ShowOptionDefaultValues; this.ShowOptionDefaultValues = settings.ShowOptionDefaultValues;
this.MaximumIndirectExamples = settings.MaximumIndirectExamples; this.MaximumIndirectExamples = settings.MaximumIndirectExamples;
this.TrimTrailingPeriod = settings.TrimTrailingPeriod; this.TrimTrailingPeriod = settings.TrimTrailingPeriod;
resources = new HelpProviderResources(settings.Culture); // 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;
/// <inheritdoc/>
resources = new HelpProviderResources(settings.Culture);
}
/// <inheritdoc/>
public virtual IEnumerable<IRenderable> Write(ICommandModel model, ICommandInfo? command) public virtual IEnumerable<IRenderable> Write(ICommandModel model, ICommandInfo? command)
{ {
var result = new List<IRenderable>(); var result = new List<IRenderable>();
result.AddRange(GetHeader(model, command)); result.AddRange(GetHeader(model, command));
result.AddRange(GetDescription(model, command)); result.AddRange(GetDescription(model, command));
result.AddRange(GetUsage(model, command)); result.AddRange(GetUsage(model, command));
result.AddRange(GetExamples(model, command)); result.AddRange(GetExamples(model, command));
result.AddRange(GetArguments(model, command)); result.AddRange(GetArguments(model, command));
result.AddRange(GetOptions(model, command)); result.AddRange(GetOptions(model, command));
result.AddRange(GetCommands(model, command)); result.AddRange(GetCommands(model, command));
result.AddRange(GetFooter(model, command)); result.AddRange(GetFooter(model, command));
return result; return result;
} }
/// <summary> /// <summary>
/// Gets the header for the help information. /// Gets the header for the help information.
/// </summary> /// </summary>
/// <param name="model">The command model to write help for.</param> /// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param> /// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns> /// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetHeader(ICommandModel model, ICommandInfo? command) public virtual IEnumerable<IRenderable> GetHeader(ICommandModel model, ICommandInfo? command)
{ {
yield break; yield break;
} }
/// <summary> /// <summary>
/// Gets the description section of the help information. /// Gets the description section of the help information.
/// </summary> /// </summary>
/// <param name="model">The command model to write help for.</param> /// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param> /// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns> /// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetDescription(ICommandModel model, ICommandInfo? command) public virtual IEnumerable<IRenderable> GetDescription(ICommandModel model, ICommandInfo? command)
{ {
if (command?.Description == null) if (command?.Description == null)
@@ -148,30 +169,30 @@ public class HelpProvider : IHelpProvider
yield break; yield break;
} }
var composer = new Composer(); var composer = NewComposer();
composer.Style("yellow", $"{resources.Description}:").LineBreak(); composer.Style(helpStyles?.Description?.Header ?? Style.Plain, $"{resources.Description}:").LineBreak();
composer.Text(command.Description).LineBreak(); composer.Text(command.Description).LineBreak();
yield return composer.LineBreak(); yield return composer.LineBreak();
} }
/// <summary> /// <summary>
/// Gets the usage section of the help information. /// Gets the usage section of the help information.
/// </summary> /// </summary>
/// <param name="model">The command model to write help for.</param> /// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param> /// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns> /// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetUsage(ICommandModel model, ICommandInfo? command) public virtual IEnumerable<IRenderable> GetUsage(ICommandModel model, ICommandInfo? command)
{ {
var composer = new Composer(); var composer = NewComposer();
composer.Style("yellow", $"{resources.Usage}:").LineBreak(); composer.Style(helpStyles?.Usage?.Header ?? Style.Plain, $"{resources.Usage}:").LineBreak();
composer.Tab().Text(model.ApplicationName); composer.Tab().Text(model.ApplicationName);
var parameters = new List<string>(); var parameters = new List<Composer>();
if (command == null) if (command == null)
{ {
parameters.Add($"[grey][[{resources.Options}]][/]"); parameters.Add(NewComposer().Style(helpStyles?.Usage?.Options ?? Style.Plain, $"[{resources.Options}]"));
parameters.Add($"[aqua]<{resources.Command}>[/]"); parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"<{resources.Command}>"));
} }
else else
{ {
@@ -183,11 +204,11 @@ public class HelpProvider : IHelpProvider
{ {
if (isCurrent) if (isCurrent)
{ {
parameters.Add($"[underline]{current.Name.EscapeMarkup()}[/]"); parameters.Add(NewComposer().Style(helpStyles?.Usage?.CurrentCommand ?? Style.Plain, $"{current.Name}"));
} }
else 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>() foreach (var argument in current.Parameters.OfType<ICommandArgument>()
.Where(a => a.Required).OrderBy(a => a.Position).ToArray()) .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,61 +228,58 @@ public class HelpProvider : IHelpProvider
{ {
foreach (var optionalArgument in optionalArguments) foreach (var optionalArgument in optionalArguments)
{ {
parameters.Add($"[silver][[{optionalArgument.Value.EscapeMarkup()}]][/]"); parameters.Add(NewComposer().Style(helpStyles?.Usage?.OptionalArgument ?? Style.Plain, $"[{optionalArgument.Value}]"));
} }
} }
} }
if (isCurrent) if (isCurrent)
{ {
parameters.Add($"[grey][[{resources.Options}]][/]"); parameters.Add(NewComposer().Style(helpStyles?.Usage?.Options ?? Style.Plain, $"[{resources.Options}]"));
} }
} }
if (command.IsBranch && command.DefaultCommand == null) if (command.IsBranch && command.DefaultCommand == null)
{ {
// The user must specify the command // 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) else if (command.IsBranch && command.DefaultCommand != null && command.Commands.Count > 0)
{ {
// We are on a branch with a default command // We are on a branch with a default command
// The user can optionally specify the 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) else if (command.IsDefaultCommand)
{ {
var commands = model.Commands.Where(x => !x.IsHidden && !x.IsDefaultCommand).ToList(); var commands = model.Commands.Where(x => !x.IsHidden && !x.IsDefaultCommand).ToList();
if (commands.Count > 0) if (commands.Count > 0)
{ {
// Commands other than the default are present // Commands other than the default are present
// So make these optional in the usage statement // So make these optional in the usage statement
parameters.Add($"[aqua][[{resources.Command}]][/]"); parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]"));
} }
} }
} }
composer.Join(" ", parameters); composer.Join(" ", parameters);
composer.LineBreak(); composer.LineBreak();
return new[] return new[] { composer };
{ }
composer,
}; /// <summary>
} /// Gets the examples section of the help information.
/// </summary>
/// <summary> /// <param name="model">The command model to write help for.</param>
/// Gets the examples section of the help information. /// <param name="command">The command for which to write help information (optional).</param>
/// </summary> /// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
/// <param name="model">The command model to write help for.</param> /// <remarks>
/// <param name="command">The command for which to write help information (optional).</param> /// Examples from the command's direct children are used
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns> /// if no examples have been set on the specified command or model.
/// <remarks> /// </remarks>
/// Examples from the command's direct children are used public virtual IEnumerable<IRenderable> GetExamples(ICommandModel model, ICommandInfo? command)
/// if no examples have been set on the specified command or model.
/// </remarks>
public virtual IEnumerable<IRenderable> GetExamples(ICommandModel model, ICommandInfo? command)
{ {
var maxExamples = int.MaxValue; var maxExamples = int.MaxValue;
@@ -272,12 +290,12 @@ public class HelpProvider : IHelpProvider
// make sure that we limit the number of examples. // make sure that we limit the number of examples.
maxExamples = MaximumIndirectExamples; maxExamples = MaximumIndirectExamples;
// Start at the current command (if exists) // Start at the current command (if exists)
// or alternatively commence at the model. // or alternatively commence at the model.
var commandContainer = command ?? (ICommandContainer)model; var commandContainer = command ?? (ICommandContainer)model;
var queue = new Queue<ICommandContainer>(new[] { commandContainer }); var queue = new Queue<ICommandContainer>(new[] { commandContainer });
// Traverse the command tree and look for examples. // Traverse the command tree and look for examples.
// As soon as a node contains commands, bail. // As soon as a node contains commands, bail.
while (queue.Count > 0) while (queue.Count > 0)
{ {
@@ -302,14 +320,14 @@ public class HelpProvider : IHelpProvider
if (Math.Min(maxExamples, examples.Count) > 0) if (Math.Min(maxExamples, examples.Count) > 0)
{ {
var composer = new Composer(); var composer = NewComposer();
composer.LineBreak(); 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++) for (var index = 0; index < Math.Min(maxExamples, examples.Count); index++)
{ {
var args = string.Join(" ", examples[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(); composer.LineBreak();
} }
@@ -317,14 +335,14 @@ public class HelpProvider : IHelpProvider
} }
return Array.Empty<IRenderable>(); return Array.Empty<IRenderable>();
} }
/// <summary> /// <summary>
/// Gets the arguments section of the help information. /// Gets the arguments section of the help information.
/// </summary> /// </summary>
/// <param name="model">The command model to write help for.</param> /// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param> /// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns> /// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetArguments(ICommandModel model, ICommandInfo? command) public virtual IEnumerable<IRenderable> GetArguments(ICommandModel model, ICommandInfo? command)
{ {
var arguments = HelpArgument.Get(command); var arguments = HelpArgument.Get(command);
@@ -334,11 +352,9 @@ public class HelpProvider : IHelpProvider
} }
var result = new List<IRenderable> var result = new List<IRenderable>
{ {
new Markup(Environment.NewLine), NewComposer().LineBreak().Style(helpStyles?.Arguments?.Header ?? Style.Plain, $"{resources.Arguments}:").LineBreak(),
new Markup($"[yellow]{resources.Arguments}:[/]"), };
new Markup(Environment.NewLine),
};
var grid = new Grid(); var grid = new Grid();
grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true }); grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true });
@@ -347,43 +363,41 @@ public class HelpProvider : IHelpProvider
foreach (var argument in arguments.Where(x => x.Required).OrderBy(x => x.Position)) foreach (var argument in arguments.Where(x => x.Required).OrderBy(x => x.Position))
{ {
grid.AddRow( grid.AddRow(
$"[silver]<{argument.Name.EscapeMarkup()}>[/]", NewComposer().Style(helpStyles?.Arguments?.RequiredArgument ?? Style.Plain, $"<{argument.Name}>"),
argument.Description?.TrimEnd('.') ?? " "); NewComposer().Text(argument.Description?.TrimEnd('.') ?? " "));
} }
foreach (var argument in arguments.Where(x => !x.Required).OrderBy(x => x.Position)) foreach (var argument in arguments.Where(x => !x.Required).OrderBy(x => x.Position))
{ {
grid.AddRow( grid.AddRow(
$"[grey][[{argument.Name.EscapeMarkup()}]][/]", NewComposer().Style(helpStyles?.Arguments?.OptionalArgument ?? Style.Plain, $"[{argument.Name}]"),
argument.Description?.TrimEnd('.') ?? " "); NewComposer().Text(argument.Description?.TrimEnd('.') ?? " "));
} }
result.Add(grid); result.Add(grid);
return result; return result;
} }
/// <summary> /// <summary>
/// Gets the options section of the help information. /// Gets the options section of the help information.
/// </summary> /// </summary>
/// <param name="model">The command model to write help for.</param> /// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param> /// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns> /// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetOptions(ICommandModel model, ICommandInfo? command) public virtual IEnumerable<IRenderable> GetOptions(ICommandModel model, ICommandInfo? command)
{ {
// Collect all options into a single structure. // Collect all options into a single structure.
var parameters = HelpOption.Get(command, resources); var parameters = HelpOption.Get(model, command, resources);
if (parameters.Count == 0) if (parameters.Count == 0)
{ {
return Array.Empty<IRenderable>(); return Array.Empty<IRenderable>();
} }
var result = new List<IRenderable> var result = new List<IRenderable>
{ {
new Markup(Environment.NewLine), NewComposer().LineBreak().Style(helpStyles?.Options?.Header ?? Style.Plain, $"{resources.Options}:").LineBreak(),
new Markup($"[yellow]{resources.Options}:[/]"), };
new Markup(Environment.NewLine),
};
var helpOptions = parameters.ToArray(); var helpOptions = parameters.ToArray();
var defaultValueColumn = ShowOptionDefaultValues && helpOptions.Any(e => e.DefaultValue != null); var defaultValueColumn = ShowOptionDefaultValues && helpOptions.Any(e => e.DefaultValue != null);
@@ -397,71 +411,24 @@ public class HelpProvider : IHelpProvider
grid.AddColumn(new GridColumn { Padding = new Padding(0, 0) }); 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) 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) foreach (var option in helpOptions)
{ {
var columns = new List<string> { GetOptionParts(option) }; var columns = new List<IRenderable>() { GetOptionParts(option) };
if (defaultValueColumn) if (defaultValueColumn)
{ {
static string Bold(object obj) => $"[bold]{obj.ToString().EscapeMarkup()}[/]"; columns.Add(GetDefaultValueForOption(option.DefaultValue));
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(option.Description?.TrimEnd('.') ?? " "); columns.Add(NewComposer().Text(option.Description?.TrimEnd('.') ?? " "));
grid.AddRow(columns.ToArray()); grid.AddRow(columns.ToArray());
} }
@@ -469,18 +436,18 @@ public class HelpProvider : IHelpProvider
result.Add(grid); result.Add(grid);
return result; return result;
} }
/// <summary> /// <summary>
/// Gets the commands section of the help information. /// Gets the commands section of the help information.
/// </summary> /// </summary>
/// <param name="model">The command model to write help for.</param> /// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param> /// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns> /// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetCommands(ICommandModel model, ICommandInfo? command) public virtual IEnumerable<IRenderable> GetCommands(ICommandModel model, ICommandInfo? command)
{ {
var commandContainer = command ?? (ICommandContainer)model; var commandContainer = command ?? (ICommandContainer)model;
bool isDefaultCommand = command?.IsDefaultCommand ?? false; bool isDefaultCommand = command?.IsDefaultCommand ?? false;
var commands = isDefaultCommand ? model.Commands : commandContainer.Commands; var commands = isDefaultCommand ? model.Commands : commandContainer.Commands;
commands = commands.Where(x => !x.IsHidden).ToList(); commands = commands.Where(x => !x.IsHidden).ToList();
@@ -491,11 +458,9 @@ public class HelpProvider : IHelpProvider
} }
var result = new List<IRenderable> var result = new List<IRenderable>
{ {
new Markup(Environment.NewLine), NewComposer().LineBreak().Style(helpStyles?.Commands?.Header ?? Style.Plain, $"{resources.Commands}:").LineBreak(),
new Markup($"[yellow]{resources.Commands}:[/]"), };
new Markup(Environment.NewLine),
};
var grid = new Grid(); var grid = new Grid();
grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true }); grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true });
@@ -503,43 +468,102 @@ public class HelpProvider : IHelpProvider
foreach (var child in commands) foreach (var child in commands)
{ {
var arguments = new Composer(); var arguments = NewComposer();
arguments.Style("silver", child.Name.EscapeMarkup()); arguments.Style(helpStyles?.Commands?.ChildCommand ?? Style.Plain, child.Name);
arguments.Space(); arguments.Space();
foreach (var argument in HelpArgument.Get(child).Where(a => a.Required)) 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(); arguments.Space();
} }
if (TrimTrailingPeriod) if (TrimTrailingPeriod)
{ {
grid.AddRow( grid.AddRow(
arguments.ToString().TrimEnd(), NewComposer().Text(arguments.ToString().TrimEnd()),
child.Description?.TrimEnd('.') ?? " "); NewComposer().Text(child.Description?.TrimEnd('.') ?? " "));
} }
else else
{ {
grid.AddRow( grid.AddRow(
arguments.ToString().TrimEnd(), NewComposer().Text(arguments.ToString().TrimEnd()),
child.Description ?? " "); NewComposer().Text(child.Description ?? " "));
} }
} }
result.Add(grid); result.Add(grid);
return result; return result;
} }
/// <summary> /// <summary>
/// Gets the footer for the help information. /// Gets the footer for the help information.
/// </summary> /// </summary>
/// <param name="model">The command model to write help for.</param> /// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param> /// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns> /// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetFooter(ICommandModel model, ICommandInfo? command) public virtual IEnumerable<IRenderable> GetFooter(ICommandModel model, ICommandInfo? command)
{ {
yield break; 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

@@ -4,7 +4,7 @@ namespace Spectre.Console.Cli.Help;
/// Represents a command container. /// Represents a command container.
/// </summary> /// </summary>
public interface ICommandContainer public interface ICommandContainer
{ {
/// <summary> /// <summary>
/// Gets all the examples for the container. /// Gets all the examples for the container.
/// </summary> /// </summary>

View File

@@ -1,42 +1,42 @@
namespace Spectre.Console.Cli.Help; namespace Spectre.Console.Cli.Help;
/// <summary> /// <summary>
/// Represents an executable command. /// Represents an executable command.
/// </summary> /// </summary>
public interface ICommandInfo : ICommandContainer public interface ICommandInfo : ICommandContainer
{ {
/// <summary> /// <summary>
/// Gets the name of the command. /// Gets the name of the command.
/// </summary> /// </summary>
string Name { get; } string Name { get; }
/// <summary> /// <summary>
/// Gets the description of the command. /// Gets the description of the command.
/// </summary> /// </summary>
string? Description { get; } string? Description { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether the command is a branch. /// Gets a value indicating whether the command is a branch.
/// </summary> /// </summary>
bool IsBranch { get; } bool IsBranch { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether the command is the default command within its container. /// Gets a value indicating whether the command is the default command within its container.
/// </summary> /// </summary>
bool IsDefaultCommand { get; } bool IsDefaultCommand { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether the command is hidden. /// Gets a value indicating whether the command is hidden.
/// </summary> /// </summary>
bool IsHidden { get; } bool IsHidden { get; }
/// <summary> /// <summary>
/// Gets the parameters associated with the command. /// Gets the parameters associated with the command.
/// </summary> /// </summary>
IReadOnlyList<ICommandParameter> Parameters { get; } IReadOnlyList<ICommandParameter> Parameters { get; }
/// <summary> /// <summary>
/// Gets the parent command, if any. /// Gets the parent command, if any.
/// </summary> /// </summary>
ICommandInfo? Parent { get; } ICommandInfo? Parent { get; }
} }

View File

@@ -1,23 +1,23 @@
namespace Spectre.Console.Cli.Help; namespace Spectre.Console.Cli.Help;
internal static class ICommandInfoExtensions internal static class ICommandInfoExtensions
{ {
/// <summary> /// <summary>
/// Walks up the command.Parent tree, adding each command into a list as it goes. /// Walks up the command.Parent tree, adding each command into a list as it goes.
/// </summary> /// </summary>
/// <remarks>The first command added to the list is the current (ie. this one).</remarks> /// <remarks>The first command added to the list is the current (ie. this one).</remarks>
/// <returns>The list of commands from current to root, as traversed by <see cref="CommandInfo.Parent"/>.</returns> /// <returns>The list of commands from current to root, as traversed by <see cref="CommandInfo.Parent"/>.</returns>
public static List<ICommandInfo> Flatten(this ICommandInfo commandInfo) public static List<ICommandInfo> Flatten(this ICommandInfo commandInfo)
{ {
var result = new Stack<Help.ICommandInfo>(); var result = new Stack<Help.ICommandInfo>();
var current = commandInfo; var current = commandInfo;
while (current != null) while (current != null)
{ {
result.Push(current); result.Push(current);
current = current.Parent; current = current.Parent;
} }
return result.ToList(); return result.ToList();
} }
} }

View File

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

View File

@@ -1,27 +1,27 @@
namespace Spectre.Console.Cli.Help; namespace Spectre.Console.Cli.Help;
/// <summary> /// <summary>
/// Represents a command option. /// Represents a command option.
/// </summary> /// </summary>
public interface ICommandOption : ICommandParameter public interface ICommandOption : ICommandParameter
{ {
/// <summary> /// <summary>
/// Gets the long names of the option. /// Gets the long names of the option.
/// </summary> /// </summary>
IReadOnlyList<string> LongNames { get; } IReadOnlyList<string> LongNames { get; }
/// <summary> /// <summary>
/// Gets the short names of the option. /// Gets the short names of the option.
/// </summary> /// </summary>
IReadOnlyList<string> ShortNames { get; } IReadOnlyList<string> ShortNames { get; }
/// <summary> /// <summary>
/// Gets the value name of the option, if applicable. /// Gets the value name of the option, if applicable.
/// </summary> /// </summary>
string? ValueName { get; } string? ValueName { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether the option value is optional. /// Gets a value indicating whether the option value is optional.
/// </summary> /// </summary>
bool ValueIsOptional { get; } bool ValueIsOptional { get; }
} }

View File

@@ -1,32 +1,32 @@
namespace Spectre.Console.Cli.Help; namespace Spectre.Console.Cli.Help;
/// <summary> /// <summary>
/// Represents a command parameter. /// Represents a command parameter.
/// </summary> /// </summary>
public interface ICommandParameter public interface ICommandParameter
{ {
/// <summary> /// <summary>
/// Gets a value indicating whether the parameter is a flag. /// Gets a value indicating whether the parameter is a flag.
/// </summary> /// </summary>
bool IsFlag { get; } bool IsFlag { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether the parameter is required. /// Gets a value indicating whether the parameter is required.
/// </summary> /// </summary>
bool Required { get; } bool Required { get; }
/// <summary> /// <summary>
/// Gets the description of the parameter. /// Gets the description of the parameter.
/// </summary> /// </summary>
string? Description { get; } string? Description { get; }
/// <summary> /// <summary>
/// Gets the default value of the parameter, if specified. /// Gets the default value of the parameter, if specified.
/// </summary> /// </summary>
DefaultValueAttribute? DefaultValue { get; } DefaultValueAttribute? DefaultValue { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether the parameter is hidden. /// Gets a value indicating whether the parameter is hidden.
/// </summary> /// </summary>
bool IsHidden { get; } bool IsHidden { get; }
} }

View File

@@ -1,20 +1,20 @@
namespace Spectre.Console.Cli.Help; namespace Spectre.Console.Cli.Help;
/// <summary> /// <summary>
/// The help provider interface for Spectre.Console. /// The help provider interface for Spectre.Console.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Implementations of this interface are responsbile /// Implementations of this interface are responsbile
/// for writing command help to the terminal when the /// for writing command help to the terminal when the
/// `-h` or `--help` has been specified on the command line. /// `-h` or `--help` has been specified on the command line.
/// </remarks> /// </remarks>
public interface IHelpProvider public interface IHelpProvider
{ {
/// <summary> /// <summary>
/// Writes help information for the application. /// Writes help information for the application.
/// </summary> /// </summary>
/// <param name="model">The command model to write help for.</param> /// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param> /// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects representing the help information.</returns> /// <returns>An enumerable collection of <see cref="IRenderable"/> objects representing the help information.</returns>
IEnumerable<IRenderable> Write(ICommandModel model, ICommandInfo? command); IEnumerable<IRenderable> Write(ICommandModel model, ICommandInfo? command);
} }

View File

@@ -24,22 +24,27 @@ public interface ICommandAppSettings
/// <summary> /// <summary>
/// Gets or sets the application version (use it to override auto-detected value). /// Gets or sets the application version (use it to override auto-detected value).
/// </summary> /// </summary>
string? ApplicationVersion { get; set; } string? ApplicationVersion { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating how many examples from direct children to show in the help text. /// Gets or sets a value indicating how many examples from direct children to show in the help text.
/// </summary> /// </summary>
int MaximumIndirectExamples { get; set; } int MaximumIndirectExamples { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether any default values for command options are shown in the help text. /// Gets or sets a value indicating whether any default values for command options are shown in the help text.
/// </summary> /// </summary>
bool ShowOptionDefaultValues { get; set; } bool ShowOptionDefaultValues { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether a trailing period of a command description is trimmed in the help text. /// Gets or sets a value indicating whether a trailing period of a command description is trimmed in the help text.
/// </summary> /// </summary>
bool TrimTrailingPeriod { get; set; } bool TrimTrailingPeriod { get; set; }
/// <summary>
/// Gets or sets the styles to used when rendering the help text.
/// </summary>
HelpProviderStyle? HelpProviderStyles { get; set; }
/// <summary> /// <summary>
/// Gets or sets the <see cref="IAnsiConsole"/>. /// Gets or sets the <see cref="IAnsiConsole"/>.
@@ -50,6 +55,7 @@ public interface ICommandAppSettings
/// Gets or sets the <see cref="ICommandInterceptor"/> used /// Gets or sets the <see cref="ICommandInterceptor"/> used
/// to intercept settings before it's being sent to the command. /// to intercept settings before it's being sent to the command.
/// </summary> /// </summary>
[Obsolete("Register the interceptor with the ITypeRegistrar.")]
ICommandInterceptor? Interceptor { get; set; } ICommandInterceptor? Interceptor { get; set; }
/// <summary> /// <summary>
@@ -65,14 +71,14 @@ public interface ICommandAppSettings
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not parsing is strict. /// Gets or sets a value indicating whether or not parsing is strict.
/// </summary> /// </summary>
bool StrictParsing { get; set; } bool StrictParsing { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not flags found on the commnd line /// Gets or sets a value indicating whether or not flags found on the commnd line
/// that would normally result in a <see cref="CommandParseException"/> being thrown /// that would normally result in a <see cref="CommandParseException"/> being thrown
/// during parsing with the message "Flags cannot be assigned a value." /// during parsing with the message "Flags cannot be assigned a value."
/// should instead be added to the remaining arguments collection. /// should instead be added to the remaining arguments collection.
/// </summary> /// </summary>
bool ConvertFlagsToRemainingArguments { get; set; } bool ConvertFlagsToRemainingArguments { get; set; }
/// <summary> /// <summary>
@@ -90,6 +96,8 @@ public interface ICommandAppSettings
/// <summary> /// <summary>
/// Gets or sets a handler for Exceptions. /// 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> /// <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> /// </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> /// </summary>
/// <param name="context">The intercepted <see cref="CommandContext"/>.</param> /// <param name="context">The intercepted <see cref="CommandContext"/>.</param>
/// <param name="settings">The intercepted <see cref="CommandSettings"/>.</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

@@ -4,19 +4,19 @@ namespace Spectre.Console.Cli;
/// Represents a configurator. /// Represents a configurator.
/// </summary> /// </summary>
public interface IConfigurator public interface IConfigurator
{ {
/// <summary> /// <summary>
/// Sets the help provider for the application. /// Sets the help provider for the application.
/// </summary> /// </summary>
/// <param name="helpProvider">The help provider to use.</param> /// <param name="helpProvider">The help provider to use.</param>
public void SetHelpProvider(IHelpProvider helpProvider); public void SetHelpProvider(IHelpProvider helpProvider);
/// <summary> /// <summary>
/// Sets the help provider for the application. /// Sets the help provider for the application.
/// </summary> /// </summary>
/// <typeparam name="T">The type of the help provider to instantiate at runtime and use.</typeparam> /// <typeparam name="T">The type of the help provider to instantiate at runtime and use.</typeparam>
public void SetHelpProvider<T>() public void SetHelpProvider<T>()
where T : IHelpProvider; where T : IHelpProvider;
/// <summary> /// <summary>
/// Gets the command app settings. /// Gets the command app settings.
@@ -66,5 +66,5 @@ public interface IConfigurator
/// <param name="action">The command branch configurator.</param> /// <param name="action">The command branch configurator.</param>
/// <returns>A branch configurator that can be used to configure the branch further.</returns> /// <returns>A branch configurator that can be used to configure the branch further.</returns>
IBranchConfigurator AddBranch<TSettings>(string name, Action<IConfigurator<TSettings>> action) IBranchConfigurator AddBranch<TSettings>(string name, Action<IConfigurator<TSettings>> action)
where TSettings : CommandSettings; where TSettings : CommandSettings;
} }

View File

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

View File

@@ -1,146 +1,179 @@
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
internal sealed class CommandExecutor internal sealed class CommandExecutor
{ {
private readonly ITypeRegistrar _registrar; private readonly ITypeRegistrar _registrar;
public CommandExecutor(ITypeRegistrar registrar) public CommandExecutor(ITypeRegistrar registrar)
{ {
_registrar = registrar ?? throw new ArgumentNullException(nameof(registrar)); _registrar = registrar ?? throw new ArgumentNullException(nameof(registrar));
_registrar.Register(typeof(DefaultPairDeconstructor), typeof(DefaultPairDeconstructor)); _registrar.Register(typeof(DefaultPairDeconstructor), typeof(DefaultPairDeconstructor));
} }
public async Task<int> Execute(IConfiguration configuration, IEnumerable<string> args) public async Task<int> Execute(IConfiguration configuration, IEnumerable<string> args)
{ {
if (configuration == null) if (configuration == null)
{ {
throw new ArgumentNullException(nameof(configuration)); throw new ArgumentNullException(nameof(configuration));
} }
args ??= new List<string>(); var arguments = args.ToSafeReadOnlyList();
_registrar.RegisterInstance(typeof(IConfiguration), configuration); _registrar.RegisterInstance(typeof(IConfiguration), configuration);
_registrar.RegisterLazy(typeof(IAnsiConsole), () => configuration.Settings.Console.GetConsole()); _registrar.RegisterLazy(typeof(IAnsiConsole), () => configuration.Settings.Console.GetConsole());
// Create the command model. // Create the command model.
var model = CommandModelBuilder.Build(configuration); var model = CommandModelBuilder.Build(configuration);
_registrar.RegisterInstance(typeof(CommandModel), model); _registrar.RegisterInstance(typeof(CommandModel), model);
_registrar.RegisterDependencies(model); _registrar.RegisterDependencies(model);
// Asking for version? Kind of a hack, but it's alright. // No default command?
// We should probably make this a bit better in the future. if (model.DefaultCommand == null)
if (args.Contains("-v") || args.Contains("--version")) {
{ // Got at least one argument?
var console = configuration.Settings.Console.GetConsole(); var firstArgument = arguments.FirstOrDefault();
console.WriteLine(ResolveApplicationVersion(configuration)); if (firstArgument != null)
return 0; {
} // Asking for version? Kind of a hack, but it's alright.
// We should probably make this a bit better in the future.
// Parse and map the model against the arguments. if (firstArgument.Equals("--version", StringComparison.OrdinalIgnoreCase) ||
var parsedResult = ParseCommandLineArguments(model, configuration.Settings, args); firstArgument.Equals("-v", StringComparison.OrdinalIgnoreCase))
{
// Register the arguments with the container. if (configuration.Settings.ApplicationVersion != null)
_registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult); {
_registrar.RegisterInstance(typeof(IRemainingArguments), parsedResult.Remaining); var console = configuration.Settings.Console.GetConsole();
console.MarkupLine(configuration.Settings.ApplicationVersion);
// Create the resolver. return 0;
using (var resolver = new TypeResolverAdapter(_registrar.Build())) }
{ }
// Get the registered help provider, falling back to the default provider }
// if no custom implementations have been registered. }
var helpProviders = resolver.Resolve(typeof(IEnumerable<IHelpProvider>)) as IEnumerable<IHelpProvider>;
var helpProvider = helpProviders?.LastOrDefault() ?? new HelpProvider(configuration.Settings); // Parse and map the model against the arguments.
var parsedResult = ParseCommandLineArguments(model, configuration.Settings, arguments);
// Currently the root?
if (parsedResult?.Tree == null) // Register the arguments with the container.
{ _registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult);
// Display help. _registrar.RegisterInstance(typeof(IRemainingArguments), parsedResult.Remaining);
configuration.Settings.Console.SafeRender(helpProvider.Write(model, null));
return 0; // Create the resolver.
} using (var resolver = new TypeResolverAdapter(_registrar.Build()))
{
// Get the command to execute. // Get the registered help provider, falling back to the default provider
var leaf = parsedResult.Tree.GetLeafCommand(); // if no custom implementations have been registered.
if (leaf.Command.IsBranch || leaf.ShowHelp) var helpProviders = resolver.Resolve(typeof(IEnumerable<IHelpProvider>)) as IEnumerable<IHelpProvider>;
{ var helpProvider = helpProviders?.LastOrDefault() ?? new HelpProvider(configuration.Settings);
// Branches can't be executed. Show help.
configuration.Settings.Console.SafeRender(helpProvider.Write(model, leaf.Command)); // Currently the root?
return leaf.ShowHelp ? 0 : 1; if (parsedResult?.Tree == null)
} {
// Display help.
// Is this the default and is it called without arguments when there are required arguments? configuration.Settings.Console.SafeRender(helpProvider.Write(model, null));
if (leaf.Command.IsDefaultCommand && args.Count() == 0 && leaf.Command.Parameters.Any(p => p.Required)) return 0;
{ }
// Display help for default command.
configuration.Settings.Console.SafeRender(helpProvider.Write(model, leaf.Command)); // Get the command to execute.
return 1; var leaf = parsedResult.Tree.GetLeafCommand();
} if (leaf.Command.IsBranch || leaf.ShowHelp)
{
// Create the content. // Branches can't be executed. Show help.
var context = new CommandContext(parsedResult.Remaining, leaf.Command.Name, leaf.Command.Data); configuration.Settings.Console.SafeRender(helpProvider.Write(model, leaf.Command));
return leaf.ShowHelp ? 0 : 1;
// Execute the command tree. }
return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false);
} // Is this the default and is it called without arguments when there are required arguments?
} if (leaf.Command.IsDefaultCommand && arguments.Count == 0 && leaf.Command.Parameters.Any(p => p.Required))
{
#pragma warning disable CS8603 // Possible null reference return. // Display help for default command.
private CommandTreeParserResult ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IEnumerable<string> args) configuration.Settings.Console.SafeRender(helpProvider.Write(model, leaf.Command));
{ return 1;
var parser = new CommandTreeParser(model, settings.CaseSensitivity, settings.ParsingMode, settings.ConvertFlagsToRemainingArguments); }
var parserContext = new CommandTreeParserContext(args, settings.ParsingMode); // Create the content.
var tokenizerResult = CommandTreeTokenizer.Tokenize(args); var context = new CommandContext(
var parsedResult = parser.Parse(parserContext, tokenizerResult); arguments,
parsedResult.Remaining,
var lastParsedLeaf = parsedResult?.Tree?.GetLeafCommand(); leaf.Command.Name,
var lastParsedCommand = lastParsedLeaf?.Command; leaf.Command.Data);
if (lastParsedLeaf != null && lastParsedCommand != null &&
lastParsedCommand.IsBranch && !lastParsedLeaf.ShowHelp && // Execute the command tree.
lastParsedCommand.DefaultCommand != null) return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false);
{ }
// Insert this branch's default command into the command line }
// arguments and try again to see if it will parse.
var argsWithDefaultCommand = new List<string>(args); private CommandTreeParserResult ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IReadOnlyList<string> args)
{
argsWithDefaultCommand.Insert(tokenizerResult.Tokens.Position, lastParsedCommand.DefaultCommand.Name); var parser = new CommandTreeParser(model, settings.CaseSensitivity, settings.ParsingMode, settings.ConvertFlagsToRemainingArguments);
parserContext = new CommandTreeParserContext(argsWithDefaultCommand, settings.ParsingMode); var parserContext = new CommandTreeParserContext(args, settings.ParsingMode);
tokenizerResult = CommandTreeTokenizer.Tokenize(argsWithDefaultCommand); var tokenizerResult = CommandTreeTokenizer.Tokenize(args);
parsedResult = parser.Parse(parserContext, tokenizerResult); var parsedResult = parser.Parse(parserContext, tokenizerResult);
}
var lastParsedLeaf = parsedResult.Tree?.GetLeafCommand();
return parsedResult; var lastParsedCommand = lastParsedLeaf?.Command;
} if (lastParsedLeaf != null && lastParsedCommand != null &&
#pragma warning restore CS8603 // Possible null reference return. lastParsedCommand.IsBranch && !lastParsedLeaf.ShowHelp &&
lastParsedCommand.DefaultCommand != null)
private static string ResolveApplicationVersion(IConfiguration configuration) {
{ // Insert this branch's default command into the command line
return // arguments and try again to see if it will parse.
configuration.Settings.ApplicationVersion ?? // potential override var argsWithDefaultCommand = new List<string>(args);
VersionHelper.GetVersion(Assembly.GetEntryAssembly());
} argsWithDefaultCommand.Insert(tokenizerResult.Tokens.Position, lastParsedCommand.DefaultCommand.Name);
private static Task<int> Execute( parserContext = new CommandTreeParserContext(argsWithDefaultCommand, settings.ParsingMode);
CommandTree leaf, tokenizerResult = CommandTreeTokenizer.Tokenize(argsWithDefaultCommand);
CommandTree tree, parsedResult = parser.Parse(parserContext, tokenizerResult);
CommandContext context, }
ITypeResolver resolver,
IConfiguration configuration) return parsedResult;
{ }
// Bind the command tree against the settings.
var settings = CommandBinder.Bind(tree, leaf.Command.SettingsType, resolver); private static async Task<int> Execute(
configuration.Settings.Interceptor?.Intercept(context, settings); CommandTree leaf,
CommandTree tree,
// Create and validate the command. CommandContext context,
var command = leaf.CreateCommand(resolver); ITypeResolver resolver,
var validationResult = command.Validate(context, settings); IConfiguration configuration)
if (!validationResult.Successful) {
{ try
throw CommandRuntimeException.ValidationFailed(validationResult); {
} // Bind the command tree against the settings.
var settings = CommandBinder.Bind(tree, leaf.Command.SettingsType, resolver);
// Execute the command. var interceptors =
return command.Execute(context, settings); ((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);
var validationResult = command.Validate(context, settings);
if (!validationResult.Successful)
{
throw CommandRuntimeException.ValidationFailed(validationResult);
}
// Execute the command.
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); node.SetNullableAttribute("Settings", command.SettingsType?.FullName);
if (!string.IsNullOrWhiteSpace(command.Description))
{
var descriptionNode = doc.CreateElement("Description");
descriptionNode.InnerText = command.Description;
node.AppendChild(descriptionNode);
}
// Parameters // Parameters
if (command.Parameters.Count > 0) if (command.Parameters.Count > 0)
{ {
@@ -103,6 +110,27 @@ internal sealed class XmlDocCommand : Command<XmlDocCommand.Settings>
node.AppendChild(CreateCommandNode(doc, childCommand)); 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; return node;
} }

View File

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

View File

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

View File

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

View File

@@ -19,19 +19,19 @@ internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfig
Settings = new CommandAppSettings(registrar); Settings = new CommandAppSettings(registrar);
Examples = new List<string[]>(); Examples = new List<string[]>();
} }
public void SetHelpProvider(IHelpProvider helpProvider) public void SetHelpProvider(IHelpProvider helpProvider)
{ {
// Register the help provider // Register the help provider
_registrar.RegisterInstance(typeof(IHelpProvider), helpProvider); _registrar.RegisterInstance(typeof(IHelpProvider), helpProvider);
} }
public void SetHelpProvider<T>() public void SetHelpProvider<T>()
where T : IHelpProvider where T : IHelpProvider
{ {
// Register the help provider // Register the help provider
_registrar.Register(typeof(IHelpProvider), typeof(T)); _registrar.Register(typeof(IHelpProvider), typeof(T));
} }
public void AddExample(params string[] args) public void AddExample(params string[] args)
{ {

View File

@@ -124,7 +124,7 @@ internal static class TemplateParser
foreach (var character in token.Value) foreach (var character in token.Value)
{ {
if (!char.IsLetterOrDigit(character) && if (!char.IsLetterOrDigit(character) &&
character != '=' && character != '-' && character != '_') character != '=' && character != '-' && character != '_' && character != '|')
{ {
throw CommandTemplateException.InvalidCharacterInValueName(template, token, 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."); 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) if (command.CommandType != null)
{ {
registrar?.Register(command.CommandType, command.CommandType); registrar?.Register(command.CommandType, command.CommandType);

View File

@@ -1,5 +1,5 @@
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
internal sealed class CommandInfo : ICommandContainer, ICommandInfo internal sealed class CommandInfo : ICommandContainer, ICommandInfo
{ {
public string Name { get; } public string Name { get; }
@@ -20,14 +20,14 @@ internal sealed class CommandInfo : ICommandContainer, ICommandInfo
// only branches can have a default command // only branches can have a default command
public CommandInfo? DefaultCommand => IsBranch ? Children.FirstOrDefault(c => c.IsDefaultCommand) : null; public CommandInfo? DefaultCommand => IsBranch ? Children.FirstOrDefault(c => c.IsDefaultCommand) : null;
public bool IsHidden { get; } public bool IsHidden { get; }
IReadOnlyList<ICommandInfo> Help.ICommandContainer.Commands => Children.Cast<ICommandInfo>().ToList(); IReadOnlyList<ICommandInfo> Help.ICommandContainer.Commands => Children.Cast<ICommandInfo>().ToList();
ICommandInfo? Help.ICommandContainer.DefaultCommand => DefaultCommand; ICommandInfo? Help.ICommandContainer.DefaultCommand => DefaultCommand;
IReadOnlyList<ICommandParameter> ICommandInfo.Parameters => Parameters.Cast<ICommandParameter>().ToList(); IReadOnlyList<ICommandParameter> ICommandInfo.Parameters => Parameters.Cast<ICommandParameter>().ToList();
ICommandInfo? ICommandInfo.Parent => Parent; ICommandInfo? ICommandInfo.Parent => Parent;
IReadOnlyList<string[]> Help.ICommandContainer.Examples => (IReadOnlyList<string[]>)Examples; IReadOnlyList<string[]> Help.ICommandContainer.Examples => (IReadOnlyList<string[]>)Examples;
public CommandInfo(CommandInfo? parent, ConfiguredCommand prototype) public CommandInfo(CommandInfo? parent, ConfiguredCommand prototype)
{ {
Parent = parent; Parent = parent;
@@ -54,5 +54,5 @@ internal sealed class CommandInfo : ICommandContainer, ICommandInfo
Description = description.Description; Description = description.Description;
} }
} }
} }
} }

View File

@@ -1,17 +1,18 @@
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
internal sealed class CommandModel : ICommandContainer, ICommandModel internal sealed class CommandModel : ICommandContainer, ICommandModel
{ {
public string? ApplicationName { get; } public string? ApplicationName { get; }
public string? ApplicationVersion { get; }
public ParsingMode ParsingMode { get; } public ParsingMode ParsingMode { get; }
public IList<CommandInfo> Commands { get; } public IList<CommandInfo> Commands { get; }
public IList<string[]> Examples { get; } public IList<string[]> Examples { get; }
public CommandInfo? DefaultCommand => Commands.FirstOrDefault(c => c.IsDefaultCommand); public CommandInfo? DefaultCommand => Commands.FirstOrDefault(c => c.IsDefaultCommand);
string ICommandModel.ApplicationName => GetApplicationName(ApplicationName); string ICommandModel.ApplicationName => GetApplicationName(ApplicationName);
IReadOnlyList<ICommandInfo> Help.ICommandContainer.Commands => Commands.Cast<ICommandInfo>().ToList(); IReadOnlyList<ICommandInfo> Help.ICommandContainer.Commands => Commands.Cast<ICommandInfo>().ToList();
ICommandInfo? Help.ICommandContainer.DefaultCommand => DefaultCommand; ICommandInfo? Help.ICommandContainer.DefaultCommand => DefaultCommand;
IReadOnlyList<string[]> Help.ICommandContainer.Examples => (IReadOnlyList<string[]>)Examples; IReadOnlyList<string[]> Help.ICommandContainer.Examples => (IReadOnlyList<string[]>)Examples;
public CommandModel( public CommandModel(
@@ -20,20 +21,21 @@ internal sealed class CommandModel : ICommandContainer, ICommandModel
IEnumerable<string[]> examples) IEnumerable<string[]> examples)
{ {
ApplicationName = settings.ApplicationName; ApplicationName = settings.ApplicationName;
ApplicationVersion = settings.ApplicationVersion;
ParsingMode = settings.ParsingMode; ParsingMode = settings.ParsingMode;
Commands = new List<CommandInfo>(commands ?? Array.Empty<CommandInfo>()); Commands = new List<CommandInfo>(commands);
Examples = new List<string[]>(examples ?? Array.Empty<string[]>()); Examples = new List<string[]>(examples);
} }
/// <summary> /// <summary>
/// Gets the name of the application. /// Gets the name of the application.
/// If the provided <paramref name="applicationName"/> is not null or empty, /// If the provided <paramref name="applicationName"/> is not null or empty,
/// it is returned. Otherwise the name of the current application /// it is returned. Otherwise the name of the current application
/// is determined based on the executable file's name. /// is determined based on the executable file's name.
/// </summary> /// </summary>
/// <param name="applicationName">The optional name of the application.</param> /// <param name="applicationName">The optional name of the application.</param>
/// <returns> /// <returns>
/// The name of the application, or a default value of "?" if no valid application name can be determined. /// The name of the application, or a default value of "?" if no valid application name can be determined.
/// </returns> /// </returns>
private static string GetApplicationName(string? applicationName) private static string GetApplicationName(string? applicationName)
{ {
@@ -45,7 +47,7 @@ internal sealed class CommandModel : ICommandContainer, ICommandModel
private static string? GetApplicationFile() private static string? GetApplicationFile()
{ {
var location = Assembly.GetEntryAssembly()?.Location; var location = Assembly.GetEntryAssembly()?.Location;
if (string.IsNullOrWhiteSpace(location)) if (string.IsNullOrWhiteSpace(location))
{ {

View File

@@ -1,5 +1,5 @@
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
internal static class CommandModelBuilder internal static class CommandModelBuilder
{ {
// Consider removing this in favor for value tuples at some point. // Consider removing this in favor for value tuples at some point.
@@ -31,8 +31,8 @@ internal static class CommandModelBuilder
configuration.DefaultCommand.Examples.AddRange(configuration.Examples); configuration.DefaultCommand.Examples.AddRange(configuration.Examples);
// Build the default command. // Build the default command.
var defaultCommand = Build(null, configuration.DefaultCommand); var defaultCommand = Build(null, configuration.DefaultCommand);
result.Add(defaultCommand); result.Add(defaultCommand);
} }
@@ -55,7 +55,7 @@ internal static class CommandModelBuilder
foreach (var childCommand in command.Children) foreach (var childCommand in command.Children)
{ {
var child = Build(info, childCommand); var child = Build(info, childCommand);
info.Children.Add(child); info.Children.Add(child);
} }
// Normalize argument positions. // Normalize argument positions.

View File

@@ -1,5 +1,5 @@
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
internal abstract class CommandParameter : ICommandParameterInfo, ICommandParameter internal abstract class CommandParameter : ICommandParameterInfo, ICommandParameter
{ {
public Guid Id { get; } public Guid Id { get; }
@@ -17,10 +17,10 @@ internal abstract class CommandParameter : ICommandParameterInfo, ICommandParame
public string PropertyName => Property.Name; public string PropertyName => Property.Name;
public virtual bool WantRawValue => ParameterType.IsPairDeconstructable() public virtual bool WantRawValue => ParameterType.IsPairDeconstructable()
&& (PairDeconstructor != null || Converter == null); && (PairDeconstructor != null || Converter == null);
public bool IsFlag => ParameterKind == ParameterKind.Flag; public bool IsFlag => ParameterKind == ParameterKind.Flag;
protected CommandParameter( protected CommandParameter(
Type parameterType, ParameterKind parameterKind, PropertyInfo property, Type parameterType, ParameterKind parameterKind, PropertyInfo property,
string? description, TypeConverterAttribute? converter, string? description, TypeConverterAttribute? converter,

View File

@@ -8,13 +8,13 @@ internal interface ICommandContainer
/// <summary> /// <summary>
/// Gets all commands in the container. /// Gets all commands in the container.
/// </summary> /// </summary>
IList<CommandInfo> Commands { get; } IList<CommandInfo> Commands { get; }
/// <summary> /// <summary>
/// Gets the default command for the container. /// Gets the default command for the container.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Returns null if a default command has not been set. /// Returns null if a default command has not been set.
/// </remarks> /// </remarks>
CommandInfo? DefaultCommand { get; } CommandInfo? DefaultCommand { get; }
} }

View File

@@ -1,12 +1,12 @@
using static Spectre.Console.Cli.CommandTreeTokenizer; using static Spectre.Console.Cli.CommandTreeTokenizer;
namespace Spectre.Console.Cli; namespace Spectre.Console.Cli;
internal class CommandTreeParser internal class CommandTreeParser
{ {
private readonly CommandModel _configuration; private readonly CommandModel _configuration;
private readonly ParsingMode _parsingMode; private readonly ParsingMode _parsingMode;
private readonly CommandOptionAttribute _help; private readonly CommandOptionAttribute _help;
private readonly bool _convertFlagsToRemainingArguments; private readonly bool _convertFlagsToRemainingArguments;
public CaseSensitivity CaseSensitivity { get; } public CaseSensitivity CaseSensitivity { get; }
@@ -21,20 +21,20 @@ internal class CommandTreeParser
{ {
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
_parsingMode = parsingMode ?? _configuration.ParsingMode; _parsingMode = parsingMode ?? _configuration.ParsingMode;
_help = new CommandOptionAttribute("-h|--help"); _help = new CommandOptionAttribute("-h|--help");
_convertFlagsToRemainingArguments = convertFlagsToRemainingArguments ?? false; _convertFlagsToRemainingArguments = convertFlagsToRemainingArguments ?? false;
CaseSensitivity = caseSensitivity; CaseSensitivity = caseSensitivity;
} }
public CommandTreeParserResult Parse(IEnumerable<string> args) public CommandTreeParserResult Parse(IEnumerable<string> args)
{ {
var parserContext = new CommandTreeParserContext(args, _parsingMode); var parserContext = new CommandTreeParserContext(args, _parsingMode);
var tokenizerResult = CommandTreeTokenizer.Tokenize(args); var tokenizerResult = CommandTreeTokenizer.Tokenize(args);
return Parse(parserContext, tokenizerResult); return Parse(parserContext, tokenizerResult);
} }
public CommandTreeParserResult Parse(CommandTreeParserContext context, CommandTreeTokenizerResult tokenizerResult) public CommandTreeParserResult Parse(CommandTreeParserContext context, CommandTreeTokenizerResult tokenizerResult)
{ {
var tokens = tokenizerResult.Tokens; var tokens = tokenizerResult.Tokens;
@@ -258,8 +258,8 @@ internal class CommandTreeParser
// Find the option. // Find the option.
var option = node.FindOption(token.Value, isLongOption, CaseSensitivity); var option = node.FindOption(token.Value, isLongOption, CaseSensitivity);
if (option != null) if (option != null)
{ {
ParseOptionValue(context, stream, token, node, option); ParseOptionValue(context, stream, token, node, option);
return; return;
} }
@@ -291,10 +291,10 @@ internal class CommandTreeParser
CommandTreeParserContext context, CommandTreeParserContext context,
CommandTreeTokenStream stream, CommandTreeTokenStream stream,
CommandTreeToken token, CommandTreeToken token,
CommandTree current, CommandTree current,
CommandParameter? parameter = null) CommandParameter? parameter = null)
{ {
bool addToMappedCommandParameters = parameter != null; bool addToMappedCommandParameters = parameter != null;
var value = default(string); var value = default(string);
@@ -302,59 +302,55 @@ internal class CommandTreeParser
var valueToken = stream.Peek(); var valueToken = stream.Peek();
if (valueToken?.TokenKind == CommandTreeToken.Kind.String) if (valueToken?.TokenKind == CommandTreeToken.Kind.String)
{ {
var parseValue = true; bool parseValue = token is not { TokenKind: CommandTreeToken.Kind.ShortOption, IsGrouped: true };
if (token.TokenKind == CommandTreeToken.Kind.ShortOption && token.IsGrouped)
{
parseValue = false;
}
if (context.State == State.Normal && parseValue) if (context.State == State.Normal && parseValue)
{ {
// Is this a command? // Is this a command?
if (current.Command.FindCommand(valueToken.Value, CaseSensitivity) == null) if (current.Command.FindCommand(valueToken.Value, CaseSensitivity) == null)
{ {
if (parameter != null) if (parameter != null)
{ {
if (parameter.ParameterKind == ParameterKind.Flag) if (parameter.ParameterKind == ParameterKind.Flag)
{ {
if (!CliConstants.AcceptedBooleanValues.Contains(valueToken.Value, StringComparer.OrdinalIgnoreCase)) if (!CliConstants.AcceptedBooleanValues.Contains(valueToken.Value, StringComparer.OrdinalIgnoreCase))
{ {
if (!valueToken.HadSeparator) if (!valueToken.HadSeparator)
{ {
// Do nothing // Do nothing
// - assume valueToken is unrelated to the flag parameter (ie. we've parsed it unnecessarily) // - assume valueToken is unrelated to the flag parameter (ie. we've parsed it unnecessarily)
// - rely on the "No value?" code below to set the flag to its default value // - rely on the "No value?" code below to set the flag to its default value
// - valueToken will be handled on the next pass of the parser // - valueToken will be handled on the next pass of the parser
} }
else else
{ {
// Flags cannot be assigned a value. // Flags cannot be assigned a value.
if (_convertFlagsToRemainingArguments) if (_convertFlagsToRemainingArguments)
{ {
value = stream.Consume(CommandTreeToken.Kind.String)?.Value; 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 // Prevent the option and it's non-boolean value from being added to
// mapped parameters (otherwise an exception will be thrown later // mapped parameters (otherwise an exception will be thrown later
// when binding the value to the flag in the comand settings) // when binding the value to the flag in the comand settings)
addToMappedCommandParameters = false; addToMappedCommandParameters = false;
} }
else else
{ {
throw CommandParseException.CannotAssignValueToFlag(context.Arguments, token); throw CommandParseException.CannotAssignValueToFlag(context.Arguments, token);
} }
} }
} }
else else
{ {
value = stream.Consume(CommandTreeToken.Kind.String)?.Value; value = stream.Consume(CommandTreeToken.Kind.String)?.Value;
} }
} }
else else
{ {
value = stream.Consume(CommandTreeToken.Kind.String)?.Value; value = stream.Consume(CommandTreeToken.Kind.String)?.Value;
} }
} }
else else
{ {
@@ -364,22 +360,22 @@ internal class CommandTreeParser
// In relaxed parsing mode? // In relaxed parsing mode?
if (context.ParsingMode == ParsingMode.Relaxed) if (context.ParsingMode == ParsingMode.Relaxed)
{ {
context.AddRemainingArgument(token.Value, value); context.AddRemainingArgument(token.Representation, value);
} }
} }
} }
} }
else else
{ {
context.AddRemainingArgument(token.Value, parseValue ? valueToken.Value : null); context.AddRemainingArgument(token.Representation, parseValue ? valueToken.Value : null);
} }
} }
else else
{ {
if (parameter == null && // Only add tokens which have not been matched to a command parameter if (parameter == null && // Only add tokens which have not been matched to a command parameter
(context.State == State.Remaining || context.ParsingMode == ParsingMode.Relaxed)) (context.State == State.Remaining || context.ParsingMode == ParsingMode.Relaxed))
{ {
context.AddRemainingArgument(token.Value, null); context.AddRemainingArgument(token.Representation, null);
} }
} }
@@ -399,10 +395,10 @@ internal class CommandTreeParser
if (parameter.IsFlagValue()) if (parameter.IsFlagValue())
{ {
value = null; value = null;
} }
else else
{ {
throw CommandParseException.OptionHasNoValue(context.Arguments, token, option); throw CommandParseException.OptionHasNoValue(context.Arguments, token, option);
} }
} }
else else
@@ -415,9 +411,9 @@ internal class CommandTreeParser
} }
} }
if (parameter != null && addToMappedCommandParameters) if (parameter != null && addToMappedCommandParameters)
{ {
current.Mapped.Add(new MappedCommandParameter(parameter, value)); current.Mapped.Add(new MappedCommandParameter(parameter, value));
} }
} }
} }

View File

@@ -26,15 +26,15 @@ internal class CommandTreeParserContext
public void IncreaseArgumentPosition() public void IncreaseArgumentPosition()
{ {
CurrentArgumentPosition++; CurrentArgumentPosition++;
} }
public void AddRemainingArgument(string key, string? value) public void AddRemainingArgument(string key, string? value)
{ {
if (!_remaining.ContainsKey(key)) if (!_remaining.ContainsKey(key))
{ {
_remaining.Add(key, new List<string?>()); _remaining.Add(key, new List<string?>());
} }
_remaining[key].Add(value); _remaining[key].Add(value);
} }

View File

@@ -6,11 +6,11 @@ internal sealed class CommandTreeToken
public int Position { get; } public int Position { get; }
public string Value { get; } public string Value { get; }
public string Representation { get; } public string Representation { get; }
public bool IsGrouped { get; set; } public bool IsGrouped { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether a separater was encountered immediately before the <see cref="CommandTreeToken.Value"/>. /// Gets or sets a value indicating whether a separater was encountered immediately before the <see cref="CommandTreeToken.Value"/>.
/// </summary> /// </summary>
public bool HadSeparator { get; set; } public bool HadSeparator { get; set; }
public enum Kind public enum Kind

View File

@@ -5,7 +5,7 @@ internal sealed class CommandTreeTokenStream : IReadOnlyList<CommandTreeToken>
private readonly List<CommandTreeToken> _tokens; private readonly List<CommandTreeToken> _tokens;
private int _position; private int _position;
public int Count => _tokens.Count; public int Count => _tokens.Count;
public int Position => _position; public int Position => _position;
public CommandTreeToken this[int index] => _tokens[index]; public CommandTreeToken this[int index] => _tokens[index];

View File

@@ -29,13 +29,13 @@ internal static class CommandTreeTokenizer
var context = new CommandTreeTokenizerContext(); var context = new CommandTreeTokenizerContext();
foreach (var arg in args) foreach (var arg in args)
{ {
if (string.IsNullOrEmpty(arg)) if (string.IsNullOrEmpty(arg))
{ {
// Null strings in the args array are still represented as tokens // Null strings in the args array are still represented as tokens
tokens.Add(new CommandTreeToken(CommandTreeToken.Kind.String, position, string.Empty, string.Empty)); tokens.Add(new CommandTreeToken(CommandTreeToken.Kind.String, position, string.Empty, string.Empty));
continue; continue;
} }
var start = position; var start = position;
var reader = new TextBuffer(previousReader, arg); var reader = new TextBuffer(previousReader, arg);
@@ -55,30 +55,30 @@ internal static class CommandTreeTokenizer
} }
private static int ParseToken(CommandTreeTokenizerContext context, TextBuffer reader, int position, int start, List<CommandTreeToken> tokens) private static int ParseToken(CommandTreeTokenizerContext context, TextBuffer reader, int position, int start, List<CommandTreeToken> tokens)
{ {
if (!reader.ReachedEnd && reader.Peek() == '-') if (!reader.ReachedEnd && reader.Peek() == '-')
{ {
// Option // Option
tokens.AddRange(ScanOptions(context, reader)); tokens.AddRange(ScanOptions(context, reader));
} }
else else
{ {
// Command or argument // Command or argument
while (reader.Peek() != -1) while (reader.Peek() != -1)
{ {
if (reader.ReachedEnd) if (reader.ReachedEnd)
{ {
position += reader.Position - start; position += reader.Position - start;
break; break;
} }
tokens.Add(ScanString(context, reader)); tokens.Add(ScanString(context, reader));
// Flush remaining tokens // Flush remaining tokens
context.FlushRemaining(); context.FlushRemaining();
} }
} }
return position; return position;
} }
@@ -102,7 +102,7 @@ internal static class CommandTreeTokenizer
builder.Append(current); builder.Append(current);
} }
var value = builder.ToString(); var value = builder.ToString();
return new CommandTreeToken(CommandTreeToken.Kind.String, position, value, value); return new CommandTreeToken(CommandTreeToken.Kind.String, position, value, value);
} }
@@ -149,8 +149,8 @@ internal static class CommandTreeTokenizer
var token = new CommandTreeToken(CommandTreeToken.Kind.String, reader.Position, "=", "="); var token = new CommandTreeToken(CommandTreeToken.Kind.String, reader.Position, "=", "=");
throw CommandParseException.OptionValueWasExpected(reader.Original, token); throw CommandParseException.OptionValueWasExpected(reader.Original, token);
} }
var tokenValue = ScanString(context, reader); var tokenValue = ScanString(context, reader);
tokenValue.HadSeparator = true; tokenValue.HadSeparator = true;
result.Add(tokenValue); result.Add(tokenValue);
} }
@@ -171,7 +171,7 @@ internal static class CommandTreeTokenizer
} }
// Encountered a separator? // Encountered a separator?
if (current == '=' || current == ':') if (current is '=' or ':')
{ {
break; break;
} }
@@ -184,7 +184,7 @@ internal static class CommandTreeTokenizer
var value = current.ToString(CultureInfo.InvariantCulture); var value = current.ToString(CultureInfo.InvariantCulture);
result.Add(result.Count == 0 result.Add(result.Count == 0
? new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position, value, $"-{value}") ? 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)) else if (result.Count == 0 && char.IsDigit(current))
{ {
@@ -193,7 +193,7 @@ internal static class CommandTreeTokenizer
// be tokenized as strings. This block handles parsing those cases, but we only allow this // be tokenized as strings. This block handles parsing those cases, but we only allow this
// when the digit is the first character in the token (i.e. "-a1" is always an error), hence the // when the digit is the first character in the token (i.e. "-a1" is always an error), hence the
// result.Count == 0 check above. // result.Count == 0 check above.
string value = string.Empty; string value = string.Empty;
while (!reader.ReachedEnd) while (!reader.ReachedEnd)
{ {
@@ -212,12 +212,12 @@ internal static class CommandTreeTokenizer
result.Add(new CommandTreeToken(CommandTreeToken.Kind.String, position, value, value)); result.Add(new CommandTreeToken(CommandTreeToken.Kind.String, position, value, value));
} }
else else
{ {
// Create a token representing the short option. // Create a token representing the short option.
var representation = current.ToString(CultureInfo.InvariantCulture); var representation = current.ToString(CultureInfo.InvariantCulture);
var tokenPosition = position + 1 + result.Count; var tokenPosition = position + 1 + result.Count;
var token = new CommandTreeToken(CommandTreeToken.Kind.ShortOption, tokenPosition, representation, representation); var token = new CommandTreeToken(CommandTreeToken.Kind.ShortOption, tokenPosition, representation, representation);
throw CommandParseException.InvalidShortOptionName(reader.Original, token); throw CommandParseException.InvalidShortOptionName(reader.Original, token);
} }
} }

View File

@@ -10,7 +10,7 @@ global using System.Linq;
global using System.Reflection; global using System.Reflection;
global using System.Text; global using System.Text;
global using System.Threading.Tasks; global using System.Threading.Tasks;
global using System.Xml; global using System.Xml;
global using Spectre.Console.Cli.Help; global using Spectre.Console.Cli.Help;
global using Spectre.Console.Cli.Unsafe; global using Spectre.Console.Cli.Unsafe;
global using Spectre.Console.Rendering; global using Spectre.Console.Rendering;

View File

@@ -1,153 +1,153 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// <auto-generated> // <auto-generated>
// This code was generated by a tool. // This code was generated by a tool.
// Runtime Version:4.0.30319.42000 // Runtime Version:4.0.30319.42000
// //
// Changes to this file may cause incorrect behavior and will be lost if // Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated. // the code is regenerated.
// </auto-generated> // </auto-generated>
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
namespace Spectre.Console.Cli.Resources { namespace Spectre.Console.Cli.Resources {
using System; using System;
/// <summary> /// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc. /// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary> /// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder // This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio. // class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen // To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project. // with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class HelpProvider { internal class HelpProvider {
private static global::System.Resources.ResourceManager resourceMan; private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture; private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal HelpProvider() { internal HelpProvider() {
} }
/// <summary> /// <summary>
/// Returns the cached ResourceManager instance used by this class. /// Returns the cached ResourceManager instance used by this class.
/// </summary> /// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager { internal static global::System.Resources.ResourceManager ResourceManager {
get { get {
if (object.ReferenceEquals(resourceMan, null)) { if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Spectre.Console.Cli.Resources.HelpProvider", typeof(HelpProvider).Assembly); global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Spectre.Console.Cli.Resources.HelpProvider", typeof(HelpProvider).Assembly);
resourceMan = temp; resourceMan = temp;
} }
return resourceMan; return resourceMan;
} }
} }
/// <summary> /// <summary>
/// Overrides the current thread's CurrentUICulture property for all /// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class. /// resource lookups using this strongly typed resource class.
/// </summary> /// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture { internal static global::System.Globalization.CultureInfo Culture {
get { get {
return resourceCulture; return resourceCulture;
} }
set { set {
resourceCulture = value; resourceCulture = value;
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to ARGUMENTS. /// Looks up a localized string similar to ARGUMENTS.
/// </summary> /// </summary>
internal static string Arguments { internal static string Arguments {
get { get {
return ResourceManager.GetString("Arguments", resourceCulture); return ResourceManager.GetString("Arguments", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to COMMAND. /// Looks up a localized string similar to COMMAND.
/// </summary> /// </summary>
internal static string Command { internal static string Command {
get { get {
return ResourceManager.GetString("Command", resourceCulture); return ResourceManager.GetString("Command", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to COMMANDS. /// Looks up a localized string similar to COMMANDS.
/// </summary> /// </summary>
internal static string Commands { internal static string Commands {
get { get {
return ResourceManager.GetString("Commands", resourceCulture); return ResourceManager.GetString("Commands", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to DEFAULT. /// Looks up a localized string similar to DEFAULT.
/// </summary> /// </summary>
internal static string Default { internal static string Default {
get { get {
return ResourceManager.GetString("Default", resourceCulture); return ResourceManager.GetString("Default", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to DESCRIPTION. /// Looks up a localized string similar to DESCRIPTION.
/// </summary> /// </summary>
internal static string Description { internal static string Description {
get { get {
return ResourceManager.GetString("Description", resourceCulture); return ResourceManager.GetString("Description", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to EXAMPLES. /// Looks up a localized string similar to EXAMPLES.
/// </summary> /// </summary>
internal static string Examples { internal static string Examples {
get { get {
return ResourceManager.GetString("Examples", resourceCulture); return ResourceManager.GetString("Examples", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to OPTIONS. /// Looks up a localized string similar to OPTIONS.
/// </summary> /// </summary>
internal static string Options { internal static string Options {
get { get {
return ResourceManager.GetString("Options", resourceCulture); return ResourceManager.GetString("Options", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Prints help information. /// Looks up a localized string similar to Prints help information.
/// </summary> /// </summary>
internal static string PrintHelpDescription { internal static string PrintHelpDescription {
get { get {
return ResourceManager.GetString("PrintHelpDescription", resourceCulture); return ResourceManager.GetString("PrintHelpDescription", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Prints version information. /// Looks up a localized string similar to Prints version information.
/// </summary> /// </summary>
internal static string PrintVersionDescription { internal static string PrintVersionDescription {
get { get {
return ResourceManager.GetString("PrintVersionDescription", resourceCulture); return ResourceManager.GetString("PrintVersionDescription", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to USAGE. /// Looks up a localized string similar to USAGE.
/// </summary> /// </summary>
internal static string Usage { internal static string Usage {
get { get {
return ResourceManager.GetString("Usage", resourceCulture); return ResourceManager.GetString("Usage", resourceCulture);
} }
} }
} }
} }

View File

@@ -31,19 +31,19 @@
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Update="Resources\HelpProvider.Designer.cs"> <Compile Update="Resources\HelpProvider.Designer.cs">
<DesignTime>True</DesignTime> <DesignTime>True</DesignTime>
<AutoGen>True</AutoGen> <AutoGen>True</AutoGen>
<DependentUpon>HelpProvider.resx</DependentUpon> <DependentUpon>HelpProvider.resx</DependentUpon>
</Compile> </Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Update="Resources\HelpProvider.resx"> <EmbeddedResource Update="Resources\HelpProvider.resx">
<Generator>ResXFileCodeGenerator</Generator> <Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>HelpProvider.Designer.cs</LastGenOutput> <LastGenOutput>HelpProvider.Designer.cs</LastGenOutput>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

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

View File

@@ -13,7 +13,7 @@
<Compile Include="..\Spectre.Console\Internal\Extensions\EnumerableExtensions.cs" Link="Internal\EnumerableExtensions.cs" /> <Compile Include="..\Spectre.Console\Internal\Extensions\EnumerableExtensions.cs" Link="Internal\EnumerableExtensions.cs" />
<Compile Include="..\Spectre.Console\Internal\Text\StringBuffer.cs" Link="Internal\StringBuffer.cs" /> <Compile Include="..\Spectre.Console\Internal\Text\StringBuffer.cs" Link="Internal\StringBuffer.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="..\stylecop.json" Link="Properties/stylecop.json" /> <AdditionalFiles Include="..\stylecop.json" Link="Properties/stylecop.json" />
<None Include="../../resources/gfx/small-logo.png" Pack="true" PackagePath="\" Link="Properties/small-logo.png" /> <None Include="../../resources/gfx/small-logo.png" Pack="true" PackagePath="\" Link="Properties/small-logo.png" />

View File

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

View File

@@ -141,8 +141,8 @@ public sealed class CommandAppTester
.Trim(); .Trim();
return new CommandAppResult(result, output, context, settings); return new CommandAppResult(result, output, context, settings);
} }
/// <summary> /// <summary>
/// Runs the command application asynchronously. /// Runs the command application asynchronously.
/// </summary> /// </summary>

View File

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

View File

@@ -19,6 +19,7 @@ public static partial class AnsiConsoleExtensions
while (true) while (true)
{ {
cancellationToken.ThrowIfCancellationRequested();
var rawKey = await console.Input.ReadKeyAsync(true, cancellationToken).ConfigureAwait(false); var rawKey = await console.Input.ReadKeyAsync(true, cancellationToken).ConfigureAwait(false);
if (rawKey == null) if (rawKey == null)
{ {
@@ -52,7 +53,11 @@ public static partial class AnsiConsoleExtensions
if (text.Length > 0) if (text.Length > 0)
{ {
text = text.Substring(0, text.Length - 1); text = text.Substring(0, text.Length - 1);
console.Write("\b \b");
if (mask != null)
{
console.Write("\b \b");
}
} }
continue; continue;
@@ -60,7 +65,7 @@ public static partial class AnsiConsoleExtensions
if (!char.IsControl(key.KeyChar)) if (!char.IsControl(key.KeyChar))
{ {
text += key.KeyChar.ToString(); text += key.KeyChar.ToString();
var output = key.KeyChar.ToString(); var output = key.KeyChar.ToString();
console.Write(secret ? output.Mask(mask) : output, style); console.Write(secret ? output.Mask(mask) : output, style);
} }

View File

@@ -116,6 +116,43 @@ public static class BarChartExtensions
return chart; 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> /// <summary>
/// Sets the width of the bar chart. /// Sets the width of the bar chart.
/// </summary> /// </summary>

View File

@@ -185,28 +185,122 @@ public static class StringExtensions
#else #else
return text.Contains(value, StringComparison.Ordinal); return text.Contains(value, StringComparison.Ordinal);
#endif #endif
} }
/// <summary> #if NETSTANDARD2_0
/// "Masks" every character in a string. internal static bool Contains(this string target, string value, System.StringComparison comparisonType)
/// </summary> {
/// <param name="value">String value to mask.</param> return target.IndexOf(value, comparisonType) != -1;
/// <param name="mask">Character to use for masking.</param> }
/// <returns>Masked string.</returns> #endif
public static string Mask(this string value, char? mask)
{ /// <summary>
var output = string.Empty; /// "Masks" every character in a string.
/// </summary>
if (mask is null) /// <param name="value">String value to mask.</param>
{ /// <param name="mask">Character to use for masking.</param>
return output; /// <returns>Masked string.</returns>
} public static string Mask(this string value, char? mask)
{
foreach (var c in value) if (mask is null)
{ {
output += mask; return string.Empty;
} }
return output; 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)
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
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> /// <summary>
/// Represents horizontal alignment. /// Represents horizontal alignment.

View File

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

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