Compare commits

...

9 Commits

Author SHA1 Message Date
Patrik Svensson
3c504155bc Fix progress rendering bug 2020-12-04 10:19:09 +01:00
Patrik Svensson
ae32785f21 Add progress task list support 2020-12-04 07:29:48 +01:00
chenxuuu
c61e386440 Add Chinese README 2020-11-30 05:56:25 +01:00
Patrik Svensson
b7cd7dd53e Fix grammar in Canvas Image docs 2020-11-25 13:52:17 +01:00
Patrik Svensson
3e1251b86a Fix heading size 2020-11-25 13:41:11 +01:00
Patrik Svensson
01fdbac51e Fix typo in package description 2020-11-25 12:16:26 +01:00
Patrik Svensson
b0b988a1e7 Add Canvas and CanvasImage docs 2020-11-25 12:15:25 +01:00
Patrik Svensson
2a9fa223de Add canvas and image support
Adds support for drawing "pixels" and displaying
images in the terminal.
2020-11-25 10:07:34 +01:00
Patrik Svensson
4f6eca4fcb Fix rendering of exceptions with generic params
Closes #145
2020-11-24 22:16:17 +01:00
99 changed files with 3396 additions and 223 deletions

View File

@@ -69,14 +69,7 @@ jobs:
shell: bash shell: bash
run: | run: |
dotnet tool restore dotnet tool restore
dotnet example info dotnet example --all
dotnet example tables
dotnet example grids
dotnet example panels
dotnet example colors
dotnet example emojis
dotnet example exceptions
dotnet example calendars
- name: Build - name: Build
shell: bash shell: bash

110
README.md
View File

