Compare commits

..

5 Commits

Author SHA1 Message Date
Patrik Svensson
ae6d2c63a3 Add column support
Adds support for rendering arbitrary data into columns.

Closes #67
2020-09-05 07:45:38 +02:00
Patrik Svensson
e946289bd9 Make styles composable
Also adds some new extension methods and make some APIs a bit more consistent.

Closes #64
2020-09-03 21:26:20 +02:00
Patrik Svensson
bcaaa6b2d3 Update table of contents (skip-ci) 2020-09-03 20:04:06 +02:00
Patrik Svensson
d3588a4b06 Remove styles and colors (skip-ci) 2020-09-03 20:03:26 +02:00
Patrik Svensson
7471e9d38c Add panel header support
Closes #63
2020-09-03 19:02:29 +02:00
32 changed files with 934 additions and 435 deletions

281
README.md
View File

@@ -14,8 +14,6 @@ for Python.
3.1. [Using the static API](#using-the-static-api)
3.2. [Creating a console](#creating-a-console)
4. [Running examples](#running-examples)
5. [Available styles](#available-styles)
6. [Predefined colors](#predefined-colors)
## Features
@@ -122,281 +120,4 @@ And to run an example:
│ Bonjour │ le │ monde! │
│ Hej │ Världen! │ │
└──────────┴──────────┴────────┘
```
## Available styles
_NOTE: Not all styles are supported in every terminal._
Name | Description
--- | ---
`bold` | Bold text
`dim` | Dim or faint text
`italic` | Italic text
`underline` | Underlined text
`invert` | Swaps the foreground and background colors
`conceal` | Hides the text
`slowblink` | Makes text blink slowly
`rapidblink` | Makes text blink
`strikethrough` | Shows text with a horizontal line through the center
## Predefined colors
Number | Name | RGB | Hex | System.ConsoleColor
--- | --- | --- | --- | ---
`0` | `black` | `0,0,0` | `#000000` | `Black`
`1` | `maroon` | `128,0,0` | `#800000` | `DarkRed`
`2` | `green` | `0,128,0` | `#008000` | `DarkGreen`
`3` | `olive` | `128,128,0` | `#808000` | `DarkYellow`
`4` | `navy` | `0,0,128` | `#000080` | `DarkBlue`
`5` | `purple` | `128,0,128` | `#800080` | `DarkMagenta`
`6` | `teal` | `0,128,128` | `#008080` | `DarkCyan`
`7` | `silver` | `192,192,192` | `#c0c0c0` | `Gray`
`8` | `grey` | `128,128,128` | `#808080` | `DarkGray`
`9` | `red` | `255,0,0` | `#ff0000` | `Red`
`10` | `lime` | `0,255,0` | `#00ff00` | `Green`
`11` | `yellow` | `255,255,0` | `#ffff00` | `Yellow`
`12` | `blue` | `0,0,255` | `#0000ff` | `Blue`
`13` | `fuchsia` | `255,0,255` | `#ff00ff` | `Magenta`
`14` | `aqua` | `0,255,255` | `#00ffff` | `Cyan`
`15` | `white` | `255,255,255` | `#ffffff` | `White`
`16` | `grey0` | `0,0,0` | `#000000`
`17` | `navyblue` | `0,0,95` | `#00005f`
`18` | `darkblue` | `0,0,135` | `#000087`
`19` | `blue3` | `0,0,175` | `#0000af`
`20` | `blue3_1` | `0,0,215` | `#0000d7`
`21` | `blue1` | `0,0,255` | `#0000ff`
`22` | `darkgreen` | `0,95,0` | `#005f00`
`23` | `deepskyblue4` | `0,95,95` | `#005f5f`
`24` | `deepskyblue4_1` | `0,95,135` | `#005f87`
`25` | `deepskyblue4_2` | `0,95,175` | `#005faf`
`26` | `dodgerblue3` | `0,95,215` | `#005fd7`
`27` | `dodgerblue2` | `0,95,255` | `#005fff`
`28` | `green4` | `0,135,0` | `#008700`
`29` | `springgreen4` | `0,135,95` | `#00875f`
`30` | `turquoise4` | `0,135,135` | `#008787`
`31` | `deepskyblue3` | `0,135,175` | `#0087af`
`32` | `deepskyblue3_1` | `0,135,215` | `#0087d7`
`33` | `dodgerblue1` | `0,135,255` | `#0087ff`
`34` | `green3` | `0,175,0` | `#00af00`
`35` | `springgreen3` | `0,175,95` | `#00af5f`
`36` | `darkcyan` | `0,175,135` | `#00af87`
`37` | `lightseagreen` | `0,175,175` | `#00afaf`
`38` | `deepskyblue2` | `0,175,215` | `#00afd7`
`39` | `deepskyblue1` | `0,175,255` | `#00afff`
`40` | `green3_1` | `0,215,0` | `#00d700`
`41` | `springgreen3_1` | `0,215,95` | `#00d75f`
`42` | `springgreen2` | `0,215,135` | `#00d787`
`43` | `cyan3` | `0,215,175` | `#00d7af`
`44` | `darkturquoise` | `0,215,215` | `#00d7d7`
`45` | `turquoise2` | `0,215,255` | `#00d7ff`
`46` | `green1` | `0,255,0` | `#00ff00`
`47` | `springgreen2_1` | `0,255,95` | `#00ff5f`
`48` | `springgreen1` | `0,255,135` | `#00ff87`
`49` | `mediumspringgreen` | `0,255,175` | `#00ffaf`
`50` | `cyan2` | `0,255,215` | `#00ffd7`
`51` | `cyan1` | `0,255,255` | `#00ffff`
`52` | `darkred` | `95,0,0` | `#5f0000`
`53` | `deeppink4` | `95,0,95` | `#5f005f`
`54` | `purple4` | `95,0,135` | `#5f0087`
`55` | `purple4_1` | `95,0,175` | `#5f00af`
`56` | `purple3` | `95,0,215` | `#5f00d7`
`57` | `blueviolet` | `95,0,255` | `#5f00ff`
`58` | `orange4` | `95,95,0` | `#5f5f00`
`59` | `grey37` | `95,95,95` | `#5f5f5f`
`60` | `mediumpurple4` | `95,95,135` | `#5f5f87`
`61` | `slateblue3` | `95,95,175` | `#5f5faf`
`62` | `slateblue3_1` | `95,95,215` | `#5f5fd7`
`63` | `royalblue1` | `95,95,255` | `#5f5fff`
`64` | `chartreuse4` | `95,135,0` | `#5f8700`
`65` | `darkseagreen4` | `95,135,95` | `#5f875f`
`66` | `paleturquoise4` | `95,135,135` | `#5f8787`
`67` | `steelblue` | `95,135,175` | `#5f87af`
`68` | `steelblue3` | `95,135,215` | `#5f87d7`
`69` | `cornflowerblue` | `95,135,255` | `#5f87ff`
`70` | `chartreuse3` | `95,175,0` | `#5faf00`
`71` | `darkseagreen4_1` | `95,175,95` | `#5faf5f`
`72` | `cadetblue` | `95,175,135` | `#5faf87`
`73` | `cadetblue_1` | `95,175,175` | `#5fafaf`
`74` | `skyblue3` | `95,175,215` | `#5fafd7`
`75` | `steelblue1` | `95,175,255` | `#5fafff`
`76` | `chartreuse3_1` | `95,215,0` | `#5fd700`
`77` | `palegreen3` | `95,215,95` | `#5fd75f`
`78` | `seagreen3` | `95,215,135` | `#5fd787`
`79` | `aquamarine3` | `95,215,175` | `#5fd7af`
`80` | `mediumturquoise` | `95,215,215` | `#5fd7d7`
`81` | `steelblue1_1` | `95,215,255` | `#5fd7ff`
`82` | `chartreuse2` | `95,255,0` | `#5fff00`
`83` | `seagreen2` | `95,255,95` | `#5fff5f`
`84` | `seagreen1` | `95,255,135` | `#5fff87`
`85` | `seagreen1_1` | `95,255,175` | `#5fffaf`
`86` | `aquamarine1` | `95,255,215` | `#5fffd7`
`87` | `darkslategray2` | `95,255,255` | `#5fffff`
`88` | `darkred_1` | `135,0,0` | `#870000`
`89` | `deeppink4_1` | `135,0,95` | `#87005f`
`90` | `darkmagenta` | `135,0,135` | `#870087`
`91` | `darkmagenta_1` | `135,0,175` | `#8700af`
`92` | `darkviolet` | `135,0,215` | `#8700d7`
`93` | `purple_1` | `135,0,255` | `#8700ff`
`94` | `orange4_1` | `135,95,0` | `#875f00`
`95` | `lightpink4` | `135,95,95` | `#875f5f`
`96` | `plum4` | `135,95,135` | `#875f87`
`97` | `mediumpurple3` | `135,95,175` | `#875faf`
`98` | `mediumpurple3_1` | `135,95,215` | `#875fd7`
`99` | `slateblue1` | `135,95,255` | `#875fff`
`100` | `yellow4` | `135,135,0` | `#878700`
`101` | `wheat4` | `135,135,95` | `#87875f`
`102` | `grey53` | `135,135,135` | `#878787`
`103` | `lightslategrey` | `135,135,175` | `#8787af`
`104` | `mediumpurple` | `135,135,215` | `#8787d7`
`105` | `lightslateblue` | `135,135,255` | `#8787ff`
`106` | `yellow4_1` | `135,175,0` | `#87af00`
`107` | `darkolivegreen3` | `135,175,95` | `#87af5f`
`108` | `darkseagreen` | `135,175,135` | `#87af87`
`109` | `lightskyblue3` | `135,175,175` | `#87afaf`
`110` | `lightskyblue3_1` | `135,175,215` | `#87afd7`
`111` | `skyblue2` | `135,175,255` | `#87afff`
`112` | `chartreuse2_1` | `135,215,0` | `#87d700`
`113` | `darkolivegreen3_1` | `135,215,95` | `#87d75f`
`114` | `palegreen3_1` | `135,215,135` | `#87d787`
`115` | `darkseagreen3` | `135,215,175` | `#87d7af`
`116` | `darkslategray3` | `135,215,215` | `#87d7d7`
`117` | `skyblue1` | `135,215,255` | `#87d7ff`
`118` | `chartreuse1` | `135,255,0` | `#87ff00`
`119` | `lightgreen` | `135,255,95` | `#87ff5f`
`120` | `lightgreen_1` | `135,255,135` | `#87ff87`
`121` | `palegreen1` | `135,255,175` | `#87ffaf`
`122` | `aquamarine1_1` | `135,255,215` | `#87ffd7`
`123` | `darkslategray1` | `135,255,255` | `#87ffff`
`124` | `red3` | `175,0,0` | `#af0000`
`125` | `deeppink4_2` | `175,0,95` | `#af005f`
`126` | `mediumvioletred` | `175,0,135` | `#af0087`
`127` | `magenta3` | `175,0,175` | `#af00af`
`128` | `darkviolet_1` | `175,0,215` | `#af00d7`
`129` | `purple_2` | `175,0,255` | `#af00ff`
`130` | `darkorange3` | `175,95,0` | `#af5f00`
`131` | `indianred` | `175,95,95` | `#af5f5f`
`132` | `hotpink3` | `175,95,135` | `#af5f87`
`133` | `mediumorchid3` | `175,95,175` | `#af5faf`
`134` | `mediumorchid` | `175,95,215` | `#af5fd7`
`135` | `mediumpurple2` | `175,95,255` | `#af5fff`
`136` | `darkgoldenrod` | `175,135,0` | `#af8700`
`137` | `lightsalmon3` | `175,135,95` | `#af875f`
`138` | `rosybrown` | `175,135,135` | `#af8787`
`139` | `grey63` | `175,135,175` | `#af87af`
`140` | `mediumpurple2_1` | `175,135,215` | `#af87d7`
`141` | `mediumpurple1` | `175,135,255` | `#af87ff`
`142` | `gold3` | `175,175,0` | `#afaf00`
`143` | `darkkhaki` | `175,175,95` | `#afaf5f`
`144` | `navajowhite3` | `175,175,135` | `#afaf87`
`145` | `grey69` | `175,175,175` | `#afafaf`
`146` | `lightsteelblue3` | `175,175,215` | `#afafd7`
`147` | `lightsteelblue` | `175,175,255` | `#afafff`
`148` | `yellow3` | `175,215,0` | `#afd700`
`149` | `darkolivegreen3_2` | `175,215,95` | `#afd75f`
`150` | `darkseagreen3_1` | `175,215,135` | `#afd787`
`151` | `darkseagreen2` | `175,215,175` | `#afd7af`
`152` | `lightcyan3` | `175,215,215` | `#afd7d7`
`153` | `lightskyblue1` | `175,215,255` | `#afd7ff`
`154` | `greenyellow` | `175,255,0` | `#afff00`
`155` | `darkolivegreen2` | `175,255,95` | `#afff5f`
`156` | `palegreen1_1` | `175,255,135` | `#afff87`
`157` | `darkseagreen2_1` | `175,255,175` | `#afffaf`
`158` | `darkseagreen1` | `175,255,215` | `#afffd7`
`159` | `paleturquoise1` | `175,255,255` | `#afffff`
`160` | `red3_1` | `215,0,0` | `#d70000`
`161` | `deeppink3` | `215,0,95` | `#d7005f`
`162` | `deeppink3_1` | `215,0,135` | `#d70087`
`163` | `magenta3_1` | `215,0,175` | `#d700af`
`164` | `magenta3_2` | `215,0,215` | `#d700d7`
`165` | `magenta2` | `215,0,255` | `#d700ff`
`166` | `darkorange3_1` | `215,95,0` | `#d75f00`
`167` | `indianred_1` | `215,95,95` | `#d75f5f`
`168` | `hotpink3_1` | `215,95,135` | `#d75f87`
`169` | `hotpink2` | `215,95,175` | `#d75faf`
`170` | `orchid` | `215,95,215` | `#d75fd7`
`171` | `mediumorchid1` | `215,95,255` | `#d75fff`
`172` | `orange3` | `215,135,0` | `#d78700`
`173` | `lightsalmon3_1` | `215,135,95` | `#d7875f`
`174` | `lightpink3` | `215,135,135` | `#d78787`
`175` | `pink3` | `215,135,175` | `#d787af`
`176` | `plum3` | `215,135,215` | `#d787d7`
`177` | `violet` | `215,135,255` | `#d787ff`
`178` | `gold3_1` | `215,175,0` | `#d7af00`
`179` | `lightgoldenrod3` | `215,175,95` | `#d7af5f`
`180` | `tan` | `215,175,135` | `#d7af87`
`181` | `mistyrose3` | `215,175,175` | `#d7afaf`
`182` | `thistle3` | `215,175,215` | `#d7afd7`
`183` | `plum2` | `215,175,255` | `#d7afff`
`184` | `yellow3_1` | `215,215,0` | `#d7d700`
`185` | `khaki3` | `215,215,95` | `#d7d75f`
`186` | `lightgoldenrod2` | `215,215,135` | `#d7d787`
`187` | `lightyellow3` | `215,215,175` | `#d7d7af`
`188` | `grey84` | `215,215,215` | `#d7d7d7`
`189` | `lightsteelblue1` | `215,215,255` | `#d7d7ff`
`190` | `yellow2` | `215,255,0` | `#d7ff00`
`191` | `darkolivegreen1` | `215,255,95` | `#d7ff5f`
`192` | `darkolivegreen1_1` | `215,255,135` | `#d7ff87`
`193` | `darkseagreen1_1` | `215,255,175` | `#d7ffaf`
`194` | `honeydew2` | `215,255,215` | `#d7ffd7`
`195` | `lightcyan1` | `215,255,255` | `#d7ffff`
`196` | `red1` | `255,0,0` | `#ff0000`
`197` | `deeppink2` | `255,0,95` | `#ff005f`
`198` | `deeppink1` | `255,0,135` | `#ff0087`
`199` | `deeppink1_1` | `255,0,175` | `#ff00af`
`200` | `magenta2_1` | `255,0,215` | `#ff00d7`
`201` | `magenta1` | `255,0,255` | `#ff00ff`
`202` | `orangered1` | `255,95,0` | `#ff5f00`
`203` | `indianred1` | `255,95,95` | `#ff5f5f`
`204` | `indianred1_1` | `255,95,135` | `#ff5f87`
`205` | `hotpink` | `255,95,175` | `#ff5faf`
`206` | `hotpink_1` | `255,95,215` | `#ff5fd7`
`207` | `mediumorchid1_1` | `255,95,255` | `#ff5fff`
`208` | `darkorange` | `255,135,0` | `#ff8700`
`209` | `salmon1` | `255,135,95` | `#ff875f`
`210` | `lightcoral` | `255,135,135` | `#ff8787`
`211` | `palevioletred1` | `255,135,175` | `#ff87af`
`212` | `orchid2` | `255,135,215` | `#ff87d7`
`213` | `orchid1` | `255,135,255` | `#ff87ff`
`214` | `orange1` | `255,175,0` | `#ffaf00`
`215` | `sandybrown` | `255,175,95` | `#ffaf5f`
`216` | `lightsalmon1` | `255,175,135` | `#ffaf87`
`217` | `lightpink1` | `255,175,175` | `#ffafaf`
`218` | `pink1` | `255,175,215` | `#ffafd7`
`219` | `plum1` | `255,175,255` | `#ffafff`
`220` | `gold1` | `255,215,0` | `#ffd700`
`221` | `lightgoldenrod2_1` | `255,215,95` | `#ffd75f`
`222` | `lightgoldenrod2_2` | `255,215,135` | `#ffd787`
`223` | `navajowhite1` | `255,215,175` | `#ffd7af`
`224` | `mistyrose1` | `255,215,215` | `#ffd7d7`
`225` | `thistle1` | `255,215,255` | `#ffd7ff`
`226` | `yellow1` | `255,255,0` | `#ffff00`
`227` | `lightgoldenrod1` | `255,255,95` | `#ffff5f`
`228` | `khaki1` | `255,255,135` | `#ffff87`
`229` | `wheat1` | `255,255,175` | `#ffffaf`
`230` | `cornsilk1` | `255,255,215` | `#ffffd7`
`231` | `grey100` | `255,255,255` | `#ffffff`
`232` | `grey3` | `8,8,8` | `#080808`
`233` | `grey7` | `18,18,18` | `#121212`
`234` | `grey11` | `28,28,28` | `#1c1c1c`
`235` | `grey15` | `38,38,38` | `#262626`
`236` | `grey19` | `48,48,48` | `#303030`
`237` | `grey23` | `58,58,58` | `#3a3a3a`
`238` | `grey27` | `68,68,68` | `#444444`
`239` | `grey30` | `78,78,78` | `#4e4e4e`
`240` | `grey35` | `88,88,88` | `#585858`
`241` | `grey39` | `98,98,98` | `#626262`
`242` | `grey42` | `108,108,108` | `#6c6c6c`
`243` | `grey46` | `118,118,118` | `#767676`
`244` | `grey50` | `128,128,128` | `#808080`
`245` | `grey54` | `138,138,138` | `#8a8a8a`
`246` | `grey58` | `148,148,148` | `#949494`
`247` | `grey62` | `158,158,158` | `#9e9e9e`
`248` | `grey66` | `168,168,168` | `#a8a8a8`
`249` | `grey70` | `178,178,178` | `#b2b2b2`
`250` | `grey74` | `188,188,188` | `#bcbcbc`
`251` | `grey78` | `198,198,198` | `#c6c6c6`
`252` | `grey82` | `208,208,208` | `#d0d0d0`
`253` | `grey85` | `218,218,218` | `#dadada`
`254` | `grey89` | `228,228,228` | `#e4e4e4`
`255` | `grey93` | `238,238,238` | `#eeeeee`
```

View File

@@ -4,7 +4,7 @@ namespace ColorExample
{
public static class Program
{
public static void Main(string[] args)
public static void Main()
{
if (AnsiConsole.Capabilities.ColorSystem == ColorSystem.NoColors)
{

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Spectre.Console;
using Spectre.Console.Rendering;
namespace ColumnsExample
{
public static class Program
{
public static async Task Main()
{
// Download some random users
using var client = new HttpClient();
dynamic users = JObject.Parse(
await client.GetStringAsync("https://randomuser.me/api/?results=15"));
// Create a card for each user
var cards = new List<Panel>();
foreach(var user in users.results)
{
cards.Add(new Panel(GetCard(user))
.SetHeader($"{user.location.country}")
.RoundedBorder().Expand());
}
// Render all cards in columns
AnsiConsole.Render(new Columns(cards));
}
private static string GetCard(dynamic user)
{
var name = $"{user.name.first} {user.name.last}";
var country = $"{user.location.city}";
return $"[b]{name}[/]\n[yellow]{country}[/]";
}
}
}

View File

@@ -1,11 +1,10 @@
using System;
using Spectre.Console;
namespace Diagnostic
{
public class Program
public static class Program
{
public static void Main(string[] args)
public static void Main()
{
AnsiConsole.MarkupLine("Color system: [bold]{0}[/]", AnsiConsole.Capabilities.ColorSystem);
AnsiConsole.MarkupLine("Supports ansi? [bold]{0}[/]", AnsiConsole.Capabilities.SupportsAnsi);

View File

@@ -2,9 +2,9 @@ using Spectre.Console;
namespace GridExample
{
public sealed class Program
public static class Program
{
static void Main(string[] args)
public static void Main()
{
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("Usage: [grey]dotnet [blue]run[/] [[options]] [[[[--]] <additional arguments>...]]]][/]");

View File

@@ -1,10 +1,11 @@
using Spectre.Console;
using Spectre.Console.Rendering;
namespace PanelExample
{
class Program
public static class Program
{
static void Main(string[] args)
public static void Main()
{
var content = new Markup(
"[underline]I[/] heard [underline on blue]you[/] like panels\n\n\n\n" +
@@ -13,32 +14,28 @@ namespace PanelExample
AnsiConsole.Render(
new Panel(
new Panel(content)
{
Border = BorderKind.Rounded
}));
.SetBorderKind(BorderKind.Rounded)));
// Left adjusted panel with text
AnsiConsole.Render(new Panel(
new Text("Left adjusted\nLeft").LeftAligned())
{
Expand = true,
});
AnsiConsole.Render(
new Panel(new Text("Left adjusted\nLeft").LeftAligned())
.Expand()
.SquareBorder()
.SetHeader("Left", Style.WithForeground(Color.Red)));
// Centered ASCII panel with text
AnsiConsole.Render(new Panel(
new Text("Centered\nCenter").Centered())
{
Expand = true,
Border = BorderKind.Ascii,
});
AnsiConsole.Render(
new Panel(new Text("Centered\nCenter").Centered())
.Expand()
.AsciiBorder()
.SetHeader("Center", Style.WithForeground(Color.Green), Justify.Center));
// Right adjusted, rounded panel with text
AnsiConsole.Render(new Panel(
new Text("Right adjusted\nRight").RightAligned())
{
Expand = true,
Border = BorderKind.Rounded,
});
AnsiConsole.Render(
new Panel(new Text("Right adjusted\nRight").RightAligned())
.Expand()
.RoundedBorder()
.SetHeader("Right", Style.WithForeground(Color.Blue), Justify.Right));
}
}
}

View File

@@ -1,11 +1,10 @@
using Spectre.Console;
using Spectre.Console.Rendering;
namespace TableExample
{
public static class Program
{
public static void Main(string[] args)
public static void Main()
{
// A simple table
RenderSimpleTable();
@@ -36,7 +35,7 @@ namespace TableExample
private static void RenderBigTable()
{
// Create the table.
var table = new Table().SetBorder(BorderKind.Rounded);
var table = new Table().SetBorderKind(BorderKind.Rounded);
table.AddColumn("[red underline]Foo[/]");
table.AddColumn(new TableColumn("[blue]Bar[/]") { Alignment = Justify.Right, NoWrap = true });
@@ -58,7 +57,7 @@ namespace TableExample
private static void RenderNestedTable()
{
// Create simple table.
var simple = new Table().SetBorder(BorderKind.Rounded).SetBorderColor(Color.Red);
var simple = new Table().SetBorderKind(BorderKind.Rounded).SetBorderColor(Color.Red);
simple.AddColumn(new TableColumn("[u]Foo[/]").Centered());
simple.AddColumn(new TableColumn("[u]Bar[/]"));
simple.AddColumn(new TableColumn("[u]Baz[/]"));
@@ -67,7 +66,7 @@ namespace TableExample
simple.AddRow("[blue]Hej[/]", "[yellow]Världen![/]", "");
// Create other table.
var second = new Table().SetBorder(BorderKind.Square).SetBorderColor(Color.Green);
var second = new Table().SetBorderKind(BorderKind.Square).SetBorderColor(Color.Green);
second.AddColumn(new TableColumn("[u]Foo[/]"));
second.AddColumn(new TableColumn("[u]Bar[/]"));
second.AddColumn(new TableColumn("[u]Baz[/]"));
@@ -75,7 +74,7 @@ namespace TableExample
second.AddRow(simple, new Text("Whaaat"), new Text("Lolz"));
second.AddRow("[blue]Hej[/]", "[yellow]Världen![/]", "");
var table = new Table().SetBorder(BorderKind.Rounded);
var table = new Table().SetBorderKind(BorderKind.Rounded);
table.AddColumn(new TableColumn(new Panel("[u]Foo[/]").SetBorderColor(Color.Red)));
table.AddColumn(new TableColumn(new Panel("[u]Bar[/]").SetBorderColor(Color.Green)));
table.AddColumn(new TableColumn(new Panel("[u]Baz[/]").SetBorderColor(Color.Blue)));

View File

@@ -82,5 +82,8 @@ dotnet_diagnostic.RCS1079.severity = warning
# RCS1057: Add empty line between declarations.
dotnet_diagnostic.RCS1057.severity = none
# RCS1057: Validate arguments correctly
dotnet_diagnostic.RCS1227.severity = none
# IDE0004: Remove Unnecessary Cast
dotnet_diagnostic.IDE0004.severity = warning

View File

@@ -0,0 +1,46 @@
using System.Collections.Generic;
using Shouldly;
using Xunit;
namespace Spectre.Console.Tests.Unit
{
public sealed class ColumnsTests
{
private sealed class User
{
public string Name { get; set; }
public string Country { get; set; }
}
[Fact]
public void Should_Render_Columns_Correctly()
{
// Given
var console = new PlainConsole(width: 61);
var users = new[]
{
new User { Name = "Savannah Thompson", Country = "Australia" },
new User { Name = "Sophie Ramos", Country = "United States" },
new User { Name = "Katrin Goldberg", Country = "Germany" },
};
var cards = new List<Panel>();
foreach (var user in users)
{
cards.Add(
new Panel($"[b]{user.Name}[/]\n[yellow]{user.Country}[/]")
.RoundedBorder().Expand());
}
// When
console.Render(new Columns(cards));
// Then
console.Lines.Count.ShouldBe(4);
console.Lines[0].ShouldBe("╭────────────────────╮ ╭────────────────╮ ╭─────────────────╮");
console.Lines[1].ShouldBe("│ Savannah Thompson │ │ Sophie Ramos │ │ Katrin Goldberg │");
console.Lines[2].ShouldBe("│ Australia │ │ United States │ │ Germany │");
console.Lines[3].ShouldBe("╰────────────────────╯ ╰────────────────╯ ╰─────────────────╯");
}
}
}

View File

@@ -42,7 +42,7 @@ namespace Spectre.Console.Tests.Unit
}
[Fact]
public void Should_Throw_If_Row_Columns_Is_Less_Than_Number_Of_Columns()
public void Should_Add_Empty_Items_If_User_Provides_Less_Row_Items_Than_Columns()
{
// Given
var grid = new Grid();
@@ -50,11 +50,10 @@ namespace Spectre.Console.Tests.Unit
grid.AddColumn();
// When
var result = Record.Exception(() => grid.AddRow("Foo"));
grid.AddRow("Foo");
// Then
result.ShouldBeOfType<InvalidOperationException>();
result.Message.ShouldBe("The number of row columns are less than the number of grid columns.");
grid.RowCount.ShouldBe(1);
}
[Fact]

View File

@@ -40,6 +40,108 @@ namespace Spectre.Console.Tests.Unit
console.Lines[2].ShouldBe("└───────────────────┘");
}
[Fact]
public void Should_Render_Panel_With_Header()
{
// Given
var console = new PlainConsole(width: 80);
// When
console.Render(new Panel("Hello World")
{
Header = new Header("Greeting"),
Expand = true,
Padding = new Padding(2, 2),
});
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("┌─Greeting─────────────────────────────────────────────────────────────────────┐");
console.Lines[1].ShouldBe("│ Hello World │");
console.Lines[2].ShouldBe("└──────────────────────────────────────────────────────────────────────────────┘");
}
[Fact]
public void Should_Render_Panel_With_Left_Aligned_Header()
{
// Given
var console = new PlainConsole(width: 80);
// When
console.Render(new Panel("Hello World")
{
Header = new Header("Greeting").LeftAligned(),
Expand = true,
});
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("┌─Greeting─────────────────────────────────────────────────────────────────────┐");
console.Lines[1].ShouldBe("│ Hello World │");
console.Lines[2].ShouldBe("└──────────────────────────────────────────────────────────────────────────────┘");
}
[Fact]
public void Should_Render_Panel_With_Centered_Header()
{
// Given
var console = new PlainConsole(width: 80);
// When
console.Render(new Panel("Hello World")
{
Header = new Header("Greeting").Centered(),
Expand = true,
});
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("┌───────────────────────────────────Greeting───────────────────────────────────┐");
console.Lines[1].ShouldBe("│ Hello World │");
console.Lines[2].ShouldBe("└──────────────────────────────────────────────────────────────────────────────┘");
}
[Fact]
public void Should_Render_Panel_With_Right_Aligned_Header()
{
// Given
var console = new PlainConsole(width: 80);
// When
console.Render(new Panel("Hello World")
{
Header = new Header("Greeting").RightAligned(),
Expand = true,
});
// Then
console.Lines.Count.ShouldBe(3);
console.Lines[0].ShouldBe("┌─────────────────────────────────────────────────────────────────────Greeting─┐");
console.Lines[1].ShouldBe("│ Hello World │");
console.Lines[2].ShouldBe("└──────────────────────────────────────────────────────────────────────────────┘");
}
[Fact]
public void Should_Collapse_Header_If_It_Will_Not_Fit()
{
// Given
var console = new PlainConsole(width: 10);
// When
console.Render(new Panel("Hello World")
{
Header = new Header("Greeting"),
Expand = true,
});
// Then
console.Lines.Count.ShouldBe(4);
console.Lines[0].ShouldBe("┌─Greet…─┐");
console.Lines[1].ShouldBe("│ Hello │");
console.Lines[2].ShouldBe("│ World │");
console.Lines[3].ShouldBe("└────────┘");
}
[Fact]
public void Should_Render_Panel_With_Unicode_Correctly()
{

View File

@@ -87,7 +87,7 @@ namespace Spectre.Console.Tests.Unit
}
[Fact]
public void Should_Throw_If_Row_Columns_Is_Less_Than_Number_Of_Columns()
public void Should_Add_Empty_Items_If_User_Provides_Less_Row_Items_Than_Columns()
{
// Given
var table = new Table();
@@ -95,11 +95,10 @@ namespace Spectre.Console.Tests.Unit
table.AddColumn("World");
// When
var result = Record.Exception(() => table.AddRow("Foo"));
table.AddRow("Foo");
// Then
result.ShouldBeOfType<InvalidOperationException>();
result.Message.ShouldBe("The number of row columns are less than the number of table columns.");
table.RowCount.ShouldBe(1);
}
[Fact]
@@ -174,7 +173,7 @@ namespace Spectre.Console.Tests.Unit
{
// A simple table
var console = new PlainConsole(width: 80);
var table = new Table() { Border = BorderKind.Rounded };
var table = new Table() { BorderKind = BorderKind.Rounded };
table.AddColumn("Foo");
table.AddColumn("Bar");
table.AddColumn(new TableColumn("Baz") { Alignment = Justify.Right });
@@ -184,7 +183,7 @@ namespace Spectre.Console.Tests.Unit
// Render a table in some panels.
console.Render(new Panel(new Panel(table)
{
Border = BorderKind.Ascii,
BorderKind = BorderKind.Ascii,
}));
// Then
@@ -256,7 +255,7 @@ namespace Spectre.Console.Tests.Unit
{
// Given
var console = new PlainConsole(width: 80);
var table = new Table { Border = BorderKind.Ascii };
var table = new Table { BorderKind = BorderKind.Ascii };
table.AddColumns("Foo", "Bar", "Baz");
table.AddRow("Qux", "Corgi", "Waldo");
table.AddRow("Grault", "Garply", "Fred");
@@ -279,7 +278,7 @@ namespace Spectre.Console.Tests.Unit
{
// Given
var console = new PlainConsole(width: 80);
var table = new Table { Border = BorderKind.Rounded };
var table = new Table { BorderKind = BorderKind.Rounded };
table.AddColumns("Foo", "Bar", "Baz");
table.AddRow("Qux", "Corgi", "Waldo");
table.AddRow("Grault", "Garply", "Fred");
@@ -302,7 +301,7 @@ namespace Spectre.Console.Tests.Unit
{
// Given
var console = new PlainConsole(width: 80);
var table = new Table { Border = BorderKind.None };
var table = new Table { BorderKind = BorderKind.None };
table.AddColumns("Foo", "Bar", "Baz");
table.AddRow("Qux", "Corgi", "Waldo");
table.AddRow("Grault", "Garply", "Fred");

View File

@@ -65,6 +65,21 @@ namespace Spectre.Console.Tests.Unit
fixture.RawOutput.ShouldBe("Hello\n\nWorld\n\n");
}
[Fact]
public void Should_Render_Panel_2()
{
// Given
var console = new PlainConsole(width: 80);
// When
console.Render(new Markup("[b]Hello World[/]\n[yellow]Hello World[/]"));
// Then
console.Lines.Count.ShouldBe(2);
console.Lines[0].ShouldBe("Hello World");
console.Lines[1].ShouldBe("Hello World");
}
[Theory]
[InlineData(5, "Hello World", "Hello\nWorld")]
[InlineData(10, "Hello Sweet Nice World", "Hello \nSweet Nice\nWorld")]

View File

@@ -27,6 +27,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Colors", "..\examples\Color
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Diagnostic", "..\examples\Diagnostic\Diagnostic.csproj", "{4337F255-88E9-4408-81A3-DF1AF58AC753}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Columns", "..\examples\Columns\Columns.csproj", "{33357599-C79D-4299-888F-634E2C3EACEF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -121,6 +123,18 @@ Global
{4337F255-88E9-4408-81A3-DF1AF58AC753}.Release|x64.Build.0 = Release|Any CPU
{4337F255-88E9-4408-81A3-DF1AF58AC753}.Release|x86.ActiveCfg = Release|Any CPU
{4337F255-88E9-4408-81A3-DF1AF58AC753}.Release|x86.Build.0 = Release|Any CPU
{33357599-C79D-4299-888F-634E2C3EACEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{33357599-C79D-4299-888F-634E2C3EACEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{33357599-C79D-4299-888F-634E2C3EACEF}.Debug|x64.ActiveCfg = Debug|Any CPU
{33357599-C79D-4299-888F-634E2C3EACEF}.Debug|x64.Build.0 = Debug|Any CPU
{33357599-C79D-4299-888F-634E2C3EACEF}.Debug|x86.ActiveCfg = Debug|Any CPU
{33357599-C79D-4299-888F-634E2C3EACEF}.Debug|x86.Build.0 = Debug|Any CPU
{33357599-C79D-4299-888F-634E2C3EACEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{33357599-C79D-4299-888F-634E2C3EACEF}.Release|Any CPU.Build.0 = Release|Any CPU
{33357599-C79D-4299-888F-634E2C3EACEF}.Release|x64.ActiveCfg = Release|Any CPU
{33357599-C79D-4299-888F-634E2C3EACEF}.Release|x64.Build.0 = Release|Any CPU
{33357599-C79D-4299-888F-634E2C3EACEF}.Release|x86.ActiveCfg = Release|Any CPU
{33357599-C79D-4299-888F-634E2C3EACEF}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -131,6 +145,7 @@ Global
{C7FF6FDB-FB59-4517-8669-521C96AB7323} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{1F51C55C-BA4C-4856-9001-0F7924FFB179} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{4337F255-88E9-4408-81A3-DF1AF58AC753} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{33357599-C79D-4299-888F-634E2C3EACEF} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C}

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
@@ -30,8 +31,8 @@ namespace Spectre.Console
using (console.PushStyle(Style.Plain))
{
var segments = renderable.Render(options, console.Width);
segments = Segment.Merge(segments);
var segments = renderable.Render(options, console.Width).Where(x => !(x.Text.Length == 0 && !x.IsLineBreak)).ToArray();
segments = Segment.Merge(segments).ToArray();
var current = Style.Plain;
foreach (var segment in segments)

View File

@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// Renders things in columns.
/// </summary>
public sealed class Columns : Renderable, IPaddable, IExpandable
{
private readonly List<IRenderable> _items;
/// <inheritdoc/>
public Padding Padding { get; set; } = new Padding(0, 1);
/// <summary>
/// Gets or sets a value indicating whether or not the columns should
/// expand to the available space. If <c>false</c>, the column
/// width will be auto calculated. Defaults to <c>true</c>.
/// </summary>
public bool Expand { get; set; } = true;
/// <summary>
/// Initializes a new instance of the <see cref="Columns"/> class.
/// </summary>
/// <param name="items">The items to render.</param>
public Columns(IEnumerable<IRenderable> items)
{
if (items is null)
{
throw new ArgumentNullException(nameof(items));
}
_items = new List<IRenderable>(items);
}
/// <summary>
/// Initializes a new instance of the <see cref="Columns"/> class.
/// </summary>
/// <param name="items">The items to render.</param>
public Columns(IEnumerable<string> items)
{
if (items is null)
{
throw new ArgumentNullException(nameof(items));
}
_items = new List<IRenderable>(items.Select(item => new Markup(item)));
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var maxPadding = Math.Max(Padding.Left, Padding.Right);
var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray();
var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding);
var table = new Table();
table.NoBorder();
table.HideHeaders();
table.PadRightCell = false;
if (Expand)
{
table.Expand();
}
// Add columns
for (var index = 0; index < columnCount; index++)
{
table.AddColumn(new TableColumn(string.Empty)
{
Padding = Padding,
NoWrap = true,
});
}
// Add rows
for (var start = 0; start < _items.Count; start += columnCount)
{
table.AddRow(_items.Skip(start).Take(columnCount).ToArray());
}
return ((IRenderable)table).Render(context, maxWidth);
}
// Algorithm borrowed from https://github.com/willmcgugan/rich/blob/master/rich/columns.py
private int CalculateColumnCount(int maxWidth, int[] itemWidths, int columnCount, int padding)
{
var widths = new Dictionary<int, int>();
while (columnCount > 1)
{
var columnIndex = 0;
widths.Clear();
var exceededTotalWidth = false;
foreach (var renderableWidth in IterateWidths(itemWidths, columnCount))
{
widths[columnIndex] = Math.Max(widths.ContainsKey(columnIndex) ? widths[columnIndex] : 0, renderableWidth);
var totalWidth = widths.Values.Sum() + (padding * (widths.Count - 1));
if (totalWidth > maxWidth)
{
columnCount = widths.Count - 1;
exceededTotalWidth = true;
break;
}
else
{
columnIndex = (columnIndex + 1) % columnCount;
}
}
if (!exceededTotalWidth)
{
break;
}
}
return columnCount;
}
private IEnumerable<int> IterateWidths(int[] itemWidths, int columnCount)
{
foreach (var width in itemWidths)
{
yield return width;
}
if (_items.Count % columnCount != 0)
{
for (var i = 0; i < columnCount - (_items.Count % columnCount) - 1; i++)
{
yield return 0;
}
}
}
}
}

View File

@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console
@@ -9,10 +7,27 @@ namespace Spectre.Console
/// <summary>
/// A renderable grid.
/// </summary>
public sealed class Grid : Renderable
public sealed class Grid : Renderable, IExpandable
{
private readonly Table _table;
/// <summary>
/// Gets the number of columns in the table.
/// </summary>
public int ColumnCount => _table.ColumnCount;
/// <summary>
/// Gets the number of rows in the table.
/// </summary>
public int RowCount => _table.RowCount;
/// <inheritdoc/>
public bool Expand
{
get => _table.Expand;
set => _table.Expand = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="Grid"/> class.
/// </summary>
@@ -20,7 +35,7 @@ namespace Spectre.Console
{
_table = new Table
{
Border = BorderKind.None,
BorderKind = BorderKind.None,
ShowHeaders = false,
IsGrid = true,
PadRightCell = false,
@@ -75,45 +90,6 @@ namespace Spectre.Console
});
}
/// <summary>
/// Adds a column to the grid.
/// </summary>
/// <param name="count">The number of columns to add.</param>
public void AddColumns(int count)
{
for (var index = 0; index < count; index++)
{
AddColumn(new GridColumn());
}
}
/// <summary>
/// Adds a column to the grid.
/// </summary>
/// <param name="columns">The columns to add.</param>
public void AddColumns(params GridColumn[] columns)
{
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
foreach (var column in columns)
{
AddColumn(column);
}
}
/// <summary>
/// Adds an empty row to the grid.
/// </summary>
public void AddEmptyRow()
{
var columns = new IRenderable[_table.ColumnCount];
Enumerable.Range(0, _table.ColumnCount).ForEach(index => columns[index] = Text.Empty);
AddRow(columns);
}
/// <summary>
/// Adds a new row to the grid.
/// </summary>
@@ -125,11 +101,6 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(columns));
}
if (columns.Length < _table.ColumnCount)
{
throw new InvalidOperationException("The number of row columns are less than the number of grid columns.");
}
if (columns.Length > _table.ColumnCount)
{
throw new InvalidOperationException("The number of row columns are greater than the number of grid columns.");

View File

@@ -1,5 +1,7 @@
using System;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
@@ -8,6 +10,63 @@ namespace Spectre.Console
/// </summary>
public static class GridExtensions
{
/// <summary>
/// Adds a column to the grid.
/// </summary>
/// <param name="grid">The grid to add the column to.</param>
/// <param name="count">The number of columns to add.</param>
public static void AddColumns(this Grid grid, int count)
{
if (grid is null)
{
throw new ArgumentNullException(nameof(grid));
}
for (var index = 0; index < count; index++)
{
grid.AddColumn(new GridColumn());
}
}
/// <summary>
/// Adds a column to the grid.
/// </summary>
/// <param name="grid">The grid to add the column to.</param>
/// <param name="columns">The columns to add.</param>
public static void AddColumns(this Grid grid, params GridColumn[] columns)
{
if (grid is null)
{
throw new ArgumentNullException(nameof(grid));
}
if (columns is null)
{
throw new ArgumentNullException(nameof(columns));
}
foreach (var column in columns)
{
grid.AddColumn(column);
}
}
/// <summary>
/// Adds an empty row to the grid.
/// </summary>
/// <param name="grid">The grid to add the row to.</param>
public static void AddEmptyRow(this Grid grid)
{
if (grid is null)
{
throw new ArgumentNullException(nameof(grid));
}
var columns = new IRenderable[grid.ColumnCount];
Enumerable.Range(0, grid.ColumnCount).ForEach(index => columns[index] = Text.Empty);
grid.AddRow(columns);
}
/// <summary>
/// Adds a new row to the grid.
/// </summary>

View File

@@ -0,0 +1,60 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Represents a header.
/// </summary>
public sealed class Header : IAlignable
{
/// <summary>
/// Gets the header text.
/// </summary>
public string Text { get; }
/// <summary>
/// Gets or sets the header style.
/// </summary>
public Style? Style { get; set; }
/// <summary>
/// Gets or sets the header alignment.
/// </summary>
public Justify? Alignment { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Header"/> class.
/// </summary>
/// <param name="text">The header text.</param>
/// <param name="style">The header style.</param>
/// <param name="alignment">The header alignment.</param>
public Header(string text, Style? style = null, Justify? alignment = null)
{
Text = text ?? throw new ArgumentNullException(nameof(text));
Style = style;
Alignment = alignment;
}
/// <summary>
/// Sets the header style.
/// </summary>
/// <param name="style">The header style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Header SetStyle(Style? style)
{
Style = style ?? Style.Plain;
return this;
}
/// <summary>
/// Sets the header alignment.
/// </summary>
/// <param name="alignment">The header alignment.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public Header SetAlignment(Justify alignment)
{
Alignment = alignment;
return this;
}
}
}

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Rendering;
@@ -15,13 +16,13 @@ namespace Spectre.Console
private readonly IRenderable _child;
/// <inheritdoc/>
public BorderKind Border { get; set; } = BorderKind.Square;
public BorderKind BorderKind { get; set; } = BorderKind.Square;
/// <inheritdoc/>
public bool SafeBorder { get; set; } = true;
/// <inheritdoc/>
public Color? BorderColor { get; set; }
public Style? BorderStyle { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not the panel should
@@ -35,6 +36,11 @@ namespace Spectre.Console
/// </summary>
public Padding Padding { get; set; } = new Padding(1, 1);
/// <summary>
/// Gets or sets the header.
/// </summary>
public Header? Header { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Panel"/> class.
/// </summary>
@@ -58,15 +64,15 @@ namespace Spectre.Console
{
var childWidth = _child.Measure(context, maxWidth);
return new Measurement(
childWidth.Min + 2 + Padding.GetHorizontalPadding(),
childWidth.Max + 2 + Padding.GetHorizontalPadding());
childWidth.Min + EdgeWidth + Padding.GetHorizontalPadding(),
childWidth.Max + EdgeWidth + Padding.GetHorizontalPadding());
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var border = SpectreBorder.GetBorder(Border, (context.LegacyConsole || !context.Unicode) && SafeBorder);
var borderStyle = new Style(BorderColor, null, null);
var border = SpectreBorder.GetBorder(BorderKind, (context.LegacyConsole || !context.Unicode) && SafeBorder);
var borderStyle = BorderStyle ?? Style.Plain;
var paddingWidth = Padding.GetHorizontalPadding();
var childWidth = maxWidth - EdgeWidth - paddingWidth;
@@ -77,21 +83,16 @@ namespace Spectre.Console
childWidth = measurement.Max;
}
var panelWidth = childWidth + paddingWidth;
var panelWidth = childWidth + EdgeWidth + paddingWidth;
panelWidth = Math.Min(panelWidth, maxWidth);
var result = new List<Segment>();
// Panel top
var result = new List<Segment>
{
new Segment(border.GetPart(BorderPart.HeaderTopLeft), borderStyle),
new Segment(border.GetPart(BorderPart.HeaderTop, panelWidth), borderStyle),
new Segment(border.GetPart(BorderPart.HeaderTopRight), borderStyle),
Segment.LineBreak,
};
// Render the child.
var childSegments = _child.Render(context, childWidth);
AddTopBorder(result, context, border, borderStyle, panelWidth);
// Split the child segments into lines.
var childSegments = _child.Render(context, childWidth);
foreach (var line in Segment.SplitLines(childSegments, panelWidth))
{
result.Add(new Segment(border.GetPart(BorderPart.CellLeft), borderStyle));
@@ -126,12 +127,62 @@ namespace Spectre.Console
}
// Panel bottom
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft), borderStyle));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, panelWidth), borderStyle));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight), borderStyle));
result.Add(Segment.LineBreak);
AddBottomBorder(result, border, borderStyle, panelWidth);
return result;
}
private static void AddBottomBorder(List<Segment> result, SpectreBorder border, Style borderStyle, int panelWidth)
{
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft), borderStyle));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, panelWidth - EdgeWidth), borderStyle));
result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight), borderStyle));
result.Add(Segment.LineBreak);
}
private void AddTopBorder(List<Segment> segments, RenderContext context, SpectreBorder border, Style borderStyle, int panelWidth)
{
segments.Add(new Segment(border.GetPart(BorderPart.HeaderTopLeft), borderStyle));
if (Header != null)
{
var leftSpacing = 0;
var rightSpacing = 0;
var headerWidth = panelWidth - (EdgeWidth * 2);
var header = Segment.TruncateWithEllipsis(Header.Text, Header.Style ?? borderStyle, context.Encoding, headerWidth);
var excessWidth = headerWidth - header.CellLength(context.Encoding);
if (excessWidth > 0)
{
switch (Header.Alignment ?? Justify.Left)
{
case Justify.Left:
leftSpacing = 0;
rightSpacing = excessWidth;
break;
case Justify.Right:
leftSpacing = excessWidth;
rightSpacing = 0;
break;
case Justify.Center:
leftSpacing = excessWidth / 2;
rightSpacing = (excessWidth / 2) + (excessWidth % 2);
break;
}
}
segments.Add(new Segment(border.GetPart(BorderPart.HeaderTop, leftSpacing + 1), borderStyle));
segments.Add(header);
segments.Add(new Segment(border.GetPart(BorderPart.HeaderTop, rightSpacing + 1), borderStyle));
}
else
{
segments.Add(new Segment(border.GetPart(BorderPart.HeaderTop, panelWidth - EdgeWidth), borderStyle));
}
segments.Add(new Segment(border.GetPart(BorderPart.HeaderTopRight), borderStyle));
segments.Add(Segment.LineBreak);
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
namespace Spectre.Console.Rendering
{
/// <summary>
/// Contains extension methods for <see cref="Panel"/>.
/// </summary>
public static class PanelExtensions
{
/// <summary>
/// Sets the panel header.
/// </summary>
/// <param name="panel">The panel.</param>
/// <param name="text">The header text.</param>
/// <param name="style">The header style.</param>
/// <param name="alignment">The header alignment.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Panel SetHeader(this Panel panel, string text, Style? style = null, Justify? alignment = null)
{
if (panel is null)
{
throw new ArgumentNullException(nameof(panel));
}
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
return SetHeader(panel, new Header(text, style, alignment));
}
/// <summary>
/// Sets the panel header.
/// </summary>
/// <param name="panel">The panel.</param>
/// <param name="header">The header to use.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Panel SetHeader(this Panel panel, Header header)
{
if (panel is null)
{
throw new ArgumentNullException(nameof(panel));
}
panel.Header = header;
return panel;
}
}
}

View File

@@ -122,7 +122,7 @@ namespace Spectre.Console
var min = _lines.Max(line => line.Max(segment => segment.CellLength(context.Encoding)));
var max = _lines.Max(x => x.CellWidth(context.Encoding));
return new Measurement(min, max);
return new Measurement(min, Math.Min(max, maxWidth));
}
/// <inheritdoc/>

View File

@@ -13,10 +13,25 @@ namespace Spectre.Console.Rendering
[DebuggerDisplay("{Text,nq}")]
public class Segment
{
private readonly bool _mutable;
private string _text;
/// <summary>
/// Gets the segment text.
/// </summary>
public string Text { get; internal set; }
public string Text
{
get => _text;
private set
{
if (!_mutable)
{
throw new NotSupportedException();
}
_text = value;
}
}
/// <summary>
/// Gets a value indicating whether or not this is an expicit line break
@@ -39,12 +54,12 @@ namespace Spectre.Console.Rendering
/// <summary>
/// Gets a segment representing a line break.
/// </summary>
public static Segment LineBreak { get; } = new Segment(Environment.NewLine, Style.Plain, true);
public static Segment LineBreak { get; } = new Segment(Environment.NewLine, Style.Plain, true, false);
/// <summary>
/// Gets an empty segment.
/// </summary>
public static Segment Empty { get; } = new Segment(string.Empty, Style.Plain);
public static Segment Empty { get; } = new Segment(string.Empty, Style.Plain, false, false);
/// <summary>
/// Initializes a new instance of the <see cref="Segment"/> class.
@@ -65,14 +80,16 @@ namespace Spectre.Console.Rendering
{
}
private Segment(string text, Style style, bool lineBreak)
private Segment(string text, Style style, bool lineBreak, bool mutable = true)
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
Text = text.NormalizeLineEndings();
_mutable = mutable;
_text = text.NormalizeLineEndings();
Style = style;
IsLineBreak = lineBreak;
IsWhiteSpace = string.IsNullOrWhiteSpace(text);
@@ -305,13 +322,21 @@ namespace Spectre.Console.Rendering
}
else if (overflow == Overflow.Ellipsis)
{
result.Add(new Segment(segment.Text.Substring(0, width - 1), segment.Style));
result.Add(new Segment("…", segment.Style));
result.Add(new Segment(segment.Text.Substring(0, width - 1) + "…", segment.Style));
}
return result;
}
internal static Segment TruncateWithEllipsis(string text, Style style, Encoding encoding, int maxWidth)
{
return SplitOverflow(
new Segment(text, style),
Overflow.Ellipsis,
encoding,
maxWidth).First();
}
internal static List<List<SegmentLine>> MakeSameHeight(int cellHeight, List<List<SegmentLine>> cells)
{
foreach (var cell in cells)

View File

@@ -18,7 +18,7 @@ namespace Spectre.Console
// https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/table.py#L394
private List<int> CalculateColumnWidths(RenderContext options, int maxWidth)
{
var width_ranges = _columns.Select(column => MeasureColumn(column, options, maxWidth));
var width_ranges = _columns.Select(column => MeasureColumn(column, options, maxWidth)).ToArray();
var widths = width_ranges.Select(range => range.Max).ToList();
var tableWidth = widths.Sum();
@@ -117,9 +117,17 @@ namespace Spectre.Console
private int GetExtraWidth(bool includePadding)
{
var separators = _columns.Count - 1;
var hideBorder = BorderKind == BorderKind.None;
var separators = hideBorder ? 0 : _columns.Count - 1;
var edges = hideBorder ? 0 : EdgeCount;
var padding = includePadding ? _columns.Select(x => x.Padding.GetHorizontalPadding()).Sum() : 0;
return separators + EdgeCount + padding;
if (!PadRightCell)
{
padding -= _columns.Last().Padding.Right;
}
return separators + edges + padding;
}
}
}

View File

@@ -26,10 +26,10 @@ namespace Spectre.Console
public int RowCount => _rows.Count;
/// <inheritdoc/>
public BorderKind Border { get; set; } = BorderKind.Square;
public BorderKind BorderKind { get; set; } = BorderKind.Square;
/// <inheritdoc/>
public Color? BorderColor { get; set; }
public Style? BorderStyle { get; set; }
/// <inheritdoc/>
public bool SafeBorder { get; set; } = true;
@@ -125,17 +125,19 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(columns));
}
if (columns.Length < _columns.Count)
{
throw new InvalidOperationException("The number of row columns are less than the number of table columns.");
}
if (columns.Length > _columns.Count)
{
throw new InvalidOperationException("The number of row columns are greater than the number of table columns.");
}
_rows.Add(columns.ToList());
// Need to add missing columns?
if (columns.Length < _columns.Count)
{
var diff = _columns.Count - columns.Length;
Enumerable.Range(0, diff).ForEach(_ => _rows.Last().Add(Text.Empty));
}
}
/// <inheritdoc/>
@@ -168,13 +170,13 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(context));
}
var border = SpectreBorder.GetBorder(Border, (context.LegacyConsole || !context.Unicode) && SafeBorder);
var borderStyle = new Style(BorderColor, null, null);
var border = SpectreBorder.GetBorder(BorderKind, (context.LegacyConsole || !context.Unicode) && SafeBorder);
var borderStyle = BorderStyle ?? Style.Plain;
var tableWidth = maxWidth;
var showBorder = Border != BorderKind.None;
var hideBorder = Border == BorderKind.None;
var showBorder = BorderKind != BorderKind.None;
var hideBorder = BorderKind == BorderKind.None;
var hasRows = _rows.Count > 0;
if (Width != null)

View File

@@ -1,6 +1,6 @@
using System;
namespace Spectre.Console.Rendering
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="IHasBorder"/>.
@@ -8,13 +8,61 @@ namespace Spectre.Console.Rendering
public static class BorderExtensions
{
/// <summary>
/// Sets the border.
/// Do not display a border.
/// </summary>
/// <typeparam name="T">The object that has a border.</typeparam>
/// <typeparam name="T">An object type with a border.</typeparam>
/// <param name="obj">The object to set the border for.</param>
/// <param name="border">The border to use.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T SetBorder<T>(this T obj, BorderKind border)
public static T NoBorder<T>(this T obj)
where T : class, IHasBorder
{
return SetBorderKind(obj, BorderKind.None);
}
/// <summary>
/// Display a square border.
/// </summary>
/// <typeparam name="T">An object type with a border.</typeparam>
/// <param name="obj">The object to set the border for.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T SquareBorder<T>(this T obj)
where T : class, IHasBorder
{
return SetBorderKind(obj, BorderKind.Square);
}
/// <summary>
/// Display an ASCII border.
/// </summary>
/// <typeparam name="T">An object type with a border.</typeparam>
/// <param name="obj">The object to set the border for.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T AsciiBorder<T>(this T obj)
where T : class, IHasBorder
{
return SetBorderKind(obj, BorderKind.Ascii);
}
/// <summary>
/// Display a rounded border.
/// </summary>
/// <typeparam name="T">An object type with a border.</typeparam>
/// <param name="obj">The object to set the border for.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T RoundedBorder<T>(this T obj)
where T : class, IHasBorder
{
return SetBorderKind(obj, BorderKind.Rounded);
}
/// <summary>
/// Sets the border kind.
/// </summary>
/// <typeparam name="T">An object type with a border.</typeparam>
/// <param name="obj">The object to set the border for.</param>
/// <param name="border">The border kind to use.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T SetBorderKind<T>(this T obj, BorderKind border)
where T : class, IHasBorder
{
if (obj is null)
@@ -22,14 +70,14 @@ namespace Spectre.Console.Rendering
throw new ArgumentNullException(nameof(obj));
}
obj.Border = border;
obj.BorderKind = border;
return obj;
}
/// <summary>
/// Disables the safe border.
/// </summary>
/// <typeparam name="T">The object that has a border.</typeparam>
/// <typeparam name="T">An object type with a border.</typeparam>
/// <param name="obj">The object to set the border for.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T NoSafeBorder<T>(this T obj)
@@ -44,12 +92,31 @@ namespace Spectre.Console.Rendering
return obj;
}
/// <summary>
/// Sets the border style.
/// </summary>
/// <typeparam name="T">An object type with a border.</typeparam>
/// <param name="obj">The object to set the border color for.</param>
/// <param name="style">The border style to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T SetBorderStyle<T>(this T obj, Style style)
where T : class, IHasBorder
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.BorderStyle = style;
return obj;
}
/// <summary>
/// Sets the border color.
/// </summary>
/// <typeparam name="T">The object that has a border.</typeparam>
/// <typeparam name="T">An object type with a border.</typeparam>
/// <param name="obj">The object to set the border color for.</param>
/// <param name="color">The color to set.</param>
/// <param name="color">The border color to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T SetBorderColor<T>(this T obj, Color color)
where T : class, IHasBorder
@@ -59,7 +126,7 @@ namespace Spectre.Console.Rendering
throw new ArgumentNullException(nameof(obj));
}
obj.BorderColor = color;
obj.BorderStyle = (obj.BorderStyle ?? Style.Plain).WithForeground(color);
return obj;
}
}

View File

@@ -1,6 +1,6 @@
using System;
namespace Spectre.Console.Rendering
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="IExpandable"/>.

View File

@@ -15,11 +15,11 @@ namespace Spectre.Console
/// <summary>
/// Gets or sets the kind of border to use.
/// </summary>
public BorderKind Border { get; set; }
public BorderKind BorderKind { get; set; }
/// <summary>
/// Gets or sets the border color.
/// Gets or sets the border style.
/// </summary>
public Color? BorderColor { get; set; }
public Style? BorderStyle { get; set; }
}
}

View File

@@ -0,0 +1,40 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Represents color and text decoration.
/// </summary>
public sealed partial class Style : IEquatable<Style>
{
/// <summary>
/// Creates a new style from the specified foreground color.
/// </summary>
/// <param name="color">The foreground color.</param>
/// <returns>A new <see cref="Style"/> with the specified foreground color.</returns>
public static Style WithForeground(Color color)
{
return new Style(foreground: color);
}
/// <summary>
/// Creates a new style from the specified background color.
/// </summary>
/// <param name="color">The background color.</param>
/// <returns>A new <see cref="Style"/> with the specified background color.</returns>
public static Style WithBackground(Color color)
{
return new Style(background: color);
}
/// <summary>
/// Creates a new style from the specified text decoration.
/// </summary>
/// <param name="decoration">The text decoration.</param>
/// <returns>A new <see cref="Style"/> with the specified text decoration.</returns>
public static Style WithDecoration(Decoration decoration)
{
return new Style(decoration: decoration);
}
}
}

View File

@@ -6,7 +6,7 @@ namespace Spectre.Console
/// <summary>
/// Represents color and text decoration.
/// </summary>
public sealed class Style : IEquatable<Style>
public sealed partial class Style : IEquatable<Style>
{
/// <summary>
/// Gets the foreground color.

View File

@@ -0,0 +1,70 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="Style"/>.
/// </summary>
public static class StyleExtensions
{
/// <summary>
/// Creates a new style from the specified one with
/// the specified foreground color.
/// </summary>
/// <param name="style">The style.</param>
/// <param name="color">The foreground color.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Style WithForeground(this Style style, Color color)
{
if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
return new Style(
foreground: color,
background: style.Background,
decoration: style.Decoration);
}
/// <summary>
/// Creates a new style from the specified one with
/// the specified background color.
/// </summary>
/// <param name="style">The style.</param>
/// <param name="color">The background color.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Style WithBackground(this Style style, Color color)
{
if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
return new Style(
foreground: style.Foreground,
background: color,
decoration: style.Decoration);
}
/// <summary>
/// Creates a new style from the specified one with
/// the specified text decoration.
/// </summary>
/// <param name="style">The style.</param>
/// <param name="decoration">The text decoration.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Style WithDecoration(this Style style, Decoration decoration)
{
if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
return new Style(
foreground: style.Foreground,
background: style.Background,
decoration: decoration);
}
}
}