Compare commits

...

45 Commits

Author SHA1 Message Date
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
227 changed files with 5300 additions and 3553 deletions

View File

@@ -1,2 +1,5 @@
# 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**
Add any other context about the problem here.
---
Please upvote :+1: this issue if you are interested in it.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -1,47 +1,75 @@
Title: Command Help
Order: 13
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.
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,
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.
`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
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.
Title: Command Help
Order: 13
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.
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,
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.
`HelpProvider` is the `Spectre.Console` class responsible for determining context and preparing the help text to write to the console. It is an implementation of the public interface `IHelpProvider`.
## Styling the help text
Basic styling is applied to the generated help text by default, however this is configurable.
`HelpProviderStyle` is the `Spectre.Console` class that holds the style information for the help text.
The default theme shipped with Spectre.Console is provided by a factory method, `HelpProviderStyle.Default`.
However, you can explicitly set a custom theme when configuring a CommandApp, for example:
```csharp
config.Settings.HelpProviderStyles = new HelpProviderStyle()
{
Description = new DescriptionStyle()
{
Header = "bold",
},
};
```
Removing all styling from help text is also possible, a good choice for ensuring maximum accessibility. This is configured by clearing the style provider entirely:
```csharp
config.Settings.HelpProviderStyles = null;
```
See [Markup](../markup) for information about the use of markup in Spectre.Console, and [Styles](xref:styles) for a listing of supported styles.
## Custom help providers
Whilst it shouldn't be common place to implement your own help provider, it is however possible.
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
In the previous example we have a single command that is configured. For complex command line applications, it is common for them to have multiple commands (or verbs) defined. Examples of applications like this are `git`, `dotnet` and `gh`. For example, git would have a `commit` command and along with other commits like `add` or `rebase`. Each with their own settings and validation. With `Spectre.Console.Cli` we use the `Configure` method to add these commands.
In the previous example we have a single command that is configured. For complex command line applications, it is common for them to have multiple commands (or verbs) defined. Examples of applications like this are `git`, `dotnet` and `gh`. For example, git would have a `commit` command and along with other commands like `add` or `rebase`. Each with their own settings and validation. With `Spectre.Console.Cli` we use the `Configure` method to add these commands.
For example, to add three different commands to the application:
@@ -81,9 +81,12 @@ Hint: If you do write your own implementation of `TypeRegistrar` and `TypeResolv
there is a utility `TypeRegistrarBaseTests` available that can be used to ensure your implementations adhere to the required implementation. Simply call `TypeRegistrarBaseTests.RunAllTests()` and expect no `TypeRegistrarBaseTests.TestFailedException` to be thrown.
## Interception
Interceptors can be registered with the `TypeRegistrar` (or with a custom DI-Container). Alternatively, `CommandApp` also provides a `SetInterceptor` configuration.
`CommandApp` also provides a `SetInterceptor` configuration. An interceptor is run before all commands are executed. This is typically used for configuring logging or other infrastructure concerns.
All interceptors must implement `ICommandInterceptor`. Upon execution of a command, The `Intercept`-Method of an instance of your interceptor will be called with the parsed settings. This provides an opportunity for configuring any infrastructure or modifying the settings.
When the command has been run, the `InterceptResult`-Method of the same instance is called with the result of the command.
This provides an opportunity to modify the result and also to tear down any infrastructure in use.
All interceptors must implement `ICommandInterceptor`. Upon execution of a command, an instance of your interceptor will be called with the parsed settings. This provides an opportunity for configuring any infrastructure or modifying the settings.
The `Intercept`-Method of each interceptor is run before the command is executed and the `InterceptResult`-Method is run after it. These are typically used for configuring logging or other infrastructure concerns.
For an example of using the interceptor to configure logging, see the [Serilog demo](https://github.com/spectreconsole/spectre.console/tree/main/examples/Cli/Logging).

View File

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

View File

@@ -1,119 +1,119 @@
Title: Markup
Order: 30
Description: The Markup class allows you to output rich text to the console.
Highlights:
- Easily add *color*.
- Add hyperlinks to for supported terminals.
- Emoji 🚀 parsing.
Reference:
- M:Spectre.Console.AnsiConsole.Markup(System.String)
- M:Spectre.Console.AnsiConsole.MarkupLine(System.String)
- T:Spectre.Console.Markup
---
The `Markup` class allows you to output rich text to the console.
## Syntax
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 `[/]`.
```csharp
AnsiConsole.Write(new Markup("[bold yellow]Hello[/] [red]World![/]"));
```
The `Markup` class implements `IRenderable` which means that you
can use this in tables, grids, and panels. Most classes that support
rendering of `IRenderable` also have overloads for rendering rich text.
```csharp
var table = new Table();
table.AddColumn(new TableColumn(new Markup("[yellow]Foo[/]")));
table.AddColumn(new TableColumn("[blue]Bar[/]"));
AnsiConsole.Write(table);
```
## Convenience methods
There are also convenience methods on `AnsiConsole` that can be used
to write markup text to the console without instantiating a new `Markup`
instance.
```csharp
AnsiConsole.Markup("[underline green]Hello[/] ");
AnsiConsole.MarkupLine("[bold]World[/]");
```
## Escaping format characters
To output a `[` you use `[[`, and to output a `]` you use `]]`.
```csharp
AnsiConsole.Markup("[[Hello]] "); // [Hello]
AnsiConsole.Markup("[red][[World]][/]"); // [World]
```
You can also use the `EscapeMarkup` extension method.
```csharp
AnsiConsole.Markup("[red]{0}[/]", "Hello [World]".EscapeMarkup());
```
You can also use the `Markup.Escape` method.
```csharp
AnsiConsole.Markup("[red]{0}[/]", Markup.Escape("Hello [World]"));
```
## 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".
```csharp
string hello = "Hello [World]";
AnsiConsole.MarkupInterpolated($"[red]{hello}[/]");
```
## Setting background color
You can set the background color in markup by prefixing the color with `on`.
```csharp
AnsiConsole.Markup("[bold yellow on blue]Hello[/]");
AnsiConsole.Markup("[default on blue]World[/]");
```
## Rendering emojis
To output an emoji as part of markup, you can use emoji shortcodes.
```csharp
AnsiConsole.Markup("Hello :globe_showing_europe_africa:!");
```
For a list of emoji, see the [Emojis](xref:emojis) appendix section.
## Colors
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.
```csharp
AnsiConsole.Markup("[red]Foo[/] ");
AnsiConsole.Markup("[#ff0000]Bar[/] ");
AnsiConsole.Markup("[rgb(255,0,0)]Baz[/] ");
```
For a list of colors, see the [Colors](xref:colors) appendix section.
## Links
To output a clickable link, you can use the `[link]` style.
```csharp
AnsiConsole.Markup("[link]https://spectreconsole.net[/]");
AnsiConsole.Markup("[link=https://spectreconsole.net]Spectre Console Documentation[/]");
```
## Styles
For a list of styles, see the [Styles](xref:styles) appendix section.
Title: Markup
Order: 30
Description: The Markup class allows you to output rich text to the console.
Highlights:
- Easily add *color*.
- Add hyperlinks to for supported terminals.
- Emoji 🚀 parsing.
Reference:
- M:Spectre.Console.AnsiConsole.Markup(System.String)
- M:Spectre.Console.AnsiConsole.MarkupLine(System.String)
- T:Spectre.Console.Markup
---
The `Markup` class allows you to output rich text to the console.
## Syntax
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 `[/]`.
```csharp
AnsiConsole.Write(new Markup("[bold yellow]Hello[/] [red]World![/]"));
```
The `Markup` class implements `IRenderable` which means that you
can use this in tables, grids, and panels. Most classes that support
rendering of `IRenderable` also have overloads for rendering rich text.
```csharp
var table = new Table();
table.AddColumn(new TableColumn(new Markup("[yellow]Foo[/]")));
table.AddColumn(new TableColumn("[blue]Bar[/]"));
AnsiConsole.Write(table);
```
## Convenience methods
There are also convenience methods on `AnsiConsole` that can be used
to write markup text to the console without instantiating a new `Markup`
instance.
```csharp
AnsiConsole.Markup("[underline green]Hello[/] ");
AnsiConsole.MarkupLine("[bold]World[/]");
```
## Escaping format characters
To output a `[` you use `[[`, and to output a `]` you use `]]`.
```csharp
AnsiConsole.Markup("[[Hello]] "); // [Hello]
AnsiConsole.Markup("[red][[World]][/]"); // [World]
```
You can also use the `EscapeMarkup` extension method.
```csharp
AnsiConsole.Markup("[red]{0}[/]", "Hello [World]".EscapeMarkup());
```
You can also use the `Markup.Escape` method.
```csharp
AnsiConsole.Markup("[red]{0}[/]", Markup.Escape("Hello [World]"));
```
## 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".
```csharp
string hello = "Hello [World]";
AnsiConsole.MarkupInterpolated($"[red]{hello}[/]");
```
## Setting background color
You can set the background color in markup by prefixing the color with `on`.
```csharp
AnsiConsole.Markup("[bold yellow on blue]Hello[/]");
AnsiConsole.Markup("[default on blue]World[/]");
```
## Rendering emojis
To output an emoji as part of markup, you can use emoji shortcodes.
```csharp
AnsiConsole.Markup("Hello :globe_showing_europe_africa:!");
```
For a list of emoji, see the [Emojis](xref:emojis) appendix section.
## Colors
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.
```csharp
AnsiConsole.Markup("[red]Foo[/] ");
AnsiConsole.Markup("[#ff0000]Bar[/] ");
AnsiConsole.Markup("[rgb(255,0,0)]Baz[/] ");
```
For a list of colors, see the [Colors](xref:colors) appendix section.
## Links
To output a clickable link, you can use the `[link]` style.
```csharp
AnsiConsole.Markup("[link]https://spectreconsole.net[/]");
AnsiConsole.Markup("[link=https://spectreconsole.net]Spectre Console Documentation[/]");
```
## Styles
For a list of styles, see the [Styles](xref:styles) appendix section.

View File

@@ -63,22 +63,22 @@ What's the secret number? _
```text
Enter password: ************_
```
## Masks
```
## Masks
<?# Example symbol="M:Prompt.Program.AskPasswordWithCustomMask" project="Prompt" /?>
```text
```text
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" /?>
```text
```text
Enter password: _
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@
]
},
"dotnet-example": {
"version": "2.0.0",
"version": "3.1.0",
"commands": [
"dotnet-example"
]

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,23 @@
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);
}
}
using Spectre.Console.Cli;
using Spectre.Console.Cli.Help;
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));
// Render an unstyled help text for maximum accessibility
config.Settings.HelpProviderStyles = null;
});
return app.Run(args);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,40 +5,40 @@ namespace Spectre.Console.Cli;
/// and <see cref="IConfigurator{TSettings}"/>.
/// </summary>
public static class ConfiguratorExtensions
{
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <param name="configurator">The configurator.</param>
/// <param name="helpProvider">The help provider to use.</param>
/// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator SetHelpProvider(this IConfigurator configurator, IHelpProvider helpProvider)
{
{
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <param name="configurator">The configurator.</param>
/// <param name="helpProvider">The help provider to use.</param>
/// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator SetHelpProvider(this IConfigurator configurator, IHelpProvider helpProvider)
{
if (configurator == null)
{
throw new ArgumentNullException(nameof(configurator));
}
configurator.SetHelpProvider(helpProvider);
return configurator;
}
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <param name="configurator">The configurator.</param>
/// <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>
public static IConfigurator SetHelpProvider<T>(this IConfigurator configurator)
where T : IHelpProvider
{
}
configurator.SetHelpProvider(helpProvider);
return configurator;
}
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <param name="configurator">The configurator.</param>
/// <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>
public static IConfigurator SetHelpProvider<T>(this IConfigurator configurator)
where T : IHelpProvider
{
if (configurator == null)
{
throw new ArgumentNullException(nameof(configurator));
}
configurator.SetHelpProvider<T>();
return configurator;
}
configurator.SetHelpProvider<T>();
return configurator;
}
/// <summary>
@@ -229,7 +229,7 @@ public static class ConfiguratorExtensions
throw new ArgumentNullException(nameof(configurator));
}
configurator.Settings.Interceptor = interceptor;
configurator.Settings.Registrar.RegisterInstance<ICommandInterceptor>(interceptor);
return configurator;
}
@@ -324,11 +324,16 @@ public static class ConfiguratorExtensions
/// <param name="func">The delegate to execute as part of command execution.</param>
/// <returns>A command configurator that can be used to configure the command further.</returns>
public static ICommandConfigurator AddDelegate<TSettings>(
this IConfigurator<TSettings> configurator,
this IConfigurator<TSettings>? configurator,
string name,
Func<CommandContext, int> func)
where TSettings : CommandSettings
where TSettings : CommandSettings
{
if (typeof(TSettings).IsAbstract)
{
AddDelegate(configurator as IConfigurator<EmptyCommandSettings>, name, func);
}
if (configurator == null)
{
throw new ArgumentNullException(nameof(configurator));
@@ -367,11 +372,11 @@ public static class ConfiguratorExtensions
/// <param name="configurator">The configurator.</param>
/// <param name="exceptionHandler">The Action that handles the exception.</param>
/// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator SetExceptionHandler(this IConfigurator configurator, Action<Exception> exceptionHandler)
public static IConfigurator SetExceptionHandler(this IConfigurator configurator, Action<Exception, ITypeResolver?> exceptionHandler)
{
return configurator.SetExceptionHandler(ex =>
return configurator.SetExceptionHandler((ex, resolver) =>
{
exceptionHandler(ex);
exceptionHandler(ex, resolver);
return -1;
});
}
@@ -382,7 +387,7 @@ public static class ConfiguratorExtensions
/// <param name="configurator">The configurator.</param>
/// <param name="exceptionHandler">The Action that handles the exception.</param>
/// <returns>A configurator that can be used to configure the application further.</returns>
public static IConfigurator SetExceptionHandler(this IConfigurator configurator, Func<Exception, int>? exceptionHandler)
public static IConfigurator SetExceptionHandler(this IConfigurator configurator, Func<Exception, ITypeResolver?, int>? exceptionHandler)
{
if (configurator == null)
{

View File

@@ -1,5 +1,3 @@
using Spectre.Console.Cli.Resources;
namespace Spectre.Console.Cli.Help;
/// <summary>
@@ -10,23 +8,32 @@ namespace Spectre.Console.Cli.Help;
/// </remarks>
public class HelpProvider : IHelpProvider
{
private HelpProviderResources resources;
/// <summary>
/// Gets a value indicating how many examples from direct children to show in the help text.
/// </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>
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>
protected virtual bool TrimTrailingPeriod { get; }
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>
protected virtual int MaximumIndirectExamples { get; }
/// <summary>
/// Gets a value indicating whether any default values for command options are shown in the help text.
/// </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>
protected virtual bool TrimTrailingPeriod { get; }
/// <summary>
/// Gets a value indicating whether to emit the markup styles, inline, when rendering the help text.
/// </summary>
/// <remarks>
/// Useful for unit testing different styling of the same help text.
/// </remarks>
protected virtual bool RenderMarkupInline { get; } = false;
private sealed class HelpArgument
{
public string Name { get; }
@@ -74,14 +81,14 @@ public class HelpProvider : IHelpProvider
public static IReadOnlyList<HelpOption> Get(ICommandInfo? command, HelpProviderResources resources)
{
var parameters = new List<HelpOption>();
parameters.Add(new HelpOption("h", "help", null, null, resources.PrintHelpDescription, null));
// Version information applies to the entire application
// Include the "-v" option in the help when at the root of the command line application
// Don't allow the "-v" option if users have specified one or more sub-commands
if ((command == null || command?.Parent == null) && !(command?.IsBranch ?? false))
{
parameters.Add(new HelpOption("v", "version", null, null, resources.PrintVersionDescription, null));
parameters.Add(new HelpOption("h", "help", null, null, resources.PrintHelpDescription, null));
// Version information applies to the entire application
// Include the "-v" option in the help when at the root of the command line application
// Don't allow the "-v" option if users have specified one or more sub-commands
if ((command == null || command?.Parent == null) && !(command?.IsBranch ?? false))
{
parameters.Add(new HelpOption("v", "version", null, null, resources.PrintVersionDescription, null));
}
parameters.AddRange(command?.Parameters.OfType<ICommandOption>().Where(o => !o.IsHidden).Select(o =>
@@ -92,55 +99,64 @@ public class HelpProvider : IHelpProvider
?? Array.Empty<HelpOption>());
return parameters;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="HelpProvider"/> class.
/// </summary>
/// <param name="settings">The command line application settings used for configuration.</param>
public HelpProvider(ICommandAppSettings settings)
{
this.ShowOptionDefaultValues = settings.ShowOptionDefaultValues;
this.MaximumIndirectExamples = settings.MaximumIndirectExamples;
}
internal Composer NewComposer()
{
return new Composer(RenderMarkupInline);
}
/// <summary>
/// Initializes a new instance of the <see cref="HelpProvider"/> class.
/// </summary>
/// <param name="settings">The command line application settings used for configuration.</param>
public HelpProvider(ICommandAppSettings settings)
{
this.ShowOptionDefaultValues = settings.ShowOptionDefaultValues;
this.MaximumIndirectExamples = settings.MaximumIndirectExamples;
this.TrimTrailingPeriod = settings.TrimTrailingPeriod;
resources = new HelpProviderResources(settings.Culture);
}
/// <inheritdoc/>
// Don't provide a default style if HelpProviderStyles is null,
// as the user will have explicitly done this to output unstyled help text
this.helpStyles = settings.HelpProviderStyles;
resources = new HelpProviderResources(settings.Culture);
}
/// <inheritdoc/>
public virtual IEnumerable<IRenderable> Write(ICommandModel model, ICommandInfo? command)
{
var result = new List<IRenderable>();
result.AddRange(GetHeader(model, command));
var result = new List<IRenderable>();
result.AddRange(GetHeader(model, command));
result.AddRange(GetDescription(model, command));
result.AddRange(GetUsage(model, command));
result.AddRange(GetExamples(model, command));
result.AddRange(GetArguments(model, command));
result.AddRange(GetOptions(model, command));
result.AddRange(GetCommands(model, command));
result.AddRange(GetCommands(model, command));
result.AddRange(GetFooter(model, command));
return result;
}
/// <summary>
/// Gets the header for the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
}
/// <summary>
/// Gets the header for the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetHeader(ICommandModel model, ICommandInfo? command)
{
yield break;
}
/// <summary>
/// Gets the description section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
{
yield break;
}
/// <summary>
/// Gets the description section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetDescription(ICommandModel model, ICommandInfo? command)
{
if (command?.Description == null)
@@ -148,30 +164,30 @@ public class HelpProvider : IHelpProvider
yield break;
}
var composer = new Composer();
composer.Style("yellow", $"{resources.Description}:").LineBreak();
var composer = NewComposer();
composer.Style(helpStyles?.Description?.Header ?? Style.Plain, $"{resources.Description}:").LineBreak();
composer.Text(command.Description).LineBreak();
yield return composer.LineBreak();
}
/// <summary>
/// Gets the usage section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
}
/// <summary>
/// Gets the usage section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetUsage(ICommandModel model, ICommandInfo? command)
{
var composer = new Composer();
composer.Style("yellow", $"{resources.Usage}:").LineBreak();
var composer = NewComposer();
composer.Style(helpStyles?.Usage?.Header ?? Style.Plain, $"{resources.Usage}:").LineBreak();
composer.Tab().Text(model.ApplicationName);
var parameters = new List<string>();
var parameters = new List<Composer>();
if (command == null)
{
parameters.Add($"[grey][[{resources.Options}]][/]");
parameters.Add($"[aqua]<{resources.Command}>[/]");
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Options ?? Style.Plain, $"[{resources.Options}]"));
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"<{resources.Command}>"));
}
else
{
@@ -183,11 +199,11 @@ public class HelpProvider : IHelpProvider
{
if (isCurrent)
{
parameters.Add($"[underline]{current.Name.EscapeMarkup()}[/]");
parameters.Add(NewComposer().Style(helpStyles?.Usage?.CurrentCommand ?? Style.Plain, $"{current.Name}"));
}
else
{
parameters.Add($"{current.Name.EscapeMarkup()}");
parameters.Add(NewComposer().Text(current.Name));
}
}
@@ -198,7 +214,7 @@ public class HelpProvider : IHelpProvider
foreach (var argument in current.Parameters.OfType<ICommandArgument>()
.Where(a => a.Required).OrderBy(a => a.Position).ToArray())
{
parameters.Add($"[aqua]<{argument.Value.EscapeMarkup()}>[/]");
parameters.Add(NewComposer().Style(helpStyles?.Usage?.RequiredArgument ?? Style.Plain, $"<{argument.Value}>"));
}
}
@@ -207,61 +223,58 @@ public class HelpProvider : IHelpProvider
{
foreach (var optionalArgument in optionalArguments)
{
parameters.Add($"[silver][[{optionalArgument.Value.EscapeMarkup()}]][/]");
parameters.Add(NewComposer().Style(helpStyles?.Usage?.OptionalArgument ?? Style.Plain, $"[{optionalArgument.Value}]"));
}
}
}
if (isCurrent)
{
parameters.Add($"[grey][[{resources.Options}]][/]");
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Options ?? Style.Plain, $"[{resources.Options}]"));
}
}
if (command.IsBranch && command.DefaultCommand == null)
{
{
// The user must specify the command
parameters.Add($"[aqua]<{resources.Command}>[/]");
}
else if (command.IsBranch && command.DefaultCommand != null && command.Commands.Count > 0)
{
// We are on a branch with a default command
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"<{resources.Command}>"));
}
else if (command.IsBranch && command.DefaultCommand != null && command.Commands.Count > 0)
{
// We are on a branch with a default command
// The user can optionally specify the command
parameters.Add($"[aqua][[{resources.Command}]][/]");
}
else if (command.IsDefaultCommand)
{
var commands = model.Commands.Where(x => !x.IsHidden && !x.IsDefaultCommand).ToList();
if (commands.Count > 0)
{
// Commands other than the default are present
// So make these optional in the usage statement
parameters.Add($"[aqua][[{resources.Command}]][/]");
}
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]"));
}
else if (command.IsDefaultCommand)
{
var commands = model.Commands.Where(x => !x.IsHidden && !x.IsDefaultCommand).ToList();
if (commands.Count > 0)
{
// Commands other than the default are present
// So make these optional in the usage statement
parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]"));
}
}
}
composer.Join(" ", parameters);
composer.LineBreak();
return new[]
{
composer,
};
}
/// <summary>
/// Gets the examples section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
/// <remarks>
/// Examples from the command's direct children are used
/// if no examples have been set on the specified command or model.
/// </remarks>
public virtual IEnumerable<IRenderable> GetExamples(ICommandModel model, ICommandInfo? command)
return new[] { composer };
}
/// <summary>
/// Gets the examples section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
/// <remarks>
/// Examples from the command's direct children are used
/// 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;
@@ -272,12 +285,12 @@ public class HelpProvider : IHelpProvider
// make sure that we limit the number of examples.
maxExamples = MaximumIndirectExamples;
// Start at the current command (if exists)
// or alternatively commence at the model.
// Start at the current command (if exists)
// or alternatively commence at the model.
var commandContainer = command ?? (ICommandContainer)model;
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.
while (queue.Count > 0)
{
@@ -302,14 +315,14 @@ public class HelpProvider : IHelpProvider
if (Math.Min(maxExamples, examples.Count) > 0)
{
var composer = new Composer();
var composer = NewComposer();
composer.LineBreak();
composer.Style("yellow", $"{resources.Examples}:").LineBreak();
composer.Style(helpStyles?.Examples?.Header ?? Style.Plain, $"{resources.Examples}:").LineBreak();
for (var index = 0; index < Math.Min(maxExamples, examples.Count); index++)
{
var args = string.Join(" ", examples[index]);
composer.Tab().Text(model.ApplicationName).Space().Style("grey", args);
composer.Tab().Text(model.ApplicationName).Space().Style(helpStyles?.Examples?.Arguments ?? Style.Plain, args);
composer.LineBreak();
}
@@ -317,14 +330,14 @@ public class HelpProvider : IHelpProvider
}
return Array.Empty<IRenderable>();
}
/// <summary>
/// Gets the arguments section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
}
/// <summary>
/// Gets the arguments section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetArguments(ICommandModel model, ICommandInfo? command)
{
var arguments = HelpArgument.Get(command);
@@ -334,11 +347,9 @@ public class HelpProvider : IHelpProvider
}
var result = new List<IRenderable>
{
new Markup(Environment.NewLine),
new Markup($"[yellow]{resources.Arguments}:[/]"),
new Markup(Environment.NewLine),
};
{
NewComposer().LineBreak().Style(helpStyles?.Arguments?.Header ?? Style.Plain, $"{resources.Arguments}:").LineBreak(),
};
var grid = new Grid();
grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true });
@@ -347,27 +358,27 @@ public class HelpProvider : IHelpProvider
foreach (var argument in arguments.Where(x => x.Required).OrderBy(x => x.Position))
{
grid.AddRow(
$"[silver]<{argument.Name.EscapeMarkup()}>[/]",
argument.Description?.TrimEnd('.') ?? " ");
NewComposer().Style(helpStyles?.Arguments?.RequiredArgument ?? Style.Plain, $"<{argument.Name}>"),
NewComposer().Text(argument.Description?.TrimEnd('.') ?? " "));
}
foreach (var argument in arguments.Where(x => !x.Required).OrderBy(x => x.Position))
{
grid.AddRow(
$"[grey][[{argument.Name.EscapeMarkup()}]][/]",
argument.Description?.TrimEnd('.') ?? " ");
NewComposer().Style(helpStyles?.Arguments?.OptionalArgument ?? Style.Plain, $"[{argument.Name}]"),
NewComposer().Text(argument.Description?.TrimEnd('.') ?? " "));
}
result.Add(grid);
return result;
}
/// <summary>
/// Gets the options section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
}
/// <summary>
/// Gets the options section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetOptions(ICommandModel model, ICommandInfo? command)
{
@@ -379,11 +390,9 @@ public class HelpProvider : IHelpProvider
}
var result = new List<IRenderable>
{
new Markup(Environment.NewLine),
new Markup($"[yellow]{resources.Options}:[/]"),
new Markup(Environment.NewLine),
};
{
NewComposer().LineBreak().Style(helpStyles?.Options?.Header ?? Style.Plain, $"{resources.Options}:").LineBreak(),
};
var helpOptions = parameters.ToArray();
var defaultValueColumn = ShowOptionDefaultValues && helpOptions.Any(e => e.DefaultValue != null);
@@ -397,71 +406,24 @@ public class HelpProvider : IHelpProvider
grid.AddColumn(new GridColumn { Padding = new Padding(0, 0) });
static string GetOptionParts(HelpOption option)
{
var builder = new StringBuilder();
if (option.Short != null)
{
builder.Append('-').Append(option.Short.EscapeMarkup());
if (option.Long != null)
{
builder.Append(", ");
}
}
else
{
builder.Append(" ");
if (option.Long != null)
{
builder.Append(" ");
}
}
if (option.Long != null)
{
builder.Append("--").Append(option.Long.EscapeMarkup());
}
if (option.Value != null)
{
builder.Append(' ');
if (option.ValueIsOptional ?? false)
{
builder.Append("[grey][[").Append(option.Value.EscapeMarkup()).Append("]][/]");
}
else
{
builder.Append("[silver]<").Append(option.Value.EscapeMarkup()).Append(">[/]");
}
}
return builder.ToString();
}
if (defaultValueColumn)
{
grid.AddRow(" ", $"[lime]{resources.Default}[/]", " ");
grid.AddRow(
NewComposer().Space(),
NewComposer().Style(helpStyles?.Options?.DefaultValueHeader ?? Style.Plain, resources.Default),
NewComposer().Space());
}
foreach (var option in helpOptions)
{
var columns = new List<string> { GetOptionParts(option) };
var columns = new List<IRenderable>() { GetOptionParts(option) };
if (defaultValueColumn)
{
static string Bold(object obj) => $"[bold]{obj.ToString().EscapeMarkup()}[/]";
var defaultValue = option.DefaultValue switch
{
null => " ",
"" => " ",
Array { Length: 0 } => " ",
Array array => string.Join(", ", array.Cast<object>().Select(Bold)),
_ => Bold(option.DefaultValue),
};
columns.Add(defaultValue);
columns.Add(GetOptionDefaultValue(option.DefaultValue));
}
columns.Add(option.Description?.TrimEnd('.') ?? " ");
columns.Add(NewComposer().Text(option.Description?.TrimEnd('.') ?? " "));
grid.AddRow(columns.ToArray());
}
@@ -469,18 +431,72 @@ public class HelpProvider : IHelpProvider
result.Add(grid);
return result;
}
/// <summary>
/// Gets the commands section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
}
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 IRenderable GetOptionDefaultValue(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),
};
}
/// <summary>
/// Gets the commands section of the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetCommands(ICommandModel model, ICommandInfo? command)
{
var commandContainer = command ?? (ICommandContainer)model;
bool isDefaultCommand = command?.IsDefaultCommand ?? false;
{
var commandContainer = command ?? (ICommandContainer)model;
bool isDefaultCommand = command?.IsDefaultCommand ?? false;
var commands = isDefaultCommand ? model.Commands : commandContainer.Commands;
commands = commands.Where(x => !x.IsHidden).ToList();
@@ -491,11 +507,9 @@ public class HelpProvider : IHelpProvider
}
var result = new List<IRenderable>
{
new Markup(Environment.NewLine),
new Markup($"[yellow]{resources.Commands}:[/]"),
new Markup(Environment.NewLine),
};
{
NewComposer().LineBreak().Style(helpStyles?.Commands?.Header ?? Style.Plain, $"{resources.Commands}:").LineBreak(),
};
var grid = new Grid();
grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true });
@@ -503,43 +517,43 @@ public class HelpProvider : IHelpProvider
foreach (var child in commands)
{
var arguments = new Composer();
arguments.Style("silver", child.Name.EscapeMarkup());
var arguments = NewComposer();
arguments.Style(helpStyles?.Commands?.ChildCommand ?? Style.Plain, child.Name);
arguments.Space();
foreach (var argument in HelpArgument.Get(child).Where(a => a.Required))
{
arguments.Style("silver", $"<{argument.Name.EscapeMarkup()}>");
arguments.Style(helpStyles?.Commands?.RequiredArgument ?? Style.Plain, $"<{argument.Name}>");
arguments.Space();
}
if (TrimTrailingPeriod)
{
grid.AddRow(
arguments.ToString().TrimEnd(),
child.Description?.TrimEnd('.') ?? " ");
NewComposer().Text(arguments.ToString().TrimEnd()),
NewComposer().Text(child.Description?.TrimEnd('.') ?? " "));
}
else
{
grid.AddRow(
arguments.ToString().TrimEnd(),
child.Description ?? " ");
NewComposer().Text(arguments.ToString().TrimEnd()),
NewComposer().Text(child.Description ?? " "));
}
}
result.Add(grid);
return result;
}
/// <summary>
/// Gets the footer for the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
}
/// <summary>
/// Gets the footer for the help information.
/// </summary>
/// <param name="model">The command model to write help for.</param>
/// <param name="command">The command for which to write help information (optional).</param>
/// <returns>An enumerable collection of <see cref="IRenderable"/> objects.</returns>
public virtual IEnumerable<IRenderable> GetFooter(ICommandModel model, ICommandInfo? command)
{
yield break;
{
yield break;
}
}

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.
/// </summary>
public interface ICommandContainer
{
{
/// <summary>
/// Gets all the examples for the container.
/// </summary>

View File

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

View File

@@ -1,23 +1,23 @@
namespace Spectre.Console.Cli.Help;
internal static class ICommandInfoExtensions
{
/// <summary>
/// Walks up the command.Parent tree, adding each command into a list as it goes.
/// </summary>
/// <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>
public static List<ICommandInfo> Flatten(this ICommandInfo commandInfo)
{
var result = new Stack<Help.ICommandInfo>();
var current = commandInfo;
while (current != null)
{
result.Push(current);
current = current.Parent;
}
return result.ToList();
}
}
namespace Spectre.Console.Cli.Help;
internal static class ICommandInfoExtensions
{
/// <summary>
/// Walks up the command.Parent tree, adding each command into a list as it goes.
/// </summary>
/// <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>
public static List<ICommandInfo> Flatten(this ICommandInfo commandInfo)
{
var result = new Stack<Help.ICommandInfo>();
var current = commandInfo;
while (current != null)
{
result.Push(current);
current = current.Parent;
}
return result.ToList();
}
}

View File

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

View File

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

View File

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

View File

@@ -24,22 +24,27 @@ public interface ICommandAppSettings
/// <summary>
/// Gets or sets the application version (use it to override auto-detected value).
/// </summary>
string? ApplicationVersion { get; set; }
/// <summary>
/// Gets or sets a value indicating how many examples from direct children to show in the help text.
/// </summary>
int MaximumIndirectExamples { get; set; }
/// <summary>
/// Gets or sets a value indicating whether any default values for command options are shown in the help text.
/// </summary>
bool ShowOptionDefaultValues { get; set; }
/// <summary>
/// Gets or sets a value indicating whether a trailing period of a command description is trimmed in the help text.
/// </summary>
bool TrimTrailingPeriod { get; set; }
string? ApplicationVersion { get; set; }
/// <summary>
/// Gets or sets a value indicating how many examples from direct children to show in the help text.
/// </summary>
int MaximumIndirectExamples { get; set; }
/// <summary>
/// Gets or sets a value indicating whether any default values for command options are shown in the help text.
/// </summary>
bool ShowOptionDefaultValues { get; set; }
/// <summary>
/// Gets or sets a value indicating whether a trailing period of a command description is trimmed in the help text.
/// </summary>
bool TrimTrailingPeriod { get; set; }
/// <summary>
/// Gets or sets the styles to used when rendering the help text.
/// </summary>
HelpProviderStyle? HelpProviderStyles { get; set; }
/// <summary>
/// Gets or sets the <see cref="IAnsiConsole"/>.
@@ -50,6 +55,7 @@ public interface ICommandAppSettings
/// Gets or sets the <see cref="ICommandInterceptor"/> used
/// to intercept settings before it's being sent to the command.
/// </summary>
[Obsolete("Register the interceptor with the ITypeRegistrar.")]
ICommandInterceptor? Interceptor { get; set; }
/// <summary>
@@ -65,14 +71,14 @@ public interface ICommandAppSettings
/// <summary>
/// Gets or sets a value indicating whether or not parsing is strict.
/// </summary>
bool StrictParsing { get; set; }
bool StrictParsing { get; set; }
/// <summary>
/// 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
/// during parsing with the message "Flags cannot be assigned a value."
/// 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
/// during parsing with the message "Flags cannot be assigned a value."
/// should instead be added to the remaining arguments collection.
/// </summary>
/// </summary>
bool ConvertFlagsToRemainingArguments { get; set; }
/// <summary>
@@ -90,6 +96,8 @@ public interface ICommandAppSettings
/// <summary>
/// Gets or sets a handler for Exceptions.
/// <para>This handler will not be called, if <see cref="PropagateExceptions"/> is set to <c>true</c>.</para>
/// The <see cref="ITypeResolver"/> argument will only be not-null, when the exception occurs during execution of
/// a command. I.e. only when the resolver is available.
/// </summary>
public Func<Exception, int>? ExceptionHandler { get; set; }
public Func<Exception, ITypeResolver?, int>? ExceptionHandler { get; set; }
}

View File

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

View File

@@ -4,19 +4,19 @@ namespace Spectre.Console.Cli;
/// Represents a configurator.
/// </summary>
public interface IConfigurator
{
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <param name="helpProvider">The help provider to use.</param>
public void SetHelpProvider(IHelpProvider helpProvider);
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <typeparam name="T">The type of the help provider to instantiate at runtime and use.</typeparam>
public void SetHelpProvider<T>()
where T : IHelpProvider;
{
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <param name="helpProvider">The help provider to use.</param>
public void SetHelpProvider(IHelpProvider helpProvider);
/// <summary>
/// Sets the help provider for the application.
/// </summary>
/// <typeparam name="T">The type of the help provider to instantiate at runtime and use.</typeparam>
public void SetHelpProvider<T>()
where T : IHelpProvider;
/// <summary>
/// Gets the command app settings.
@@ -66,5 +66,5 @@ public interface IConfigurator
/// <param name="action">The command branch configurator.</param>
/// <returns>A branch configurator that can be used to configure the branch further.</returns>
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>
/// Gets the raw, non-parsed remaining arguments.
/// This is normally everything after the `--` delimiter.
/// </summary>
IReadOnlyList<string> Raw { get; }
}

View File

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

View File

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

View File

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

View File

@@ -5,29 +5,34 @@ internal sealed class CommandAppSettings : ICommandAppSettings
public CultureInfo? Culture { get; set; }
public string? ApplicationName { get; set; }
public string? ApplicationVersion { get; set; }
public int MaximumIndirectExamples { get; set; }
public bool ShowOptionDefaultValues { get; set; }
public int MaximumIndirectExamples { get; set; }
public bool ShowOptionDefaultValues { get; set; }
public IAnsiConsole? Console { get; set; }
[Obsolete("Register the interceptor with the ITypeRegistrar.")]
public ICommandInterceptor? Interceptor { get; set; }
public ITypeRegistrarFrontend Registrar { get; set; }
public CaseSensitivity CaseSensitivity { get; set; }
public bool PropagateExceptions { get; set; }
public bool ValidateExamples { get; set; }
public bool TrimTrailingPeriod { get; set; } = true;
public bool StrictParsing { get; set; }
public bool ConvertFlagsToRemainingArguments { get; set; } = false;
public bool TrimTrailingPeriod { get; set; }
public HelpProviderStyle? HelpProviderStyles { get; set; }
public bool StrictParsing { get; set; }
public bool ConvertFlagsToRemainingArguments { get; set; }
public ParsingMode ParsingMode =>
StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed;
public Func<Exception, int>? ExceptionHandler { get; set; }
public Func<Exception, ITypeResolver?, int>? ExceptionHandler { get; set; }
public CommandAppSettings(ITypeRegistrar registrar)
{
Registrar = new TypeRegistrar(registrar);
CaseSensitivity = CaseSensitivity.All;
ShowOptionDefaultValues = true;
ShowOptionDefaultValues = true;
MaximumIndirectExamples = 5;
TrimTrailingPeriod = true;
HelpProviderStyles = HelpProviderStyle.Default;
ConvertFlagsToRemainingArguments = false;
}
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);
Examples = new List<string[]>();
}
public void SetHelpProvider(IHelpProvider helpProvider)
{
// Register the help provider
_registrar.RegisterInstance(typeof(IHelpProvider), helpProvider);
}
public void SetHelpProvider<T>()
where T : IHelpProvider
{
// Register the help provider
_registrar.Register(typeof(IHelpProvider), typeof(T));
}
public void SetHelpProvider(IHelpProvider helpProvider)
{
// Register the help provider
_registrar.RegisterInstance(typeof(IHelpProvider), helpProvider);
}
public void SetHelpProvider<T>()
where T : IHelpProvider
{
// Register the help provider
_registrar.Register(typeof(IHelpProvider), typeof(T));
}
public void AddExample(params string[] args)
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
namespace Spectre.Console.Cli;
internal static class CommandModelBuilder
{
// 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);
// Build the default command.
var defaultCommand = Build(null, configuration.DefaultCommand);
var defaultCommand = Build(null, configuration.DefaultCommand);
result.Add(defaultCommand);
}
@@ -55,7 +55,7 @@ internal static class CommandModelBuilder
foreach (var childCommand in command.Children)
{
var child = Build(info, childCommand);
info.Children.Add(child);
info.Children.Add(child);
}
// Normalize argument positions.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,13 +29,13 @@ internal static class CommandTreeTokenizer
var context = new CommandTreeTokenizerContext();
foreach (var arg in args)
{
if (string.IsNullOrEmpty(arg))
{
// Null strings in the args array are still represented as tokens
tokens.Add(new CommandTreeToken(CommandTreeToken.Kind.String, position, string.Empty, string.Empty));
continue;
}
{
if (string.IsNullOrEmpty(arg))
{
// Null strings in the args array are still represented as tokens
tokens.Add(new CommandTreeToken(CommandTreeToken.Kind.String, position, string.Empty, string.Empty));
continue;
}
var start = position;
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)
{
if (!reader.ReachedEnd && reader.Peek() == '-')
{
// Option
tokens.AddRange(ScanOptions(context, reader));
}
else
{
// Command or argument
while (reader.Peek() != -1)
{
if (reader.ReachedEnd)
{
position += reader.Position - start;
break;
}
tokens.Add(ScanString(context, reader));
// Flush remaining tokens
context.FlushRemaining();
}
}
{
if (!reader.ReachedEnd && reader.Peek() == '-')
{
// Option
tokens.AddRange(ScanOptions(context, reader));
}
else
{
// Command or argument
while (reader.Peek() != -1)
{
if (reader.ReachedEnd)
{
position += reader.Position - start;
break;
}
tokens.Add(ScanString(context, reader));
// Flush remaining tokens
context.FlushRemaining();
}
}
return position;
}
@@ -102,7 +102,7 @@ internal static class CommandTreeTokenizer
builder.Append(current);
}
var value = builder.ToString();
var value = builder.ToString();
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, "=", "=");
throw CommandParseException.OptionValueWasExpected(reader.Original, token);
}
var tokenValue = ScanString(context, reader);
var tokenValue = ScanString(context, reader);
tokenValue.HadSeparator = true;
result.Add(tokenValue);
}
@@ -171,7 +171,7 @@ internal static class CommandTreeTokenizer
}
// Encountered a separator?
if (current == '=' || current == ':')
if (current is '=' or ':')
{
break;
}
@@ -184,7 +184,7 @@ internal static class CommandTreeTokenizer
var value = current.ToString(CultureInfo.InvariantCulture);
result.Add(result.Count == 0
? new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position, value, $"-{value}")
: new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position + result.Count, value, value));
: new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position + result.Count, value, $"-{value}"));
}
else if (result.Count == 0 && char.IsDigit(current))
{
@@ -193,7 +193,7 @@ internal static class CommandTreeTokenizer
// 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
// result.Count == 0 check above.
string value = string.Empty;
string value = string.Empty;
while (!reader.ReachedEnd)
{
@@ -212,12 +212,12 @@ internal static class CommandTreeTokenizer
result.Add(new CommandTreeToken(CommandTreeToken.Kind.String, position, value, value));
}
else
{
{
// Create a token representing the short option.
var representation = current.ToString(CultureInfo.InvariantCulture);
var tokenPosition = position + 1 + result.Count;
var token = new CommandTreeToken(CommandTreeToken.Kind.ShortOption, tokenPosition, representation, representation);
var representation = current.ToString(CultureInfo.InvariantCulture);
var tokenPosition = position + 1 + result.Count;
var token = new CommandTreeToken(CommandTreeToken.Kind.ShortOption, tokenPosition, representation, representation);
throw CommandParseException.InvalidShortOptionName(reader.Original, token);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@
<Compile Include="..\Spectre.Console\Internal\Extensions\EnumerableExtensions.cs" Link="Internal\EnumerableExtensions.cs" />
<Compile Include="..\Spectre.Console\Internal\Text\StringBuffer.cs" Link="Internal\StringBuffer.cs" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="..\stylecop.json" Link="Properties/stylecop.json" />
<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);
}
#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();
return new CommandAppResult(result, output, context, settings);
}
}
/// <summary>
/// Runs the command application asynchronously.
/// </summary>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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