@@ -2,19 +2,17 @@
_[![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 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)_
A .NET Standard 2.0 library that makes it easier to create beautiful console applications. A .NET 5/.NET Standard 2.0 library that makes it easier to create beautiful, cross platform, console applications.
It is heavily inspired by the excellent [Rich library](https://github.com/willmcgugan/rich) It is heavily inspired by the excellent [Rich library](https://github.com/willmcgugan/rich)
for Python. for Python.
## Table of Contents ## Table of Contents
1. [Features](#features) 1. [Features](#features)
2. [Example](#example) 2. [Installing](#installing)
3. [Installing](#installing) 3. [Documentation](#documentation)
4. [Usage](#usage) 4. [Examples](#examples)
4.1. [Using the static API](#using-the-static-api) 5. [License](#license)
4.2. [Creating a console](#creating-a-console)
5. [Running examples](#running-examples)
## Features ## Features
@@ -25,77 +23,27 @@ for Python.
and blinking text. and blinking text.
* Supports 3/4/8/24-bit colors in the terminal. * Supports 3/4/8/24-bit colors in the terminal.
The library will detect the capabilities of the current terminal The library will detect the capabilities of the current terminal
and downgrade colors as needed. and downgrade colors as needed.
## Example
![Example](resources/gfx/screenshots/example.png) ![Example](resources/gfx/screenshots/example.png)
## Installing ## Installing
The fastest way of getting started using Spectre.Console is to install the NuGet package. The fastest way of getting started using `Spectre.Console` is to install the NuGet package.
```csharp ```csharp
dotnet add package Spectre.Console dotnet add package Spectre.Console
``` ```
## Usage ## Documentation
The `Spectre.Console` API is stateful and is not thread-safe. The documentation for `Spectre.Console` can be found at
If you need to write to the console from different threads, make sure that https://spectresystems.github.io/spectre.console/
you take appropriate precautions, just like when you use the
regular `System.Console` API.
If the current terminal does not support ANSI escape sequences, ## Examples
`Spectre.Console` will fallback to using the `System.Console` API.
_NOTE: This library is currently under development and APIs To see `Spectre.Console` in action, install the
might change or get removed at any point up until a 1.0 release._
### Using the static API
The static API is perfect when you just want to output text
like you usually do with the `System.Console` API, but prettier.
```csharp
AnsiConsole.Foreground = Color.CornflowerBlue;
AnsiConsole.Decoration = Decoration.Underline | Decoration.Bold;
AnsiConsole.WriteLine("Hello World!");
AnsiConsole.Reset();
AnsiConsole.MarkupLine("[bold yellow on red]{0}[/] [underline]world[/]!", "Goodbye");
```
If you want to get a reference to the default `IAnsiConsole`,
you can access it via `AnsiConsole.Console`.
### Creating a console
Sometimes it's useful to explicitly create a console with specific
capabilities, such as during unit testing when you want control
over the environment your code runs in.
It's recommended to not use `AnsiConsole` in code that run as
part of a unit test.
```csharp
IAnsiConsole console = AnsiConsole.Create(
new AnsiConsoleSettings()
{
Ansi = AnsiSupport.Yes,
ColorSystem = ColorSystemSupport.TrueColor,
Out = new StringWriter(),
});
```
_NOTE: Even if you can specify a specific color system to use
when manually creating a console, remember that the user's terminal
might not be able to use it, so unless you're creating an IAnsiConsole
for testing, always use `ColorSystemSupport.Detect` and `AnsiSupport.Detect`._
## Running examples
To see Spectre.Console in action, install the
[dotnet-example](https://github.com/patriksvensson/dotnet-example) [dotnet-example](https://github.com/patriksvensson/dotnet-example)
global tool. global tool.
@@ -107,34 +55,18 @@ Now you can list available examples in this repository:
``` ```
> dotnet example > dotnet example
╭────────────┬───────────────────────────────────────┬──────────────────────────────────────────────────────╮
│ Name │ Path │ Description │
├────────────┼───────────────────────────────────────┼──────────────────────────────────────────────────────┤
│ Borders │ examples/Borders/Borders.csproj │ Demonstrates the different kind of borders. │
│ Calendars │ examples/Calendars/Calendars.csproj │ Demonstrates how to render calendars. │
│ Colors │ examples/Colors/Colors.csproj │ Demonstrates how to use colors in the console. │
│ Columns │ examples/Columns/Columns.csproj │ Demonstrates how to render data into columns. │
│ Emojis │ examples/Emojis/Emojis.csproj │ Demonstrates how to render emojis. │
│ Exceptions │ examples/Exceptions/Exceptions.csproj │ Demonstrates how to render formatted exceptions. │
│ Grids │ examples/Grids/Grids.csproj │ Demonstrates how to render grids in a console. │
│ Info │ examples/Info/Info.csproj │ Displays the capabilities of the current console. │
│ Links │ examples/Links/Links.csproj │ Demonstrates how to render links in a console. │
│ Panels │ examples/Panels/Panels.csproj │ Demonstrates how to render items in panels. │
│ Rules │ examples/Rules/Rules.csproj │ Demonstrates how to render horizontal rules (lines). │
│ Tables │ examples/Tables/Tables.csproj │ Demonstrates how to render tables in a console. │
╰────────────┴───────────────────────────────────────┴──────────────────────────────────────────────────────╯
``` ```
And to run an example: And to run an example:
``` ```
> dotnet example tables > dotnet example tables
┌──────────┬──────────┬────────┐ ```
│ Foo │ Bar │ Baz │
├──────────┼──────────┼────────┤ ## License
│ Hello │ World! │ │
│ Bonjour │ le │ monde! │ Copyright © Spectre Systems.
│ Hej │ Världen! │ │
└──────────┴──────────┴────────┘ Spectre.Console is provided as-is under the MIT license. For more information see LICENSE.
```
* For SixLabors.ImageSharp, see https://github.com/SixLabors/ImageSharp/blob/master/LICENSE

65
README.zh.md Normal file
View File

@@ -0,0 +1,65 @@
# `Spectre.Console`
_[![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`是一个 .NET 5/.NET Standard 2.0 的库,能让您在终端里更方便地生成精美的界面。
深受 [Rich](https://github.com/willmcgugan/rich) 这个优秀库的启发。
## 目录
1. [功能](#features)
2. [安装](#installing)
3. [文档](#documentation)
4. [例子](#examples)
5. [License](#license)
## 功能
* 编写时考虑到了单元测试。
* 支持 tables、grid、panel 和 [rich](https://github.com/willmcgugan/rich) 所支持的标记语言。
* 支持大部分的 SRG 参数,包括粗体、暗淡字、斜体、下划线、删除线和闪烁文本。
* 支持终端显示 3/4/8/24 位色。自动检测终端类型,自适应颜色范围。
![例子](resources/gfx/screenshots/example.png)
## 安装
最快的安装方式就是用NuGet包管理直接安装Spectre.Console。
```csharp
dotnet add package Spectre.Console
```
## 文档
`Spectre.Console`的文档可以在这里查看
https://spectresystems.github.io/spectre.console/
## 例子
如果想直接运行`Spectre.Console`的例子,则需要安装[dotnet-example](https://github.com/patriksvensson/dotnet-example)工具。
```
> dotnet tool restore
```
然后你可以列出仓库里的所有例子:
```
> dotnet example
```
跑一个看看效果:
```
> dotnet example tables
```
## License
版权所有 © Spectre Systems。
Spectre.Console 基于 MIT 协议提供。查看 LICENSE 文件了解更多信息。
* SixLabors.ImageSharp 的协议请查看 https://github.com/SixLabors/ImageSharp/blob/master/LICENSE

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

78
docs/input/progress.md Normal file
View File

@@ -0,0 +1,78 @@
Title: Progress
Order: 5
---
Spectre.Console can display information about long running tasks in the console.
<img src="assets/images/progress.png" style="max-width: 100%;margin-bottom:20px;">
If the current terminal isn't considered "interactive", such as when running
in a continuous integration system, or the terminal can't display
ANSI control sequence, any progress will be displayed in a simpler way.
<img src="assets/images/progress_fallback.png" style="max-width: 100%;">
# Usage
```csharp
// Synchronous
AnsiConsole.Progress()
.Start(ctx =>
{
// Define tasks
var task1 = ctx.AddTask("[green]Reticulating splines[/]");
var task2 = ctx.AddTask("[green]Folding space[/]");
while(!ctx.IsFinished)
{
task1.Increment(1.5);
task2.Increment(0.5);
}
});
```
## Asynchronous progress
If you prefer to use async/await, you can use `StartAsync` instead of `Start`.
```csharp
// Asynchronous
await AnsiConsole.Progress()
.StartAsync(async ctx =>
{
// Define tasks
var task1 = ctx.AddTask("[green]Reticulating splines[/]");
var task2 = ctx.AddTask("[green]Folding space[/]");
while (!ctx.IsFinished)
{
// Simulate some work
await Task.Delay(250);
// Increment
task1.Increment(1.5);
task2.Increment(0.5);
}
});
```
# Configure
```csharp
// Asynchronous
AnsiConsole.Progress()
.AutoRefresh(false) // Turn off auto refresh
.AutoClear(false) // Do not remove the task list when done
.Columns(new ProgressColumn[]
{
new TaskDescriptionColumn(), // Task description
new ProgressBarColumn(), // Progress bar
new PercentageColumn(), // Percentage
new RemainingTimeColumn(), // Remaining time
new SpinnerColumn(), // Spinner
})
.Start(ctx =>
{
// Omitted
});
```

View File

@@ -5,7 +5,7 @@ Order: 1
The fastest way of getting started using Spectre.Console is The fastest way of getting started using Spectre.Console is
to install the NuGet package. to install the NuGet package.
```shell ```text
> dotnet add package Spectre.Console > dotnet add package Spectre.Console
``` ```

View File

@@ -1,5 +1,5 @@
Title: Calendar Title: Calendar
Order: 4 Order: 2
RedirectFrom: calendar RedirectFrom: calendar
--- ---

View File

@@ -0,0 +1,106 @@
Title: Canvas Image
Order: 5
---
To add [ImageSharp](https://github.com/SixLabors/ImageSharp) superpowers to
your console application to draw images, you will need to install
the [Spectre.Console.ImageSharp](https://www.nuget.org/packages/Spectre.Console.ImageSharp) NuGet package.
```text
> dotnet add package Spectre.Console.ImageSharp
```
# Loading images
Once you've added the `Spectre.Console.ImageSharp` NuGet package,
you can create a new instance of `CanvasImage` to draw images to the console.
```csharp
// Load an image
var image = new CanvasImage("cake.png");
// Set the max width of the image.
// If no max width is set, the image will take
// up as much space as there is available.
image.MaxWidth(16);
// Render the image to the console
AnsiConsole.Render(image);
```
## Result
<pre style="font-size:90%;font-family:consolas,'Courier New',monospace;line-height: normal; padding: 0px;background-color: #222222; padding: 20px;">
<span> </span><span style="background-color: #542813"> </span><span style="background-color: #572F1B"> </span><span style="background-color: #4E1F09"> </span><span style="background-color: #5B3826"> </span><span style="background-color: #5E3A29"> </span><span style="background-color: #532611"> </span><span> </span>
<span> </span><span style="background-color: #562E1B"> </span><span style="background-color: #634737"> </span><span style="background-color: #562E1A"> </span><span style="background-color: #5D4132"> </span><span style="background-color: #6D584B"> </span><span style="background-color: #624332"> </span><span style="background-color: #562B17"> </span><span> </span>
<span> </span><span style="background-color: #512714"> </span><span style="background-color: #654E40"> </span><span style="background-color: #705243"> </span><span style="background-color: #745749"> </span><span style="background-color: #6D5B4F"> </span><span style="background-color: #715E52"> </span><span style="background-color: #644636"> </span><span style="background-color: #6A4433"> </span><span style="background-color: #542916"> </span><span style="background-color: #431C0B"> </span><span> </span>
<span> </span><span style="background-color: #491E0A"> </span><span style="background-color: #5C3523"> </span><span style="background-color: #695346"> </span><span style="background-color: #705C4F"> </span><span style="background-color: #654838"> </span><span style="background-color: #654A3A"> </span><span style="background-color: #726154"> </span><span style="background-color: #715D50"> </span><span style="background-color: #B8A79F"> </span><span style="background-color: #AE988F"> </span><span style="background-color: #6F4A39"> </span><span style="background-color: #441906"> </span><span> </span>
<span> </span><span style="background-color: #532916"> </span><span style="background-color: #8A6C5E"> </span><span style="background-color: #C2B3AB"> </span><span style="background-color: #8B786E"> </span><span style="background-color: #6B584C"> </span><span style="background-color: #695143"> </span><span style="background-color: #6C5648"> </span><span style="background-color: #6F5D51"> </span><span style="background-color: #816A55"> </span><span style="background-color: #E7E1DA"> </span><span style="background-color: #F9F5EE"> </span><span style="background-color: #BAA593"> </span><span style="background-color: #61381F"> </span><span> </span>
<span style="background-color: #421C0A"> </span><span style="background-color: #603826"> </span><span style="background-color: #9E8479"> </span><span style="background-color: #E2DAD6"> </span><span style="background-color: #FBF9F6"> </span><span style="background-color: #F0EADF"> </span><span style="background-color: #C4B59D"> </span><span style="background-color: #9D8663"> </span><span style="background-color: #786451"> </span><span style="background-color: #705D4E"> </span><span style="background-color: #BFA052"> </span><span style="background-color: #FEE88B"> </span><span style="background-color: #FDE580"> </span><span style="background-color: #E2C362"> </span><span style="background-color: #794E1D"> </span><span> </span>
<span style="background-color: #4B1D05"> </span><span style="background-color: #A6844C"> </span><span style="background-color: #E9D595"> </span><span style="background-color: #F1DC92"> </span><span style="background-color: #F5DD83"> </span><span style="background-color: #FBE278"> </span><span style="background-color: #FFE36E"> </span><span style="background-color: #F1D25E"> </span><span style="background-color: #866F4B"> </span><span style="background-color: #726256"> </span><span style="background-color: #967945"> </span><span style="background-color: #F5D456"> </span><span style="background-color: #F8D756"> </span><span style="background-color: #E1BE4A"> </span><span style="background-color: #7D511B"> </span><span> </span>
<span style="background-color: #4F2005"> </span><span style="background-color: #C9A441"> </span><span style="background-color: #FFE05C"> </span><span style="background-color: #FEDF5B"> </span><span style="background-color: #FCDC59"> </span><span style="background-color: #F7D555"> </span><span style="background-color: #E5C04A"> </span><span style="background-color: #795E3B"> </span><span style="background-color: #726256"> </span><span style="background-color: #755F4C"> </span><span style="background-color: #A17124"> </span><span style="background-color: #AE7414"> </span><span style="background-color: #AE791D"> </span><span style="background-color: #794D18"> </span><span> </span>
<span style="background-color: #4E1F04"> </span><span style="background-color: #B78D31"> </span><span style="background-color: #DDB33E"> </span><span style="background-color: #D0A132"> </span><span style="background-color: #C28F25"> </span><span style="background-color: #B67E1A"> </span><span style="background-color: #AC7111"> </span><span style="background-color: #9E610A"> </span><span style="background-color: #5F3212"> </span><span style="background-color: #6A574B"> </span><span style="background-color: #726256"> </span><span style="background-color: #744D2A"> </span><span style="background-color: #955401"> </span><span style="background-color: #8C5106"> </span><span style="background-color: #5F310C"> </span><span> </span>
<span style="background-color: #4B1A00"> </span><span style="background-color: #854903"> </span><span style="background-color: #9B5A02"> </span><span style="background-color: #995700"> </span><span style="background-color: #935200"> </span><span style="background-color: #592402"> </span><span style="background-color: #5B3F30"> </span><span style="background-color: #726256"> </span><span style="background-color: #705A4A"> </span><span style="background-color: #844C0C"> </span><span style="background-color: #824400"> </span><span style="background-color: #4C1B00"> </span><span> </span>
<span style="background-color: #4B1A00"> </span><span style="background-color: #824500"> </span><span style="background-color: #995700"> </span><span style="background-color: #935200"> </span><span style="background-color: #592300"> </span><span style="background-color: #4F2411"> </span><span style="background-color: #6B584C"> </span><span style="background-color: #736256"> </span><span style="background-color: #734E2C"> </span><span style="background-color: #7C4101"> </span><span style="background-color: #4C1B00"> </span><span> </span>
<span style="background-color: #4B1A00"> </span><span style="background-color: #824500"> </span><span style="background-color: #995700"> </span><span style="background-color: #935200"> </span><span style="background-color: #592300"> </span><span style="background-color: #4A1902"> </span><span style="background-color: #5C4031"> </span><span style="background-color: #726256"> </span><span style="background-color: #705B4B"> </span><span style="background-color: #6A390F"> </span><span style="background-color: #4C1A00"> </span><span> </span>
<span style="background-color: #4B1A00"> </span><span style="background-color: #824500"> </span><span style="background-color: #995700"> </span><span style="background-color: #935200"> </span><span style="background-color: #592300"> </span><span style="background-color: #4A1700"> </span><span style="background-color: #4F2512"> </span><span style="background-color: #6B594D"> </span><span style="background-color: #736256"> </span><span style="background-color: #634432"> </span><span style="background-color: #4C1D08"> </span><span> </span>
<span style="background-color: #4B1A00"> </span><span style="background-color: #814400"> </span><span style="background-color: #955400"> </span><span style="background-color: #915100"> </span><span style="background-color: #8C4D00"> </span><span style="background-color: #864800"> </span><span style="background-color: #7F4301"> </span><span style="background-color: #743A01"> </span><span style="background-color: #521E01"> </span><span style="background-color: #4A1700"> </span><span style="background-color: #4A1902"> </span><span style="background-color: #5D4132"> </span><span style="background-color: #726256"> </span><span style="background-color: #6F5B4E"> </span><span style="background-color: #5D3A28"> </span><span style="background-color: #53220C"> </span>
<span style="background-color: #471801"> </span><span style="background-color: #642D01"> </span><span style="background-color: #6B3301"> </span><span style="background-color: #642E02"> </span><span style="background-color: #5D2902"> </span><span style="background-color: #542203"> </span><span style="background-color: #4C1C04"> </span><span style="background-color: #461905"> </span><span style="background-color: #4A1C07"> </span><span style="background-color: #4C1A03"> </span><span style="background-color: #4B1801"> </span><span style="background-color: #502613"> </span><span style="background-color: #69564A"> </span><span style="background-color: #705F54"> </span><span style="background-color: #604232"> </span><span style="background-color: #51200A"> </span>
<span style="background-color: #411806"> </span><span style="background-color: #431A07"> </span><span style="background-color: #411D0D"> </span><span> </span><span style="background-color: #4D1B05"> </span><span style="background-color: #4D1D07"> </span><span style="background-color: #533324"> </span><span style="background-color: #583E30"> </span><span style="background-color: #53301F"> </span><span style="background-color: #53230D"> </span>
</pre>
# 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).
```csharp
// Load an image
var image = new CanvasImage("cake.png");
image.MaxWidth(32);
// Set a sampler that will be used when scaling the image.
image.BilinearResampler();
// Mutate the image using ImageSharp
image.Mutate(ctx => ctx.Grayscale().Rotate(-45).EntropyCrop());
// Render the image to the console
AnsiConsole.Render(image);
```
## Result
<pre style="font-size:90%;font-family:consolas,'Courier New',monospace;line-height: normal; padding: 0px;background-color: #222222; padding: 20px;">
<span> </span><span style="background-color: #282828"> </span><span style="background-color: #222222"> </span><span style="background-color: #232323"> </span><span style="background-color: #353535"> </span><span style="background-color: #4B4B4B"> </span><span style="background-color: #595959"> </span><span style="background-color: #3B3B3B"> </span><span style="background-color: #202020"> </span><span style="background-color: #191919"> </span><span> </span>
<span> </span><span style="background-color: #343434"> </span><span style="background-color: #2B2B2B"> </span><span style="background-color: #292929"> </span><span style="background-color: #272727"> </span><span style="background-color: #252525"> </span><span style="background-color: #292929"> </span><span style="background-color: #555555"> </span><span style="background-color: #929292"> </span><span style="background-color: #C7C7C7"> </span><span style="background-color: #E5E5E5"> </span><span style="background-color: #F0F0F0"> </span><span style="background-color: #E4E4E4"> </span><span style="background-color: #A8A8A8"> </span><span style="background-color: #515151"> </span><span style="background-color: #202020"> </span><span style="background-color: #191919"> </span><span> </span>
<span> </span><span style="background-color: #2E2E2E"> </span><span style="background-color: #2B2B2B"> </span><span style="background-color: #333333"> </span><span style="background-color: #373737"> </span><span style="background-color: #3C3C3C"> </span><span style="background-color: #414141"> </span><span style="background-color: #474747"> </span><span style="background-color: #4B4B4B"> </span><span style="background-color: #454545"> </span><span style="background-color: #828282"> </span><span style="background-color: #E0E0E0"> </span><span style="background-color: #FFFFFF"> </span><span style="background-color: #FCFCFC"> </span><span style="background-color: #DEDEDE"> </span><span style="background-color: #DADADA"> </span><span style="background-color: #BCBCBC"> </span><span style="background-color: #515151"> </span><span style="background-color: #202020"> </span><span style="background-color: #191919"> </span><span> </span>
<span> </span><span style="background-color: #272727"> </span><span style="background-color: #414141"> </span><span style="background-color: #5C5C5C"> </span><span style="background-color: #616161"> </span><span style="background-color: #636363"> </span><span style="background-color: #656565"> </span><span style="background-color: #666666"> </span><span style="background-color: #656565"> </span><span style="background-color: #5A5A5A"> </span><span style="background-color: #707070"> </span><span style="background-color: #F3F3F3"> </span><span style="background-color: #FFFFFF"> </span><span style="background-color: #F0F0F0"> </span><span style="background-color: #DDDDDD"> </span><span style="background-color: #BABABA"> </span><span style="background-color: #505050"> </span><span style="background-color: #202020"> </span><span style="background-color: #1B1B1B"> </span><span> </span>
<span> </span><span style="background-color: #242424"> </span><span style="background-color: #3B3B3B"> </span><span style="background-color: #545454"> </span><span style="background-color: #606060"> </span><span style="background-color: #656565"> </span><span style="background-color: #666666"> </span><span style="background-color: #606060"> </span><span style="background-color: #575757"> </span><span style="background-color: #E8E8E8"> </span><span style="background-color: #F6F6F6"> </span><span style="background-color: #E1E1E1"> </span><span style="background-color: #DDDDDD"> </span><span style="background-color: #D9D9D9"> </span><span style="background-color: #A0A0A0"> </span><span style="background-color: #989898"> </span><span style="background-color: #4E4E4E"> </span><span style="background-color: #222222"> </span><span> </span>
<span style="background-color: #2F2F2F"> </span><span style="background-color: #2C2C2C"> </span><span style="background-color: #222222"> </span><span style="background-color: #282828"> </span><span style="background-color: #2D2D2D"> </span><span style="background-color: #3E3E3E"> </span><span style="background-color: #4D4D4D"> </span><span style="background-color: #616161"> </span><span style="background-color: #636363"> </span><span style="background-color: #666666"> </span><span style="background-color: #606060"> </span><span style="background-color: #535353"> </span><span style="background-color: #D4D4D4"> </span><span style="background-color: #E2E2E2"> </span><span style="background-color: #DDDDDD"> </span><span style="background-color: #DCDCDC"> </span><span style="background-color: #AFAFAF"> </span><span style="background-color: #666666"> </span><span style="background-color: #6F6F6F"> </span><span style="background-color: #717171"> </span><span style="background-color: #242424"> </span><span style="background-color: #191919"> </span><span> </span>
<span style="background-color: #2C2C2C"> </span><span style="background-color: #343434"> </span><span style="background-color: #2E2E2E"> </span><span style="background-color: #262626"> </span><span style="background-color: #404040"> </span><span style="background-color: #868686"> </span><span style="background-color: #4D4D4D"> </span><span style="background-color: #5A5A5A"> </span><span style="background-color: #3D3D3D"> </span><span style="background-color: #474747"> </span><span style="background-color: #646464"> </span><span style="background-color: #616161"> </span><span style="background-color: #4D4D4D"> </span><span style="background-color: #9D9D9D"> </span><span style="background-color: #C8C8C8"> </span><span style="background-color: #DADADA"> </span><span style="background-color: #DDDDDD"> </span><span style="background-color: #C4C4C4"> </span><span style="background-color: #717171"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #595959"> </span><span style="background-color: #343434"> </span><span style="background-color: #1F1F1F"> </span><span style="background-color: #191919"> </span><span> </span>
<span style="background-color: #343434"> </span><span style="background-color: #575757"> </span><span style="background-color: #555555"> </span><span style="background-color: #454545"> </span><span style="background-color: #4C4C4C"> </span><span style="background-color: #656565"> </span><span style="background-color: #5B5B5B"> </span><span style="background-color: #434343"> </span><span style="background-color: #3E3E3E"> </span><span style="background-color: #595959"> </span><span style="background-color: #666666"> </span><span style="background-color: #606060"> </span><span style="background-color: #595959"> </span><span style="background-color: #5E5E5E"> </span><span style="background-color: #787878"> </span><span style="background-color: #9E9E9E"> </span><span style="background-color: #797979"> </span><span style="background-color: #5E5E5E"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #575757"> </span><span style="background-color: #343434"> </span><span style="background-color: #1F1F1F"> </span><span style="background-color: #191919"> </span><span> </span>
<span style="background-color: #2B2B2B"> </span><span style="background-color: #3B3B3B"> </span><span style="background-color: #575757"> </span><span style="background-color: #646464"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #5E5E5E"> </span><span style="background-color: #575757"> </span><span style="background-color: #3D3D3D"> </span><span style="background-color: #525252"> </span><span style="background-color: #656565"> </span><span style="background-color: #666666"> </span><span style="background-color: #656565"> </span><span style="background-color: #616161"> </span><span style="background-color: #595959"> </span><span style="background-color: #4B4B4B"> </span><span style="background-color: #454545"> </span><span style="background-color: #4B4B4B"> </span><span style="background-color: #555555"> </span><span style="background-color: #5D5D5D"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #575757"> </span><span style="background-color: #343434"> </span><span style="background-color: #1F1F1F"> </span><span style="background-color: #191919"> </span><span> </span>
<span style="background-color: #3A3A3A"> </span><span style="background-color: #292929"> </span><span style="background-color: #323232"> </span><span style="background-color: #4A4A4A"> </span><span style="background-color: #626262"> </span><span style="background-color: #666666"> </span><span style="background-color: #656565"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #616161"> </span><span style="background-color: #5E5E5E"> </span><span style="background-color: #616161"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #666666"> </span><span style="background-color: #626262"> </span><span style="background-color: #575757"> </span><span style="background-color: #4B4B4B"> </span><span style="background-color: #454545"> </span><span style="background-color: #4A4A4A"> </span><span style="background-color: #545454"> </span><span style="background-color: #343434"> </span><span style="background-color: #1F1F1F"> </span><span style="background-color: #191919"> </span><span> </span>
<span> </span><span style="background-color: #252525"> </span><span style="background-color: #383838"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #616161"> </span><span style="background-color: #5B5B5B"> </span><span style="background-color: #505050"> </span><span style="background-color: #545454"> </span><span style="background-color: #8A8A8A"> </span><span style="background-color: #C5C5C5"> </span><span style="background-color: #959595"> </span><span style="background-color: #5E5E5E"> </span><span style="background-color: #636363"> </span><span style="background-color: #666666"> </span><span style="background-color: #626262"> </span><span style="background-color: #595959"> </span><span style="background-color: #4D4D4D"> </span><span style="background-color: #454545"> </span><span style="background-color: #414141"> </span><span style="background-color: #282828"> </span><span style="background-color: #1E1E1E"> </span><span style="background-color: #1D1D1D"> </span><span> </span>
<span> </span><span style="background-color: #212121"> </span><span style="background-color: #2C2C2C"> </span><span style="background-color: #4F4F4F"> </span><span style="background-color: #515151"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #898989"> </span><span style="background-color: #CDCDCD"> </span><span style="background-color: #E8E8E8"> </span><span style="background-color: #DEDEDE"> </span><span style="background-color: #D8D8D8"> </span><span style="background-color: #939393"> </span><span style="background-color: #4D4D4D"> </span><span style="background-color: #525252"> </span><span style="background-color: #5E5E5E"> </span><span style="background-color: #646464"> </span><span style="background-color: #666666"> </span><span style="background-color: #636363"> </span><span style="background-color: #5A5A5A"> </span><span style="background-color: #4A4A4A"> </span><span style="background-color: #383838"> </span><span style="background-color: #323232"> </span><span style="background-color: #2A2A2A"> </span><span style="background-color: #282828"> </span><span> </span>
<span> </span><span style="background-color: #272727"> </span><span style="background-color: #404040"> </span><span style="background-color: #C8C8C8"> </span><span style="background-color: #DFDFDF"> </span><span style="background-color: #F0F0F0"> </span><span style="background-color: #FDFDFD"> </span><span style="background-color: #F3F3F3"> </span><span style="background-color: #DFDFDF"> </span><span style="background-color: #DDDDDD"> </span><span style="background-color: #D7D7D7"> </span><span style="background-color: #757575"> </span><span style="background-color: #2B2B2B"> </span><span style="background-color: #333333"> </span><span style="background-color: #444444"> </span><span style="background-color: #535353"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #646464"> </span><span style="background-color: #666666"> </span><span style="background-color: #646464"> </span><span style="background-color: #5B5B5B"> </span><span style="background-color: #4F4F4F"> </span><span style="background-color: #3A3A3A"> </span><span style="background-color: #292929"> </span>
<span> </span><span style="background-color: #242424"> </span><span style="background-color: #4F4F4F"> </span><span style="background-color: #E7E7E7"> </span><span style="background-color: #FFFFFF"> </span><span style="background-color: #F2F2F2"> </span><span style="background-color: #DFDFDF"> </span><span style="background-color: #DDDDDD"> </span><span style="background-color: #C2C2C2"> </span><span style="background-color: #6E6E6E"> </span><span style="background-color: #434343"> </span><span style="background-color: #242424"> </span><span style="background-color: #222222"> </span><span style="background-color: #282828"> </span><span style="background-color: #343434"> </span><span style="background-color: #454545"> </span><span style="background-color: #555555"> </span><span style="background-color: #606060"> </span><span style="background-color: #656565"> </span><span style="background-color: #666666"> </span><span style="background-color: #595959"> </span><span style="background-color: #313131"> </span>
<span> </span><span style="background-color: #222222"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #F2F2F2"> </span><span style="background-color: #FFFFFF"> </span><span style="background-color: #F4F4F4"> </span><span style="background-color: #D7D7D7"> </span><span style="background-color: #DCDCDC"> </span><span style="background-color: #DDDDDD"> </span><span style="background-color: #D1D1D1"> </span><span style="background-color: #818181"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #5D5D5D"> </span><span style="background-color: #434343"> </span><span style="background-color: #242424"> </span><span style="background-color: #202020"> </span><span style="background-color: #222222"> </span><span style="background-color: #282828"> </span><span style="background-color: #353535"> </span><span style="background-color: #464646"> </span><span style="background-color: #565656"> </span><span style="background-color: #606060"> </span><span style="background-color: #656565"> </span><span style="background-color: #666666"> </span><span style="background-color: #585858"> </span><span style="background-color: #333333"> </span>
<span> </span><span style="background-color: #222222"> </span><span style="background-color: #707070"> </span><span style="background-color: #FAFAFA"> </span><span style="background-color: #D2D2D2"> </span><span style="background-color: #D9D9D9"> </span><span style="background-color: #DDDDDD"> </span><span style="background-color: #D9D9D9"> </span><span style="background-color: #979797"> </span><span style="background-color: #616161"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #5D5D5D"> </span><span style="background-color: #434343"> </span><span style="background-color: #242424"> </span><span style="background-color: #202020"> </span><span style="background-color: #222222"> </span><span style="background-color: #292929"> </span><span style="background-color: #363636"> </span><span style="background-color: #474747"> </span><span style="background-color: #575757"> </span><span style="background-color: #606060"> </span><span style="background-color: #616161"> </span><span style="background-color: #575757"> </span><span style="background-color: #404040"> </span><span style="background-color: #2B2B2B"> </span>
<span> </span><span style="background-color: #212121"> </span><span style="background-color: #858585"> </span><span style="background-color: #FCFCFC"> </span><span style="background-color: #D9D9D9"> </span><span style="background-color: #D2D2D2"> </span><span style="background-color: #DDDDDD"> </span><span style="background-color: #DCDCDC"> </span><span style="background-color: #AEAEAE"> </span><span style="background-color: #666666"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #5D5D5D"> </span><span style="background-color: #434343"> </span><span style="background-color: #242424"> </span><span style="background-color: #202020"> </span><span style="background-color: #222222"> </span><span style="background-color: #292929"> </span><span style="background-color: #363636"> </span><span style="background-color: #3E3E3E"> </span><span style="background-color: #363636"> </span><span style="background-color: #2B2B2B"> </span><span style="background-color: #282828"> </span>
<span> </span><span style="background-color: #222222"> </span><span style="background-color: #9B9B9B"> </span><span style="background-color: #EAEAEA"> </span><span style="background-color: #D0D0D0"> </span><span style="background-color: #DDDDDD"> </span><span style="background-color: #C3C3C3"> </span><span style="background-color: #707070"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #5D5D5D"> </span><span style="background-color: #434343"> </span><span style="background-color: #242424"> </span><span style="background-color: #202020"> </span><span style="background-color: #212121"> </span><span style="background-color: #242424"> </span><span style="background-color: #272727"> </span><span style="background-color: #2C2C2C"> </span><span> </span>
<span> </span><span style="background-color: #292929"> </span><span style="background-color: #ACACAC"> </span><span style="background-color: #DDDDDD"> </span><span style="background-color: #DCDCDC"> </span><span style="background-color: #DDDDDD"> </span><span style="background-color: #D1D1D1"> </span><span style="background-color: #818181"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #5D5D5D"> </span><span style="background-color: #434343"> </span><span style="background-color: #242424"> </span><span style="background-color: #202020"> </span><span style="background-color: #212121"> </span><span style="background-color: #222222"> </span><span style="background-color: #232323"> </span><span style="background-color: #242424"> </span><span style="background-color: #262626"> </span><span style="background-color: #2E2E2E"> </span><span> </span>
<span> </span><span style="background-color: #2D2D2D"> </span><span style="background-color: #A6A6A6"> </span><span style="background-color: #DDDDDD"> </span><span style="background-color: #D9D9D9"> </span><span style="background-color: #989898"> </span><span style="background-color: #616161"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #5D5D5D"> </span><span style="background-color: #3E3E3E"> </span><span style="background-color: #222222"> </span><span style="background-color: #242424"> </span><span style="background-color: #262626"> </span><span style="background-color: #2B2B2B"> </span><span style="background-color: #363636"> </span><span> </span>
<span> </span><span style="background-color: #212121"> </span><span style="background-color: #575757"> </span><span style="background-color: #BEBEBE"> </span><span style="background-color: #DDDDDD"> </span><span style="background-color: #DCDCDC"> </span><span style="background-color: #AFAFAF"> </span><span style="background-color: #666666"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #5B5B5B"> </span><span style="background-color: #373737"> </span><span style="background-color: #222222"> </span><span> </span>
<span> </span><span style="background-color: #171717"> </span><span style="background-color: #212121"> </span><span style="background-color: #585858"> </span><span style="background-color: #BEBEBE"> </span><span style="background-color: #C3C3C3"> </span><span style="background-color: #717171"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #5E5E5E"> </span><span style="background-color: #424242"> </span><span style="background-color: #252525"> </span><span style="background-color: #242424"> </span><span> </span>
<span> </span><span style="background-color: #171717"> </span><span style="background-color: #212121"> </span><span style="background-color: #545454"> </span><span style="background-color: #717171"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #4D4D4D"> </span><span style="background-color: #292929"> </span><span style="background-color: #232323"> </span><span> </span>
<span> </span><span style="background-color: #171717"> </span><span style="background-color: #1F1F1F"> </span><span style="background-color: #343434"> </span><span style="background-color: #565656"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #565656"> </span><span style="background-color: #303030"> </span><span style="background-color: #222222"> </span><span> </span>
<span> </span><span style="background-color: #171717"> </span><span style="background-color: #1F1F1F"> </span><span style="background-color: #343434"> </span><span style="background-color: #565656"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #5C5C5C"> </span><span style="background-color: #393939"> </span><span style="background-color: #232323"> </span><span style="background-color: #252525"> </span><span> </span>
<span> </span><span style="background-color: #171717"> </span><span style="background-color: #1F1F1F"> </span><span style="background-color: #343434"> </span><span style="background-color: #565656"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #5E5E5E"> </span><span style="background-color: #444444"> </span><span style="background-color: #252525"> </span><span style="background-color: #222222"> </span><span> </span>
<span> </span><span style="background-color: #171717"> </span><span style="background-color: #1F1F1F"> </span><span style="background-color: #343434"> </span><span style="background-color: #565656"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #4F4F4F"> </span><span style="background-color: #2A2A2A"> </span><span style="background-color: #222222"> </span><span> </span>
<span> </span><span style="background-color: #171717"> </span><span style="background-color: #1F1F1F"> </span><span style="background-color: #343434"> </span><span style="background-color: #565656"> </span><span style="background-color: #5F5F5F"> </span><span style="background-color: #575757"> </span><span style="background-color: #323232"> </span><span style="background-color: #222222"> </span><span> </span>
<span> </span><span style="background-color: #171717"> </span><span style="background-color: #1F1F1F"> </span><span style="background-color: #343434"> </span><span style="background-color: #565656"> </span><span style="background-color: #5C5C5C"> </span><span style="background-color: #3C3C3C"> </span><span style="background-color: #232323"> </span><span style="background-color: #252525"> </span><span> </span>
<span> </span><span style="background-color: #171717"> </span><span style="background-color: #1F1F1F"> </span><span style="background-color: #343434"> </span><span style="background-color: #404040"> </span><span style="background-color: #262626"> </span><span style="background-color: #232323"> </span><span> </span>
<span> </span><span style="background-color: #171717"> </span><span style="background-color: #1E1E1E"> </span><span style="background-color: #222222"> </span><span> </span>
</pre>

View File

@@ -0,0 +1,52 @@
Title: Canvas
Order: 4
---
`Canvas` is a widget that allows you to render arbitrary "pixels"
(or _coxels_, as [Simon Cropp](https://twitter.com/SimonCropp/status/1331554791726534657?s=20)
suggested we should call them).
# Drawing primitives
```csharp
// Create a canvas
var canvas = new Canvas(16, 16);
// Draw some shapes
for(var i = 0; i < canvas.Width; i++)
{
// Cross
canvas.SetPixel(i, i, Color.White);
canvas.SetPixel(canvas.Width - i - 1, i, Color.White);
// Border
canvas.SetPixel(i, 0, Color.Red);
canvas.SetPixel(0, i, Color.Green);
canvas.SetPixel(i, canvas.Height - 1, Color.Blue);
canvas.SetPixel(canvas.Width - 1, i, Color.Yellow);
}
// Render the canvas
AnsiConsole.Render(canvas);
```
## Result
<pre style="font-size:100%;font-family:consolas,'Courier New',monospace;line-height: normal; padding: 0px;background-color: #222222; padding: 20px;">
<span style="background-color: #008000"> </span><span style="background-color: #FF0000"> </span>
<span style="background-color: #008000"> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #800080"> </span><span style="background-color: #FFFF00"> </span>
<span style="background-color: #008000"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #FFFF00"> </span>
<span style="background-color: #008000"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #FFFF00"> </span>
<span style="background-color: #008000"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #FFFF00"> </span>
<span style="background-color: #008000"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #FFFF00"> </span>
<span style="background-color: #008000"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #FFFF00"> </span>
<span style="background-color: #008000"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #FFFF00"> </span>
<span style="background-color: #008000"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #FFFF00"> </span>
<span style="background-color: #008000"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #FFFF00"> </span>
<span style="background-color: #008000"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #FFFF00"> </span>
<span style="background-color: #008000"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #FFFF00"> </span>
<span style="background-color: #008000"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #FFFF00"> </span>
<span style="background-color: #008000"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #FFFF00"> </span>
<span style="background-color: #008000"> </span><span style="background-color: #800080"> </span><span> </span><span style="background-color: #800080"> </span><span style="background-color: #FFFF00"> </span>
<span style="background-color: #008000"> </span><span style="background-color: #0000FF"> </span><span style="background-color: #FFFF00"> </span>
</pre>

View File

@@ -1,5 +1,6 @@
Title: Figlet Title: Figlet
Order: 5 Order: 3
RedirectFrom: figlet
--- ---
Spectre.Console can render [FIGlet](http://www.figlet.org/) text by using the `FigletText` class. Spectre.Console can render [FIGlet](http://www.figlet.org/) text by using the `FigletText` class.

View File

@@ -0,0 +1,12 @@
Title: Widgets
Order: 9
---
<h1>Sections</h1>
<ul>
@foreach (IDocument child in OutputPages.GetChildrenOf(Document))
{
<li>@Html.DocumentLink(child)</li>
}
</ul>

View File

@@ -1,3 +0,0 @@
Title: Widgets
Order: 9
---

View File

@@ -1,5 +1,5 @@
Title: Rule Title: Rule
Order: 5 Order: 1
RedirectFrom: rule RedirectFrom: rule
--- ---

View File

@@ -1,5 +1,5 @@
Title: Table Title: Table
Order: 3 Order: 0
RedirectFrom: tables RedirectFrom: tables
--- ---

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<Title>Canvas</Title>
<Description>Demonstrates how to render pixels and images.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Spectre.Console.ImageSharp\Spectre.Console.ImageSharp.csproj" />
<ProjectReference Include="..\..\src\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="cake.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,87 @@
/*
Ported from: https://rosettacode.org/wiki/Mandelbrot_set#C.23
Licensed under GNU Free Documentation License 1.2
*/
using System;
using Spectre.Console;
namespace CanvasExample
{
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 Canvas Generate(int width, int height)
{
var canvas = new 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

@@ -0,0 +1,36 @@
using SixLabors.ImageSharp.Processing;
using Spectre.Console;
using Spectre.Console.Rendering;
namespace CanvasExample
{
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");
image.BilinearResampler();
image.MaxWidth(16);
Render(image, "Image from file (16 wide)");
// Draw image again, but without render width
image.NoMaxWidth();
image.Mutate(ctx => ctx.Grayscale().Rotate(-45).EntropyCrop());
Render(image, "Image from file (fit, greyscale, rotated)");
}
private static void Render(IRenderable canvas, string title)
{
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule($"[yellow]{title}[/]").LeftAligned().RuleStyle("grey"));
AnsiConsole.WriteLine();
AnsiConsole.Render(canvas);
}
}
}

BIN
examples/Canvas/cake.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -1,4 +1,3 @@
using System;
using Spectre.Console; using Spectre.Console;
namespace InfoExample namespace InfoExample
@@ -13,7 +12,7 @@ namespace InfoExample
.AddRow("[b]Color system[/]", $"{AnsiConsole.Capabilities.ColorSystem}") .AddRow("[b]Color system[/]", $"{AnsiConsole.Capabilities.ColorSystem}")
.AddRow("[b]Supports ansi?[/]", $"{YesNo(AnsiConsole.Capabilities.SupportsAnsi)}") .AddRow("[b]Supports ansi?[/]", $"{YesNo(AnsiConsole.Capabilities.SupportsAnsi)}")
.AddRow("[b]Legacy console?[/]", $"{YesNo(AnsiConsole.Capabilities.LegacyConsole)}") .AddRow("[b]Legacy console?[/]", $"{YesNo(AnsiConsole.Capabilities.LegacyConsole)}")
.AddRow("[b]Interactive?[/]", $"{YesNo(Environment.UserInteractive)}") .AddRow("[b]Interactive?[/]", $"{YesNo(AnsiConsole.Capabilities.SupportsInteraction)}")
.AddRow("[b]Buffer width[/]", $"{AnsiConsole.Console.Width}") .AddRow("[b]Buffer width[/]", $"{AnsiConsole.Console.Width}")
.AddRow("[b]Buffer height[/]", $"{AnsiConsole.Console.Height}"); .AddRow("[b]Buffer height[/]", $"{AnsiConsole.Console.Height}");

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
namespace ProgressExample
{
public static class DescriptionGenerator
{
private static readonly string[] _verbs = new[] { "Downloading", "Rerouting", "Retriculating", "Collapsing", "Folding", "Solving", "Colliding", "Measuring" };
private static readonly string[] _nouns = new[] { "internet", "splines", "space", "capacitators", "quarks", "algorithms", "data structures", "spacetime" };
private static readonly Random _random;
private static readonly HashSet<string> _used;
static DescriptionGenerator()
{
_random = new Random(DateTime.Now.Millisecond);
_used = new HashSet<string>();
}
public static bool TryGenerate(out string name)
{
var iterations = 0;
while (iterations < 25)
{
name = Generate();
if (!_used.Contains(name))
{
_used.Add(name);
return true;
}
iterations++;
}
name = Generate();
return false;
}
public static string Generate()
{
return _verbs[_random.Next(0, _verbs.Length)]
+ " " + _nouns[_random.Next(0, _nouns.Length)];
}
}
}

View File

@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Spectre.Console;
namespace ProgressExample
{
public static class Program
{
public static void Main()
{
AnsiConsole.MarkupLine("[yellow]Initializing warp drive[/]...");
// Show progress
AnsiConsole.Progress()
.AutoClear(false)
.Columns(new ProgressColumn[]
{
new TaskDescriptionColumn(), // Task description
new ProgressBarColumn(), // Progress bar
new PercentageColumn(), // Percentage
new RemainingTimeColumn(), // Remaining time
new SpinnerColumn(), // Spinner
})
.Start(ctx =>
{
var random = new Random(DateTime.Now.Millisecond);
var tasks = CreateTasks(ctx, random);
while (!ctx.IsFinished)
{
// Increment progress
foreach (var (task, increment) in tasks)
{
task.Increment(random.NextDouble() * increment);
}
// Write some random things to the terminal
if (random.NextDouble() < 0.1)
{
WriteLogMessage();
}
// Simulate some delay
Thread.Sleep(100);
}
});
// Done
AnsiConsole.MarkupLine("[green]Done![/]");
}
private static List<(ProgressTask, int)> CreateTasks(ProgressContext progress, Random random)
{
var tasks = new List<(ProgressTask, int)>();
while (tasks.Count < 5)
{
if (DescriptionGenerator.TryGenerate(out var name))
{
tasks.Add((progress.AddTask(name), random.Next(2, 10)));
}
}
return tasks;
}
private static void WriteLogMessage()
{
AnsiConsole.MarkupLine(
"[grey]LOG:[/] " +
DescriptionGenerator.Generate() +
"[grey]...[/]");
}
}
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<Title>Progress</Title>
<Description>Demonstrates how to show progress bars.</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>
</Project>

View File

@@ -6,6 +6,13 @@ namespace Cursor
{ {
public static void Main(string[] args) public static void Main(string[] args)
{ {
// Check if we can accept key strokes
if (!AnsiConsole.Capabilities.SupportsInteraction)
{
AnsiConsole.MarkupLine("[red]Environment does not support interaction.[/]");
return;
}
// Confirmation // Confirmation
if (!AnsiConsole.Confirm("Run prompt example?")) if (!AnsiConsole.Confirm("Run prompt example?"))
{ {

View File

@@ -7,34 +7,34 @@ namespace EmojiExample
public static void Main(string[] args) public static void Main(string[] args)
{ {
// No title // No title
WrapInPanel( Render(
new Rule() new Rule()
.RuleStyle(Style.Parse("yellow")) .RuleStyle(Style.Parse("yellow"))
.AsciiBorder() .AsciiBorder()
.LeftAligned()); .LeftAligned());
// Left aligned title // Left aligned title
WrapInPanel( Render(
new Rule("[blue]Left aligned[/]") new Rule("[blue]Left aligned[/]")
.RuleStyle(Style.Parse("red")) .RuleStyle(Style.Parse("red"))
.DoubleBorder() .DoubleBorder()
.LeftAligned()); .LeftAligned());
// Centered title // Centered title
WrapInPanel( Render(
new Rule("[green]Centered[/]") new Rule("[green]Centered[/]")
.RuleStyle(Style.Parse("green")) .RuleStyle(Style.Parse("green"))
.HeavyBorder() .HeavyBorder()
.Centered()); .Centered());
// Right aligned title // Right aligned title
WrapInPanel( Render(
new Rule("[red]Right aligned[/]") new Rule("[red]Right aligned[/]")
.RuleStyle(Style.Parse("blue")) .RuleStyle(Style.Parse("blue"))
.RightAligned()); .RightAligned());
} }
private static void WrapInPanel(Rule rule) private static void Render(Rule rule)
{ {
AnsiConsole.Render(rule); AnsiConsole.Render(rule);
AnsiConsole.WriteLine(); AnsiConsole.WriteLine();

View File

@@ -89,4 +89,7 @@ dotnet_diagnostic.RCS1227.severity = none
dotnet_diagnostic.IDE0004.severity = warning dotnet_diagnostic.IDE0004.severity = warning
# CA1810: Initialize reference type static fields inline # CA1810: Initialize reference type static fields inline
dotnet_diagnostic.CA1810.severity = none dotnet_diagnostic.CA1810.severity = none
# IDE0044: Add readonly modifier
dotnet_diagnostic.IDE0044.severity = warning

View File

@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// Represents a renderable image.
/// </summary>
public sealed class CanvasImage : Renderable
{
private static readonly IResampler _defaultResampler = KnownResamplers.Bicubic;
/// <summary>
/// Gets the image width.
/// </summary>
public int Width => Image.Width;
/// <summary>
/// Gets the image height.
/// </summary>
public int Height => Image.Height;
/// <summary>
/// Gets or sets the render width of the canvas.
/// </summary>
public int? MaxWidth { get; set; }
/// <summary>
/// Gets or sets the render width of the canvas.
/// </summary>
public int PixelWidth { get; set; } = 2;
/// <summary>
/// Gets or sets the <see cref="IResampler"/> that should
/// be used when scaling the image. Defaults to bicubic sampling.
/// </summary>
public IResampler? Resampler { get; set; }
internal SixLabors.ImageSharp.Image<Rgba32> Image { get; }
/// <summary>
/// Initializes a new instance of the <see cref="CanvasImage"/> class.
/// </summary>
/// <param name="filename">The image filename.</param>
public CanvasImage(string filename)
{
Image = SixLabors.ImageSharp.Image.Load<Rgba32>(filename);
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
if (PixelWidth < 0)
{
throw new InvalidOperationException("Pixel width must be greater than zero.");
}
var width = MaxWidth ?? Width;
if (maxWidth < width * PixelWidth)
{
return new Measurement(maxWidth, maxWidth);
}
return new Measurement(width * PixelWidth, width * PixelWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var image = Image;
var width = Width;
var height = Height;
// Got a max width?
if (MaxWidth != null)
{
height = (int)(height * ((float)MaxWidth.Value) / Width);
width = MaxWidth.Value;
}
// Exceed the max width when we take pixel width into account?
if (width * PixelWidth > maxWidth)
{
height = (int)(height * (maxWidth / (float)(width * PixelWidth)));
width = maxWidth / PixelWidth;
}
// Need to rescale the pixel buffer?
if (width != Width || height != Height)
{
var resampler = Resampler ?? _defaultResampler;
image = image.Clone(); // Clone the original image
image.Mutate(i => i.Resize(width, height, resampler));
}
var canvas = new Canvas(width, height)
{
MaxWidth = MaxWidth,
PixelWidth = PixelWidth,
Scale = false,
};
for (var y = 0; y < image.Height; y++)
{
for (var x = 0; x < image.Width; x++)
{
if (image[x, y].A == 0)
{
continue;
}
canvas.SetPixel(x, y, new Color(
image[x, y].R, image[x, y].G, image[x, y].B));
}
}
return ((IRenderable)canvas).Render(context, maxWidth);
}
}
}

View File

@@ -0,0 +1,135 @@
using System;
using SixLabors.ImageSharp.Processing;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="CanvasImage"/>.
/// </summary>
public static class CanvasImageExtensions
{
/// <summary>
/// Sets the maximum width of the rendered image.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <param name="maxWidth">The maximum width.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage MaxWidth(this CanvasImage image, int? maxWidth)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.MaxWidth = maxWidth;
return image;
}
/// <summary>
/// Disables the maximum width of the rendered image.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage NoMaxWidth(this CanvasImage image)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.MaxWidth = null;
return image;
}
/// <summary>
/// Sets the pixel width.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <param name="width">The pixel width.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage PixelWidth(this CanvasImage image, int width)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.PixelWidth = width;
return image;
}
/// <summary>
/// Mutates the underlying image.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <param name="action">The action that mutates the underlying image.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage Mutate(this CanvasImage image, Action<IImageProcessingContext> action)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
image.Image.Mutate(action);
return image;
}
/// <summary>
/// Uses a bicubic sampler that implements the bicubic kernel algorithm W(x).
/// </summary>
/// <param name="image">The canvas image.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage BicubicResampler(this CanvasImage image)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.Resampler = KnownResamplers.Bicubic;
return image;
}
/// <summary>
/// Uses a bilinear sampler. This interpolation algorithm
/// can be used where perfect image transformation with pixel matching is impossible,
/// so that one can calculate and assign appropriate intensity values to pixels.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage BilinearResampler(this CanvasImage image)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.Resampler = KnownResamplers.Triangle;
return image;
}
/// <summary>
/// Uses a Nearest-Neighbour sampler that implements the nearest neighbor algorithm.
/// This uses a very fast, unscaled filter which will select the closest pixel to
/// the new pixels position.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage NearestNeighborResampler(this CanvasImage image)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.Resampler = KnownResamplers.NearestNeighbor;
return image;
}
}
}

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<Nullable>enable</Nullable>
<Description>A library that extends Spectre.Console with ImageSharp superpowers.</Description>
</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="SixLabors.ImageSharp" Version="1.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>
</Project>

View File

@@ -8,6 +8,9 @@ namespace Spectre.Console.Tests.Data
[SuppressMessage("Usage", "CA1801:Review unused parameters", Justification = "<Pending>")] [SuppressMessage("Usage", "CA1801:Review unused parameters", Justification = "<Pending>")]
public static bool MethodThatThrows(int? number) => throw new InvalidOperationException("Throwing!"); public static bool MethodThatThrows(int? number) => throw new InvalidOperationException("Throwing!");
[SuppressMessage("Usage", "CA1801:Review unused parameters", Justification = "<Pending>")]
public static bool GenericMethodThatThrows<T0, T1, TRet>(int? number) => throw new InvalidOperationException("Throwing!");
public static void ThrowWithInnerException() public static void ThrowWithInnerException()
{ {
try try
@@ -19,5 +22,17 @@ namespace Spectre.Console.Tests.Data
throw new InvalidOperationException("Something threw!", ex); throw new InvalidOperationException("Something threw!", ex);
} }
} }
public static void ThrowWithGenericInnerException()
{
try
{
GenericMethodThatThrows<int, float, double>(null);
}
catch (Exception ex)
{
throw new InvalidOperationException("Something threw!", ex);
}
}
} }
} }

View File

@@ -0,0 +1,7 @@
System.InvalidOperationException: Something threw!
System.InvalidOperationException: Throwing!
at Spectre.Console.Tests.Data.TestExceptions.GenericMethodThatThrows[[T0,T1,TRet]](Nullable`1 number) in /xyz/Exceptions.cs:nn
at Spectre.Console.Tests.Data.TestExceptions.ThrowWithGenericInnerException() in /xyz/Exceptions.cs:nn
at Spectre.Console.Tests.Data.TestExceptions.ThrowWithGenericInnerException() in /xyz/Exceptions.cs:nn
at Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exceptions_With_Generic_Type_Parameters_In_Callsite_As_Expected>b__4_0() in /xyz/ExceptionTests.cs:nn
at Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn

View File

@@ -0,0 +1 @@
━━━━━━━━━━━━━━━━━━━━

View File

@@ -0,0 +1,5 @@
foo ━━━ 0% -:--:-- ⣷
bar ━━━ 0% -:--:-- ⣷
baz ━━━ 0% -:--:-- ⣷

View File

@@ -0,0 +1,17 @@
namespace Spectre.Console.Tests
{
public sealed class DummyCursor : IAnsiConsoleCursor
{
public void Move(CursorDirection direction, int steps)
{
}
public void SetPosition(int column, int line)
{
}
public void Show(bool show)
{
}
}
}

View File

@@ -11,13 +11,14 @@ namespace Spectre.Console.Tests
{ {
public Capabilities Capabilities { get; } public Capabilities Capabilities { get; }
public Encoding Encoding { get; } public Encoding Encoding { get; }
public IAnsiConsoleCursor Cursor => throw new NotSupportedException(); public IAnsiConsoleCursor Cursor => new DummyCursor();
public TestableConsoleInput Input { get; } public TestableConsoleInput Input { get; }
public int Width { get; } public int Width { get; }
public int Height { get; } public int Height { get; }
IAnsiConsoleInput IAnsiConsole.Input => Input; IAnsiConsoleInput IAnsiConsole.Input => Input;
public RenderPipeline Pipeline { get; }
public Decoration Decoration { get; set; } public Decoration Decoration { get; set; }
public Color Foreground { get; set; } public Color Foreground { get; set; }
@@ -31,14 +32,15 @@ namespace Spectre.Console.Tests
public PlainConsole( public PlainConsole(
int width = 80, int height = 9000, Encoding encoding = null, int width = 80, int height = 9000, Encoding encoding = null,
bool supportsAnsi = true, ColorSystem colorSystem = ColorSystem.Standard, bool supportsAnsi = true, ColorSystem colorSystem = ColorSystem.Standard,
bool legacyConsole = false) bool legacyConsole = false, bool interactive = true)
{ {
Capabilities = new Capabilities(supportsAnsi, colorSystem, legacyConsole); Capabilities = new Capabilities(supportsAnsi, colorSystem, legacyConsole, interactive);
Encoding = encoding ?? Encoding.UTF8; Encoding = encoding ?? Encoding.UTF8;
Width = width; Width = width;
Height = height; Height = height;
Writer = new StringWriter(); Writer = new StringWriter();
Input = new TestableConsoleInput(); Input = new TestableConsoleInput();
Pipeline = new RenderPipeline();
} }
public void Dispose() public void Dispose()
@@ -50,14 +52,17 @@ namespace Spectre.Console.Tests
{ {
} }
public void Write(Segment segment) public void Write(IEnumerable<Segment> segments)
{ {
if (segment is null) if (segments is null)
{ {
throw new ArgumentNullException(nameof(segment)); return;
} }
Writer.Write(segment.Text); foreach (var segment in segments)
{
Writer.Write(segment.Text);
}
} }
public string WriteNormalizedException(Exception ex, ExceptionFormats formats = ExceptionFormats.Default) public string WriteNormalizedException(Exception ex, ExceptionFormats formats = ExceptionFormats.Default)

View File

@@ -1,4 +1,4 @@
namespace Spectre.Console.Tests.Tools namespace Spectre.Console.Tests
{ {
public sealed class TestLinkIdentityGenerator : ILinkIdentityGenerator public sealed class TestLinkIdentityGenerator : ILinkIdentityGenerator
{ {

View File

@@ -1,8 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
using Spectre.Console.Tests.Tools;
namespace Spectre.Console.Tests namespace Spectre.Console.Tests
{ {
@@ -19,16 +19,21 @@ namespace Spectre.Console.Tests
public int Height => _console.Height; public int Height => _console.Height;
public IAnsiConsoleCursor Cursor => _console.Cursor; public IAnsiConsoleCursor Cursor => _console.Cursor;
public TestableConsoleInput Input { get; } public TestableConsoleInput Input { get; }
public RenderPipeline Pipeline => _console.Pipeline;
IAnsiConsoleInput IAnsiConsole.Input => Input; IAnsiConsoleInput IAnsiConsole.Input => Input;
public TestableAnsiConsole(ColorSystem system, AnsiSupport ansi = AnsiSupport.Yes, int width = 80) public TestableAnsiConsole(
ColorSystem system, AnsiSupport ansi = AnsiSupport.Yes,
InteractionSupport interaction = InteractionSupport.Yes,
int width = 80)
{ {
_writer = new StringWriter(); _writer = new StringWriter();
_console = AnsiConsole.Create(new AnsiConsoleSettings _console = AnsiConsole.Create(new AnsiConsoleSettings
{ {
Ansi = ansi, Ansi = ansi,
ColorSystem = (ColorSystemSupport)system, ColorSystem = (ColorSystemSupport)system,
Interactive = interaction,
Out = _writer, Out = _writer,
LinkIdentityGenerator = new TestLinkIdentityGenerator(), LinkIdentityGenerator = new TestLinkIdentityGenerator(),
}); });
@@ -47,9 +52,17 @@ namespace Spectre.Console.Tests
_console.Clear(home); _console.Clear(home);
} }
public void Write(Segment segment) public void Write(IEnumerable<Segment> segments)
{ {
_console.Write(segment); if (segments is null)
{
return;
}
foreach (var segment in segments)
{
_console.Write(segment);
}
} }
} }
} }

View File

@@ -65,6 +65,20 @@ namespace Spectre.Console.Tests.Unit
return Verifier.Verify(result); return Verifier.Verify(result);
} }
[Fact]
public Task Should_Write_Exceptions_With_Generic_Type_Parameters_In_Callsite_As_Expected()
{
// Given
var console = new PlainConsole(width: 1024);
var dex = GetException(() => TestExceptions.ThrowWithGenericInnerException());
// When
var result = console.WriteNormalizedException(dex);
// Then
return Verifier.Verify(result);
}
public static Exception GetException(Action action) public static Exception GetException(Action action)
{ {
try try

View File

@@ -0,0 +1,91 @@
using System.Threading.Tasks;
using Shouldly;
using VerifyXunit;
using Xunit;
namespace Spectre.Console.Tests.Unit
{
[UsesVerify]
public sealed class ProgressTests
{
[Fact]
public void Should_Render_Task_Correctly()
{
// Given
var console = new TestableAnsiConsole(ColorSystem.TrueColor, width: 10);
var progress = new Progress(console)
.Columns(new[] { new ProgressBarColumn() })
.AutoRefresh(false)
.AutoClear(true);
// When
progress.Start(ctx => ctx.AddTask("foo"));
// Then
console.Output
.NormalizeLineEndings()
.ShouldBe(
"[?25l" + // Hide cursor
" \n" + // Top padding
"━━━━━━━━━━\n" + // Task
" " + // Bottom padding
"[?25h"); // Clear + show cursor
}
[Fact]
public void Should_Not_Auto_Clear_If_Specified()
{
// Given
var console = new TestableAnsiConsole(ColorSystem.TrueColor, width: 10);
var progress = new Progress(console)
.Columns(new[] { new ProgressBarColumn() })
.AutoRefresh(false)
.AutoClear(false);
// When
progress.Start(ctx => ctx.AddTask("foo"));
// Then
console.Output
.NormalizeLineEndings()
.ShouldBe(
"[?25l" + // Hide cursor
" \n" + // Top padding
"━━━━━━━━━━\n" + // Task
" \n" + // Bottom padding
"[?25h"); // show cursor
}
[Fact]
public Task Foo()
{
// Given
var console = new PlainConsole(width: 20);
var progress = new Progress(console)
.Columns(new ProgressColumn[]
{
new TaskDescriptionColumn(),
new ProgressBarColumn(),
new PercentageColumn(),
new RemainingTimeColumn(),
new SpinnerColumn(),
})
.AutoRefresh(false)
.AutoClear(false);
// When
progress.Start(ctx =>
{
ctx.AddTask("foo");
ctx.AddTask("bar");
ctx.AddTask("baz");
});
// Then
return Verifier.Verify(console.Output);
}
}
}

View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Linq;
using Shouldly;
using Spectre.Console.Rendering;
using Xunit;
namespace Spectre.Console.Tests.Unit
{
public sealed class RenderHookTests
{
private sealed class HelloRenderHook : IRenderHook
{
public IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables)
{
return new IRenderable[] { new Text("Hello\n") }.Concat(renderables);
}
}
[Fact]
public void Should_Inject_Renderable_Before_Writing_To_Console()
{
// Given
var console = new PlainConsole();
console.Pipeline.Attach(new HelloRenderHook());
// When
console.Render(new Text("World"));
// Then
console.Lines[0].ShouldBe("Hello");
console.Lines[1].ShouldBe("World");
}
}
}

View File

@@ -54,6 +54,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prompt", "..\examples\Promp
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Figlet", "..\examples\Figlet\Figlet.csproj", "{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Figlet", "..\examples\Figlet\Figlet.csproj", "{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Canvas", "..\examples\Canvas\Canvas.csproj", "{5693761A-754A-40A8-9144-36510D6A4D69}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.ImageSharp", "Spectre.Console.ImageSharp\Spectre.Console.ImageSharp.csproj", "{0EFE694D-0770-4E71-BF4E-EC2B41362F79}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Progress", "..\examples\Progress\Progress.csproj", "{2B712A52-40F1-4C1C-833E-7C869ACA91F3}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -268,6 +274,42 @@ Global
{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}.Release|x64.Build.0 = Release|Any CPU {45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}.Release|x64.Build.0 = Release|Any CPU
{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}.Release|x86.ActiveCfg = Release|Any CPU {45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}.Release|x86.ActiveCfg = Release|Any CPU
{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}.Release|x86.Build.0 = Release|Any CPU {45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}.Release|x86.Build.0 = Release|Any CPU
{5693761A-754A-40A8-9144-36510D6A4D69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5693761A-754A-40A8-9144-36510D6A4D69}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5693761A-754A-40A8-9144-36510D6A4D69}.Debug|x64.ActiveCfg = Debug|Any CPU
{5693761A-754A-40A8-9144-36510D6A4D69}.Debug|x64.Build.0 = Debug|Any CPU
{5693761A-754A-40A8-9144-36510D6A4D69}.Debug|x86.ActiveCfg = Debug|Any CPU
{5693761A-754A-40A8-9144-36510D6A4D69}.Debug|x86.Build.0 = Debug|Any CPU
{5693761A-754A-40A8-9144-36510D6A4D69}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5693761A-754A-40A8-9144-36510D6A4D69}.Release|Any CPU.Build.0 = Release|Any CPU
{5693761A-754A-40A8-9144-36510D6A4D69}.Release|x64.ActiveCfg = Release|Any CPU
{5693761A-754A-40A8-9144-36510D6A4D69}.Release|x64.Build.0 = Release|Any CPU
{5693761A-754A-40A8-9144-36510D6A4D69}.Release|x86.ActiveCfg = Release|Any CPU
{5693761A-754A-40A8-9144-36510D6A4D69}.Release|x86.Build.0 = Release|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|x64.ActiveCfg = Debug|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|x64.Build.0 = Debug|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|x86.ActiveCfg = Debug|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Debug|x86.Build.0 = Debug|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|Any CPU.Build.0 = Release|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x64.ActiveCfg = Release|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x64.Build.0 = Release|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x86.ActiveCfg = Release|Any CPU
{0EFE694D-0770-4E71-BF4E-EC2B41362F79}.Release|x86.Build.0 = Release|Any CPU
{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|x64.ActiveCfg = Debug|Any CPU
{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|x64.Build.0 = Debug|Any CPU
{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|x86.ActiveCfg = Debug|Any CPU
{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Debug|x86.Build.0 = Debug|Any CPU
{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|Any CPU.Build.0 = Release|Any CPU
{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|x64.ActiveCfg = Release|Any CPU
{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|x64.Build.0 = Release|Any CPU
{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|x86.ActiveCfg = Release|Any CPU
{2B712A52-40F1-4C1C-833E-7C869ACA91F3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -289,6 +331,8 @@ Global
{75C608C3-ABB4-4168-A229-7F8250B946D1} = {F0575243-121F-4DEE-9F6B-246E26DC0844} {75C608C3-ABB4-4168-A229-7F8250B946D1} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{6351C70F-F368-46DB-BAED-9B87CCD69353} = {F0575243-121F-4DEE-9F6B-246E26DC0844} {6351C70F-F368-46DB-BAED-9B87CCD69353} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1} = {F0575243-121F-4DEE-9F6B-246E26DC0844} {45BF6302-6553-4E52-BF0F-B10D1AA9A6D1} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{5693761A-754A-40A8-9144-36510D6A4D69} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{2B712A52-40F1-4C1C-833E-7C869ACA91F3} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C} SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C}

View File

@@ -0,0 +1,17 @@
namespace Spectre.Console
{
/// <summary>
/// A console capable of writing ANSI escape sequences.
/// </summary>
public static partial class AnsiConsole
{
/// <summary>
/// Creates a new <see cref="Progress"/> instance.
/// </summary>
/// <returns>A <see cref="Progress"/> instance.</returns>
public static Progress Progress()
{
return Console.Progress();
}
}
}

View File

@@ -18,6 +18,12 @@ namespace Spectre.Console
/// </summary> /// </summary>
public ColorSystemSupport ColorSystem { get; set; } public ColorSystemSupport ColorSystem { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or
/// not the console is interactive.
/// </summary>
public InteractionSupport Interactive { get; set; }
/// <summary> /// <summary>
/// Gets or sets the link identity generator. /// Gets or sets the link identity generator.
/// </summary> /// </summary>

View File

@@ -36,17 +36,24 @@ namespace Spectre.Console
/// </remarks> /// </remarks>
public bool LegacyConsole { get; } public bool LegacyConsole { get; }
/// <summary>
/// Gets or sets a value indicating whether or not the console supports interaction.
/// </summary>
public bool SupportsInteraction { get; set; }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Capabilities"/> class. /// Initializes a new instance of the <see cref="Capabilities"/> class.
/// </summary> /// </summary>
/// <param name="supportsAnsi">Whether or not ANSI escape sequences are supported.</param> /// <param name="supportsAnsi">Whether or not ANSI escape sequences are supported.</param>
/// <param name="colorSystem">The color system that is supported.</param> /// <param name="colorSystem">The color system that is supported.</param>
/// <param name="legacyConsole">Whether or not this is a legacy console.</param> /// <param name="legacyConsole">Whether or not this is a legacy console.</param>
public Capabilities(bool supportsAnsi, ColorSystem colorSystem, bool legacyConsole) /// <param name="supportsInteraction">Whether or not the console supports interaction.</param>
public Capabilities(bool supportsAnsi, ColorSystem colorSystem, bool legacyConsole, bool supportsInteraction)
{ {
SupportsAnsi = supportsAnsi; SupportsAnsi = supportsAnsi;
ColorSystem = colorSystem; ColorSystem = colorSystem;
LegacyConsole = legacyConsole; LegacyConsole = legacyConsole;
SupportsInteraction = supportsInteraction;
} }
/// <summary> /// <summary>

View File

@@ -8,31 +8,31 @@ namespace Spectre.Console
/// <summary> /// <summary>
/// Try to detect the color system. /// Try to detect the color system.
/// </summary> /// </summary>
Detect = -1, Detect = 0,
/// <summary> /// <summary>
/// No colors. /// No colors.
/// </summary> /// </summary>
NoColors = 0, NoColors = 1,
/// <summary> /// <summary>
/// Legacy, 3-bit mode. /// Legacy, 3-bit mode.
/// </summary> /// </summary>
Legacy = 1, Legacy = 2,
/// <summary> /// <summary>
/// Standard, 4-bit mode. /// Standard, 4-bit mode.
/// </summary> /// </summary>
Standard = 2, Standard = 3,
/// <summary> /// <summary>
/// 8-bit mode. /// 8-bit mode.
/// </summary> /// </summary>
EightBit = 3, EightBit = 4,
/// <summary> /// <summary>
/// 24-bit mode. /// 24-bit mode.
/// </summary> /// </summary>
TrueColor = 4, TrueColor = 5,
} }
} }

View File

@@ -52,8 +52,7 @@ namespace Spectre.Console
/// <param name="args">An array of objects to write.</param> /// <param name="args">An array of objects to write.</param>
public static void MarkupLine(this IAnsiConsole console, IFormatProvider provider, string format, params object[] args) public static void MarkupLine(this IAnsiConsole console, IFormatProvider provider, string format, params object[] args)
{ {
Markup(console, provider, format, args); Markup(console, provider, format + Environment.NewLine, args);
console.WriteLine();
} }
} }
} }

View File

@@ -0,0 +1,25 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="IAnsiConsole"/>.
/// </summary>
public static partial class AnsiConsoleExtensions
{
/// <summary>
/// Creates a new <see cref="Progress"/> instance for the console.
/// </summary>
/// <param name="console">The console.</param>
/// <returns>A <see cref="Progress"/> instance.</returns>
public static Progress Progress(this IAnsiConsole console)
{
if (console is null)
{
throw new ArgumentNullException(nameof(console));
}
return new Progress(console);
}
}
}

View File

@@ -1,5 +1,5 @@
using System; using System;
using System.Linq; using System.Collections.Generic;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
namespace Spectre.Console namespace Spectre.Console
@@ -26,19 +26,26 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(renderable)); throw new ArgumentNullException(nameof(renderable));
} }
var options = new RenderContext(console.Encoding, console.Capabilities.LegacyConsole); var context = new RenderContext(console.Encoding, console.Capabilities.LegacyConsole);
var segments = renderable.Render(options, console.Width).ToArray(); var renderables = console.Pipeline.Process(context, new[] { renderable });
segments = Segment.Merge(segments).ToArray();
foreach (var segment in segments) Render(console, context, renderables);
}
private static void Render(IAnsiConsole console, RenderContext options, IEnumerable<IRenderable> renderables)
{
if (renderables is null)
{ {
if (string.IsNullOrEmpty(segment.Text)) return;
{
continue;
}
console.Write(segment.Text, segment.Style);
} }
var result = new List<Segment>();
foreach (var renderable in renderables)
{
result.AddRange(renderable.Render(options, console.Width));
}
console.Write(Segment.Merge(result));
} }
} }
} }

View File

@@ -18,6 +18,26 @@ namespace Spectre.Console
return new Recorder(console); return new Recorder(console);
} }
/// <summary>
/// Writes the specified string value to the console.
/// </summary>
/// <param name="console">The console to write to.</param>
/// <param name="segment">The segment to write.</param>
public static void Write(this IAnsiConsole console, Segment segment)
{
if (console is null)
{
throw new ArgumentNullException(nameof(console));
}
if (segment is null)
{
throw new ArgumentNullException(nameof(segment));
}
console.Write(new[] { segment });
}
/// <summary> /// <summary>
/// Writes the specified string value to the console. /// Writes the specified string value to the console.
/// </summary> /// </summary>
@@ -25,7 +45,7 @@ namespace Spectre.Console
/// <param name="text">The text to write.</param> /// <param name="text">The text to write.</param>
public static void Write(this IAnsiConsole console, string text) public static void Write(this IAnsiConsole console, string text)
{ {
Write(console, text, Style.Plain); Render(console, new Text(text, Style.Plain));
} }
/// <summary> /// <summary>
@@ -36,17 +56,7 @@ namespace Spectre.Console
/// <param name="style">The text style.</param> /// <param name="style">The text style.</param>
public static void Write(this IAnsiConsole console, string text, Style style) public static void Write(this IAnsiConsole console, string text, Style style)
{ {
if (console is null) Render(console, new Text(text, style));
{
throw new ArgumentNullException(nameof(console));
}
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
console.Write(new Segment(text, style));
} }
/// <summary> /// <summary>
@@ -60,7 +70,7 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(console)); throw new ArgumentNullException(nameof(console));
} }
console.Write(Environment.NewLine, Style.Plain); Render(console, new Text(Environment.NewLine, Style.Plain));
} }
/// <summary> /// <summary>
@@ -91,8 +101,7 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(text)); throw new ArgumentNullException(nameof(text));
} }
console.Write(new Segment(text, style)); console.Write(text + Environment.NewLine, style);
console.WriteLine();
} }
} }
} }

