Compare commits

...

31 Commits

Author SHA1 Message Date
Patrik Svensson
e8e92e7f44 Remove path criteria for publish workflow 2021-10-05 17:57:25 +02:00
Patrik Svensson
3718502eee Update workflow 2021-10-05 17:55:08 +02:00
Patrik Svensson
39cfc7a62f Allow workflows to run for workflow changes 2021-10-05 17:54:02 +02:00
Patrik Svensson
2e90ef28e4 Allow workflows to run for test and example changes 2021-10-05 17:52:30 +02:00
Patrik Svensson
2a3763cdc7 Prove that remaining args works in strict mode
Closes #496
2021-10-05 09:34:08 -04:00
Patrik Svensson
e0395dfa2b Add AnsiConsole.Write method
This commit also obsoletes the `AnsiConsole.Render` method.

Closes #576
2021-10-05 09:33:33 -04:00
Cédric Luthi
ca2e6ce0ad Improve the error message when acquiring the interactive semaphore fails
Using the same error message as in the sync version of the `Run` method which is indeed much better than "Could not aquire the interactive semaphore".
2021-10-05 00:51:54 +02:00
rifatx
fa15389158 Add support custom max value for barcharts (#545) 2021-10-05 00:49:09 +02:00
Cédric Luthi
a5716a35e2 Future-proof conditional compilation
* Invert `#if NET5_0` conditions so that when adding net6.0 target framework, the _new_ APIs are used.
* Use `NET5_0_OR_GREATER` instead of `NET5_0` to ensure consistent behaviour on future target frameworks.
2021-09-29 10:01:31 +02:00
GitHubPang
644fb76d61 Fix a typo for Spectre1021 2021-09-27 20:50:51 -04:00
Christopher Rollings
e3dfe23b59 Work done to allow user to update table cell. (#546) 2021-09-27 13:03:45 +02:00
Phil Scott
ad23855b0a Adds a segment builder for merging multiple segments
When merging a large amount of segments together we were doing a tremendous amount of allocation especially related to strings due to concatination.

This adds an internal SegmentBuilder that uses a stringbuilder for building up the text rather than creating a new instance and doing a concat operation for each segment.
2021-09-27 10:49:44 +02:00
GitHubPang
64f444114a Fix typos 2021-09-23 19:08:03 -04:00
Patrik Svensson
b058c54f3c Disable GH Action workflow for docs 2021-09-23 23:26:31 +02:00
Patrik Svensson
8cfe06e77a Expose extension method that gets the cell width of text 2021-09-18 23:20:28 +02:00
Patrik Svensson
48d49d6e18 Add extension method to get the cell width of a character 2021-09-18 23:20:28 +02:00
Nils Andresen
49e8a980a7 Removed the additional registration of ICommand 2021-09-18 23:19:34 +02:00
GitHubPang
d34012cad0 Fix typos in code comments 2021-09-13 22:58:52 -04:00
Nils Andresen
c3510f3036 fixed 404 in documentation
links were pointing to ./commandApp, however
the generated page is ./commandapp
2021-09-08 12:02:01 +02:00
Cédric Luthi
786b7670da Fix the style parameter nullable annotation on AnsiConsoleExtensions
The style parameter is actually nullable. Also, the documentation of the style parameter has been made explicit that `Style.Plain` is used when a `null` style is passed.
2021-08-31 18:52:34 +02:00
Cédric Luthi
ffd24ec451 Fix intermittent test failure
When running on .NET Framework, the `Should_Report_Max_Remaining_Time_For_Extremely_Small_Progress` would intermittently fail with the following error:
```
task.RemainingTime
    should be
10675199.02:48:05.4775807
    but was
null
```

This is because it's possible that the two increment share the same timestamp thus making the `RemainingTime` null. To ensure the two increments don't share the same timestamp, we sleep for one millisecond.

Although I have only observed this issue on .NET Framework it would be possible that it occasionally also occur on .NET Core.
2021-08-30 11:08:24 +02:00
Cédric Luthi
e081593012 Fix parsing of exceptions on .NET Framework
On .NET Framework, `exception.ToString()` uses a slightly different format than on .NET Core.

So in order to properly transform an `Exception` into an `ExceptionInfo` on both .NET Core and .NET Framework we use `exception.StackTrace` + `exception.InnerException`. As an added benefit, it greatly simplifies the implementation of the `ExceptionParser` class.
2021-08-30 11:08:24 +02:00
Cédric Luthi
bf95564ebb Make tests run on .NET Framework 4.8
Since Spectre.Console targets .NET Standard 2.0 it makes sense to also run tests on .NET Framework (Windows only)

This makes two tests fail: `Should_Write_Exception_With_Inner_Exception` and `Should_Write_Exceptions_With_Generic_Type_Parameters_In_Callsite_As_Expected`.

Received (inner exception not handled):
```
System.InvalidOperationException: Something threw!System.InvalidOperationException: Throwing!
  at Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(Nullable`1 number) in /xyz/Exceptions.cs:nn
  at Spectre.Console.Tests.Data.TestExceptions.ThrowWithInnerException() in /xyz/Exceptions.cs:nn
```

Verified:
```
System.InvalidOperationException: Something threw!
     System.InvalidOperationException: Throwing!
       at Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(Nullable`1 number) in /xyz/Exceptions.cs:nn
       at Spectre.Console.Tests.Data.TestExceptions.ThrowWithInnerException() in /xyz/Exceptions.cs:nn
  at Spectre.Console.Tests.Data.TestExceptions.ThrowWithInnerException() in /xyz/Exceptions.cs:nn
  at Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exception_With_Inner_Exception>b__3_0() in /xyz/ExceptionTests.cs:nn
  at Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn
```
2021-08-30 11:08:24 +02:00
Daniel Cazzulino
3c5b98123b Make building more flexible by allowing feature bands
Currently, the latest stable .NET5 is version 5.0.400, which 
fails with `latestPatch` but works with `latestFeature`. 

Given that the project doesn't appear to depend on any low
level primitives or behaviors that might break from .NET 
feature band upgrades, it's far more convenient for contributors 
to have a more flexible requirement.
2021-08-24 17:18:03 +02:00
Liam Sho
7276e11ecc Add a test for rendering table with EA characters
Test rending a table with East Asia characters (Chinese, Japanese, Korean).

The verified text file may look weird, but it looks normal and correctly in Console (tested in Windows Terminal, Terminal.app in macOS Monterey)
2021-08-24 16:45:30 +02:00
Liam Sho
d306ad82d1 Fix ArgumentOutOfRangeException when rendering a table
When rendering a table with East Asia characters (take 2 English alphabets width) will throw ArgumentOutOfRangeException.
2021-08-24 16:45:30 +02:00
Patrik Svensson
d96817dc9c Do not share semaphore between consoles
Closes #494
2021-08-14 23:43:09 +02:00
Patrik Svensson
e169df6303 Add support for manipulating individual table rows
Closes #500
2021-08-14 23:35:07 +02:00
Patrik Svensson
57731c0d55 Add release notes for version 0.41 2021-07-19 23:12:38 +02:00
Phil Scott
170901f584 Adds additional check that analyzer is within a method
Resolve #487
2021-07-19 22:31:25 +02:00
Phil Scott
c2b25eea8a Using browser context for social cards
Scott Hanselman recommended using the context instead of the browser object. Browser object creates a new context on each call which is a new process. Obviously we don't want that. 

Also added an extra check for a load based on network idle. This will not only ensure things are loaded, but there is a built in 500ms timeout looking for inactivity which will let the font rendering process do it's thing which seems to lag a tad with Chromium.

And while we are at it, preloading the font can't hurt.
2021-07-18 11:23:23 +02:00
98 changed files with 1531 additions and 299 deletions

View File

@@ -13,6 +13,7 @@ jobs:
docs:
name: Documentation
if: false # Disable for now
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -89,7 +90,7 @@ jobs:
shell: bash
run: |
dotnet tool restore
dotnet example --all
dotnet example --all --skip live --skip livetable --skip prompt
- name: Build
shell: bash

View File

@@ -5,6 +5,9 @@ on:
paths:
- 'docs/**'
- 'src/**'
- 'test/**'
- 'examples/**'
- '.github/**'
jobs:
@@ -14,6 +17,7 @@ jobs:
build:
name: Deploy
if: false # Disable for now
runs-on: windows-latest
steps:
- name: Checkout

View File

@@ -6,8 +6,6 @@ on:
- '*'
branches:
- main
paths:
- 'src/**'
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
@@ -21,6 +19,7 @@ jobs:
docs:
name: Documentation
if: false # Disable for now
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -46,7 +45,7 @@ jobs:
build:
name: Build
needs: [docs]
# needs: [docs]
if: "!contains(github.event.head_commit.message, 'skip-ci') || startsWith(github.ref, 'refs/tags/')"
strategy:
matrix:

View File

@@ -7,7 +7,7 @@ Severity: Warning
## Cause
A violation of this rule occurs when a AnsiConsole prompt is called within the context of an executing renderable e.g. `Progress`, `Status` and `Live`. Concurrent LiveRenderable are not supported and will cause issues when running simultaneously.
A violation of this rule occurs when an AnsiConsole prompt is called within the context of an executing renderable e.g. `Progress`, `Status` and `Live`. Concurrent LiveRenderable are not supported and will cause issues when running simultaneously.
## Reason for rule

View File

@@ -8,7 +8,7 @@ Highlights:
- And more...
---
There is different built-in borders you can use for tables and panels.
There are different built-in borders you can use for tables and panels.
## Table borders

View File

@@ -25,7 +25,7 @@ AnsiConsole.Status()
To implement your own spinner, all you have to do is
inherit from the `Spinner` base class.
In the example below, the spinner will alterate between
In the example below, the spinner will alternate between
the characters `A`, `B` and `C` every 100 ms.
```csharp

View File

@@ -0,0 +1,24 @@
Title: Spectre.Console 0.41 released!
Description: In this release we (mostly Phil) have been focusing on getting the new fancy Roslyn Analyzers out the door...<br /><br /><br /><br /><br />[More]
Published: 20210719
Category: Release Notes
Excluded: false
---
In this release, we (mostly [Phil](https://twitter.com/philco78)) have been focusing on getting the new fancy Roslyn Analyzers out the door.
If you want to try them out, add a reference to [Spectre.Console.Analyzer](https://www.nuget.org/packages/spectre.console.analyzer) in your project, and you should get some best practice tips in your favorite IDE!
It's summer in the northern hemisphere, so it will probably be a couple of weeks until the next release.
## Features
* [#417 - Support cancellation in prompts](https://github.com/spectreconsole/spectre.console/issues/417)
* [#324 - Remove AsciiTreeGuide as default tree guide](https://github.com/spectreconsole/spectre.console/issues/324)
* [#413 - Support custom characters at the end of a TextPrompt](https://github.com/spectreconsole/spectre.console/issues/413)
* [#447 - Alternative to the obsolete 'Select' function for selecting default items in SelectionPrompt](https://github.com/spectreconsole/spectre.console/issues/447)
* [#460 - Default values for Ask()](https://github.com/spectreconsole/spectre.console/issues/460)
## Bugs
* [#480 - IAnsiConsole.Confirm extension is missing default value parameter](https://github.com/spectreconsole/spectre.console/issues/480)
* [#442 - Allow dynamic Figlet hardblank](https://github.com/spectreconsole/spectre.console/pull/442)

View File

@@ -20,7 +20,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 an `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 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.
For example, to add three different commands to the application:
@@ -34,7 +34,7 @@ app.Configure(config =>
});
```
This configuration would allow users to run `app.exe add`, `app.exe commit`, or `app.exe rebase` and have the settings routed the appropriate command.
This configuration would allow users to run `app.exe add`, `app.exe commit`, or `app.exe rebase` and have the settings routed to the appropriate command.
For more complex command hierarchical configurations, they can also be composed via inheritance and branching. See [Composing Commands](./composing).
@@ -76,7 +76,7 @@ return app.Run(args);
## Interception
`CommandApp` also provides a `SetInterceptor` configuration. An interceptor is ran before all commands are executed. This is typically used for configuring logging or other infrastructure concerns.
`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, an instance of your interceptor will be called with the parsed settings. This provides an opportunity for configuring any infrastructure or modifying the settings.

View File

@@ -25,7 +25,7 @@ public class HelloCommand : Command<HelloCommand.Settings>
## Configuring
Commands are configured via the [`CommandApp`](commandApp)'s `Configure` method.
Commands are configured via the [`CommandApp`](commandapp)'s `Configure` method.
```csharp
var app = new CommandApp();
@@ -45,7 +45,7 @@ app.Configure(config =>
## Dependency Injection
Constructor injection is supported on commands. See the [`CommandApp`](commandApp) documentation for further information on configuring `Spectre.Console` for your container.
Constructor injection is supported on commands. See the [`CommandApp`](commandapp) documentation for further information on configuring `Spectre.Console` for your container.
## Validation

View File

@@ -111,7 +111,7 @@ Now you might wonder, why do things like this? Well, for starters the different
of the application are separated, while still having the option to share things like options,
flags and arguments between them.
This make the resulting code very clean and easy to navigate, not to mention to unit test.
This makes the resulting code very clean and easy to navigate, not to mention to unit test.
And most importantly at all, the type system guides me to do the right thing. I can't configure
commands in non-compatible ways, and if I want to add a new top-level `add-package` command
(or move the command completely), it's just a single line to change. This makes it easy to

View File

@@ -67,7 +67,7 @@ This command will have three parameters.
When `args` is passed into the `CommandApp`'s run method, `Spectre.Console.Cli` will parse those arguments and populate an instance of your settings. Upon success, it will then pass those settings into an instance of the specified command's `Execute` method.
With this in place, we can the following commands will all work
With this in place, the following commands will all work
```text
app.exe
@@ -78,6 +78,6 @@ app.exe c:\windows --hidden --pattern *.dll
Much more is possible. You can have multiple commands per application, settings can be customized and extended and the default behavior of the `CommandApp` can be extended and customized.
* See [CommandApp](./commandApp) for customizing how Spectre.Console.Cli builds the settings.
* See [CommandApp](./commandapp) for customizing how Spectre.Console.Cli builds the settings.
* See [Create Commands](./commands) for information about different command types and their configurations.
* See [Specifying Settings](./settings) for information about defining the settings.

View File

@@ -117,7 +117,7 @@ Now you might wonder, why do things like this? Well, for starters the different
of the application are separated, while still having the option to share things like options,
flags and arguments between them.
This make the resulting code very clean and easy to navigate, not to mention to unit test.
This makes the resulting code very clean and easy to navigate, not to mention to unit test.
And most importantly at all, the type system guides me to do the right thing. I can't configure
commands in non-compatible ways, and if I want to add a new top-level `add-package` command
(or move the command completely), it's just a single line to change. This makes it easy to

View File

@@ -1,6 +1,6 @@
Title: Migrate from Spectre.Cli
Order: 10
Description: "Migrating from *Specte.Cli* to *Spectre.Console.Cli*"
Description: "Migrating from *Spectre.Cli* to *Spectre.Console.Cli*"
---
The functionality in `Spectre.Cli` has been moved into the `Spectre.Console`
@@ -26,7 +26,7 @@ Add the [Spectre.Console](https://www.nuget.org/packages/spectre.console) NuGet
## 3. Change using statements
Change all using statements from `Spectre.Cli`
to `Spectre.Console.CLi`.
to `Spectre.Console.Cli`.
```diff
- using Spectre.Cli;

View File

@@ -22,7 +22,7 @@ This setting file tells `Spectre.Console.Cli` that our command has two parameter
## CommandArgument
Arguments have a position and a name. The name is not only used for generating help, but it's formatting is used to determine whether or not the argument is optional. The name must either be surrounded by square brackets (e.g. `[name]`) or angle brackets (e.g. `<name>`). Angle brackets denote required whereas square brackets denote optional. If neither are specified an exception will be thrown.
Arguments have a position and a name. The name is not only used for generating help, but its formatting is used to determine whether or not the argument is optional. The name must either be surrounded by square brackets (e.g. `[name]`) or angle brackets (e.g. `<name>`). Angle brackets denote required whereas square brackets denote optional. If neither are specified an exception will be thrown.
The position is used for scenarios where there could be more than one argument.

View File

@@ -81,7 +81,7 @@ For a list of emoji, see the [Emojis](xref:emojis) appendix section.
## Colors
In the examples above, all colors was referenced by their name,
In the examples above, all colors were referenced by their name,
but you can also use the hex or rgb representation for colors in markdown.
```csharp

View File

@@ -36,7 +36,7 @@ AnsiConsole.Render(image);
## Manipulating images
You can take full advantage of [ImageSharp](https://github.com/SixLabors/ImageSharp)
and manipulate images directly via it's [Processing API](https://docs.sixlabors.com/api/ImageSharp/SixLabors.ImageSharp.Processing.html).
and manipulate images directly via its [Processing API](https://docs.sixlabors.com/api/ImageSharp/SixLabors.ImageSharp.Processing.html).
```csharp
// Load an image

View File

@@ -51,6 +51,7 @@ namespace Docs.Pipelines
private IPlaywright _playwright;
private IBrowser _browser;
private WebApplication _app;
private IBrowserContext _context;
protected override async Task BeforeExecutionAsync(IExecutionContext context)
{
@@ -74,10 +75,14 @@ namespace Docs.Pipelines
_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);
@@ -87,18 +92,19 @@ namespace Docs.Pipelines
protected override async Task<IEnumerable<IDocument>> ExecuteInputAsync(IDocument input, IExecutionContext context)
{
var url = _app.Urls.FirstOrDefault(u => u.StartsWith("http://"));
var page = await _browser.NewPageAsync(new BrowserNewPageOptions
{
ViewportSize = new ViewportSize { Width = 1200, Height = 618 },
}
);
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)}");
var bytes = await page.ScreenshotAsync();
// 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(
@@ -107,7 +113,7 @@ namespace Docs.Pipelines
new MetadataItems { { "DocId", input.Id }},
context.GetContentProvider(bytes));
return new[] { doc };
return new[] { doc };
}
}
}

View File

@@ -8,6 +8,7 @@
<html>
<head>
<link rel="preload" as="font" href="/static/CascadiaCodePL.woff2">
<link rel="stylesheet" href="static/styles.css" />
</head>
<body>

View File

@@ -15,7 +15,7 @@
]
},
"dotnet-example": {
"version": "1.3.1",
"version": "1.5.0",
"commands": [
"dotnet-example"
]

View File

@@ -23,7 +23,7 @@ namespace Demo.Utilities
value ?? "[grey]null[/]");
}
AnsiConsole.Render(table);
AnsiConsole.Write(table);
}
}
}

View File

@@ -8,7 +8,7 @@ using Spectre.Console.Cli;
* This works around the chicken and egg situation with configuring serilog via the command line.
* The logger needs to be configured prior to executing the parser, but the logger needs the parsed values
* to be configured. By using serilog.sinks.map we can defer configuration. We use a LogLevelSwitch to control the
* logging levels dynamically, and then we use a serilog enricher that has it's state populated via a
* logging levels dynamically, and then we use a serilog enricher that has its state populated via a
* Spectre.Console CommandInterceptor
*/

View File

@@ -35,7 +35,7 @@ namespace Spectre.Console.Examples
CreatePanel("None", BoxBorder.None),
};
AnsiConsole.Render(
AnsiConsole.Write(
new Padder(
new Columns(items).PadRight(2),
new Padding(2,0,0,0)));
@@ -77,13 +77,13 @@ namespace Spectre.Console.Examples
CreateTable("Markdown", TableBorder.Markdown),
};
AnsiConsole.Render(new Columns(items).Collapse());
AnsiConsole.Write(new Columns(items).Collapse());
}
private static void HorizontalRule(string title)
{
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule($"[white bold]{title}[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.Write(new Rule($"[white bold]{title}[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.WriteLine();
}
}

View File

@@ -5,7 +5,7 @@ namespace Spectre.Console.Examples
public static void Main(string[] args)
{
AnsiConsole.WriteLine();
AnsiConsole.Render(new Calendar(2020, 10)
AnsiConsole.Write(new Calendar(2020, 10)
.RoundedBorder()
.HighlightStyle(Style.Parse("red"))
.HeaderStyle(Style.Parse("yellow"))

View File

@@ -39,9 +39,9 @@ namespace Spectre.Console.Examples
private static void Render(IRenderable canvas, string title)
{
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule($"[yellow]{title}[/]").LeftAligned().RuleStyle("grey"));
AnsiConsole.Write(new Rule($"[yellow]{title}[/]").LeftAligned().RuleStyle("grey"));
AnsiConsole.WriteLine();
AnsiConsole.Render(canvas);
AnsiConsole.Write(canvas);
}
}
}

View File

@@ -32,7 +32,7 @@ namespace Spectre.Console.Examples
private static void Render(string title, IRenderable chart)
{
AnsiConsole.Render(
AnsiConsole.Write(
new Panel(chart)
.Padding(1, 1)
.Header(title));

View File

@@ -20,7 +20,7 @@ namespace Spectre.Console.Examples
{
AnsiConsole.ResetColors();
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule("[yellow bold underline]3-bit Colors[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.Write(new Rule("[yellow bold underline]3-bit Colors[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.WriteLine();
for (var i = 0; i < 8; i++)
@@ -43,7 +43,7 @@ namespace Spectre.Console.Examples
{
AnsiConsole.ResetColors();
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule("[yellow bold underline]4-bit Colors[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.Write(new Rule("[yellow bold underline]4-bit Colors[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.WriteLine();
for (var i = 0; i < 16; i++)
@@ -66,7 +66,7 @@ namespace Spectre.Console.Examples
{
AnsiConsole.ResetColors();
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule("[yellow bold underline]8-bit Colors[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.Write(new Rule("[yellow bold underline]8-bit Colors[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.WriteLine();
for (var i = 0; i < 16; i++)
@@ -93,10 +93,10 @@ namespace Spectre.Console.Examples
{
AnsiConsole.ResetColors();
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule("[yellow bold underline]24-bit Colors[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.Write(new Rule("[yellow bold underline]24-bit Colors[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.WriteLine();
AnsiConsole.Render(new ColorBox(width: 80, height: 15));
AnsiConsole.Write(new ColorBox(width: 80, height: 15));
}
}
}

View File

@@ -16,7 +16,7 @@ namespace Spectre.Console.Examples
}
// Render all cards in columns
AnsiConsole.Render(new Columns(cards));
AnsiConsole.Write(new Columns(cards));
}
private static string GetCardContent(User user)

View File

@@ -14,7 +14,7 @@ namespace Spectre.Console.Examples
private static void RenderEmoji()
{
AnsiConsole.Render(
AnsiConsole.Write(
new Panel("[yellow]Hello :globe_showing_europe_africa:![/]")
.RoundedBorder());
}

View File

@@ -14,17 +14,17 @@ namespace Spectre.Console.Examples
catch (Exception ex)
{
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule("Default").LeftAligned());
AnsiConsole.Write(new Rule("Default").LeftAligned());
AnsiConsole.WriteLine();
AnsiConsole.WriteException(ex);
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule("Compact").LeftAligned());
AnsiConsole.Write(new Rule("Compact").LeftAligned());
AnsiConsole.WriteLine();
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks);
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule("Compact + Custom colors").LeftAligned());
AnsiConsole.Write(new Rule("Compact + Custom colors").LeftAligned());
AnsiConsole.WriteLine();
AnsiConsole.WriteException(ex, new ExceptionSettings
{

View File

@@ -4,9 +4,9 @@ namespace Spectre.Console.Examples
{
public static void Main(string[] args)
{
AnsiConsole.Render(new FigletText("Left aligned").LeftAligned().Color(Color.Red));
AnsiConsole.Render(new FigletText("Centered").Centered().Color(Color.Green));
AnsiConsole.Render(new FigletText("Right aligned").RightAligned().Color(Color.Blue));
AnsiConsole.Write(new FigletText("Left aligned").LeftAligned().Color(Color.Red));
AnsiConsole.Write(new FigletText("Centered").Centered().Color(Color.Green));
AnsiConsole.Write(new FigletText("Right aligned").RightAligned().Color(Color.Blue));
}
}
}

View File

@@ -16,7 +16,7 @@ namespace Spectre.Console.Examples
grid.AddRow(" [blue]-c[/], [blue]--configuration[/] <CONFIGURATION>", "The configuration to run for.");
grid.AddRow(" [blue]-v[/], [blue]--verbosity[/] <LEVEL>", "Set the [grey]MSBuild[/] verbosity level.");
AnsiConsole.Render(grid);
AnsiConsole.Write(grid);
}
}
}

View File

@@ -19,7 +19,7 @@ namespace Spectre.Console.Examples
.AddRow("[b]Buffer height[/]", $"{AnsiConsole.Console.Profile.Height}")
.AddRow("[b]Encoding[/]", $"{AnsiConsole.Console.Profile.Encoding.EncodingName}");
AnsiConsole.Render(
AnsiConsole.Write(
new Panel(grid)
.Header("Information"));
}

View File

@@ -5,7 +5,7 @@
<TargetFramework>net5.0</TargetFramework>
<ExampleTitle>Live</ExampleTitle>
<ExampleDescription>Demonstrates how to do live updates.</ExampleDescription>
<ExampleGroup>Misc</ExampleGroup>
<ExampleGroup>Live</ExampleGroup>
</PropertyGroup>
<ItemGroup>

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<ExampleTitle>LiveTable</ExampleTitle>
<ExampleDescription>Demonstrates how to do live updates in a table.</ExampleDescription>
<ExampleGroup>Live</ExampleGroup>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Shared\Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,86 @@
using System;
using System.Linq;
using System.Threading.Tasks;
namespace Spectre.Console.Examples
{
public static class Program
{
private const int NumberOfRows = 10;
private static readonly Random _random = new();
private static readonly string[] _exchanges = new string[]
{
"SGD", "SEK", "PLN",
"MYR", "EUR", "USD",
"AUD", "JPY", "CNH",
"HKD", "CAD", "INR",
"DKK", "GBP", "RUB",
"NZD", "MXN", "IDR",
"TWD", "THB", "VND",
};
public static async Task Main(string[] args)
{
var table = new Table().Expand().BorderColor(Color.Grey);
table.AddColumn("[yellow]Source currency[/]");
table.AddColumn("[yellow]Destination currency[/]");
table.AddColumn("[yellow]Exchange rate[/]");
AnsiConsole.MarkupLine("Press [yellow]CTRL+C[/] to exit");
await AnsiConsole.Live(table)
.AutoClear(false)
.Overflow(VerticalOverflow.Ellipsis)
.Cropping(VerticalOverflowCropping.Bottom)
.StartAsync(async ctx =>
{
// Add some initial rows
foreach (var _ in Enumerable.Range(0, NumberOfRows))
{
AddExchangeRateRow(table);
}
// Continously update the table
while (true)
{
// More rows than we want?
if (table.Rows.Count > NumberOfRows)
{
// Remove the first one
table.Rows.RemoveAt(0);
}
// Add a new row
AddExchangeRateRow(table);
// Refresh and wait for a while
ctx.Refresh();
await Task.Delay(400);
}
});
}
private static void AddExchangeRateRow(Table table)
{
var (source, destination, rate) = GetExchangeRate();
table.AddRow(
source, destination,
_random.NextDouble() > 0.35D ? $"[green]{rate}[/]" : $"[red]{rate}[/]");
}
private static (string Source, string Destination, double Rate) GetExchangeRate()
{
var source = _exchanges[_random.Next(0, _exchanges.Length)];
var dest = _exchanges[_random.Next(0, _exchanges.Length)];
var rate = 200 / ((_random.NextDouble() * 320) + 1);
while (source == dest)
{
dest = _exchanges[_random.Next(0, _exchanges.Length)];
}
return (source, dest, rate);
}
}
}

View File

@@ -8,20 +8,20 @@ namespace Spectre.Console.Examples
"[underline]I[/] heard [underline on blue]you[/] like panels\n\n\n\n" +
"So I put a panel in a panel").Centered();
AnsiConsole.Render(
AnsiConsole.Write(
new Panel(
new Panel(content)
.Border(BoxBorder.Rounded)));
// Left adjusted panel with text
AnsiConsole.Render(
AnsiConsole.Write(
new Panel(new Text("Left adjusted\nLeft").LeftAligned())
.Expand()
.SquareBorder()
.Header("[red]Left[/]"));
// Centered ASCII panel with text
AnsiConsole.Render(
AnsiConsole.Write(
new Panel(new Text("Centered\nCenter").Centered())
.Expand()
.AsciiBorder()
@@ -29,7 +29,7 @@ namespace Spectre.Console.Examples
.HeaderAlignment(Justify.Center));
// Right adjusted, rounded panel with text
AnsiConsole.Render(
AnsiConsole.Write(
new Panel(new Text("Right adjusted\nRight").RightAligned())
.Expand()
.RoundedBorder()

View File

@@ -28,8 +28,8 @@ namespace Spectre.Console.Examples
// Summary
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule("[yellow]Results[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.Render(new Table().AddColumns("[grey]Question[/]", "[grey]Answer[/]")
AnsiConsole.Write(new Rule("[yellow]Results[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.Write(new Table().AddColumns("[grey]Question[/]", "[grey]Answer[/]")
.RoundedBorder()
.BorderColor(Color.Grey)
.AddRow("[grey]Name[/]", name)
@@ -43,7 +43,7 @@ namespace Spectre.Console.Examples
private static string AskName()
{
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule("[yellow]Strings[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.Write(new Rule("[yellow]Strings[/]").RuleStyle("grey").LeftAligned());
var name = AnsiConsole.Ask<string>("What's your [green]name[/]?");
return name;
}
@@ -51,7 +51,7 @@ namespace Spectre.Console.Examples
private static string AskFruit()
{
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule("[yellow]Lists[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.Write(new Rule("[yellow]Lists[/]").RuleStyle("grey").LeftAligned());
var favorites = AnsiConsole.Prompt(
new MultiSelectionPrompt<string>()
@@ -90,7 +90,7 @@ namespace Spectre.Console.Examples
private static string AskSport()
{
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule("[yellow]Choices[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.Write(new Rule("[yellow]Choices[/]").RuleStyle("grey").LeftAligned());
return AnsiConsole.Prompt(
new TextPrompt<string>("What's your [green]favorite sport[/]?")
@@ -104,7 +104,7 @@ namespace Spectre.Console.Examples
private static int AskAge()
{
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule("[yellow]Integers[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.Write(new Rule("[yellow]Integers[/]").RuleStyle("grey").LeftAligned());
return AnsiConsole.Prompt(
new TextPrompt<int>("How [green]old[/] are you?")
@@ -124,7 +124,7 @@ namespace Spectre.Console.Examples
private static string AskPassword()
{
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule("[yellow]Secrets[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.Write(new Rule("[yellow]Secrets[/]").RuleStyle("grey").LeftAligned());
return AnsiConsole.Prompt(
new TextPrompt<string>("Enter [green]password[/]?")
@@ -135,7 +135,7 @@ namespace Spectre.Console.Examples
private static string AskColor()
{
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule("[yellow]Optional[/]").RuleStyle("grey").LeftAligned());
AnsiConsole.Write(new Rule("[yellow]Optional[/]").RuleStyle("grey").LeftAligned());
return AnsiConsole.Prompt(
new TextPrompt<string>("[grey][[Optional]][/] What is your [green]favorite color[/]?")

View File

@@ -34,7 +34,7 @@ namespace Spectre.Console.Examples
private static void Render(Rule rule)
{
AnsiConsole.Render(rule);
AnsiConsole.Write(rule);
AnsiConsole.WriteLine();
}
}

View File

@@ -83,7 +83,7 @@ namespace Spectre.Console.Examples
// Render the table
AnsiConsole.WriteLine();
AnsiConsole.Render(table);
AnsiConsole.Write(table);
}
private static IRenderable GetColorTable()

View File

@@ -4,7 +4,7 @@ namespace Spectre.Console.Examples
{
public static void Main()
{
AnsiConsole.Render(CreateTable());
AnsiConsole.Write(CreateTable());
}
private static Table CreateTable()

View File

@@ -8,7 +8,7 @@ namespace Spectre.Console.Examples
// Render the tree
var tree = BuildTree();
AnsiConsole.Render(tree);
AnsiConsole.Write(tree);
}
private static Tree BuildTree()

View File

@@ -63,6 +63,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tables", "Console\Tables\Ta
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trees", "Console\Trees\Trees.csproj", "{2BD88288-E05D-4978-B045-17937078E63C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiveTable", "Console\LiveTable\LiveTable.csproj", "{E5FAAFB4-1D0F-4E29-A94F-A647D64AE64E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -409,6 +411,18 @@ Global
{2BD88288-E05D-4978-B045-17937078E63C}.Release|x64.Build.0 = Release|Any CPU
{2BD88288-E05D-4978-B045-17937078E63C}.Release|x86.ActiveCfg = Release|Any CPU
{2BD88288-E05D-4978-B045-17937078E63C}.Release|x86.Build.0 = Release|Any CPU
{E5FAAFB4-1D0F-4E29-A94F-A647D64AE64E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5FAAFB4-1D0F-4E29-A94F-A647D64AE64E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5FAAFB4-1D0F-4E29-A94F-A647D64AE64E}.Debug|x64.ActiveCfg = Debug|Any CPU
{E5FAAFB4-1D0F-4E29-A94F-A647D64AE64E}.Debug|x64.Build.0 = Debug|Any CPU
{E5FAAFB4-1D0F-4E29-A94F-A647D64AE64E}.Debug|x86.ActiveCfg = Debug|Any CPU
{E5FAAFB4-1D0F-4E29-A94F-A647D64AE64E}.Debug|x86.Build.0 = Debug|Any CPU
{E5FAAFB4-1D0F-4E29-A94F-A647D64AE64E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E5FAAFB4-1D0F-4E29-A94F-A647D64AE64E}.Release|Any CPU.Build.0 = Release|Any CPU
{E5FAAFB4-1D0F-4E29-A94F-A647D64AE64E}.Release|x64.ActiveCfg = Release|Any CPU
{E5FAAFB4-1D0F-4E29-A94F-A647D64AE64E}.Release|x64.Build.0 = Release|Any CPU
{E5FAAFB4-1D0F-4E29-A94F-A647D64AE64E}.Release|x86.ActiveCfg = Release|Any CPU
{E5FAAFB4-1D0F-4E29-A94F-A647D64AE64E}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -2,6 +2,6 @@
"projects": [ "src", "tests" ],
"sdk": {
"version": "5.0.301",
"rollForward": "latestPatch"
"rollForward": "latestFeature"
}
}
}

View File

@@ -37,6 +37,12 @@ namespace Spectre.Console.Analyzer
return;
}
// if we aren't in a method then it might be too complex for us to handle.
if (!invocationOperation.Syntax.Ancestors().OfType<MethodDeclarationSyntax>().Any())
{
return;
}
if (!HasFieldAnsiConsole(invocationOperation.Syntax) &&
!HasParameterAnsiConsole(invocationOperation.Syntax))
{

View File

@@ -12,7 +12,17 @@ namespace Spectre.Console
/// Renders the specified object to the console.
/// </summary>
/// <param name="renderable">The object to render.</param>
[Obsolete("Consider using AnsiConsole.Write instead.")]
public static void Render(IRenderable renderable)
{
Write(renderable);
}
/// <summary>
/// Renders the specified <see cref="IRenderable"/> to the console.
/// </summary>
/// <param name="renderable">The object to render.</param>
public static void Write(IRenderable renderable)
{
if (renderable is null)
{

View File

@@ -3,7 +3,7 @@ using System;
namespace Spectre.Console.Cli
{
/// <summary>
/// An base class attribute used for parameter validation.
/// A base class attribute used for parameter validation.
/// </summary>
/// <seealso cref="Attribute" />
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]

View File

@@ -3,7 +3,7 @@ using System;
namespace Spectre.Console.Cli
{
/// <summary>
/// An base class attribute used for parameter completion.
/// A base class attribute used for parameter completion.
/// </summary>
/// <seealso cref="Attribute" />
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]

View File

@@ -32,7 +32,7 @@ namespace Spectre.Console.Cli
var validationResult = validator.Validate(context);
if (!validationResult.Successful)
{
// If there is a error message specified in the parameter validator attribute,
// If there is an error message specified in the parameter validator attribute,
// then use that one, otherwise use the validation result.
var result = string.IsNullOrWhiteSpace(validator.ErrorMessage)
? validationResult

View File

@@ -1,17 +1,13 @@
#if NET5_0
using System;
#endif
namespace Spectre.Console.Cli
{
internal static class StringExtensions
{
internal static int OrdinalIndexOf(this string text, char token)
{
#if NET5_0
return text.IndexOf(token, StringComparison.Ordinal);
#else
#if NETSTANDARD2_0
return text.IndexOf(token);
#else
return text.IndexOf(token, System.StringComparison.Ordinal);
#endif
}
}

View File

@@ -27,7 +27,6 @@ namespace Spectre.Console.Cli
if (command.CommandType != null)
{
registrar?.Register(typeof(ICommand), command.CommandType);
registrar?.Register(command.CommandType, command.CommandType);
}

View File

@@ -51,8 +51,8 @@ namespace Spectre.Console
/// </summary>
/// <param name="console">The console to write to.</param>
/// <param name="text">The text to write.</param>
/// <param name="style">The text style.</param>
public static void Write(this IAnsiConsole console, string text, Style style)
/// <param name="style">The text style or <see cref="Style.Plain"/> if <see langword="null"/>.</param>
public static void Write(this IAnsiConsole console, string text, Style? style)
{
if (console is null)
{
@@ -91,8 +91,8 @@ namespace Spectre.Console
/// </summary>
/// <param name="console">The console to write to.</param>
/// <param name="text">The text to write.</param>
/// <param name="style">The text style.</param>
public static void WriteLine(this IAnsiConsole console, string text, Style style)
/// <param name="style">The text style or <see cref="Style.Plain"/> if <see langword="null"/>.</param>
public static void WriteLine(this IAnsiConsole console, string text, Style? style)
{
if (console is null)
{

View File

@@ -238,5 +238,22 @@ namespace Spectre.Console
chart.LabelAlignment = Justify.Right;
return chart;
}
/// <summary>
/// Sets the max fixed value for the chart.
/// </summary>
/// <param name="chart">The bar chart.</param>
/// <param name="maxValue">Max value for the chart.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static BarChart WithMaxValue(this BarChart chart, double maxValue)
{
if (chart is null)
{
throw new ArgumentNullException(nameof(chart));
}
chart.MaxValue = maxValue;
return chart;
}
}
}
}

View File

@@ -0,0 +1,18 @@
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="char"/>.
/// </summary>
public static class CharExtensions
{
/// <summary>
/// Gets the cell width of a character.
/// </summary>
/// <param name="character">The character to get the cell width of.</param>
/// <returns>The cell width of the character.</returns>
public static int GetCellWidth(this char character)
{
return Cell.GetCellLength(character);
}
}
}

View File

@@ -59,7 +59,12 @@ namespace Spectre.Console
return result.ToString();
}
internal static int CellLength(this string text)
/// <summary>
/// Gets the cell width of the specified text.
/// </summary>
/// <param name="text">The text to get the cell width of.</param>
/// <returns>The cell width of the text.</returns>
public static int GetCellWidth(this string text)
{
return Cell.GetCellLength(text);
}
@@ -172,19 +177,19 @@ namespace Spectre.Console
internal static string ReplaceExact(this string text, string oldValue, string? newValue)
{
#if NET5_0
return text.Replace(oldValue, newValue, StringComparison.Ordinal);
#else
#if NETSTANDARD2_0
return text.Replace(oldValue, newValue);
#else
return text.Replace(oldValue, newValue, StringComparison.Ordinal);
#endif
}
internal static bool ContainsExact(this string text, string value)
{
#if NET5_0
return text.Contains(value, StringComparison.Ordinal);
#else
#if NETSTANDARD2_0
return text.Contains(value);
#else
return text.Contains(value, StringComparison.Ordinal);
#endif
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
@@ -35,6 +36,28 @@ namespace Spectre.Console
return table;
}
/// <summary>
/// Adds a row to the table.
/// </summary>
/// <param name="table">The table to add the row to.</param>
/// <param name="columns">The row columns to add.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Table AddRow(this Table table, IEnumerable<IRenderable> columns)
{
if (table is null)
{
throw new ArgumentNullException(nameof(table));
}
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
table.Rows.Add(new TableRow(columns));
return table;
}
/// <summary>
/// Adds a row to the table.
/// </summary>
@@ -48,7 +71,7 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(table));
}
return table.AddRow(columns);
return table.AddRow((IEnumerable<IRenderable>)columns);
}
/// <summary>
@@ -143,6 +166,130 @@ namespace Spectre.Console
return table;
}
/// <summary>
/// Inserts a row in the table at the specified index.
/// </summary>
/// <param name="table">The table to add the row to.</param>
/// <param name="index">The index to insert the row at.</param>
/// <param name="columns">The row columns to add.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Table InsertRow(this Table table, int index, IEnumerable<IRenderable> columns)
{
if (table is null)
{
throw new ArgumentNullException(nameof(table));
}
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
table.Rows.Insert(index, new TableRow(columns));
return table;
}
/// <summary>
/// Updates a tables cell.
/// </summary>
/// <param name="table">The table to update.</param>
/// <param name="rowIndex">The index of row to update.</param>
/// <param name="columnIndex">The index of column to update.</param>
/// <param name="cellData">New cell data.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Table UpdateCell(this Table table, int rowIndex, int columnIndex, IRenderable cellData)
{
if (table is null)
{
throw new ArgumentNullException(nameof(table));
}
if (cellData is null)
{
throw new ArgumentNullException(nameof(cellData));
}
table.Rows.Update(rowIndex, columnIndex, cellData);
return table;
}
/// <summary>
/// Updates a tables cell.
/// </summary>
/// <param name="table">The table to update.</param>
/// <param name="rowIndex">The index of row to update.</param>
/// <param name="columnIndex">The index of column to update.</param>
/// <param name="cellData">New cell data.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Table UpdateCell(this Table table, int rowIndex, int columnIndex, string cellData)
{
if (table is null)
{
throw new ArgumentNullException(nameof(table));
}
if (cellData is null)
{
throw new ArgumentNullException(nameof(cellData));
}
table.Rows.Update(rowIndex, columnIndex, new Markup(cellData));
return table;
}
/// <summary>
/// Inserts a row in the table at the specified index.
/// </summary>
/// <param name="table">The table to add the row to.</param>
/// <param name="index">The index to insert the row at.</param>
/// <param name="columns">The row columns to add.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Table InsertRow(this Table table, int index, params IRenderable[] columns)
{
if (table is null)
{
throw new ArgumentNullException(nameof(table));
}
return InsertRow(table, index, (IEnumerable<IRenderable>)columns);
}
/// <summary>
/// Inserts a row in the table at the specified index.
/// </summary>
/// <param name="table">The table to add the row to.</param>
/// <param name="index">The index to insert the row at.</param>
/// <param name="columns">The row columns to add.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Table InsertRow(this Table table, int index, params string[] columns)
{
if (table is null)
{
throw new ArgumentNullException(nameof(table));
}
return InsertRow(table, index, columns.Select(column => new Markup(column)));
}
/// <summary>
/// Removes a row from the table with the specified index.
/// </summary>
/// <param name="table">The table to add the row to.</param>
/// <param name="index">The index to remove the row at.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Table RemoveRow(this Table table, int index)
{
if (table is null)
{
throw new ArgumentNullException(nameof(table));
}
table.Rows.RemoveAt(index);
return table;
}
/// <summary>
/// Sets the table width.
/// </summary>

View File

@@ -32,10 +32,10 @@ namespace Spectre.Console
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetLinkHashCode(string link)
{
#if NET5_0
return link.GetHashCode(StringComparison.Ordinal);
#else
#if NETSTANDARD2_0
return link.GetHashCode();
#else
return link.GetHashCode(StringComparison.Ordinal);
#endif
}
}

View File

@@ -1,10 +1,6 @@
using System;
using System.Runtime.InteropServices;
#if !NET5_0
using System.Text.RegularExpressions;
#endif
namespace Spectre.Console
{
internal static class ColorSystemDetector
@@ -61,7 +57,6 @@ namespace Spectre.Console
return ColorSystem.EightBit;
}
// See https://docs.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/5.0/environment-osversion-returns-correct-version
private static bool GetWindowsVersionInformation(out int major, out int build)
{
major = 0;
@@ -72,15 +67,15 @@ namespace Spectre.Console
return false;
}
#if NET5_0
// The reason we're not always using this, is because it
// will return wrong values on other runtimes than net5.0
#if NET5_0_OR_GREATER
// The reason we're not always using this, is because it will return wrong values on other runtimes than .NET 5+
// See https://docs.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/5.0/environment-osversion-returns-correct-version
var version = Environment.OSVersion.Version;
major = version.Major;
build = version.Build;
return true;
#else
var regex = new Regex("Microsoft Windows (?'major'[0-9]*).(?'minor'[0-9]*).(?'build'[0-9]*)\\s*$");
var regex = new System.Text.RegularExpressions.Regex("Microsoft Windows (?'major'[0-9]*).(?'minor'[0-9]*).(?'build'[0-9]*)\\s*$");
var match = regex.Match(RuntimeInformation.OSDescription);
if (match.Success && int.TryParse(match.Groups["major"].Value, out major))
{

View File

@@ -6,22 +6,19 @@ namespace Spectre.Console.Internal
{
internal sealed class DefaultExclusivityMode : IExclusivityMode
{
private static readonly SemaphoreSlim _semaphore;
private readonly SemaphoreSlim _semaphore;
static DefaultExclusivityMode()
public DefaultExclusivityMode()
{
_semaphore = new SemaphoreSlim(1, 1);
}
public T Run<T>(Func<T> func)
{
// Try aquiring the exclusivity semaphore
// Try acquiring the exclusivity semaphore
if (!_semaphore.Wait(0))
{
throw new InvalidOperationException(
"Trying to run one or more interactive functions concurrently. " +
"Operations with dynamic displays (e.g. a prompt and a progress display) " +
"cannot be running at the same time.");
throw CreateExclusivityException();
}
try
@@ -36,12 +33,10 @@ namespace Spectre.Console.Internal
public async Task<T> Run<T>(Func<Task<T>> func)
{
// Try aquiring the exclusivity semaphore
// Try acquiring the exclusivity semaphore
if (!await _semaphore.WaitAsync(0).ConfigureAwait(false))
{
// TODO: Need a better message here
throw new InvalidOperationException(
"Could not aquire the interactive semaphore");
throw CreateExclusivityException();
}
try
@@ -53,5 +48,10 @@ namespace Spectre.Console.Internal
_semaphore.Release(1);
}
}
private static Exception CreateExclusivityException() => new InvalidOperationException(
"Trying to run one or more interactive functions concurrently. " +
"Operations with dynamic displays (e.g. a prompt and a progress display) " +
"cannot be running at the same time.");
}
}

View File

@@ -134,13 +134,13 @@ namespace Spectre.Console
if (Mode == SelectionMode.Leaf)
{
// Select the node and all it's children
// Select the node and all its children
foreach (var item in current.Traverse(includeSelf: true))
{
item.IsSelected = select;
}
// Visit every parent and evaluate if it's selection
// Visit every parent and evaluate if its selection
// status need to be updated
var parent = current.Parent;
while (parent != null)

View File

@@ -5,7 +5,7 @@ namespace Spectre.Console.Rendering
{
/// <summary>
/// Represents something renderable that's reconstructed
/// when it's state change in any way.
/// when its state change in any way.
/// </summary>
public abstract class JustInTimeRenderable : Renderable
{

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Wcwidth;
namespace Spectre.Console.Rendering
{
@@ -329,23 +330,10 @@ namespace Spectre.Console.Rendering
if (overflow == Overflow.Fold)
{
var totalLength = segment.Text.CellLength();
var lengthLeft = totalLength;
while (lengthLeft > 0)
var splitted = SplitSegment(segment.Text, maxWidth);
foreach (var str in splitted)
{
var index = totalLength - lengthLeft;
// How many characters should we take?
var take = Math.Min(maxWidth, totalLength - index);
if (take <= 0)
{
// This shouldn't really occur, but I don't like
// never ending loops if it does...
return new List<Segment>();
}
result.Add(new Segment(segment.Text.Substring(index, take), segment.Style));
lengthLeft -= take;
result.Add(new Segment(str, segment.Style));
}
}
else if (overflow == Overflow.Crop)
@@ -435,7 +423,7 @@ namespace Spectre.Console.Rendering
var builder = new StringBuilder();
foreach (var character in segment.Text)
{
var accumulatedCellWidth = builder.ToString().CellLength();
var accumulatedCellWidth = builder.ToString().GetCellWidth();
if (accumulatedCellWidth >= maxWidth)
{
break;
@@ -461,36 +449,36 @@ namespace Spectre.Console.Rendering
var result = new List<Segment>();
var previous = (Segment?)null;
var segmentBuilder = (SegmentBuilder?)null;
foreach (var segment in segments)
{
if (previous == null)
if (segmentBuilder == null)
{
previous = segment;
segmentBuilder = new SegmentBuilder(segment);
continue;
}
// Both control codes?
if (segment.IsControlCode && previous.IsControlCode)
if (segment.IsControlCode && segmentBuilder.IsControlCode())
{
previous = Control(previous.Text + segment.Text);
segmentBuilder.Append(segment.Text);
continue;
}
// Same style?
if (previous.Style.Equals(segment.Style) && !previous.IsLineBreak && !previous.IsControlCode)
if (segmentBuilder.StyleEquals(segment.Style) && !segmentBuilder.IsLineBreak() && !segmentBuilder.IsControlCode())
{
previous = new Segment(previous.Text + segment.Text, previous.Style);
segmentBuilder.Append(segment.Text);
continue;
}
result.Add(previous);
previous = segment;
result.Add(segmentBuilder.Build());
segmentBuilder.Reset(segment);
}
if (previous != null)
if (segmentBuilder != null)
{
result.Add(previous);
result.Add(segmentBuilder.Build());
}
return result;
@@ -567,5 +555,63 @@ namespace Spectre.Console.Rendering
return cells;
}
internal static List<string> SplitSegment(string text, int maxCellLength)
{
var list = new List<string>();
var length = 0;
var sb = new StringBuilder();
foreach (var ch in text)
{
if (length + UnicodeCalculator.GetWidth(ch) > maxCellLength)
{
list.Add(sb.ToString());
sb.Clear();
length = 0;
}
length += UnicodeCalculator.GetWidth(ch);
sb.Append(ch);
}
list.Add(sb.ToString());
return list;
}
private class SegmentBuilder
{
private readonly StringBuilder _textBuilder = new();
private Segment _originalSegment;
public SegmentBuilder(Segment originalSegment)
{
_originalSegment = originalSegment;
Reset(originalSegment);
}
public bool IsControlCode() => _originalSegment.IsControlCode;
public bool IsLineBreak() => _originalSegment.IsLineBreak;
public bool StyleEquals(Style segmentStyle) => segmentStyle.Equals(_originalSegment.Style);
public void Append(string text)
{
_textBuilder.Append(text);
}
public Segment Build()
{
return new Segment(_textBuilder.ToString(), _originalSegment.Style, _originalSegment.IsLineBreak,
_originalSegment.IsControlCode);
}
public void Reset(Segment segment)
{
_textBuilder.Clear();
_textBuilder.Append(segment.Text);
_originalSegment = segment;
}
}
}
}

View File

@@ -29,7 +29,7 @@ namespace Spectre.Console
public string? Link { get; }
/// <summary>
/// Gets an <see cref="Style"/> with the
/// Gets a <see cref="Style"/> with the
/// default colors and without text decoration.
/// </summary>
public static Style Plain { get; } = new Style();
@@ -165,10 +165,10 @@ namespace Spectre.Console
{
int? GetLinkHashCode()
{
#if NET5_0
return Link?.GetHashCode(StringComparison.Ordinal);
#else
#if NETSTANDARD2_0
return Link?.GetHashCode();
#else
return Link?.GetHashCode(StringComparison.Ordinal);
#endif
}

View File

@@ -43,6 +43,12 @@ namespace Spectre.Console
/// <remarks>Defaults to invariant culture.</remarks>
public CultureInfo? Culture { get; set; }
/// <summary>
/// Gets or sets the fixed max value for a bar chart.
/// </summary>
/// <remarks>Defaults to null, which corresponds to largest value in chart.</remarks>
public double? MaxValue { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="BarChart"/> class.
/// </summary>
@@ -62,7 +68,7 @@ namespace Spectre.Console
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
var maxValue = Data.Max(item => item.Value);
var maxValue = Math.Max(MaxValue ?? 0d, Data.Max(item => item.Value));
var grid = new Grid();
grid.Collapse();
@@ -96,4 +102,4 @@ namespace Spectre.Console
return ((IRenderable)grid).Render(context, width);
}
}
}
}

View File

@@ -14,11 +14,7 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(exception));
}
var info = ExceptionParser.Parse(exception.ToString());
if (info == null)
{
return new Text(exception.ToString());
}
var info = ExceptionParser.Parse(exception);
return GetException(info, settings);
}

View File

@@ -1,90 +1,27 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
namespace Spectre.Console
{
internal static class ExceptionParser
{
private static readonly Regex _messageRegex = new Regex(@"^(?'type'.*):\s(?'message'.*)$");
private static readonly Regex _stackFrameRegex = new Regex(@"^\s*\w*\s(?'method'.*)\((?'params'.*)\)");
private static readonly Regex _fullStackFrameRegex = new Regex(@"^\s*(?'at'\w*)\s(?'method'.*)\((?'params'.*)\)\s(?'in'\w*)\s(?'path'.*)\:(?'line'\w*)\s(?'linenumber'\d*)$");
public static ExceptionInfo? Parse(string exception)
public static ExceptionInfo Parse(Exception exception)
{
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
var lines = exception.SplitLines();
return Parse(new Queue<string>(lines));
}
private static ExceptionInfo? Parse(Queue<string> lines)
{
if (lines.Count == 0)
{
// Error: No lines to parse
return null;
}
var line = lines.Dequeue();
line = line.ReplaceExact(" ---> ", string.Empty);
var match = _messageRegex.Match(line);
if (!match.Success)
{
return null;
}
var inner = (ExceptionInfo?)null;
// Stack frames
var frames = new List<StackFrameInfo>();
while (lines.Count > 0)
{
if (lines.Peek().TrimStart().StartsWith("---> ", StringComparison.OrdinalIgnoreCase))
{
inner = Parse(lines);
if (inner == null)
{
// Error: Could not parse inner exception
return null;
}
continue;
}
line = lines.Dequeue();
if (string.IsNullOrWhiteSpace(line))
{
// Empty line
continue;
}
if (line.TrimStart().StartsWith("--- ", StringComparison.OrdinalIgnoreCase))
{
// End of inner exception
break;
}
var stackFrame = ParseStackFrame(line);
if (stackFrame == null)
{
// Error: Could not parse stack frame
return null;
}
frames.Add(stackFrame);
}
return new ExceptionInfo(
match.Groups["type"].Value,
match.Groups["message"].Value,
frames, inner);
var exceptionType = exception.GetType();
var frames = exception.StackTrace?.SplitLines().Select(ParseStackFrame).Where(e => e != null).Cast<StackFrameInfo>().ToList() ?? new List<StackFrameInfo>();
var inner = exception.InnerException is null ? null : Parse(exception.InnerException);
return new ExceptionInfo(exceptionType.FullName ?? exceptionType.Name, exception.Message, frames, inner);
}
private static StackFrameInfo? ParseStackFrame(string frame)

View File

@@ -107,7 +107,7 @@ namespace Spectre.Console
}
else
{
// Does it fit on it's own line?
// Does it fit on its own line?
if (width < maxWidth)
{
// Flush the line

View File

@@ -19,7 +19,7 @@ namespace Spectre.Console
/// <summary>
/// Gets or sets the width of the column.
/// If <c>null</c>, the column will adapt to it's contents.
/// If <c>null</c>, the column will adapt to its contents.
/// </summary>
public int? Width
{

View File

@@ -47,7 +47,7 @@ namespace Spectre.Console
/// Initializes a new instance of the <see cref="Paragraph"/> class.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="style">The style of the text.</param>
/// <param name="style">The style of the text or <see cref="Style.Plain"/> if <see langword="null"/>.</param>
public Paragraph(string text, Style? style = null)
: this()
{
@@ -63,7 +63,7 @@ namespace Spectre.Console
/// Appends some text to this paragraph.
/// </summary>
/// <param name="text">The text to append.</param>
/// <param name="style">The style of the appended text.</param>
/// <param name="style">The style of the appended text or <see cref="Style.Plain"/> if <see langword="null"/>.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Paragraph Append(string text, Style? style = null)
{

View File

@@ -11,7 +11,6 @@ namespace Spectre.Console
public sealed class Table : Renderable, IHasTableBorder, IExpandable, IAlignable
{
private readonly List<TableColumn> _columns;
private readonly List<TableRow> _rows;
/// <summary>
/// Gets the table columns.
@@ -21,7 +20,7 @@ namespace Spectre.Console
/// <summary>
/// Gets the table rows.
/// </summary>
public IReadOnlyList<TableRow> Rows => _rows;
public TableRowCollection Rows { get; }
/// <inheritdoc/>
public TableBorder Border { get; set; } = TableBorder.Square;
@@ -81,7 +80,7 @@ namespace Spectre.Console
public Table()
{
_columns = new List<TableColumn>();
_rows = new List<TableRow>();
Rows = new TableRowCollection(this);
}
/// <summary>
@@ -96,7 +95,7 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(column));
}
if (_rows.Count > 0)
if (Rows.Count > 0)
{
throw new InvalidOperationException("Cannot add new columns to table with existing rows.");
}
@@ -105,36 +104,6 @@ namespace Spectre.Console
return this;
}
/// <summary>
/// Adds a row to the table.
/// </summary>
/// <param name="columns">The row columns to add.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Table AddRow(IEnumerable<IRenderable> columns)
{
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
var rowColumnCount = columns.GetCount();
if (rowColumnCount > _columns.Count)
{
throw new InvalidOperationException("The number of row columns are greater than the number of table columns.");
}
_rows.Add(new TableRow(columns));
// Need to add missing columns?
if (rowColumnCount < _columns.Count)
{
var diff = _columns.Count - rowColumnCount;
Enumerable.Range(0, diff).ForEach(_ => _rows.Last().Add(Text.Empty));
}
return this;
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
@@ -190,7 +159,7 @@ namespace Spectre.Console
}
// Add rows
rows.AddRange(_rows);
rows.AddRange(Rows);
// Show footers?
if (ShowFooters && _columns.Any(c => c.Footer != null))

View File

@@ -20,7 +20,7 @@ namespace Spectre.Console
/// <summary>
/// Gets or sets the width of the column.
/// If <c>null</c>, the column will adapt to it's contents.
/// If <c>null</c>, the column will adapt to its contents.
/// </summary>
public int? Width { get; set; }

View File

@@ -12,6 +12,11 @@ namespace Spectre.Console
{
private readonly List<IRenderable> _items;
/// <summary>
/// Gets the number of columns in the row.
/// </summary>
public int Count => _items.Count;
internal bool IsHeader { get; }
internal bool IsFooter { get; }

View File

@@ -0,0 +1,209 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// Represents a collection holding table rows.
/// </summary>
public sealed class TableRowCollection : IReadOnlyList<TableRow>
{
private readonly Table _table;
private readonly IList<TableRow> _list;
private readonly object _lock;
/// <inheritdoc/>
TableRow IReadOnlyList<TableRow>.this[int index]
{
get
{
lock (_lock)
{
return _list[index];
}
}
}
/// <summary>
/// Gets the number of rows in the collection.
/// </summary>
public int Count
{
get
{
lock (_lock)
{
return _list.Count;
}
}
}
internal TableRowCollection(Table table)
{
_table = table ?? throw new ArgumentNullException(nameof(table));
_list = new List<TableRow>();
_lock = new object();
}
/// <summary>
/// Adds a new row.
/// </summary>
/// <param name="columns">The columns that are part of the row to add.</param>
/// <returns>The index of the added item.</returns>
public int Add(IEnumerable<IRenderable> columns)
{
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
lock (_lock)
{
var row = CreateRow(columns);
_list.Add(row);
return _list.IndexOf(row);
}
}
/// <summary>
/// Inserts a new row at the specified index.
/// </summary>
/// <param name="index">The index to insert the row at.</param>
/// <param name="columns">The columns that are part of the row to insert.</param>
/// <returns>The index of the inserted item.</returns>
public int Insert(int index, IEnumerable<IRenderable> columns)
{
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
lock (_lock)
{
var row = CreateRow(columns);
_list.Insert(index, row);
return _list.IndexOf(row);
}
}
/// <summary>
/// Update a table cell at the specified index.
/// </summary>
/// <param name="row">Index of cell row.</param>
/// <param name="column">index of cell column.</param>
/// <param name="cellData">The new cells details.</param>
public void Update(int row, int column, IRenderable cellData)
{
if (cellData is null)
{
throw new ArgumentNullException(nameof(cellData));
}
lock (_lock)
{
if (row < 0)
{
throw new IndexOutOfRangeException("Table row index cannot be negative.");
}
else if (row >= _list.Count)
{
throw new IndexOutOfRangeException("Table row index cannot exceed the number of rows in the table.");
}
var tableRow = _list.ElementAtOrDefault(row);
var currentRenderables = tableRow.ToList();
if (column < 0)
{
throw new IndexOutOfRangeException("Table column index cannot be negative.");
}
else if (column >= currentRenderables.Count)
{
throw new IndexOutOfRangeException("Table column index cannot exceed the number of rows in the table.");
}
currentRenderables.RemoveAt(column);
currentRenderables.Insert(column, cellData);
var newTableRow = new TableRow(currentRenderables);
_list.RemoveAt(row);
_list.Insert(row, newTableRow);
}
}
/// <summary>
/// Removes a row at the specified index.
/// </summary>
/// <param name="index">The index to remove a row at.</param>
public void RemoveAt(int index)
{
lock (_lock)
{
if (index < 0)
{
throw new IndexOutOfRangeException("Table row index cannot be negative.");
}
else if (index >= _list.Count)
{
throw new IndexOutOfRangeException("Table row index cannot exceed the number of rows in the table.");
}
_list.RemoveAt(index);
}
}
/// <summary>
/// Clears all rows.
/// </summary>
public void Clear()
{
lock (_lock)
{
_list.Clear();
}
}
/// <inheritdoc/>
public IEnumerator<TableRow> GetEnumerator()
{
lock (_lock)
{
var items = new TableRow[_list.Count];
_list.CopyTo(items, 0);
return new TableRowEnumerator(items);
}
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private TableRow CreateRow(IEnumerable<IRenderable> columns)
{
var row = new TableRow(columns);
if (row.Count > _table.Columns.Count)
{
throw new InvalidOperationException("The number of row columns are greater than the number of table columns.");
}
// Need to add missing columns
if (row.Count < _table.Columns.Count)
{
var diff = _table.Columns.Count - row.Count;
Enumerable.Range(0, diff).ForEach(_ => row.Add(Text.Empty));
}
return row;
}
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace Spectre.Console
{
internal sealed class TableRowEnumerator : IEnumerator<TableRow>
{
private readonly TableRow[] _items;
private int _index;
public TableRow Current => _items[_index];
object? IEnumerator.Current => _items[_index];
public TableRowEnumerator(TableRow[] items)
{
_items = items ?? throw new ArgumentNullException(nameof(items));
_index = -1;
}
public void Dispose()
{
}
public bool MoveNext()
{
_index++;
return _index < _items.Length;
}
public void Reset()
{
_index = -1;
}
}
}

View File

@@ -29,7 +29,7 @@ namespace Spectre.Console
/// Initializes a new instance of the <see cref="Text"/> class.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="style">The style of the text.</param>
/// <param name="style">The style of the text or <see cref="Style.Plain"/> if <see langword="null"/>.</param>
public Text(string text, Style? style = null)
{
_paragraph = new Paragraph(text, style);

View File

@@ -10,6 +10,28 @@ namespace Spectre.Console.Analyzer.Tests.Unit.Analyzers
Descriptors.S1010_FavorInstanceAnsiConsoleOverStatic.Id,
DiagnosticSeverity.Info);
[Fact]
public async void Should_only_warn_within_methods()
{
const string Source = @"
using Spectre.Console;
internal sealed class Foo
{
private readonly IAnsiConsole _console;
public Foo(IAnsiConsole console = null)
{
_console = console ?? AnsiConsole.Create(new AnsiConsoleSettings());
}
}
";
await SpectreAnalyzerVerifier<FavorInstanceAnsiConsoleOverStaticAnalyzer>
.VerifyAnalyzerAsync(Source)
.ConfigureAwait(false);
}
[Fact]
public async void Instance_console_has_no_warnings()
{

View File

@@ -0,0 +1,7 @@
┌───────────┬───────────┬───────────┐
│ Column #1 │ Column #2 │ Column #3 │
├───────────┼───────────┼───────────┤
│ 1 │ │ │
│ 2 │ │ │
│ 3 │ 4 │ 5 │
└───────────┴───────────┴───────────┘

View File

@@ -0,0 +1,7 @@
┌───────────┬───────────┬───────────┐
│ Column #1 │ Column #2 │ Column #3 │
├───────────┼───────────┼───────────┤
│ 1 │ │ │
│ 2 │ │ │
│ 3 │ 4 │ 5 │
└───────────┴───────────┴───────────┘

View File

@@ -0,0 +1,7 @@
┌───────────┬───────────┬───────────┐
│ Column #1 │ Column #2 │ Column #3 │
├───────────┼───────────┼───────────┤
│ 1 │ │ │
│ 2 │ │ │
│ 3 │ 4 │ 5 │
└───────────┴───────────┴───────────┘

View File

@@ -0,0 +1,4 @@
Number of fruits
Apple ███ 12
Orange █████████████████████████ 54
Banana ██████████████ 33

View File

@@ -0,0 +1,8 @@
┌───────────────┬───────────────┬──────────────┐
│ Foo │ Bar │ Baz │
├───────────────┼───────────────┼──────────────┤
│ 中文 │ 日本語 │ 한국어 │
│ 这是中文测试 │ これは日本語 │ 이것은한국어 │
│ 字符串 │ のテスト文字 │ 테스트문자열 │
│ │ 列です │ 입니다 │
└───────────────┴───────────────┴──────────────┘

View File

@@ -0,0 +1,7 @@
┌───────────┐
│ Column #1 │
├───────────┤
│ 1 │
│ 2 │
│ 3 │
└───────────┘

View File

@@ -0,0 +1,7 @@
┌───────────┬───────────┐
│ Column #1 │ Column #2 │
├───────────┼───────────┤
│ 1 │ 1-2 │
│ 2 │ 2-2 │
│ 3 │ 3-2 │
└───────────┴───────────┘

View File

@@ -0,0 +1,7 @@
┌───────────┬───────────┐
│ Column #1 │ Column #2 │
├───────────┼───────────┤
│ 1 │ 1-2 │
│ 2 │ 2-2 │
│ 3 │ 3-2 │
└───────────┴───────────┘

View File

@@ -0,0 +1,7 @@
┌───────────┬───────────┐
│ Column #1 │ Column #2 │
├───────────┼───────────┤
│ 1 │ 1-2 │
│ 3 │ 3-2 │
│ 2 │ 2-2 │
└───────────┴───────────┘

View File

@@ -0,0 +1,7 @@
┌───────────┬───────────┐
│ Column #1 │ Column #2 │
├───────────┼───────────┤
│ 1 │ 1-2 │
│ 3 │ 3-2 │
│ 2 │ 2-2 │
└───────────┴───────────┘

View File

@@ -0,0 +1,6 @@
┌───────────┬───────────┐
│ Column #1 │ Column #2 │
├───────────┼───────────┤
│ 1 │ 1-2 │
│ 3 │ 3-2 │
└───────────┴───────────┘

View File

@@ -0,0 +1,7 @@
┌───────────┐
│ Column #1 │
├───────────┤
│ 1 │
│ 3 │
│ 2 │
└───────────┘

View File

@@ -0,0 +1,6 @@
┌───────────┐
│ Column #1 │
├───────────┤
│ 1 │
│ 3 │
└───────────┘

View File

@@ -1,7 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('Windows'))">net5.0;net48</TargetFrameworks>
<TargetFramework Condition="!$([MSBuild]::IsOSPlatform('Windows'))">net5.0</TargetFramework>
<LangVersion>9.0</LangVersion>
</PropertyGroup>
<ItemGroup>
@@ -17,10 +19,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="IsExternalInit" Version="1.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" Version="1.1.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" Version="1.1.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.6.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Nullable" Version="1.3.0" PrivateAssets="all" />
<PackageReference Include="Shouldly" Version="4.0.3" />
<PackageReference Include="Spectre.Verify.Extensions" Version="0.3.0" />
<PackageReference Include="Verify.Xunit" Version="9.0.0-beta.1" />
@@ -37,6 +41,7 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Expectations\Widgets\Table\Rows\Extensions\" />
<Folder Include="Expectations\Widgets\Tree\" />
</ItemGroup>

View File

@@ -1,5 +1,3 @@
using System;
using System.Collections.Generic;
using Shouldly;
using Spectre.Console.Testing;
using Spectre.Console.Tests.Data;

View File

@@ -382,10 +382,12 @@ namespace Spectre.Console.Tests.Unit.Cli
});
// Then
registrar.Registrations.ContainsKey(typeof(ICommand)).ShouldBeTrue();
registrar.Registrations[typeof(ICommand)].ShouldContain(typeof(GenericCommand<FooCommandSettings>));
registrar.Registrations[typeof(ICommand)].ShouldContain(typeof(DogCommand));
registrar.Registrations[typeof(ICommand)].ShouldContain(typeof(HorseCommand));
registrar.Registrations.ContainsKey(typeof(GenericCommand<FooCommandSettings>)).ShouldBeTrue();
registrar.Registrations.ContainsKey(typeof(DogCommand)).ShouldBeTrue();
registrar.Registrations.ContainsKey(typeof(HorseCommand)).ShouldBeTrue();
registrar.Registrations[typeof(GenericCommand<FooCommandSettings>)].ShouldContain(typeof(GenericCommand<FooCommandSettings>));
registrar.Registrations[typeof(DogCommand)].ShouldContain(typeof(DogCommand));
registrar.Registrations[typeof(HorseCommand)].ShouldContain(typeof(HorseCommand));
}
[Fact]
@@ -406,8 +408,8 @@ namespace Spectre.Console.Tests.Unit.Cli
});
// Then
registrar.Registrations.ContainsKey(typeof(ICommand)).ShouldBeTrue();
registrar.Registrations[typeof(ICommand)].ShouldContain(typeof(DogCommand));
registrar.Registrations.ContainsKey(typeof(DogCommand)).ShouldBeTrue();
registrar.Registrations[typeof(DogCommand)].ShouldContain(typeof(DogCommand));
}
[Fact]
@@ -545,6 +547,45 @@ namespace Spectre.Console.Tests.Unit.Cli
result.Context.ShouldHaveRemainingArgument("foo", values: new[] { "bar" });
}
[Fact]
public void Should_Add_Unknown_Option_To_Remaining_Arguments_In_Strict_Mode()
{
// Given
var app = new CommandAppTester();
app.Configure(config =>
{
config.UseStrictParsing();
config.PropagateExceptions();
config.AddBranch<AnimalSettings>("animal", animal =>
{
animal.AddCommand<DogCommand>("dog");
});
});
// When
var result = app.Run(new[]
{
"animal", "4", "dog", "12",
"--",
"--foo", "bar",
"-f", "baz",
"qux"
});
// Then
result.Context.ShouldNotBeNull();
result.Context.Remaining.Parsed.Count.ShouldBe(2);
result.Context.ShouldHaveRemainingArgument("foo", values: new[] { "bar" });
result.Context.ShouldHaveRemainingArgument("f", values: new[] { "baz" });
result.Context.Remaining.Raw.Count.ShouldBe(5);
result.Context.Remaining.Raw.ShouldBe(new[]
{
"--foo", "bar",
"-f", "baz",
"qux",
});
}
[Fact]
public void Should_Add_Unknown_Boolean_Option_To_Remaining_Arguments_In_Relaxed_Mode()
{

View File

@@ -1,4 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Shouldly;
using Spectre.Console.Testing;
@@ -261,6 +262,9 @@ namespace Spectre.Console.Tests.Unit
{
task = ctx.AddTask("foo");
task.Increment(double.Epsilon);
// Make sure that at least one millisecond has elapsed between the increments else the RemainingTime is null
// when the last timestamp is equal to the first timestamp of the samples.
Thread.Sleep(1);
task.Increment(double.Epsilon);
});

View File

@@ -47,5 +47,26 @@ namespace Spectre.Console.Tests.Unit
// Then
await Verifier.Verify(console.Output);
}
[Fact]
[Expectation("Fixed_Max_Value")]
public async Task Should_Render_Correctly_3()
{
// Given
var console = new TestConsole();
// When
console.Write(new BarChart()
.Width(60)
.WithMaxValue(100)
.Label("Number of fruits")
.AddItem("Apple", 12)
.AddItem("Orange", 54)
.AddItem("Banana", 33));
// Then
await Verifier.Verify(console.Output);
}
}
}

View File

@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spectre.Console.Testing;
using Spectre.Verify.Extensions;
using VerifyXunit;
using Xunit;
namespace Spectre.Console.Tests.Unit
{
[UsesVerify]
[ExpectationPath("Widgets/Table/Rows/Extensions")]
public sealed class TableRowCollectionExtensionsTests
{
[UsesVerify]
public sealed class TheAddRowMethod
{
[Fact]
[Expectation("Add", "Renderables")]
public Task Should_Add_Renderables()
{
// Given
var console = new TestConsole();
var table = new Table();
table.AddColumn("Column #1");
table.AddColumn("Column #2");
table.AddRow(new[] { new Text("1"), new Text("1-2") });
table.AddRow(new[] { new Text("2"), new Text("2-2") });
table.AddRow(new[] { new Text("3"), new Text("3-2") });
// When
console.Write(table);
// Then
return Verifier.Verify(console.Output);
}
[Fact]
[Expectation("Add", "Strings")]
public Task Should_Add_Strings()
{
// Given
var console = new TestConsole();
var table = new Table();
table.AddColumn("Column #1");
table.AddColumn("Column #2");
table.AddRow("1", "1-2");
table.AddRow("2", "2-2");
table.AddRow("3", "3-2");
// When
console.Write(table);
// Then
return Verifier.Verify(console.Output);
}
}
[UsesVerify]
public sealed class TheInsertRowMethod
{
[Fact]
[Expectation("Insert", "Renderables")]
public Task Should_Insert_Renderables()
{
// Given
var console = new TestConsole();
var table = new Table();
table.AddColumn("Column #1");
table.AddColumn("Column #2");
table.AddRow(new[] { new Text("1"), new Text("1-2") });
table.AddRow(new[] { new Text("2"), new Text("2-2") });
// When
table.InsertRow(1, new[] { new Text("3"), new Text("3-2") });
// Then
console.Write(table);
return Verifier.Verify(console.Output);
}
[Fact]
[Expectation("Insert", "Strings")]
public Task Should_Insert_Strings()
{
// Given
var console = new TestConsole();
var table = new Table();
table.AddColumn("Column #1");
table.AddColumn("Column #2");
table.AddRow("1", "1-2");
table.AddRow("2", "2-2");
// When
table.InsertRow(1, "3", "3-2");
// Then
console.Write(table);
return Verifier.Verify(console.Output);
}
}
[UsesVerify]
public sealed class TheRemoveRowMethod
{
[Fact]
[Expectation("Remove")]
public Task Should_Remove_Row()
{
// Given
var console = new TestConsole();
var table = new Table();
table.AddColumn("Column #1");
table.AddColumn("Column #2");
table.AddRow(new[] { new Text("1"), new Text("1-2") });
table.AddRow(new[] { new Text("2"), new Text("2-2") });
table.AddRow(new[] { new Text("3"), new Text("3-2") });
// When
table.RemoveRow(1);
// Then
console.Write(table);
return Verifier.Verify(console.Output);
}
}
}
}

View File

@@ -0,0 +1,366 @@
using System;
using System.Threading.Tasks;
using Shouldly;
using Spectre.Console.Testing;
using Spectre.Verify.Extensions;
using VerifyXunit;
using Xunit;
namespace Spectre.Console.Tests.Unit
{
[ExpectationPath("Widgets/Table/Rows")]
public sealed class TableRowCollectionTests
{
[UsesVerify]
public sealed class TheAddMethod
{
[Fact]
public void Should_Throw_If_Columns_Are_Null()
{
// Given
var table = new Table();
// When
var result = Record.Exception(() => table.Rows.Add(null));
// Then
result.ShouldBeOfType<ArgumentNullException>()
.ParamName.ShouldBe("columns");
}
[Fact]
public void Should_Add_Row_To_Collection()
{
// Given
var table = new Table();
table.AddColumn("Column #1");
// When
table.Rows.Add(new[] { Text.Empty });
// Then
table.Rows.Count.ShouldBe(1);
}
[Fact]
public void Should_Return_Index_Of_Added_Row()
{
// Given
var table = new Table();
table.AddColumn("Column #1");
table.Rows.Add(new[] { Text.Empty });
// When
var result = table.Rows.Add(new[] { Text.Empty });
// Then
result.ShouldBe(1);
}
[Fact]
[Expectation("Add")]
public Task Should_Add_Item_At_Correct_Place()
{
// Given
var console = new TestConsole();
var table = new Table();
table.AddColumn("Column #1");
table.Rows.Add(new[] { new Text("1") });
table.Rows.Add(new[] { new Text("2") });
table.Rows.Add(new[] { new Text("3") });
// When
console.Write(table);
// Then
return Verifier.Verify(console.Output);
}
}
[UsesVerify]
public sealed class TheInsertMethod
{
[Fact]
public void Should_Throw_If_Columns_Are_Null()
{
// Given
var table = new Table();
// When
var result = Record.Exception(() => table.Rows.Insert(0, null));
// Then
result.ShouldBeOfType<ArgumentNullException>()
.ParamName.ShouldBe("columns");
}
[Fact]
public void Should_Insert_Row()
{
// Given
var table = new Table();
table.AddColumn("Column #1");
table.Rows.Add(new[] { Text.Empty });
// When
table.Rows.Insert(0, new[] { Text.Empty });
// Then
table.Rows.Count.ShouldBe(2);
}
[Fact]
public void Should_Return_Index_Of_Inserted_Row()
{
// Given
var table = new Table();
table.AddColumn("Column #1");
table.Rows.Add(new[] { new Text("1") });
table.Rows.Add(new[] { new Text("2") });
// When
var result = table.Rows.Insert(1, new[] { new Text("3") });
// Then
result.ShouldBe(1);
}
[Fact]
[Expectation("Insert")]
public Task Should_Insert_Item_At_Correct_Place()
{
// Given
var console = new TestConsole();
var table = new Table();
table.AddColumn("Column #1");
table.Rows.Add(new[] { new Text("1") });
table.Rows.Add(new[] { new Text("2") });
table.Rows.Insert(1, new[] { new Text("3") });
// When
console.Write(table);
// Then
return Verifier.Verify(console.Output);
}
}
[UsesVerify]
public sealed class TheRemoveMethod
{
[Fact]
public void Should_Throw_If_Index_Is_Negative()
{
// Given
var table = new Table();
table.AddColumn("Column #1");
// When
var result = Record.Exception(() => table.Rows.RemoveAt(-1));
// Then
result.ShouldBeOfType<IndexOutOfRangeException>()
.Message.ShouldBe("Table row index cannot be negative.");
}
[Fact]
public void Should_Throw_If_Index_Is_Larger_Than_Number_Of_Rows()
{
// Given
var table = new Table();
table.AddColumn("Column #1");
table.Rows.Add(new[] { new Text("1") });
table.Rows.Add(new[] { new Text("2") });
table.Rows.Add(new[] { new Text("3") });
// When
var result = Record.Exception(() => table.Rows.RemoveAt(3));
// Then
result.ShouldBeOfType<IndexOutOfRangeException>()
.Message.ShouldBe("Table row index cannot exceed the number of rows in the table.");
}
[Fact]
[Expectation("Remove")]
public Task Should_Remove_Row()
{
// Given
var console = new TestConsole();
var table = new Table();
table.AddColumn("Column #1");
table.Rows.Add(new[] { new Text("1") });
table.Rows.Add(new[] { new Text("2") });
table.Rows.Add(new[] { new Text("3") });
table.Rows.RemoveAt(1);
// When
console.Write(table);
// Then
return Verifier.Verify(console.Output);
}
}
public sealed class TheClearMethod
{
[Fact]
public void Should_Remove_All_Rows()
{
// Given
var table = new Table();
table.AddColumn("Column #1");
table.Rows.Add(new[] { new Text("1") });
table.Rows.Add(new[] { new Text("2") });
table.Rows.Add(new[] { new Text("3") });
table.Rows.Clear();
// When
var result = table.Rows.Count;
// Then
result.ShouldBe(0);
}
}
[UsesVerify]
public sealed class TheUpdateMethod
{
[Fact]
public Task Should_Update_Row_With_String()
{
// Given
var console = new TestConsole();
var table = new Table();
table.AddColumn("Column #1");
table.AddColumn("Column #2");
table.AddColumn("Column #3");
table.Rows.Add(new[] { new Text("1") });
table.Rows.Add(new[] { new Text("2") });
table.Rows.Add(new[] { new Text("3"), new Text("4"), new Text("8") });
table.UpdateCell(2, 2, "5");
// When
console.Write(table);
// Then
return Verifier.Verify(console.Output);
}
[Fact]
public Task Should_Update_Row_With_Renderable()
{
// Given
var console = new TestConsole();
var table = new Table();
table.AddColumn("Column #1");
table.AddColumn("Column #2");
table.AddColumn("Column #3");
table.Rows.Add(new[] { new Text("1") });
table.Rows.Add(new[] { new Text("2") });
table.Rows.Add(new[] { new Text("3"), new Text("4"), new Text("8") });
table.UpdateCell(2, 2, new Markup("5"));
// When
console.Write(table);
// Then
return Verifier.Verify(console.Output);
}
[Fact]
public void Should_Throw_If_Index_Is_Larger_Than_Number_Of_Rows()
{
// Given
var console = new TestConsole();
var table = new Table();
table.AddColumn("Column #1");
table.AddColumn("Column #2");
table.AddColumn("Column #3");
table.Rows.Add(new[] { new Text("1") });
table.Rows.Add(new[] { new Text("2") });
table.Rows.Add(new[] { new Text("3"), new Text("4"), new Text("8") });
table.UpdateCell(2, 2, "5");
// When
var result = Record.Exception(() => table.UpdateCell(5, 2, "5"));
// Then
result.ShouldBeOfType<IndexOutOfRangeException>()
.Message.ShouldBe("Table row index cannot exceed the number of rows in the table.");
}
[Fact]
public void Should_Throw_If_Index_Is_Larger_Than_Number_Of_Columns()
{
// Given
var console = new TestConsole();
var table = new Table();
table.AddColumn("Column #1");
table.AddColumn("Column #2");
table.AddColumn("Column #3");
table.Rows.Add(new[] { new Text("1") });
table.Rows.Add(new[] { new Text("2") });
table.Rows.Add(new[] { new Text("3"), new Text("4"), new Text("8") });
table.UpdateCell(2, 2, "5");
// When
var result = Record.Exception(() => table.UpdateCell(2, 5, "5"));
// Then
result.ShouldBeOfType<IndexOutOfRangeException>()
.Message.ShouldBe("Table column index cannot exceed the number of rows in the table.");
}
[Fact]
public void Should_Throw_If_Index_Row_Is_Negative()
{
// Given
var console = new TestConsole();
var table = new Table();
table.AddColumn("Column #1");
table.AddColumn("Column #2");
table.AddColumn("Column #3");
table.Rows.Add(new[] { new Text("1") });
table.Rows.Add(new[] { new Text("2") });
table.Rows.Add(new[] { new Text("3"), new Text("4"), new Text("8") });
table.UpdateCell(2, 2, "5");
// When
var result = Record.Exception(() => table.UpdateCell(-1, 2, "5"));
// Then
result.ShouldBeOfType<IndexOutOfRangeException>()
.Message.ShouldBe("Table row index cannot be negative.");
}
[Fact]
public void Should_Throw_If_Index_Column_Is_Negative()
{
// Given
var console = new TestConsole();
var table = new Table();
table.AddColumn("Column #1");
table.AddColumn("Column #2");
table.AddColumn("Column #3");
table.Rows.Add(new[] { new Text("1") });
table.Rows.Add(new[] { new Text("2") });
table.Rows.Add(new[] { new Text("3"), new Text("4"), new Text("8") });
table.UpdateCell(2, 2, "5");
// When
var result = Record.Exception(() => table.UpdateCell(2, -1, "5"));
// Then
result.ShouldBeOfType<IndexOutOfRangeException>()
.Message.ShouldBe("Table column index cannot be negative.");
}
}
}
}

View File

@@ -78,20 +78,6 @@ namespace Spectre.Console.Tests.Unit
.ParamName.ShouldBe("columns");
}
[Fact]
public void Should_Throw_If_Renderable_Rows_Are_Null()
{
// Given
var table = new Table();
// When
var result = Record.Exception(() => table.AddRow(null));
// Then
result.ShouldBeOfType<ArgumentNullException>()
.ParamName.ShouldBe("columns");
}
[Fact]
public void Should_Add_Empty_Items_If_User_Provides_Less_Row_Items_Than_Columns()
{
@@ -164,6 +150,24 @@ namespace Spectre.Console.Tests.Unit
return Verifier.Verify(console.Output);
}
[Fact]
[Expectation("Render_EA_Character")]
public Task Should_Render_Table_With_EA_Character_Correctly()
{
// Given
var console = new TestConsole().Width(48);
var table = new Table();
table.AddColumns("Foo", "Bar", "Baz");
table.AddRow("中文", "日本語", "한국어");
table.AddRow("这是中文测试字符串", "これは日本語のテスト文字列です", "이것은한국어테스트문자열입니다");
// When
console.Write(table);
// Then
return Verifier.Verify(console.Output);
}
[Fact]
[Expectation("Render_Footers")]
public Task Should_Render_Table_With_Footers_Correctly()

View File

@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Spectre.Console.Tests
{

View File

@@ -12,4 +12,14 @@ namespace Spectre.Console.Tests
VerifierSettings.DerivePathInfo(Expectations.Initialize);
}
}
}
}
#if !NET5_0_OR_GREATER
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public sealed class ModuleInitializerAttribute : Attribute
{
}
}
#endif