View File

@@ -0,0 +1,54 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="PercentageColumn"/>.
/// </summary>
public static class PercentageColumnExtensions
{
/// <summary>
/// Sets the style for a non-complete task.
/// </summary>
/// <param name="column">The column.</param>
/// <param name="style">The style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static PercentageColumn Style(this PercentageColumn column, Style style)
{
if (column is null)
{
throw new ArgumentNullException(nameof(column));
}
if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
column.Style = style;
return column;
}
/// <summary>
/// Sets the style for a completed task.
/// </summary>
/// <param name="column">The column.</param>
/// <param name="style">The style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static PercentageColumn CompletedStyle(this PercentageColumn column, Style style)
{
if (column is null)
{
throw new ArgumentNullException(nameof(column));
}
if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
column.CompletedStyle = style;
return column;
}
}
}

View File

@@ -0,0 +1,76 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="ProgressBarColumn"/>.
/// </summary>
public static class ProgressBarColumnExtensions
{
/// <summary>
/// Sets the style of completed portions of the progress bar.
/// </summary>
/// <param name="column">The column.</param>
/// <param name="style">The style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static ProgressBarColumn CompletedStyle(this ProgressBarColumn column, Style style)
{
if (column is null)
{
throw new ArgumentNullException(nameof(column));
}
if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
column.CompletedStyle = style;
return column;
}
/// <summary>
/// Sets the style of a finished progress bar.
/// </summary>
/// <param name="column">The column.</param>
/// <param name="style">The style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static ProgressBarColumn FinishedStyle(this ProgressBarColumn column, Style style)
{
if (column is null)
{
throw new ArgumentNullException(nameof(column));
}
if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
column.FinishedStyle = style;
return column;
}
/// <summary>
/// Sets the style of remaining portions of the progress bar.
/// </summary>
/// <param name="column">The column.</param>
/// <param name="style">The style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static ProgressBarColumn RemainingStyle(this ProgressBarColumn column, Style style)
{
if (column is null)
{
throw new ArgumentNullException(nameof(column));
}
if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
column.RemainingStyle = style;
return column;
}
}
}

View File

@@ -0,0 +1,32 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="RemainingTimeColumn"/>.
/// </summary>
public static class RemainingTimeColumnExtensions
{
/// <summary>
/// Sets the style of the remaining time text.
/// </summary>
/// <param name="column">The column.</param>
/// <param name="style">The style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static RemainingTimeColumn Style(this RemainingTimeColumn column, Style style)
{
if (column is null)
{
throw new ArgumentNullException(nameof(column));
}
if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
column.Style = style;
return column;
}
}
}

View File

@@ -0,0 +1,32 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="SpinnerColumn"/>.
/// </summary>
public static class SpinnerColumnExtensions
{
/// <summary>
/// Sets the style of the spinner.
/// </summary>
/// <param name="column">The column.</param>
/// <param name="style">The style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static SpinnerColumn Style(this SpinnerColumn column, Style style)
{
if (column is null)
{
throw new ArgumentNullException(nameof(column));
}
if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
column.Style = style;
return column;
}
}
}

View File

@@ -27,7 +27,7 @@ namespace Spectre.Console
} }
alignment ??= panel.Header?.Alignment; alignment ??= panel.Header?.Alignment;
return Header(panel, new PanelHeader(text, alignment)); return Header(panel, new PanelHeader(text, alignment));
} }
/// <summary> /// <summary>

View File

@@ -0,0 +1,79 @@
using System;
using System.Linq;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="Progress"/>.
/// </summary>
public static class ProgressExtensions
{
/// <summary>
/// Sets the columns to be used for an <see cref="Progress"/> instance.
/// </summary>
/// <param name="progress">The <see cref="Progress"/> instance.</param>
/// <param name="columns">The columns to use.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Progress Columns(this Progress progress, ProgressColumn[] columns)
{
if (progress is null)
{
throw new ArgumentNullException(nameof(progress));
}
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
if (!columns.Any())
{
throw new InvalidOperationException("At least one column must be specified.");
}
progress.Columns.Clear();
progress.Columns.AddRange(columns);
return progress;
}
/// <summary>
/// Sets whether or not auto refresh is enabled.
/// If disabled, you will manually have to refresh the progress.
/// </summary>
/// <param name="progress">The <see cref="Progress"/> instance.</param>
/// <param name="enabled">Whether or not auto refresh is enabled.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Progress AutoRefresh(this Progress progress, bool enabled)
{
if (progress is null)
{
throw new ArgumentNullException(nameof(progress));
}
progress.AutoRefresh = enabled;
return progress;
}
/// <summary>
/// Sets whether or not auto clear is enabled.
/// If enabled, the task tabled will be removed once
/// all tasks have completed.
/// </summary>
/// <param name="progress">The <see cref="Progress"/> instance.</param>
/// <param name="enabled">Whether or not auto clear is enabled.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Progress AutoClear(this Progress progress, bool enabled)
{
if (progress is null)
{
throw new ArgumentNullException(nameof(progress));
}
progress.AutoClear = enabled;
return progress;
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="ProgressTask"/>.
/// </summary>
public static class ProgressTaskExtensions
{
/// <summary>
/// Sets the task description.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="description">The description.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static ProgressTask Description(this ProgressTask task, string description)
{
if (task is null)
{
throw new ArgumentNullException(nameof(task));
}
task.Description = description;
return task;
}
/// <summary>
/// Sets the max value of the task.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="value">The max value.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static ProgressTask MaxValue(this ProgressTask task, double value)
{
if (task is null)
{
throw new ArgumentNullException(nameof(task));
}
task.MaxValue = value;
return task;
}
}
}

View File

@@ -0,0 +1,29 @@
using System.Globalization;
using System.Text;
namespace Spectre.Console.Internal
{
internal static class StringBuilderExtensions
{
public static StringBuilder AppendWithStyle(this StringBuilder builder, Style? style, int? value)
{
return AppendWithStyle(builder, style, value?.ToString(CultureInfo.InvariantCulture));
}
public static StringBuilder AppendWithStyle(this StringBuilder builder, Style? style, string? value)
{
value ??= string.Empty;
if (style != null)
{
return builder.Append('[')
.Append(style.ToMarkup())
.Append(']')
.Append(value.EscapeMarkup())
.Append("[/]");
}
return builder.Append(value);
}
}
}

View File

@@ -62,10 +62,15 @@ namespace Spectre.Console
return text; return text;
} }
internal static string NormalizeLineEndings(this string? text, bool native = false) internal static string? RemoveNewLines(this string? text)
{
return text?.ReplaceExact("\r\n", string.Empty)
?.ReplaceExact("\n", string.Empty);
}
internal static string NormalizeNewLines(this string? text, bool native = false)
{ {
text = text?.ReplaceExact("\r\n", "\n"); text = text?.ReplaceExact("\r\n", "\n");
text = text?.ReplaceExact("\r", string.Empty);
text ??= string.Empty; text ??= string.Empty;
if (native && !_alreadyNormalized) if (native && !_alreadyNormalized)
@@ -78,7 +83,7 @@ namespace Spectre.Console
internal static string[] SplitLines(this string text) internal static string[] SplitLines(this string text)
{ {
var result = text?.NormalizeLineEndings()?.Split(new[] { '\n' }, StringSplitOptions.None); var result = text?.NormalizeNewLines()?.Split(new[] { '\n' }, StringSplitOptions.None);
return result ?? Array.Empty<string>(); return result ?? Array.Empty<string>();
} }

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Text; using System.Text;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
@@ -28,6 +29,11 @@ namespace Spectre.Console
/// </summary> /// </summary>
IAnsiConsoleInput Input { get; } IAnsiConsoleInput Input { get; }
/// <summary>
/// Gets the render pipeline.
/// </summary>
RenderPipeline Pipeline { get; }
/// <summary> /// <summary>
/// Gets the buffer width of the console. /// Gets the buffer width of the console.
/// </summary> /// </summary>
@@ -45,9 +51,9 @@ namespace Spectre.Console
void Clear(bool home); void Clear(bool home);
/// <summary> /// <summary>
/// Writes a string followed by a line terminator to the console. /// Writes multiple segments to the console.
/// </summary> /// </summary>
/// <param name="segment">The segment to write.</param> /// <param name="segments">The segments to write.</param>
void Write(Segment segment); void Write(IEnumerable<Segment> segments);
} }
} }

View File

@@ -0,0 +1,24 @@
namespace Spectre.Console
{
/// <summary>
/// Determines interactivity support.
/// </summary>
public enum InteractionSupport
{
/// <summary>
/// Interaction support should be
/// detected by the system.
/// </summary>
Detect = 0,
/// <summary>
/// Interactivity is supported.
/// </summary>
Yes = 1,
/// <summary>
/// Interactivity is not supported.
/// </summary>
No = 2,
}
}

View File

@@ -121,6 +121,8 @@ namespace Spectre.Console.Internal
// Enabling failed. // Enabling failed.
return false; return false;
} }
isLegacy = false;
} }
return true; return true;

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
@@ -11,9 +12,11 @@ namespace Spectre.Console.Internal
private readonly AnsiBuilder _ansiBuilder; private readonly AnsiBuilder _ansiBuilder;
private readonly AnsiCursor _cursor; private readonly AnsiCursor _cursor;
private readonly ConsoleInput _input; private readonly ConsoleInput _input;
private readonly object _lock;
public Capabilities Capabilities { get; } public Capabilities Capabilities { get; }
public Encoding Encoding { get; } public Encoding Encoding { get; }
public RenderPipeline Pipeline { get; }
public IAnsiConsoleCursor Cursor => _cursor; public IAnsiConsoleCursor Cursor => _cursor;
public IAnsiConsoleInput Input => _input; public IAnsiConsoleInput Input => _input;
@@ -49,35 +52,59 @@ namespace Spectre.Console.Internal
Capabilities = capabilities ?? throw new ArgumentNullException(nameof(capabilities)); Capabilities = capabilities ?? throw new ArgumentNullException(nameof(capabilities));
Encoding = _out.IsStandardOut() ? System.Console.OutputEncoding : Encoding.UTF8; Encoding = _out.IsStandardOut() ? System.Console.OutputEncoding : Encoding.UTF8;
Pipeline = new RenderPipeline();
_ansiBuilder = new AnsiBuilder(Capabilities, linkHasher); _ansiBuilder = new AnsiBuilder(Capabilities, linkHasher);
_cursor = new AnsiCursor(this); _cursor = new AnsiCursor(this);
_input = new ConsoleInput(); _input = new ConsoleInput();
_lock = new object();
} }
public void Clear(bool home) public void Clear(bool home)
{ {
Write(Segment.Control("\u001b[2J")); lock (_lock)
if (home)
{ {
Cursor.SetPosition(0, 0); Write(new[] { Segment.Control("\u001b[2J") });
if (home)
{
Cursor.SetPosition(0, 0);
}
} }
} }
public void Write(Segment segment) public void Write(IEnumerable<Segment> segments)
{ {
var parts = segment.Text.NormalizeLineEndings().Split(new[] { '\n' }); lock (_lock)
foreach (var (_, _, last, part) in parts.Enumerate())
{ {
if (!string.IsNullOrEmpty(part)) var builder = new StringBuilder();
foreach (var segment in segments)
{ {
_out.Write(_ansiBuilder.GetAnsi(part, segment.Style)); if (segment.IsControlCode)
{
builder.Append(segment.Text);
continue;
}
var parts = segment.Text.NormalizeNewLines().Split(new[] { '\n' });
foreach (var (_, _, last, part) in parts.Enumerate())
{
if (!string.IsNullOrEmpty(part))
{
builder.Append(_ansiBuilder.GetAnsi(part, segment.Style));
}
if (!last)
{
builder.Append(Environment.NewLine);
}
}
} }
if (!last) if (builder.Length > 0)
{ {
_out.Write(Environment.NewLine); _out.Write(builder.ToString());
_out.Flush();
} }
} }
} }

View File

@@ -50,12 +50,18 @@ namespace Spectre.Console.Internal
} }
} }
var supportsInteraction = settings.Interactive == InteractionSupport.Yes;
if (settings.Interactive == InteractionSupport.Detect)
{
supportsInteraction = InteractivityDetector.IsInteractive();
}
var colorSystem = settings.ColorSystem == ColorSystemSupport.Detect var colorSystem = settings.ColorSystem == ColorSystemSupport.Detect
? ColorSystemDetector.Detect(supportsAnsi) ? ColorSystemDetector.Detect(supportsAnsi)
: (ColorSystem)settings.ColorSystem; : (ColorSystem)settings.ColorSystem;
// Get the capabilities // Get the capabilities
var capabilities = new Capabilities(supportsAnsi, colorSystem, legacyConsole); var capabilities = new Capabilities(supportsAnsi, colorSystem, legacyConsole, supportsInteraction);
// Create the renderer // Create the renderer
if (supportsAnsi) if (supportsAnsi)

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
@@ -14,6 +15,7 @@ namespace Spectre.Console.Internal
public Capabilities Capabilities { get; } public Capabilities Capabilities { get; }
public Encoding Encoding { get; } public Encoding Encoding { get; }
public RenderPipeline Pipeline { get; }
public IAnsiConsoleCursor Cursor => _cursor; public IAnsiConsoleCursor Cursor => _cursor;
public IAnsiConsoleInput Input => _input; public IAnsiConsoleInput Input => _input;
@@ -43,8 +45,9 @@ namespace Spectre.Console.Internal
System.Console.SetOut(@out ?? throw new ArgumentNullException(nameof(@out))); System.Console.SetOut(@out ?? throw new ArgumentNullException(nameof(@out)));
} }
Encoding = System.Console.OutputEncoding;
Capabilities = capabilities; Capabilities = capabilities;
Encoding = System.Console.OutputEncoding;
Pipeline = new RenderPipeline();
} }
public void Clear(bool home) public void Clear(bool home)
@@ -60,14 +63,22 @@ namespace Spectre.Console.Internal
} }
} }
public void Write(Segment segment) public void Write(IEnumerable<Segment> segments)
{ {
if (_lastStyle?.Equals(segment.Style) != true) foreach (var segment in segments)
{ {
SetStyle(segment.Style); if (segment.IsControlCode)
} {
continue;
}
System.Console.Write(segment.Text.NormalizeLineEndings(native: true)); if (_lastStyle?.Equals(segment.Style) != true)
{
SetStyle(segment.Style);
}
System.Console.Write(segment.Text.NormalizeNewLines(native: true));
}
} }
private void SetStyle(Style style) private void SetStyle(Style style)

View File

@@ -41,12 +41,15 @@ namespace Spectre.Console.Internal
{ {
var shortenTypes = (settings.Format & ExceptionFormats.ShortenTypes) != 0; var shortenTypes = (settings.Format & ExceptionFormats.ShortenTypes) != 0;
var type = Emphasize(ex.Type, new[] { '.' }, settings.Style.Exception, shortenTypes, settings); var type = Emphasize(ex.Type, new[] { '.' }, settings.Style.Exception, shortenTypes, settings);
var message = $"[{settings.Style.Message.ToMarkup()}]{ex.Message.EscapeMarkup()}[/]"; var message = $"[{settings.Style.Message.ToMarkup()}]{ex.Message.EscapeMarkup()}[/]";
return new Markup(string.Concat(type, ": ", message)); return new Markup(string.Concat(type, ": ", message));
} }
private static Grid GetStackFrames(ExceptionInfo ex, ExceptionSettings settings) private static Grid GetStackFrames(ExceptionInfo ex, ExceptionSettings settings)
{ {
var styles = settings.Style;
var grid = new Grid(); var grid = new Grid();
grid.AddColumn(new GridColumn().PadLeft(2).PadRight(0).NoWrap()); grid.AddColumn(new GridColumn().PadLeft(2).PadRight(0).NoWrap());
grid.AddColumn(new GridColumn().PadLeft(1).PadRight(0)); grid.AddColumn(new GridColumn().PadLeft(1).PadRight(0));
@@ -66,14 +69,16 @@ namespace Spectre.Console.Internal
// Method // Method
var shortenMethods = (settings.Format & ExceptionFormats.ShortenMethods) != 0; var shortenMethods = (settings.Format & ExceptionFormats.ShortenMethods) != 0;
builder.Append(Emphasize(frame.Method, new[] { '.' }, settings.Style.Method, shortenMethods, settings)); builder.Append(Emphasize(frame.Method, new[] { '.' }, styles.Method, shortenMethods, settings));
builder.Append('[').Append(settings.Style.Parenthesis.ToMarkup()).Append(']').Append('(').Append("[/]"); builder.AppendWithStyle(styles.Parenthesis, "(");
AppendParameters(builder, frame, settings); AppendParameters(builder, frame, settings);
builder.Append('[').Append(settings.Style.Parenthesis.ToMarkup()).Append(']').Append(')').Append("[/]"); builder.AppendWithStyle(styles.Parenthesis, ")");
if (frame.Path != null) if (frame.Path != null)
{ {
builder.Append(" [").Append(settings.Style.Dimmed.ToMarkup()).Append("]in[/] "); builder.Append(' ');
builder.AppendWithStyle(styles.Dimmed, "in");
builder.Append(' ');
// Path // Path
AppendPath(builder, frame, settings); AppendPath(builder, frame, settings);
@@ -81,13 +86,13 @@ namespace Spectre.Console.Internal
// Line number // Line number
if (frame.LineNumber != null) if (frame.LineNumber != null)
{ {
builder.Append('[').Append(settings.Style.Dimmed.ToMarkup()).Append("]:[/]"); builder.AppendWithStyle(styles.Dimmed, ":");
builder.Append('[').Append(settings.Style.LineNumber.ToMarkup()).Append(']').Append(frame.LineNumber).Append("[/]"); builder.AppendWithStyle(styles.LineNumber, frame.LineNumber);
} }
} }
grid.AddRow( grid.AddRow(
$"[{settings.Style.Dimmed.ToMarkup()}]at[/]", $"[{styles.Dimmed.ToMarkup()}]at[/]",
builder.ToString()); builder.ToString());
} }
@@ -98,7 +103,7 @@ namespace Spectre.Console.Internal
{ {
var typeColor = settings.Style.ParameterType.ToMarkup(); var typeColor = settings.Style.ParameterType.ToMarkup();
var nameColor = settings.Style.ParameterName.ToMarkup(); var nameColor = settings.Style.ParameterName.ToMarkup();
var parameters = frame.Parameters.Select(x => $"[{typeColor}]{x.Type.EscapeMarkup()}[/] [{nameColor}]{x.Name}[/]"); var parameters = frame.Parameters.Select(x => $"[{typeColor}]{x.Type.EscapeMarkup()}[/] [{nameColor}]{x.Name.EscapeMarkup()}[/]");
builder.Append(string.Join(", ", parameters)); builder.Append(string.Join(", ", parameters));
} }
@@ -146,16 +151,18 @@ namespace Spectre.Console.Internal
{ {
if (!compact) if (!compact)
{ {
builder.Append('[').Append(settings.Style.NonEmphasized.ToMarkup()).Append(']') builder.AppendWithStyle(
.Append(type, 0, index + 1).Append("[/]"); settings.Style.NonEmphasized,
type.Substring(0, index + 1).EscapeMarkup());
} }
builder.Append('[').Append(color.ToMarkup()).Append(']') builder.AppendWithStyle(
.Append(type, index + 1, type.Length - index - 1).Append("[/]"); color,
type.Substring(index + 1, type.Length - index - 1).EscapeMarkup());
} }
else else
{ {
builder.Append(type); builder.Append(type.EscapeMarkup());
} }
return builder.ToString(); return builder.ToString();

View File

@@ -15,6 +15,11 @@ namespace Spectre.Console.Internal
foreach (var (_, first, _, segment) in segments.Enumerate()) foreach (var (_, first, _, segment) in segments.Enumerate())
{ {
if (segment.IsControlCode)
{
continue;
}
if (segment.Text == "\n" && !first) if (segment.Text == "\n" && !first)
{ {
builder.Append('\n'); builder.Append('\n');

View File

@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
namespace Spectre.Console.Internal
{
internal static class InteractivityDetector
{
private static readonly Dictionary<string, Func<string, bool>> _environmentVariables;
static InteractivityDetector()
{
_environmentVariables = new Dictionary<string, Func<string, bool>>
{
{ "APPVEYOR", v => !string.IsNullOrWhiteSpace(v) },
{ "bamboo_buildNumber", v => !string.IsNullOrWhiteSpace(v) },
{ "BITBUCKET_REPO_OWNER", v => !string.IsNullOrWhiteSpace(v) },
{ "BITBUCKET_REPO_SLUG", v => !string.IsNullOrWhiteSpace(v) },
{ "BITBUCKET_COMMIT", v => !string.IsNullOrWhiteSpace(v) },
{ "BITRISE_BUILD_URL", v => !string.IsNullOrWhiteSpace(v) },
{ "ContinuaCI.Version", v => !string.IsNullOrWhiteSpace(v) },
{ "CI_SERVER", v => v.Equals("yes", StringComparison.OrdinalIgnoreCase) }, // GitLab
{ "GITHUB_ACTIONS", v => v.Equals("true", StringComparison.OrdinalIgnoreCase) },
{ "GO_SERVER_URL", v => !string.IsNullOrWhiteSpace(v) },
{ "JENKINS_URL", v => !string.IsNullOrWhiteSpace(v) },
{ "BuildRunner", v => v.Equals("MyGet", StringComparison.OrdinalIgnoreCase) },
{ "TEAMCITY_VERSION", v => !string.IsNullOrWhiteSpace(v) },
{ "TF_BUILD", v => !string.IsNullOrWhiteSpace(v) }, // TFS and Azure
{ "TRAVIS", v => !string.IsNullOrWhiteSpace(v) },
};
}
public static bool IsInteractive()
{
if (!Environment.UserInteractive)
{
return false;
}
foreach (var variable in _environmentVariables)
{
var func = variable.Value;
var value = Environment.GetEnvironmentVariable(variable.Key);
if (!string.IsNullOrWhiteSpace(value) && variable.Value(value))
{
return false;
}
}
return true;
}
}
}

View File

@@ -24,7 +24,7 @@ namespace Spectre.Console.Internal
using (var reader = new StreamReader(stream)) using (var reader = new StreamReader(stream))
{ {
return reader.ReadToEnd().NormalizeLineEndings(); return reader.ReadToEnd().NormalizeNewLines();
} }
} }
} }

View File

@@ -12,6 +12,11 @@ namespace Spectre.Console.Internal
foreach (var segment in Segment.Merge(segments)) foreach (var segment in Segment.Merge(segments))
{ {
if (segment.IsControlCode)
{
continue;
}
builder.Append(segment.Text); builder.Append(segment.Text);
} }

View File

@@ -0,0 +1,32 @@
using System;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// A column showing task progress in percentage.
/// </summary>
public sealed class PercentageColumn : ProgressColumn
{
/// <inheritdoc/>
protected internal override int? ColumnWidth => 4;
/// <summary>
/// Gets or sets the style for a non-complete task.
/// </summary>
public Style Style { get; set; } = Style.Plain;
/// <summary>
/// Gets or sets the style for a completed task.
/// </summary>
public Style CompletedStyle { get; set; } = new Style(foreground: Color.Green);
/// <inheritdoc/>
public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
{
var percentage = (int)task.Percentage;
var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain;
return new Text($"{percentage}%", style).RightAligned();
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// A column showing task progress as a progress bar.
/// </summary>
public sealed class ProgressBarColumn : ProgressColumn
{
/// <summary>
/// Gets or sets the width of the column.
/// </summary>
public int? Width { get; set; } = 40;
/// <summary>
/// Gets or sets the style of completed portions of the progress bar.
/// </summary>
public Style CompletedStyle { get; set; } = new Style(foreground: Color.Yellow);
/// <summary>
/// Gets or sets the style of a finished progress bar.
/// </summary>
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);
/// <summary>
/// Gets or sets the style of remaining portions of the progress bar.
/// </summary>
public Style RemainingStyle { get; set; } = new Style(foreground: Color.Grey);
/// <inheritdoc/>
public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
{
return new ProgressBar
{
MaxValue = task.MaxValue,
Value = task.Value,
Width = Width,
CompletedStyle = CompletedStyle,
FinishedStyle = FinishedStyle,
RemainingStyle = RemainingStyle,
};
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// A column showing the remaining time of a task.
/// </summary>
public sealed class RemainingTimeColumn : ProgressColumn
{
/// <inheritdoc/>
protected internal override int? ColumnWidth => 7;
/// <inheritdoc/>
protected internal override bool NoWrap => true;
/// <summary>
/// Gets or sets the style of the remaining time text.
/// </summary>
public Style Style { get; set; } = new Style(foreground: Color.Blue);
/// <inheritdoc/>
public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
{
var remaining = task.RemainingTime;
if (remaining == null)
{
return new Markup("-:--:--");
}
return new Text($"{remaining.Value:h\\:mm\\:ss}", Style ?? Style.Plain);
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// A column showing a spinner.
/// </summary>
public sealed class SpinnerColumn : ProgressColumn
{
private const string ACCUMULATED = "SPINNER_ACCUMULATED";
private const string INDEX = "SPINNER_INDEX";
private readonly string _ansiSequence = "⣷⣯⣟⡿⢿⣻⣽⣾";
private readonly string _asciiSequence = "-\\|/-\\|/";
/// <inheritdoc/>
protected internal override int? ColumnWidth => 1;
/// <inheritdoc/>
protected internal override bool NoWrap => true;
/// <summary>
/// Gets or sets the style of the spinner.
/// </summary>
public Style Style { get; set; } = new Style(foreground: Color.Yellow);
/// <inheritdoc/>
public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
{
if (!task.IsStarted || task.IsFinished)
{
return new Markup(" ");
}
var accumulated = task.State.Update<double>(ACCUMULATED, acc => acc + deltaTime.TotalMilliseconds);
if (accumulated >= 100)
{
task.State.Update<double>(ACCUMULATED, _ => 0);
task.State.Update<int>(INDEX, index => index + 1);
}
var useAscii = context.LegacyConsole || !context.Unicode;
var sequence = useAscii ? _asciiSequence : _ansiSequence;
var index = task.State.Get<int>(INDEX);
return new Markup(sequence[index % sequence.Length].ToString(), Style ?? Style.Plain);
}
}
}

View File

@@ -0,0 +1,21 @@
using System;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// A column showing the task description.
/// </summary>
public sealed class TaskDescriptionColumn : ProgressColumn
{
/// <inheritdoc/>
protected internal override bool NoWrap => true;
/// <inheritdoc/>
public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
{
var text = task.Description?.RemoveNewLines()?.Trim();
return new Markup(text ?? string.Empty).Overflow(Overflow.Ellipsis).RightAligned();
}
}
}

View File

@@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// Represents a task list.
/// </summary>
public sealed class Progress
{
private readonly IAnsiConsole _console;
/// <summary>
/// Gets or sets a value indicating whether or not task list should auto refresh.
/// Defaults to <c>true</c>.
/// </summary>
public bool AutoRefresh { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not the task list should
/// be cleared once it completes.
/// Defaults to <c>false</c>.
/// </summary>
public bool AutoClear { get; set; }
internal List<ProgressColumn> Columns { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Progress"/> class.
/// </summary>
/// <param name="console">The console to render to.</param>
public Progress(IAnsiConsole console)
{
_console = console ?? throw new ArgumentNullException(nameof(console));
// Initialize with default columns
Columns = new List<ProgressColumn>
{
new TaskDescriptionColumn(),
new ProgressBarColumn(),
new PercentageColumn(),
};
}
/// <summary>
/// Starts the progress task list.
/// </summary>
/// <param name="action">The action to execute.</param>
public void Start(Action<ProgressContext> action)
{
var task = StartAsync(ctx =>
{
action(ctx);
return Task.CompletedTask;
});
task.GetAwaiter().GetResult();
}
/// <summary>
/// Starts the progress task list.
/// </summary>
/// <param name="action">The action to execute.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task StartAsync(Func<ProgressContext, Task> action)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
var renderer = CreateRenderer();
renderer.Started();
try
{
using (new RenderHookScope(_console, renderer))
{
var context = new ProgressContext(_console, renderer);
if (AutoRefresh)
{
using (var thread = new ProgressRefreshThread(context, renderer.RefreshRate))
{
await action(context).ConfigureAwait(false);
}
}
else
{
await action(context).ConfigureAwait(false);
}
context.Refresh();
}
}
finally
{
renderer.Completed(AutoClear);
}
}
private ProgressRenderer CreateRenderer()
{
var caps = _console.Capabilities;
var interactive = caps.SupportsInteraction && caps.SupportsAnsi;
if (interactive)
{
var columns = new List<ProgressColumn>(Columns);
return new InteractiveProgressRenderer(_console, columns);
}
else
{
return new NonInteractiveProgressRenderer();
}
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// Represents a progress column.
/// </summary>
public abstract class ProgressColumn
{
/// <summary>
/// Gets a value indicating whether or not content should not wrap.
/// </summary>
protected internal virtual bool NoWrap { get; }
/// <summary>
/// Gets the requested column width for the column.
/// </summary>
protected internal virtual int? ColumnWidth { get; }
/// <summary>
/// Gets a renderable representing the column.
/// </summary>
/// <param name="context">The render context.</param>
/// <param name="task">The task.</param>
/// <param name="deltaTime">The elapsed time since last call.</param>
/// <returns>A renderable representing the column.</returns>
public abstract IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime);
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Internal;
namespace Spectre.Console
{
/// <summary>
/// Represents a context that can be used to interact with a <see cref="Progress"/>.
/// </summary>
public sealed class ProgressContext
{
private readonly List<ProgressTask> _tasks;
private readonly object _taskLock;
private readonly IAnsiConsole _console;
private readonly ProgressRenderer _renderer;
private int _taskId;
/// <summary>
/// Gets a value indicating whether or not all tasks have completed.
/// </summary>
public bool IsFinished => _tasks.All(task => task.IsFinished);
internal ProgressContext(IAnsiConsole console, ProgressRenderer renderer)
{
_tasks = new List<ProgressTask>();
_taskLock = new object();
_console = console ?? throw new ArgumentNullException(nameof(console));
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
}
/// <summary>
/// Adds a task.
/// </summary>
/// <param name="description">The task description.</param>
/// <param name="settings">The task settings.</param>
/// <returns>The task's ID.</returns>
public ProgressTask AddTask(string description, ProgressTaskSettings? settings = null)
{
lock (_taskLock)
{
settings ??= new ProgressTaskSettings();
var task = new ProgressTask(_taskId++, description, settings.MaxValue, settings.AutoStart);
_tasks.Add(task);
return task;
}
}
/// <summary>
/// Refreshes the current progress.
/// </summary>
public void Refresh()
{
_renderer.Update(this);
_console.Render(new ControlSequence(string.Empty));
}
internal void EnumerateTasks(Action<ProgressTask> action)
{
lock (_taskLock)
{
foreach (var task in _tasks)
{
action(task);
}
}
}
}
}

View File

@@ -0,0 +1,58 @@
using System;
using System.Threading;
namespace Spectre.Console.Internal
{
internal sealed class ProgressRefreshThread : IDisposable
{
private readonly ProgressContext _context;
private readonly TimeSpan _refreshRate;
private readonly ManualResetEvent _running;
private readonly ManualResetEvent _stopped;
private readonly Thread? _thread;
public ProgressRefreshThread(ProgressContext context, TimeSpan refreshRate)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_refreshRate = refreshRate;
_running = new ManualResetEvent(false);
_stopped = new ManualResetEvent(false);
_thread = new Thread(Run);
_thread.IsBackground = true;
_thread.Start();
}
public void Dispose()
{
if (_thread == null || !_running.WaitOne(0))
{
return;
}
_stopped.Set();
_thread.Join();
_stopped.Dispose();
_running.Dispose();
}
private void Run()
{
_running.Set();
try
{
while (!_stopped.WaitOne(_refreshRate))
{
_context.Refresh();
}
}
finally
{
_stopped.Reset();
_running.Reset();
}
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console.Internal
{
internal abstract class ProgressRenderer : IRenderHook
{
public abstract TimeSpan RefreshRate { get; }
public virtual void Started()
{
}
public virtual void Completed(bool clear)
{
}
public abstract void Update(ProgressContext context);
public abstract IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables);
}
}

View File

@@ -0,0 +1,16 @@
using System;
namespace Spectre.Console.Internal
{
internal readonly struct ProgressSample
{
public double Value { get; }
public DateTime Timestamp { get; }
public ProgressSample(DateTime timestamp, double value)
{
Timestamp = timestamp;
Value = value;
}
}
}

View File

@@ -0,0 +1,267 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Internal;
namespace Spectre.Console
{
/// <summary>
/// Represents a progress task.
/// </summary>
public sealed class ProgressTask
{
private readonly List<ProgressSample> _samples;
private readonly object _lock;
private double _maxValue;
private string _description;
/// <summary>
/// Gets the task ID.
/// </summary>
public int Id { get; }
/// <summary>
/// Gets or sets the task description.
/// </summary>
public string Description
{
get => _description;
set => Update(description: value);
}
/// <summary>
/// Gets or sets the max value of the task.
/// </summary>
public double MaxValue
{
get => _maxValue;
set => Update(maxValue: value);
}
/// <summary>
/// Gets the value of the task.
/// </summary>
public double Value { get; private set; }
/// <summary>
/// Gets the start time of the task.
/// </summary>
public DateTime? StartTime { get; private set; }
/// <summary>
/// Gets the stop time of the task.
/// </summary>
public DateTime? StopTime { get; private set; }
/// <summary>
/// Gets the task state.
/// </summary>
public ProgressTaskState State { get; }
/// <summary>
/// Gets a value indicating whether or not the task has started.
/// </summary>
public bool IsStarted => StartTime != null;
/// <summary>
/// Gets a value indicating whether or not the task has finished.
/// </summary>
public bool IsFinished => Value >= MaxValue;
/// <summary>
/// Gets the percentage done of the task.
/// </summary>
public double Percentage => GetPercentage();
/// <summary>
/// Gets the speed measured in steps/second.
/// </summary>
public double? Speed => GetSpeed();
/// <summary>
/// Gets the elapsed time.
/// </summary>
public TimeSpan? ElapsedTime => GetElapsedTime();
/// <summary>
/// Gets the remaining time.
/// </summary>
public TimeSpan? RemainingTime => GetRemainingTime();
internal ProgressTask(int id, string description, double maxValue, bool autoStart)
{
_samples = new List<ProgressSample>();
_lock = new object();
_maxValue = maxValue;
_description = description?.RemoveNewLines()?.Trim() ?? throw new ArgumentNullException(nameof(description));
if (string.IsNullOrWhiteSpace(_description))
{
throw new ArgumentException("Task name cannot be empty", nameof(description));
}
Id = id;
State = new ProgressTaskState();
Value = 0;
StartTime = autoStart ? DateTime.Now : null;
}
/// <summary>
/// Starts the task.
/// </summary>
public void StartTask()
{
lock (_lock)
{
StartTime = DateTime.Now;
StopTime = null;
}
}
/// <summary>
/// Stops the task.
/// </summary>
public void StopTask()
{
lock (_lock)
{
var now = DateTime.Now;
if (StartTime == null)
{
StartTime = now;
}
StopTime = now;
}
}
/// <summary>
/// Increments the task's value.
/// </summary>
/// <param name="value">The value to increment with.</param>
public void Increment(double value)
{
Update(increment: value);
}
private void Update(
string? description = null,
double? maxValue = null,
double? increment = null)
{
lock (_lock)
{
var startValue = Value;
if (description != null)
{
description = description?.RemoveNewLines()?.Trim();
if (string.IsNullOrWhiteSpace(description))
{
throw new InvalidOperationException("Task name cannot be empty.");
}
_description = description;
}
if (maxValue != null)
{
_maxValue += maxValue.Value;
}
if (increment != null)
{
Value += increment.Value;
}
var timestamp = DateTime.Now;
var threshold = timestamp - TimeSpan.FromSeconds(30);
// Remove samples that's too old
while (_samples.Count > 0 && _samples[0].Timestamp < threshold)
{
_samples.RemoveAt(0);
}
// Keep maximum of 1000 samples
while (_samples.Count > 1000)
{
_samples.RemoveAt(0);
}
_samples.Add(new ProgressSample(timestamp, Value - startValue));
}
}
private double GetPercentage()
{
var percentage = (Value / MaxValue) * 100;
percentage = Math.Min(100, Math.Max(0, percentage));
return percentage;
}
private double? GetSpeed()
{
lock (_lock)
{
if (StartTime == null)
{
return null;
}
if (_samples.Count == 0)
{
return null;
}
var totalTime = _samples.Last().Timestamp - _samples[0].Timestamp;
if (totalTime == TimeSpan.Zero)
{
return null;
}
var totalCompleted = _samples.Sum(x => x.Value);
return totalCompleted / totalTime.TotalSeconds;
}
}
private TimeSpan? GetElapsedTime()
{
lock (_lock)
{
if (StartTime == null)
{
return null;
}
if (StopTime != null)
{
return StopTime - StartTime;
}
return DateTime.Now - StartTime;
}
}
private TimeSpan? GetRemainingTime()
{
lock (_lock)
{
if (IsFinished)
{
return TimeSpan.Zero;
}
var speed = GetSpeed();
if (speed == null)
{
return null;
}
var estimate = (MaxValue - Value) / speed.Value;
return TimeSpan.FromSeconds(estimate);
}
}
}
}

View File

@@ -0,0 +1,20 @@
namespace Spectre.Console
{
/// <summary>
/// Represents settings for a progress task.
/// </summary>
public sealed class ProgressTaskSettings
{
/// <summary>
/// Gets or sets the task's max value.
/// Defaults to <c>100</c>.
/// </summary>
public double MaxValue { get; set; } = 100;
/// <summary>
/// Gets or sets a value indicating whether or not the task
/// will be auto started. Defaults to <c>true</c>.
/// </summary>
public bool AutoStart { get; set; } = true;
}
}

View File

@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
namespace Spectre.Console
{
/// <summary>
/// Represents progress task state.
/// </summary>
public sealed class ProgressTaskState
{
private readonly Dictionary<string, object> _state;
private readonly object _lock;
/// <summary>
/// Initializes a new instance of the <see cref="ProgressTaskState"/> class.
/// </summary>
public ProgressTaskState()
{
_state = new Dictionary<string, object>();
_lock = new object();
}
/// <summary>
/// Gets the state value for the specified key.
/// </summary>
/// <typeparam name="T">The state value type.</typeparam>
/// <param name="key">The state key.</param>
/// <returns>The value for the specified key.</returns>
public T Get<T>(string key)
where T : struct
{
lock (_lock)
{
if (!_state.TryGetValue(key, out var value))
{
return default;
}
if (!(value is T))
{
throw new InvalidOperationException("State value is of the wrong type.");
}
return (T)value;
}
}
/// <summary>
/// Updates a task state value.
/// </summary>
/// <typeparam name="T">The state value type.</typeparam>
/// <param name="key">The key.</param>
/// <param name="func">The transformation function.</param>
/// <returns>The updated value.</returns>
public T Update<T>(string key, Func<T, T> func)
where T : struct
{
lock (_lock)
{
if (func is null)
{
throw new ArgumentNullException(nameof(func));
}
var old = default(T);
if (_state.TryGetValue(key, out var value))
{
if (!(value is T))
{
throw new InvalidOperationException("State value is of the wrong type.");
}
old = (T)value;
}
_state[key] = func(old);
return (T)_state[key];
}
}
}
}

View File

@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console.Internal
{
internal sealed class InteractiveProgressRenderer : ProgressRenderer
{
private readonly IAnsiConsole _console;
private readonly List<ProgressColumn> _columns;
private readonly LiveRenderable _live;
private readonly object _lock;
private readonly Stopwatch _stopwatch;
private TimeSpan _lastUpdate;
public override TimeSpan RefreshRate => TimeSpan.FromMilliseconds(100);
public InteractiveProgressRenderer(IAnsiConsole console, List<ProgressColumn> columns)
{
_console = console ?? throw new ArgumentNullException(nameof(console));
_columns = columns ?? throw new ArgumentNullException(nameof(columns));
_live = new LiveRenderable();
_lock = new object();
_stopwatch = new Stopwatch();
_lastUpdate = TimeSpan.Zero;
}
public override void Started()
{
_console.Cursor.Hide();
}
public override void Completed(bool clear)
{
lock (_lock)
{
if (clear)
{
_console.Render(_live.RestoreCursor());
}
else
{
_console.WriteLine();
}
_console.Cursor.Show();
}
}
public override void Update(ProgressContext context)
{
lock (_lock)
{
if (!_stopwatch.IsRunning)
{
_stopwatch.Start();
}
var delta = _stopwatch.Elapsed - _lastUpdate;
_lastUpdate = _stopwatch.Elapsed;
var grid = new Grid();
for (var columnIndex = 0; columnIndex < _columns.Count; columnIndex++)
{
var column = new GridColumn().PadRight(1);
if (_columns[columnIndex].ColumnWidth != null)
{
column.Width = _columns[columnIndex].ColumnWidth;
}
if (_columns[columnIndex].NoWrap)
{
column.NoWrap();
}
// Last column?
if (columnIndex == _columns.Count - 1)
{
column.PadRight(0);
}
grid.AddColumn(column);
}
// Add rows
var renderContext = new RenderContext(_console.Encoding, _console.Capabilities.LegacyConsole);
context.EnumerateTasks(task =>
{
var columns = _columns.Select(column => column.Render(renderContext, task, delta));
grid.AddRow(columns.ToArray());
});
_live.SetRenderable(new Padder(grid, new Padding(0, 1)));
}
}
public override IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables)
{
lock (_lock)
{
yield return _live.PositionCursor();
foreach (var renderable in renderables)
{
yield return renderable;
}
yield return _live;
}
}
}
}

View File

@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console.Internal
{
internal sealed class NonInteractiveProgressRenderer : ProgressRenderer
{
private const double FirstMilestone = 25;
private static readonly double?[] _milestones = new double?[] { FirstMilestone, 50, 75, 95, 96, 97, 98, 99, 100 };
private readonly Dictionary<int, double> _taskMilestones;
private readonly object _lock;
private IRenderable? _renderable;
private DateTime _lastUpdate;
public override TimeSpan RefreshRate => TimeSpan.FromSeconds(1);
public NonInteractiveProgressRenderer()
{
_taskMilestones = new Dictionary<int, double>();
_lock = new object();
}
public override void Update(ProgressContext context)
{
lock (_lock)
{
var hasStartedTasks = false;
var updates = new List<(string, double)>();
context.EnumerateTasks(task =>
{
if (!task.IsStarted || task.IsFinished)
{
return;
}
hasStartedTasks = true;
if (TryAdvance(task.Id, task.Percentage))
{
updates.Add((task.Description, task.Percentage));
}
});
// Got started tasks but no updates for 30 seconds?
if (hasStartedTasks && updates.Count == 0 && (DateTime.Now - _lastUpdate) > TimeSpan.FromSeconds(30))
{
context.EnumerateTasks(task => updates.Add((task.Description, task.Percentage)));
}
if (updates.Count > 0)
{
_lastUpdate = DateTime.Now;
}
_renderable = BuildTaskGrid(updates);
}
}
public override IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables)
{
lock (_lock)
{
var result = new List<IRenderable>();
result.AddRange(renderables);
if (_renderable != null)
{
result.Add(_renderable);
}
_renderable = null;
return result;
}
}
private bool TryAdvance(int task, double percentage)
{
if (!_taskMilestones.TryGetValue(task, out var milestone))
{
_taskMilestones.Add(task, FirstMilestone);
return true;
}
if (percentage > milestone)
{
var nextMilestone = GetNextMilestone(percentage);
if (nextMilestone != null && _taskMilestones[task] != nextMilestone)
{
_taskMilestones[task] = nextMilestone.Value;
return true;
}
}
return false;
}
private static double? GetNextMilestone(double percentage)
{
return Array.Find(_milestones, p => p > percentage);
}
private static IRenderable? BuildTaskGrid(List<(string Name, double Percentage)> updates)
{
if (updates.Count > 0)
{
var renderables = new List<IRenderable>();
foreach (var (name, percentage) in updates)
{
renderables.Add(new Markup($"[blue]{name}[/]: {(int)percentage}%"));
}
return new Rows(renderables);
}
return null;
}
}
}

View File

@@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text; using System.Text;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
@@ -8,7 +10,8 @@ namespace Spectre.Console
/// <summary> /// <summary>
/// A console recorder used to record output from a console. /// A console recorder used to record output from a console.
/// </summary> /// </summary>
public sealed class Recorder : IAnsiConsole, IDisposable [SuppressMessage("Design", "CA1063:Implement IDisposable Correctly")]
public class Recorder : IAnsiConsole, IDisposable
{ {
private readonly IAnsiConsole _console; private readonly IAnsiConsole _console;
private readonly List<Segment> _recorded; private readonly List<Segment> _recorded;
@@ -31,6 +34,14 @@ namespace Spectre.Console
/// <inheritdoc/> /// <inheritdoc/>
public int Height => _console.Height; public int Height => _console.Height;
/// <inheritdoc/>
public RenderPipeline Pipeline => _console.Pipeline;
/// <summary>
/// Gets a list containing all recorded segments.
/// </summary>
protected List<Segment> Recorded => _recorded;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Recorder"/> class. /// Initializes a new instance of the <see cref="Recorder"/> class.
/// </summary> /// </summary>
@@ -42,6 +53,7 @@ namespace Spectre.Console
} }
/// <inheritdoc/> /// <inheritdoc/>
[SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize")]
public void Dispose() public void Dispose()
{ {
// Only used for scoping. // Only used for scoping.
@@ -54,20 +66,25 @@ namespace Spectre.Console
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Write(Segment segment) public void Write(IEnumerable<Segment> segments)
{ {
if (segment is null) if (segments is null)
{ {
throw new ArgumentNullException(nameof(segment)); throw new ArgumentNullException(nameof(segments));
} }
// Don't record control codes. Record(segments);
if (!segment.IsControlCode)
{
_recorded.Add(segment);
}
_console.Write(segment); _console.Write(segments);
}
/// <summary>
/// Records the specified segments.
/// </summary>
/// <param name="segments">The segments to be recorded.</param>
protected virtual void Record(IEnumerable<Segment> segments)
{
Recorded.AddRange(segments.Where(s => !s.IsControlCode));
} }
/// <summary> /// <summary>

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace Spectre.Console.Rendering
{
/// <summary>
/// Represents a render hook.
/// </summary>
public interface IRenderHook
{
/// <summary>
/// Processes the specified renderables.
/// </summary>
/// <param name="context">The render context.</param>
/// <param name="renderables">The renderables to process.</param>
/// <returns>The processed renderables.</returns>
IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables);
}
}

View File

@@ -0,0 +1,81 @@
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console.Rendering
{
internal sealed class LiveRenderable : Renderable
{
private readonly object _lock = new object();
private IRenderable? _renderable;
private int? _height;
public void SetRenderable(IRenderable renderable)
{
lock (_lock)
{
_renderable = renderable;
}
}
public IRenderable PositionCursor()
{
lock (_lock)
{
if (_height == null)
{
return new ControlSequence(string.Empty);
}
return new ControlSequence("\r" + "\u001b[1A".Repeat(_height.Value - 1));
}
}
public IRenderable RestoreCursor()
{
lock (_lock)
{
if (_height == null)
{
return new ControlSequence(string.Empty);
}
return new ControlSequence("\r\u001b[2K" + "\u001b[1A\u001b[2K".Repeat(_height.Value - 1));
}
}
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
lock (_lock)
{
if (_renderable != null)
{
var segments = _renderable.Render(context, maxWidth);
var lines = Segment.SplitLines(context, segments);
_height = lines.Count;
var result = new List<Segment>();
foreach (var (_, _, last, line) in lines.Enumerate())
{
foreach (var item in line)
{
result.Add(item);
}
if (!last)
{
result.Add(Segment.LineBreak);
}
}
return result;
}
_height = 0;
return Enumerable.Empty<Segment>();
}
}
}
}

View File

@@ -49,7 +49,7 @@ namespace Spectre.Console.Rendering
Encoding = encoding ?? throw new System.ArgumentNullException(nameof(encoding)); Encoding = encoding ?? throw new System.ArgumentNullException(nameof(encoding));
LegacyConsole = legacyConsole; LegacyConsole = legacyConsole;
Justification = justification; Justification = justification;
Unicode = Encoding == Encoding.UTF8 || Encoding == Encoding.Unicode; Unicode = Encoding.EncodingName.ContainsExact("Unicode");
SingleLine = singleLine; SingleLine = singleLine;
} }

View File

@@ -0,0 +1,31 @@
using System;
namespace Spectre.Console.Rendering
{
/// <summary>
/// Represents a render hook scope.
/// </summary>
public sealed class RenderHookScope : IDisposable
{
private readonly IAnsiConsole _console;
private readonly IRenderHook _hook;
/// <summary>
/// Initializes a new instance of the <see cref="RenderHookScope"/> class.
/// </summary>
/// <param name="console">The console to attach the render hook to.</param>
/// <param name="hook">The render hook.</param>
public RenderHookScope(IAnsiConsole console, IRenderHook hook)
{
_console = console ?? throw new ArgumentNullException(nameof(console));
_hook = hook ?? throw new ArgumentNullException(nameof(hook));
_console.Pipeline.Attach(_hook);
}
/// <inheritdoc/>
public void Dispose()
{
_console.Pipeline.Detach(_hook);
}
}
}

View File

@@ -0,0 +1,66 @@
using System.Collections.Generic;
namespace Spectre.Console.Rendering
{
/// <summary>
/// Represents the render pipeline.
/// </summary>
public sealed class RenderPipeline
{
private readonly List<IRenderHook> _hooks;
private readonly object _lock;
/// <summary>
/// Initializes a new instance of the <see cref="RenderPipeline"/> class.
/// </summary>
public RenderPipeline()
{
_hooks = new List<IRenderHook>();
_lock = new object();
}
/// <summary>
/// Attaches a new render hook onto the pipeline.
/// </summary>
/// <param name="hook">The render hook to attach.</param>
public void Attach(IRenderHook hook)
{
lock (_lock)
{
_hooks.Add(hook);
}
}
/// <summary>
/// Detaches a render hook from the pipeline.
/// </summary>
/// <param name="hook">The render hook to detach.</param>
public void Detach(IRenderHook hook)
{
lock (_lock)
{
_hooks.Remove(hook);
}
}
/// <summary>
/// Processes the specified renderables.
/// </summary>
/// <param name="context">The render context.</param>
/// <param name="renderables">The renderables to process.</param>
/// <returns>The processed renderables.</returns>
public IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables)
{
lock (_lock)
{
var current = renderables;
for (var index = _hooks.Count - 1; index >= 0; index--)
{
current = _hooks[index].Process(context, current);
}
return current;
}
}
}
}

View File

@@ -72,7 +72,7 @@ namespace Spectre.Console.Rendering
private Segment(string text, Style style, bool lineBreak, bool control) private Segment(string text, Style style, bool lineBreak, bool control)
{ {
Text = text?.NormalizeLineEndings() ?? throw new ArgumentNullException(nameof(text)); Text = text?.NormalizeNewLines() ?? throw new ArgumentNullException(nameof(text));
Style = style ?? throw new ArgumentNullException(nameof(style)); Style = style ?? throw new ArgumentNullException(nameof(style));
IsLineBreak = lineBreak; IsLineBreak = lineBreak;
IsWhiteSpace = string.IsNullOrWhiteSpace(text); IsWhiteSpace = string.IsNullOrWhiteSpace(text);
@@ -102,6 +102,11 @@ namespace Spectre.Console.Rendering
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (IsControlCode)
{
return 0;
}
return Text.CellLength(context); return Text.CellLength(context);
} }
@@ -477,16 +482,22 @@ namespace Spectre.Console.Rendering
continue; continue;
} }
// Both control codes?
if (segment.IsControlCode && previous.IsControlCode)
{
previous = Control(previous.Text + segment.Text);
continue;
}
// Same style? // Same style?
if (previous.Style.Equals(segment.Style) && !previous.IsLineBreak) if (previous.Style.Equals(segment.Style) && !previous.IsLineBreak && !previous.IsControlCode)
{ {
previous = new Segment(previous.Text + segment.Text, previous.Style); previous = new Segment(previous.Text + segment.Text, previous.Style);
continue;
} }
else
{ result.Add(previous);
result.Add(previous); previous = segment;
previous = segment;
}
} }
if (previous != null) if (previous != null)

View File

@@ -0,0 +1,151 @@
using System;
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// Represents a renderable canvas.
/// </summary>
public sealed class Canvas : Renderable
{
private readonly Color?[,] _pixels;
/// <summary>
/// Gets the width of the canvas.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the height of the canvas.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets or sets the render width of the canvas.
/// </summary>
public int? MaxWidth { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not
/// to scale the canvas when rendering.
/// </summary>
public bool Scale { get; set; } = true;
/// <summary>
/// Gets or sets the pixel width.
/// </summary>
public int PixelWidth { get; set; } = 2;
/// <summary>
/// Initializes a new instance of the <see cref="Canvas"/> class.
/// </summary>
/// <param name="width">The canvas width.</param>
/// <param name="height">The canvas height.</param>
public Canvas(int width, int height)
{
Width = width;
Height = height;
_pixels = new Color?[Width, Height];
}
/// <summary>
/// Sets a pixel with the specified color in the canvas at the specified location.
/// </summary>
/// <param name="x">The X coordinate for the pixel.</param>
/// <param name="y">The Y coordinate for the pixel.</param>
/// <param name="color">The pixel color.</param>
public void SetPixel(int x, int y, Color color)
{
_pixels[x, y] = color;
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
if (PixelWidth < 0)
{
throw new InvalidOperationException("Pixel width must be greater than zero.");
}
var width = MaxWidth ?? Width;
if (maxWidth < width * PixelWidth)
{
return new Measurement(maxWidth, maxWidth);
}
return new Measurement(width * PixelWidth, width * PixelWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
if (PixelWidth < 0)
{
throw new InvalidOperationException("Pixel width must be greater than zero.");
}
var pixels = _pixels;
var pixel = new string(' ', PixelWidth);
var width = Width;
var height = Height;
// Got a max width?
if (MaxWidth != null)
{
height = (int)(height * ((float)MaxWidth.Value) / Width);
width = MaxWidth.Value;
}
// Exceed the max width when we take pixel width into account?
if (width * PixelWidth > maxWidth)
{
height = (int)(height * (maxWidth / (float)(width * PixelWidth)));
width = maxWidth / PixelWidth;
}
// Need to rescale the pixel buffer?
if (Scale && (width != Width || height != Height))
{
pixels = ScaleDown(width, height);
}
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
var color = pixels[x, y];
if (color != null)
{
yield return new Segment(pixel, new Style(background: color));
}
else
{
yield return new Segment(pixel);
}
}
yield return Segment.LineBreak;
}
}
private Color?[,] ScaleDown(int newWidth, int newHeight)
{
var buffer = new Color?[newWidth, newHeight];
var xRatio = ((Width << 16) / newWidth) + 1;
var yRatio = ((Height << 16) / newHeight) + 1;
for (var i = 0; i < newHeight; i++)
{
for (var j = 0; j < newWidth; j++)
{
buffer[j, i] = _pixels[(j * xRatio) >> 16, (i * yRatio) >> 16];
}
}
return buffer;
}
}
}

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
internal sealed class ControlSequence : Renderable
{
private readonly Segment _segment;
public ControlSequence(string control)
{
_segment = Segment.Control(control);
}
protected override Measurement Measure(RenderContext context, int maxWidth)
{
return new Measurement(0, 0);
}
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
yield return _segment;
}
}
}

View File

@@ -58,6 +58,11 @@ namespace Spectre.Console
var width = childWidth + paddingWidth; var width = childWidth + paddingWidth;
var result = new List<Segment>(); var result = new List<Segment>();
if (width > maxWidth)
{
width = maxWidth;
}
// Top padding // Top padding
for (var i = 0; i < Padding.GetTopSafe(); i++) for (var i = 0; i < Padding.GetTopSafe(); i++)
{ {

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
internal sealed class ProgressBar : Renderable
{
public double Value { get; set; }
public double MaxValue { get; set; } = 100;
public int? Width { get; set; }
public Style CompletedStyle { get; set; } = new Style(foreground: Color.Yellow);
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);
public Style RemainingStyle { get; set; } = new Style(foreground: Color.Grey);
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
return new Measurement(4, width);
}
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var width = Math.Min(Width ?? maxWidth, maxWidth);
var completed = Math.Min(MaxValue, Math.Max(0, Value));
var token = !context.Unicode || context.LegacyConsole ? '-' : '━';
var style = completed >= MaxValue ? FinishedStyle : CompletedStyle;
var bars = Math.Max(0, (int)(width * (completed / MaxValue)));
yield return new Segment(new string(token, bars), style);
if (bars < width)
{
yield return new Segment(new string(token, width - bars), RemainingStyle);
}
}
}
}

View File

@@ -95,7 +95,7 @@ namespace Spectre.Console
private IEnumerable<Segment> GetTitleSegments(RenderContext context, string title, int width) private IEnumerable<Segment> GetTitleSegments(RenderContext context, string title, int width)
{ {
title = title.NormalizeLineEndings().ReplaceExact("\n", " ").Trim(); title = title.NormalizeNewLines().ReplaceExact("\n", " ").Trim();
var markup = new Markup(title, Style); var markup = new Markup(title, Style);
return ((IRenderable)markup).Render(context.WithSingleLine(), width); return ((IRenderable)markup).Render(context.WithSingleLine(), width);
} }

View File

@@ -16,8 +16,8 @@ namespace Spectre.Console
private readonly List<TableColumn> _columns; private readonly List<TableColumn> _columns;
private readonly List<TableRow> _rows; private readonly List<TableRow> _rows;
private static Style _defaultHeadingStyle = new Style(Color.Silver); private static readonly Style _defaultHeadingStyle = new Style(Color.Silver);
private static Style _defaultCaptionStyle = new Style(Color.Grey); private static readonly Style _defaultCaptionStyle = new Style(Color.Grey);
/// <summary> /// <summary>
/// Gets the table columns. /// Gets the table columns.
@@ -447,12 +447,10 @@ namespace Spectre.Console
private (int Min, int Max) MeasureColumn(TableColumn column, RenderContext options, int maxWidth) private (int Min, int Max) MeasureColumn(TableColumn column, RenderContext options, int maxWidth)
{ {
var padding = column.Padding?.GetWidth() ?? 0;
// Predetermined width? // Predetermined width?
if (column.Width != null) if (column.Width != null)
{ {
return (column.Width.Value + padding, column.Width.Value + padding); return (column.Width.Value, column.Width.Value);
} }
var columnIndex = _columns.IndexOf(column); var columnIndex = _columns.IndexOf(column);
@@ -474,6 +472,8 @@ namespace Spectre.Console
maxWidths.Add(rowMeasure.Max); maxWidths.Add(rowMeasure.Max);
} }
var padding = column.Padding?.GetWidth() ?? 0;
return (minWidths.Count > 0 ? minWidths.Max() : padding, return (minWidths.Count > 0 ? minWidths.Max() : padding,
maxWidths.Count > 0 ? maxWidths.Max() : maxWidth); maxWidths.Count > 0 ? maxWidths.Max() : maxWidth);
} }