diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 91baa8429..5ca1f8400 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -77,12 +77,6 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@v3 - - name: Integration Tests - shell: bash - run: | - dotnet tool restore - dotnet example --all --skip live --skip livetable --skip prompt --skip screens - - name: Build shell: bash run: | diff --git a/README.fa.md b/README.fa.md index 5b3a4464d..9eec6edb0 100644 --- a/README.fa.md +++ b/README.fa.md @@ -4,7 +4,7 @@ _[![Spectre.Console NuGet Version](https://img.shields.io/nuget/v/spectre.consol
-یک کتابخانه NET Standard 2.0/.NET 6. که ایجاد Console Applicationهای زیبا و cross platform را آسان‌تر می‌کند. +یک کتابخانه .NET. که ایجاد Console Applicationهای زیبا و cross platform را آسان‌تر می‌کند. از کتابخانه عالی [Rich](https://github.com/willmcgugan/rich) برای پایتون، بسیار الهام گرفته شده است. ## فهرست diff --git a/README.jp.md b/README.jp.md index 2354682a5..6f3c55fe5 100644 --- a/README.jp.md +++ b/README.jp.md @@ -2,7 +2,7 @@ _[![Spectre.Console NuGet Version](https://img.shields.io/nuget/v/spectre.console.svg?style=flat&label=NuGet%3A%20Spectre.Console)](https://www.nuget.org/packages/spectre.console)_ -綺麗なコンソールアプリケーションを簡単に作成するための.NET Standard 2.0ライブラリです。 +綺麗なコンソールアプリケーションを簡単に作成するための.NETライブラリです。 Python用の素晴らしい[Rich ライブラリ](https://github.com/willmcgugan/rich)に強く影響を受けています。 ## 目次 diff --git a/README.md b/README.md index 6a9bf49cd..fd01732a0 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ _[![Spectre.Console NuGet Version](https://img.shields.io/nuget/v/spectre.console.svg?style=flat&label=NuGet%3A%20Spectre.Console)](https://www.nuget.org/packages/spectre.console)_ _[![Spectre.Console CLI NuGet Version](https://img.shields.io/nuget/v/spectre.console.cli.svg?style=flat&label=NuGet%3A%20Spectre.Console.Cli)](https://www.nuget.org/packages/spectre.console.cli)_ [![Netlify Status](https://api.netlify.com/api/v1/badges/1eaf215a-eb9c-45e4-8c64-c90b62963149/deploy-status)](https://app.netlify.com/sites/spectreconsole/deploys) -A .NET 6/.NET Standard 2.0 library that makes it easier to create beautiful, cross platform, console applications. +A .NET library that makes it easier to create beautiful, cross platform, console applications. It is heavily inspired by the excellent [Rich library](https://github.com/willmcgugan/rich) -for Python. +for Python. For detailed usage instructions, [please refer to the documentation at https://spectreconsole.net/.](https://spectreconsole.net/) ## Table of Contents @@ -81,6 +81,7 @@ Spectre.Console to show their support and to ensure the longevity of the project * [Steven Knox](https://github.com/stevenknox) * [David Pendray](https://github.com/dpen2000) * [Elmah.io](https://github.com/elmahio) +* [Tom Kerkhove](https://github.com/tomkerkhove) We really appreciate it. **Thank you very much!** @@ -100,4 +101,4 @@ Copyright © Patrik Svensson, Phil Scott, Nils Andresen 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 \ No newline at end of file +* SixLabors.ImageSharp, a library which Spectre.Console relies upon, is licensed under Apache 2.0 when distributed as part of Spectre.Console. The Six Labors Split License covers all other usage, see: https://github.com/SixLabors/ImageSharp/blob/master/LICENSE diff --git a/README.pt-BR.md b/README.pt-BR.md index 8012999f0..f88a3e7de 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -2,7 +2,7 @@ _[![Spectre.Console NuGet Versão](https://img.shields.io/nuget/v/spectre.console.svg?style=flat&label=NuGet%3A%20Spectre.Console)](https://www.nuget.org/packages/spectre.console)_ -Uma biblioteca .NET 6/.NET Standard 2.0 que torna mais fácil criar aplicativos de console bonitos e multiplataforma. +Uma biblioteca .NET que torna mais fácil criar aplicativos de console bonitos e multiplataforma. É fortemente inspirada na excelente [biblioteca Rich](https://github.com/willmcgugan/rich) para Python. diff --git a/README.zh.md b/README.zh.md index fe7598d3e..981bb5bb2 100644 --- a/README.zh.md +++ b/README.zh.md @@ -2,7 +2,7 @@ _[![Spectre.Console NuGet Version](https://img.shields.io/nuget/v/spectre.console.svg?style=flat&label=NuGet%3A%20Spectre.Console)](https://www.nuget.org/packages/spectre.console)_ -`Spectre.Console`是一个 .NET 6/.NET Standard 2.0 的库,可以更轻松地创建美观的跨平台控制台应用程序。 +`Spectre.Console`是一个 .NET 的库,可以更轻松地创建美观的跨平台控制台应用程序。 深受 [Rich](https://github.com/willmcgugan/rich) 这个Python优秀库的启发。 diff --git a/build.cake b/build.cake index a75d9d515..d61b978fc 100644 --- a/build.cake +++ b/build.cake @@ -17,7 +17,7 @@ Task("Build") DotNetBuild("./src/Spectre.Console.sln", new DotNetBuildSettings { Configuration = configuration, NoIncremental = context.HasArgument("rebuild"), - MSBuildSettings = new DotNetCoreMSBuildSettings() + MSBuildSettings = new DotNetMSBuildSettings() .TreatAllWarningsAs(MSBuildTreatAllWarningsAs.Error) }); }); @@ -29,7 +29,7 @@ Task("Build-Analyzer") DotNetBuild("./src/Spectre.Console.Analyzer.sln", new DotNetBuildSettings { Configuration = configuration, NoIncremental = context.HasArgument("rebuild"), - MSBuildSettings = new DotNetCoreMSBuildSettings() + MSBuildSettings = new DotNetMSBuildSettings() .TreatAllWarningsAs(MSBuildTreatAllWarningsAs.Error) }); }); @@ -41,7 +41,7 @@ Task("Build-Examples") DotNetBuild("./examples/Examples.sln", new DotNetBuildSettings { Configuration = configuration, NoIncremental = context.HasArgument("rebuild"), - MSBuildSettings = new DotNetCoreMSBuildSettings() + MSBuildSettings = new DotNetMSBuildSettings() .TreatAllWarningsAs(MSBuildTreatAllWarningsAs.Error) }); }); @@ -80,7 +80,7 @@ Task("Package") NoRestore = true, NoBuild = true, OutputDirectory = "./.artifacts", - MSBuildSettings = new DotNetCoreMSBuildSettings() + MSBuildSettings = new DotNetMSBuildSettings() .TreatAllWarningsAs(MSBuildTreatAllWarningsAs.Error) }); @@ -89,7 +89,7 @@ Task("Package") NoRestore = true, NoBuild = true, OutputDirectory = "./.artifacts", - MSBuildSettings = new DotNetCoreMSBuildSettings() + MSBuildSettings = new DotNetMSBuildSettings() .TreatAllWarningsAs(MSBuildTreatAllWarningsAs.Error) }); }); diff --git a/docs/Docs.csproj b/docs/Docs.csproj index 14d9b7fbd..5ae29edd3 100644 --- a/docs/Docs.csproj +++ b/docs/Docs.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 $(MSBuildProjectDirectory) $(DefaultItemExcludes);output\**;.gitignore MVC1000 diff --git a/docs/README.md b/docs/README.md index 531931dd7..29a681d04 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Documentation -To start contributing to the [Spectre.Console](https://github.com/spectreconsole/spectre.console) documentation, you will need the [.NET Core SDK](https://dot.net) 5.0.202 or higher (as defined in the repository root `global.json` file). +To start contributing to the [Spectre.Console](https://github.com/spectreconsole/spectre.console) documentation, you will need the [.NET Core SDK](https://dot.net) 7.0.100 or higher (as defined in the repository root `global.json` file). ## Running Preview Site diff --git a/docs/global.json b/docs/global.json index 49d225529..77c776f82 100644 --- a/docs/global.json +++ b/docs/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.300", + "version": "7.0.100", "rollForward": "latestFeature" } } diff --git a/docs/input/_layout.cshtml b/docs/input/_layout.cshtml index a8143e98b..b0a94d71c 100644 --- a/docs/input/_layout.cshtml +++ b/docs/input/_layout.cshtml @@ -17,7 +17,7 @@ @{ string title = Document.ContainsKey(Keys.Title) ? $"Spectre.Console - {Document.GetString(Keys.Title)}" : "Spectre.Console"; - string description = Document.ContainsKey(Constants.Description) ? Document.GetString(Constants.Description) : "Spectre.Console is a .NET Standard 2.0 library that makes it easier to create beautiful console applications"; + string description = Document.ContainsKey(Constants.Description) ? Document.GetString(Constants.Description) : "Spectre.Console is a .NET library that makes it easier to create beautiful console applications"; var card = Context.FindCard(Model.Id); var urlBase = $"https://{Document.GetString(Keys.Host)}/"; } diff --git a/docs/input/assets/casts/padder-rich.cast b/docs/input/assets/casts/padder-rich.cast new file mode 100644 index 000000000..0988b4c2f --- /dev/null +++ b/docs/input/assets/casts/padder-rich.cast @@ -0,0 +1,2 @@ +{"version": 2, "width": 40, "height": 6, "timestamp": 1667448519, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}} +[0.0, "o", " \r\n \u001b[38;5;9;48;5;0mPadded Text I\u001b[0m \u001b[38;5;12;48;5;0mPadded Text II\u001b[0m \r\n \r\n"] diff --git a/docs/input/index.md b/docs/input/index.md index a9f16e3b8..8490224e4 100644 --- a/docs/input/index.md +++ b/docs/input/index.md @@ -1,9 +1,9 @@ Title: Welcome! -Description: Spectre.Console is a .NET Standard 2.0 library that makes it easier to create beautiful console applications. +Description: Spectre.Console is a .NET library that makes it easier to create beautiful console applications. Order: 0 --- -Spectre.Console is a `.NET Standard 2.0` library that makes it easier +Spectre.Console is a `.NET` library that makes it easier to create beautiful console applications. ## Spectre.Console.AnsiConsole Features diff --git a/docs/input/widgets/columns.md b/docs/input/widgets/columns.md new file mode 100644 index 000000000..26ebba688 --- /dev/null +++ b/docs/input/widgets/columns.md @@ -0,0 +1,54 @@ +Title: Columns +Description: "Use **Columns** to render widgets in vertical columns to the console." +Highlights: + - Custom colors + - Labels + - Use your own data with a converter. +Reference: T:Spectre.Console.Columns + +--- + +Use `Columns` to render widgets in vertical columns to the console. + + + +## Usage + +### Basic usage + +```csharp +// Render two items on separate columns to Console +AnsiConsole.Write(new Columns( + new Text("Item 1"), + new Text("Item 2") + )); +``` + +### Add items from an IEnumerable + +```csharp +// Create a list of Items +var columns = new List(){ + new Text("Item 1"), + new Text("Item 2"), + new Text("Item 3") + }; + +// Render each item in list on separate line +AnsiConsole.Write(new Columns(columns)); +``` + +### Apply custom styles to each column + +```csharp +// Create a list of Items, apply separate styles to each +var columns = new List(){ + new Text("Item 1", new Style(Color.Red, Color.Black)), + new Text("Item 2", new Style(Color.Green, Color.Black)), + new Text("Item 3", new Style(Color.Blue, Color.Black)) +}; + +// Renders each item with own style +AnsiConsole.Write(new Columns(columns)); +``` + diff --git a/docs/input/widgets/padder.md b/docs/input/widgets/padder.md new file mode 100644 index 000000000..d63bc5440 --- /dev/null +++ b/docs/input/widgets/padder.md @@ -0,0 +1,68 @@ +Title: Padder +Order: 55 +Description: "Use **Padder** to add padding around a Widget." +Highlights: + - Custom colors + - Labels + - Use your own data with a converter. +Reference: T:Spectre.Console.Padder + +--- + +Use `Padder` to add padding around a Widget. + + + +## Usage + +### Basic usage + +```csharp +// Create three text elements +var paddedText_I = new Text("Padded Text I", new Style(Color.Red, Color.Black)); +var paddedText_II = new Text("Padded Text II", new Style(Color.Green, Color.Black)); +var paddedText_III = new Text("Padded Text III", new Style(Color.Blue, Color.Black)); + +// Apply padding to the three text elements +var pad_I = new Padder(paddedText_I).PadRight(16).PadBottom(0).PadTop(4); +var pad_II = new Padder(paddedText_II).PadBottom(0).PadTop(2); +var pad_III = new Padder(paddedText_III).PadLeft(16).PadBottom(0).PadTop(0); + +// Insert padded elements within single-row grid +var grid = new Grid(); + +grid.AddColumn(); +grid.AddColumn(); +grid.AddColumn(); + +grid.AddRow(pad_I, pad_II, pad_III); + +// Write grid and it's padded contents to the Console +AnsiConsole.Write(grid); +``` + +### Padding element within a padded element + +```csharp +// Create two text elements +var paddedText_I = new Text("Padded Text I", new Style(Color.Red, Color.Black)); +var paddedText_II = new Text("Padded Text II", new Style(Color.Blue, Color.Black)); + +// Create, apply padding on text elements +var pad_I = new Padder(paddedText_I).PadRight(2).PadBottom(0).PadTop(0); +var pad_II = new Padder(paddedText_II).PadLeft(2).PadBottom(0).PadTop(0); + +// Insert the text elements into a single row grid +var grid = new Grid(); + +grid.AddColumn(); +grid.AddColumn(); + +grid.AddRow(pad_I, pad_II); + +// Apply horizontal and vertical padding on the grid +var paddedGrid = new Padder(grid).Padding(4,1); + +// Write the padded grid to the Console +AnsiConsole.Write(paddedGrid); +``` \ No newline at end of file diff --git a/docs/input/widgets/panel.md b/docs/input/widgets/panel.md index 745d7e6d0..bffcffb58 100644 --- a/docs/input/widgets/panel.md +++ b/docs/input/widgets/panel.md @@ -12,7 +12,7 @@ The `Panel` widget can be used to organize text into a rendered box. ## Usage -To render a table, create a `Table` instance, passing a string to its constructor to assign the contents. +To render a panel, create a `Panel` instance, passing a string to its constructor to assign the contents. ```csharp var panel = new Panel("Hello World"); @@ -58,4 +58,4 @@ is long in length may get truncated with certain content. ```csharp // Sets the expand property panel.Expand = true; -``` \ No newline at end of file +``` diff --git a/docs/src/SocialCards/index.cshtml b/docs/src/SocialCards/index.cshtml index dc0a9b37e..749052fdf 100644 --- a/docs/src/SocialCards/index.cshtml +++ b/docs/src/SocialCards/index.cshtml @@ -15,7 +15,7 @@
-
╭─ ~/spectre.console .NET 6.0  main
+
╭─ ~/spectre.console .NET 7.0  main
╰─ dotnet run
╭────────────────────────────────────────────────────────╮
diff --git a/dotnet-tools.json b/dotnet-tools.json index c02d1a378..a0b331cdd 100644 --- a/dotnet-tools.json +++ b/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "cake.tool": { - "version": "2.3.0", + "version": "3.0.0", "commands": [ "dotnet-cake" ] diff --git a/examples/Cli/Delegates/Delegates.csproj b/examples/Cli/Delegates/Delegates.csproj index e08f84a5c..53b2a23eb 100644 --- a/examples/Cli/Delegates/Delegates.csproj +++ b/examples/Cli/Delegates/Delegates.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 false Delegates Demonstrates how to specify commands as delegates. diff --git a/examples/Cli/Demo/Demo.csproj b/examples/Cli/Demo/Demo.csproj index 6b6904a52..104414957 100644 --- a/examples/Cli/Demo/Demo.csproj +++ b/examples/Cli/Demo/Demo.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 false Demo Demonstrates the most common use cases of Spectre.Cli. diff --git a/examples/Cli/Dynamic/Dynamic.csproj b/examples/Cli/Dynamic/Dynamic.csproj index 86c9639dc..a5353a546 100644 --- a/examples/Cli/Dynamic/Dynamic.csproj +++ b/examples/Cli/Dynamic/Dynamic.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 false Dynamic Demonstrates how to define dynamic commands. diff --git a/examples/Cli/Injection/Injection.csproj b/examples/Cli/Injection/Injection.csproj index 9d9db3965..6bebd6998 100644 --- a/examples/Cli/Injection/Injection.csproj +++ b/examples/Cli/Injection/Injection.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 false Injection Demonstrates how to use dependency injection with Spectre.Cli. diff --git a/examples/Cli/Logging/Logging.csproj b/examples/Cli/Logging/Logging.csproj index e7c4cd029..5579c3c05 100644 --- a/examples/Cli/Logging/Logging.csproj +++ b/examples/Cli/Logging/Logging.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 false Logging Demonstrates how to dynamically configure Serilog for logging using parameters from a command. diff --git a/examples/Console/AlternateScreen/AlternateScreen.csproj b/examples/Console/AlternateScreen/AlternateScreen.csproj index 24be5e73c..8e18a0082 100644 --- a/examples/Console/AlternateScreen/AlternateScreen.csproj +++ b/examples/Console/AlternateScreen/AlternateScreen.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Screens Demonstrates how to use alternate screens. Widgets diff --git a/examples/Console/Borders/Borders.csproj b/examples/Console/Borders/Borders.csproj index 128b1b626..3f01d3e36 100644 --- a/examples/Console/Borders/Borders.csproj +++ b/examples/Console/Borders/Borders.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Borders Demonstrates the different kind of borders. Widgets diff --git a/examples/Console/Borders/Program.cs b/examples/Console/Borders/Program.cs index 854ef7fd8..0dca10135 100644 --- a/examples/Console/Borders/Program.cs +++ b/examples/Console/Borders/Program.cs @@ -84,7 +84,7 @@ static IRenderable CreateTable(string name, TableBorder border) private static void HorizontalRule(string title) { AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule($"[white bold]{title}[/]").RuleStyle("grey").LeftAligned()); + AnsiConsole.Write(new Rule($"[white bold]{title}[/]").RuleStyle("grey").LeftJustified()); AnsiConsole.WriteLine(); } } diff --git a/examples/Console/Calendars/Calendars.csproj b/examples/Console/Calendars/Calendars.csproj index bcf976444..21606c333 100644 --- a/examples/Console/Calendars/Calendars.csproj +++ b/examples/Console/Calendars/Calendars.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Calendars Demonstrates how to render calendars. Widgets diff --git a/examples/Console/Canvas/Canvas.csproj b/examples/Console/Canvas/Canvas.csproj index 63dfeebb7..a1a1e956b 100644 --- a/examples/Console/Canvas/Canvas.csproj +++ b/examples/Console/Canvas/Canvas.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Canvas Demonstrates how to render pixels and images. Widgets diff --git a/examples/Console/Canvas/Program.cs b/examples/Console/Canvas/Program.cs index 57135a30e..941d39fc2 100644 --- a/examples/Console/Canvas/Program.cs +++ b/examples/Console/Canvas/Program.cs @@ -40,7 +40,7 @@ public static void Main() private static void Render(IRenderable canvas, string title) { AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule($"[yellow]{title}[/]").LeftAligned().RuleStyle("grey")); + AnsiConsole.Write(new Rule($"[yellow]{title}[/]").LeftJustified().RuleStyle("grey")); AnsiConsole.WriteLine(); AnsiConsole.Write(canvas); } diff --git a/examples/Console/Charts/Charts.csproj b/examples/Console/Charts/Charts.csproj index 3fe640f2a..1d78fa695 100644 --- a/examples/Console/Charts/Charts.csproj +++ b/examples/Console/Charts/Charts.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Charts Demonstrates how to render charts in a console. Widgets diff --git a/examples/Console/Colors/Colors.csproj b/examples/Console/Colors/Colors.csproj index 642d5d0f2..5ed956574 100644 --- a/examples/Console/Colors/Colors.csproj +++ b/examples/Console/Colors/Colors.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Colors Demonstrates how to use [yellow]c[/][red]o[/][green]l[/][blue]o[/][aqua]r[/][lime]s[/] in the console. Misc diff --git a/examples/Console/Colors/Program.cs b/examples/Console/Colors/Program.cs index 8ee60dbb5..946cb3de3 100644 --- a/examples/Console/Colors/Program.cs +++ b/examples/Console/Colors/Program.cs @@ -23,7 +23,7 @@ public static void Main() { AnsiConsole.ResetColors(); AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("[yellow bold underline]3-bit Colors[/]").RuleStyle("grey").LeftAligned()); + AnsiConsole.Write(new Rule("[yellow bold underline]3-bit Colors[/]").RuleStyle("grey").LeftJustified()); AnsiConsole.WriteLine(); for (var i = 0; i < 8; i++) @@ -46,7 +46,7 @@ public static void Main() { AnsiConsole.ResetColors(); AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("[yellow bold underline]4-bit Colors[/]").RuleStyle("grey").LeftAligned()); + AnsiConsole.Write(new Rule("[yellow bold underline]4-bit Colors[/]").RuleStyle("grey").LeftJustified()); AnsiConsole.WriteLine(); for (var i = 0; i < 16; i++) @@ -69,7 +69,7 @@ public static void Main() { AnsiConsole.ResetColors(); AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("[yellow bold underline]8-bit Colors[/]").RuleStyle("grey").LeftAligned()); + AnsiConsole.Write(new Rule("[yellow bold underline]8-bit Colors[/]").RuleStyle("grey").LeftJustified()); AnsiConsole.WriteLine(); for (var i = 0; i < 16; i++) @@ -96,7 +96,7 @@ public static void Main() { AnsiConsole.ResetColors(); AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("[yellow bold underline]24-bit Colors[/]").RuleStyle("grey").LeftAligned()); + AnsiConsole.Write(new Rule("[yellow bold underline]24-bit Colors[/]").RuleStyle("grey").LeftJustified()); AnsiConsole.WriteLine(); AnsiConsole.Write(new ColorBox(width: 80, height: 15)); diff --git a/examples/Console/Columns/Columns.csproj b/examples/Console/Columns/Columns.csproj index b004dde0c..aab263561 100644 --- a/examples/Console/Columns/Columns.csproj +++ b/examples/Console/Columns/Columns.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Columns Demonstrates how to render data into columns. Widgets diff --git a/examples/Console/Cursor/Cursor.csproj b/examples/Console/Cursor/Cursor.csproj index e2153cd90..2a0a8615b 100644 --- a/examples/Console/Cursor/Cursor.csproj +++ b/examples/Console/Cursor/Cursor.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Cursor Demonstrates how to move the cursor. Misc diff --git a/examples/Console/Emojis/Emojis.csproj b/examples/Console/Emojis/Emojis.csproj index e30aea92c..71c43b741 100644 --- a/examples/Console/Emojis/Emojis.csproj +++ b/examples/Console/Emojis/Emojis.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Emojis Demonstrates how to render emojis. Misc diff --git a/examples/Console/Exceptions/Exceptions.csproj b/examples/Console/Exceptions/Exceptions.csproj index d90cfb2c8..3e67e5ede 100644 --- a/examples/Console/Exceptions/Exceptions.csproj +++ b/examples/Console/Exceptions/Exceptions.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Exceptions Demonstrates how to render formatted exceptions. Misc diff --git a/examples/Console/Exceptions/Program.cs b/examples/Console/Exceptions/Program.cs index d78e647f3..032908a93 100644 --- a/examples/Console/Exceptions/Program.cs +++ b/examples/Console/Exceptions/Program.cs @@ -19,17 +19,17 @@ public static async Task Main(string[] args) catch (Exception ex) { AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("Default").LeftAligned()); + AnsiConsole.Write(new Rule("Default").LeftJustified()); AnsiConsole.WriteLine(); AnsiConsole.WriteException(ex); AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("Compact").LeftAligned()); + AnsiConsole.Write(new Rule("Compact").LeftJustified()); AnsiConsole.WriteLine(); AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks); AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("Compact + Custom colors").LeftAligned()); + AnsiConsole.Write(new Rule("Compact + Custom colors").LeftJustified()); AnsiConsole.WriteLine(); AnsiConsole.WriteException(ex, new ExceptionSettings { @@ -56,7 +56,7 @@ public static async Task Main(string[] args) catch (Exception ex) { AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("Async").LeftAligned()); + AnsiConsole.Write(new Rule("Async").LeftJustified()); AnsiConsole.WriteLine(); AnsiConsole.WriteException(ex, ExceptionFormats.ShortenPaths); } diff --git a/examples/Console/Figlet/Figlet.csproj b/examples/Console/Figlet/Figlet.csproj index a59f0d7c0..3db0e85f0 100644 --- a/examples/Console/Figlet/Figlet.csproj +++ b/examples/Console/Figlet/Figlet.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Figlet Demonstrates how to render FIGlet text. Widgets diff --git a/examples/Console/Figlet/Program.cs b/examples/Console/Figlet/Program.cs index cda5ed55c..a8310145e 100644 --- a/examples/Console/Figlet/Program.cs +++ b/examples/Console/Figlet/Program.cs @@ -6,8 +6,8 @@ public static class Program { public static void Main(string[] args) { - AnsiConsole.Write(new FigletText("Left aligned").LeftAligned().Color(Color.Red)); + AnsiConsole.Write(new FigletText("Left aligned").LeftJustified().Color(Color.Red)); AnsiConsole.Write(new FigletText("Centered").Centered().Color(Color.Green)); - AnsiConsole.Write(new FigletText("Right aligned").RightAligned().Color(Color.Blue)); + AnsiConsole.Write(new FigletText("Right aligned").RightJustified().Color(Color.Blue)); } } diff --git a/examples/Console/Grids/Grids.csproj b/examples/Console/Grids/Grids.csproj index 4d761834b..c6991f300 100644 --- a/examples/Console/Grids/Grids.csproj +++ b/examples/Console/Grids/Grids.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Grids Demonstrates how to render grids in a console. Widgets diff --git a/examples/Console/Info/Info.csproj b/examples/Console/Info/Info.csproj index ff94b84b1..ab7310fad 100644 --- a/examples/Console/Info/Info.csproj +++ b/examples/Console/Info/Info.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Info Displays the capabilities of the current console. Misc diff --git a/examples/Console/Layout/Layout.csproj b/examples/Console/Layout/Layout.csproj new file mode 100644 index 000000000..5bb2daa1c --- /dev/null +++ b/examples/Console/Layout/Layout.csproj @@ -0,0 +1,15 @@ + + + + Exe + net7.0 + Layout + Demonstrates how to use layouts. + Widgets + + + + + + + diff --git a/examples/Console/Layout/Program.cs b/examples/Console/Layout/Program.cs new file mode 100644 index 000000000..835b6147e --- /dev/null +++ b/examples/Console/Layout/Program.cs @@ -0,0 +1,60 @@ +using System; +using Spectre.Console; + +namespace Layouts; + +public static class Program +{ + public static void Main() + { + var layout = CreateLayout(); + AnsiConsole.Write(layout); + + Console.ReadKey(true); + } + + private static Layout CreateLayout() + { + var layout = new Layout(); + + layout.SplitRows( + new Layout("Top") + .SplitColumns( + new Layout("Left") + .SplitRows( + new Layout("LeftTop"), + new Layout("LeftBottom")), + new Layout("Right").Ratio(2), + new Layout("RightRight").Size(3)), + new Layout("Bottom")); + + layout["LeftBottom"].Update( + new Panel("[blink]PRESS ANY KEY TO QUIT[/]") + .Expand() + .BorderColor(Color.Yellow) + .Padding(0, 0)); + + layout["Right"].Update( + new Panel( + new Table() + .AddColumns("[blue]Qux[/]", "[green]Corgi[/]") + .AddRow("9", "8") + .AddRow("7", "6") + .Expand()) + .Header("A [yellow]Table[/] in a [blue]Panel[/] (Ratio=2)") + .Expand()); + + layout["RightRight"].Update( + new Panel("Explicit-size-is-[yellow]3[/]") + .BorderColor(Color.Yellow) + .Padding(0, 0)); + + layout["Bottom"].Update( + new Panel( + new FigletText("Hello World")) + .Header("Some [green]Figlet[/] text") + .Expand()); + + return layout; + } +} diff --git a/examples/Console/Links/Links.csproj b/examples/Console/Links/Links.csproj index 746a0f4d2..badb83cc7 100644 --- a/examples/Console/Links/Links.csproj +++ b/examples/Console/Links/Links.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Links Demonstrates how to render links in a console. Misc diff --git a/examples/Console/Live/Live.csproj b/examples/Console/Live/Live.csproj index edfe36c6f..8aacf07a8 100644 --- a/examples/Console/Live/Live.csproj +++ b/examples/Console/Live/Live.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Live Demonstrates how to do live updates. Live diff --git a/examples/Console/LiveTable/LiveTable.csproj b/examples/Console/LiveTable/LiveTable.csproj index fb9635f5d..47d53619f 100644 --- a/examples/Console/LiveTable/LiveTable.csproj +++ b/examples/Console/LiveTable/LiveTable.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 LiveTable Demonstrates how to do live updates in a table. Live diff --git a/examples/Console/Minimal/Minimal.csproj b/examples/Console/Minimal/Minimal.csproj index 0ac0897e9..e680684b2 100644 --- a/examples/Console/Minimal/Minimal.csproj +++ b/examples/Console/Minimal/Minimal.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 enable Minimal Demonstrates a minimal console application. diff --git a/examples/Console/Panels/Panels.csproj b/examples/Console/Panels/Panels.csproj index 07f230df6..9b7969b5f 100644 --- a/examples/Console/Panels/Panels.csproj +++ b/examples/Console/Panels/Panels.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Panels Demonstrates how to render items in panels. Widgets diff --git a/examples/Console/Panels/Program.cs b/examples/Console/Panels/Program.cs index 79d18a8d3..dc9ecb845 100644 --- a/examples/Console/Panels/Program.cs +++ b/examples/Console/Panels/Program.cs @@ -17,7 +17,7 @@ public static void Main() // Left adjusted panel with text AnsiConsole.Write( - new Panel(new Text("Left adjusted\nLeft").LeftAligned()) + new Panel(new Text("Left adjusted\nLeft").LeftJustified()) .Expand() .SquareBorder() .Header("[red]Left[/]")); @@ -32,7 +32,7 @@ public static void Main() // Right adjusted, rounded panel with text AnsiConsole.Write( - new Panel(new Text("Right adjusted\nRight").RightAligned()) + new Panel(new Text("Right adjusted\nRight").RightJustified()) .Expand() .RoundedBorder() .Header("[blue]Right[/]") diff --git a/examples/Console/Paths/Paths.csproj b/examples/Console/Paths/Paths.csproj index d6d9e16b6..e86a8cfbf 100644 --- a/examples/Console/Paths/Paths.csproj +++ b/examples/Console/Paths/Paths.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Paths Demonstrates how to render paths. Widgets diff --git a/examples/Console/Paths/Program.cs b/examples/Console/Paths/Program.cs index baeac890d..ff88429c1 100644 --- a/examples/Console/Paths/Program.cs +++ b/examples/Console/Paths/Program.cs @@ -58,9 +58,9 @@ private static void WriteAligned(string path) var table = new Table().BorderColor(Color.Grey).Title("Aligned").RoundedBorder(); table.AddColumns("[grey]Alignment[/]", "[grey]Path[/]"); - table.AddRow(new Text("Left"), new TextPath(path).LeftAligned()); + table.AddRow(new Text("Left"), new TextPath(path).LeftJustified()); table.AddRow(new Text("Center"), new TextPath(path).Centered()); - table.AddRow(new Text("Right"), new TextPath(path).RightAligned()); + table.AddRow(new Text("Right"), new TextPath(path).RightJustified()); AnsiConsole.Write(table); } diff --git a/examples/Console/Progress/Progress.csproj b/examples/Console/Progress/Progress.csproj index 06a9a2873..d34e3ec9e 100644 --- a/examples/Console/Progress/Progress.csproj +++ b/examples/Console/Progress/Progress.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Progress Demonstrates how to show progress bars. Status diff --git a/examples/Console/Prompt/Program.cs b/examples/Console/Prompt/Program.cs index 07f01cca5..58dadacac 100644 --- a/examples/Console/Prompt/Program.cs +++ b/examples/Console/Prompt/Program.cs @@ -46,7 +46,7 @@ public static void Main(string[] args) // Summary AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule("[yellow]Results[/]").RuleStyle("grey").LeftAligned()); + AnsiConsole.Write(new Rule("[yellow]Results[/]").RuleStyle("grey").LeftJustified()); AnsiConsole.Write(new Table().AddColumns("[grey]Question[/]", "[grey]Answer[/]") .RoundedBorder() .BorderColor(Color.Grey) @@ -63,7 +63,7 @@ public static void Main(string[] args) private static void WriteDivider(string text) { AnsiConsole.WriteLine(); - AnsiConsole.Write(new Rule($"[yellow]{text}[/]").RuleStyle("grey").LeftAligned()); + AnsiConsole.Write(new Rule($"[yellow]{text}[/]").RuleStyle("grey").LeftJustified()); } public static bool AskConfirmation() diff --git a/examples/Console/Prompt/Prompt.csproj b/examples/Console/Prompt/Prompt.csproj index 76e9f57c6..760579cd0 100644 --- a/examples/Console/Prompt/Prompt.csproj +++ b/examples/Console/Prompt/Prompt.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 9 Prompt Demonstrates how to get input from a user. diff --git a/examples/Console/Rules/Program.cs b/examples/Console/Rules/Program.cs index 2f158eb41..442ed3fa4 100644 --- a/examples/Console/Rules/Program.cs +++ b/examples/Console/Rules/Program.cs @@ -11,14 +11,14 @@ public static void Main(string[] args) new Rule() .RuleStyle(Style.Parse("yellow")) .AsciiBorder() - .LeftAligned()); + .LeftJustified()); // Left aligned title Render( new Rule("[blue]Left aligned[/]") .RuleStyle(Style.Parse("red")) .DoubleBorder() - .LeftAligned()); + .LeftJustified()); // Centered title Render( @@ -31,7 +31,7 @@ public static void Main(string[] args) Render( new Rule("[red]Right aligned[/]") .RuleStyle(Style.Parse("blue")) - .RightAligned()); + .RightJustified()); } private static void Render(Rule rule) diff --git a/examples/Console/Rules/Rules.csproj b/examples/Console/Rules/Rules.csproj index f4840367c..7bcddd420 100644 --- a/examples/Console/Rules/Rules.csproj +++ b/examples/Console/Rules/Rules.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Rules Demonstrates how to render horizontal rules (lines). Widgets diff --git a/examples/Console/Showcase/Showcase.csproj b/examples/Console/Showcase/Showcase.csproj index cdc667f0f..3944555ea 100644 --- a/examples/Console/Showcase/Showcase.csproj +++ b/examples/Console/Showcase/Showcase.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Showcase Demonstation of Spectre.Console. Misc diff --git a/examples/Console/Status/Status.csproj b/examples/Console/Status/Status.csproj index 96a7472cb..886f93477 100644 --- a/examples/Console/Status/Status.csproj +++ b/examples/Console/Status/Status.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Status Demonstrates how to show status updates. Status diff --git a/examples/Console/Tables/Tables.csproj b/examples/Console/Tables/Tables.csproj index 7ea8eb21d..bed52c6cc 100644 --- a/examples/Console/Tables/Tables.csproj +++ b/examples/Console/Tables/Tables.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Tables Demonstrates how to render tables in a console. Widgets diff --git a/examples/Console/Trees/Trees.csproj b/examples/Console/Trees/Trees.csproj index d933118b0..0517fab11 100644 --- a/examples/Console/Trees/Trees.csproj +++ b/examples/Console/Trees/Trees.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 Trees Demonstrates how to render trees in a console. Widgets diff --git a/examples/Examples.sln b/examples/Examples.sln index 317ec667c..01a07001f 100644 --- a/examples/Examples.sln +++ b/examples/Examples.sln @@ -77,6 +77,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paths", "Console\Paths\Path EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Cli", "..\src\Spectre.Console.Cli\Spectre.Console.Cli.csproj", "{EFAADF6A-C77D-41EC-83F5-BBB4FFC5A6D7}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Layout", "Console\Layout\Layout.csproj", "{A9FDE73A-8452-4CA3-B366-3F900597E132}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -507,6 +509,18 @@ Global {EFAADF6A-C77D-41EC-83F5-BBB4FFC5A6D7}.Release|x64.Build.0 = Release|Any CPU {EFAADF6A-C77D-41EC-83F5-BBB4FFC5A6D7}.Release|x86.ActiveCfg = Release|Any CPU {EFAADF6A-C77D-41EC-83F5-BBB4FFC5A6D7}.Release|x86.Build.0 = Release|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Debug|x64.ActiveCfg = Debug|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Debug|x64.Build.0 = Debug|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Debug|x86.ActiveCfg = Debug|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Debug|x86.Build.0 = Debug|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Release|Any CPU.Build.0 = Release|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Release|x64.ActiveCfg = Release|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Release|x64.Build.0 = Release|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Release|x86.ActiveCfg = Release|Any CPU + {A9FDE73A-8452-4CA3-B366-3F900597E132}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/examples/Shared/ColorBox.cs b/examples/Shared/ColorBox.cs index ef007874a..7438c9dca 100644 --- a/examples/Shared/ColorBox.cs +++ b/examples/Shared/ColorBox.cs @@ -20,12 +20,12 @@ public ColorBox(int width, int height) _width = width; } - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { return new Measurement(1, GetWidth(maxWidth)); } - protected override IEnumerable Render(RenderContext context, int maxWidth) + protected override IEnumerable Render(RenderOptions options, int maxWidth) { maxWidth = GetWidth(maxWidth); diff --git a/examples/Shared/Shared.csproj b/examples/Shared/Shared.csproj index 358378de8..b7c9678e6 100644 --- a/examples/Shared/Shared.csproj +++ b/examples/Shared/Shared.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 false enable diff --git a/global.json b/global.json index 0b738cd89..70e3dcc50 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "6.0.400" + "version": "7.0.100" } } diff --git a/resources/scripts/Generator/Generator.csproj b/resources/scripts/Generator/Generator.csproj index b63f006c4..7b9238034 100644 --- a/resources/scripts/Generator/Generator.csproj +++ b/resources/scripts/Generator/Generator.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net7.0 default diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 64057c44e..6bf853f6b 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -32,12 +32,12 @@ - + All - + All diff --git a/src/Spectre.Console.Analyzer.Sandbox/Spectre.Console.Analyzer.Sandbox.csproj b/src/Spectre.Console.Analyzer.Sandbox/Spectre.Console.Analyzer.Sandbox.csproj index 3f799ca9b..9ca43306b 100644 --- a/src/Spectre.Console.Analyzer.Sandbox/Spectre.Console.Analyzer.Sandbox.csproj +++ b/src/Spectre.Console.Analyzer.Sandbox/Spectre.Console.Analyzer.Sandbox.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 diff --git a/src/Spectre.Console.Cli/Annotations/CommandArgumentAttribute.cs b/src/Spectre.Console.Cli/Annotations/CommandArgumentAttribute.cs index 4d7e6b02e..a3716059a 100644 --- a/src/Spectre.Console.Cli/Annotations/CommandArgumentAttribute.cs +++ b/src/Spectre.Console.Cli/Annotations/CommandArgumentAttribute.cs @@ -31,7 +31,7 @@ public sealed class CommandArgumentAttribute : Attribute /// Initializes a new instance of the class. /// /// The argument position. - /// The argument template. + /// The argument template. Wrap in <> for required arguments, [] for optional ones. For example "[MyArgument]". public CommandArgumentAttribute(int position, string template) { if (template == null) diff --git a/src/Spectre.Console.Cli/CommandParseException.cs b/src/Spectre.Console.Cli/CommandParseException.cs index 2520da89c..0f4dd1ba7 100644 --- a/src/Spectre.Console.Cli/CommandParseException.cs +++ b/src/Spectre.Console.Cli/CommandParseException.cs @@ -91,11 +91,6 @@ internal static CommandParseException LongOptionNameContainSymbol(TextBuffer rea return CommandLineParseExceptionFactory.Create(reader.Original, token, "Invalid long option name.", "Invalid character."); } - internal static CommandParseException UnterminatedQuote(string input, CommandTreeToken token) - { - return CommandLineParseExceptionFactory.Create(input, token, $"Encountered unterminated quoted string '{token.Value}'.", "Did you forget the closing quotation mark?"); - } - internal static CommandParseException UnknownCommand(CommandModel model, CommandTree? node, IEnumerable args, CommandTreeToken token) { var suggestion = CommandSuggestor.Suggest(model, node?.Command, token.Value); diff --git a/src/Spectre.Console.Cli/Internal/Binding/CommandValueResolver.cs b/src/Spectre.Console.Cli/Internal/Binding/CommandValueResolver.cs index 6812c7a7f..d2d5334ea 100644 --- a/src/Spectre.Console.Cli/Internal/Binding/CommandValueResolver.cs +++ b/src/Spectre.Console.Cli/Internal/Binding/CommandValueResolver.cs @@ -85,7 +85,7 @@ public static CommandValueLookup GetParameterValues(CommandTree? tree, ITypeReso } // Assign the value to the parameter. - binder.Bind(mapped.Parameter, resolver, converter.ConvertFromInvariantString(mapped.Value)); + binder.Bind(mapped.Parameter, resolver, converter.ConvertFromInvariantString(mapped.Value ?? string.Empty)); } } @@ -130,7 +130,13 @@ public static CommandValueLookup GetParameterValues(CommandTree? tree, ITypeReso if (parameter.ParameterType.IsArray) { // Return a converter for each array item (not the whole array) - return TypeDescriptor.GetConverter(parameter.ParameterType.GetElementType()); + var elementType = parameter.ParameterType.GetElementType(); + if (elementType == null) + { + throw new InvalidOperationException("Could not get element type"); + } + + return TypeDescriptor.GetConverter(elementType); } if (parameter.IsFlagValue()) diff --git a/src/Spectre.Console.Cli/Internal/Composer.cs b/src/Spectre.Console.Cli/Internal/Composer.cs index 2d8964b47..e692e90ce 100644 --- a/src/Spectre.Console.Cli/Internal/Composer.cs +++ b/src/Spectre.Console.Cli/Internal/Composer.cs @@ -83,14 +83,14 @@ public Composer Join(string separator, IEnumerable composers) return this; } - public Measurement Measure(RenderContext context, int maxWidth) + public Measurement Measure(RenderOptions options, int maxWidth) { - return ((IRenderable)new Markup(_content.ToString())).Measure(context, maxWidth); + return ((IRenderable)new Markup(_content.ToString())).Measure(options, maxWidth); } - public IEnumerable Render(RenderContext context, int maxWidth) + public IEnumerable Render(RenderOptions options, int maxWidth) { - return ((IRenderable)new Markup(_content.ToString())).Render(context, maxWidth); + return ((IRenderable)new Markup(_content.ToString())).Render(options, maxWidth); } public override string ToString() diff --git a/src/Spectre.Console.Cli/Internal/HelpWriter.cs b/src/Spectre.Console.Cli/Internal/HelpWriter.cs index 8d6dcfa17..d6b0b3877 100644 --- a/src/Spectre.Console.Cli/Internal/HelpWriter.cs +++ b/src/Spectre.Console.Cli/Internal/HelpWriter.cs @@ -29,13 +29,13 @@ public static IReadOnlyList Get(CommandInfo? command) private sealed class HelpOption { - public string Short { get; } - public string Long { get; } + public string? Short { get; } + public string? Long { get; } public string? Value { get; } public bool? ValueIsOptional { get; } public string? Description { get; } - public HelpOption(string @short, string @long, string? @value, bool? valueIsOptional, string? description) + public HelpOption(string? @short, string? @long, string? @value, bool? valueIsOptional, string? description) { Short = @short; Long = @long; diff --git a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs index 8801ed0b1..197f4233d 100644 --- a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs +++ b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs @@ -308,19 +308,35 @@ private void ParseOption( { // Is this a command? if (current.Command.FindCommand(valueToken.Value, CaseSensitivity) == null) - { - if (parameter != null) - { - if (parameter.ParameterKind == ParameterKind.Flag) - { - if (!CliConstants.AcceptedBooleanValues.Contains(valueToken.Value, StringComparer.OrdinalIgnoreCase)) - { - // Flags cannot be assigned a value. - throw CommandParseException.CannotAssignValueToFlag(context.Arguments, token); - } - } - - value = stream.Consume(CommandTreeToken.Kind.String)?.Value; + { + if (parameter != null) + { + if (parameter.ParameterKind == ParameterKind.Flag) + { + if (!CliConstants.AcceptedBooleanValues.Contains(valueToken.Value, StringComparer.OrdinalIgnoreCase)) + { + if (!valueToken.HadSeparator) + { + // Do nothing + // - assume valueToken is unrelated to the flag parameter (ie. we've parsed it unnecessarily) + // - rely on the "No value?" code below to set the flag to its default value + // - valueToken will be handled on the next pass of the parser + } + else + { + // Flags cannot be assigned a value. + throw CommandParseException.CannotAssignValueToFlag(context.Arguments, token); + } + } + else + { + value = stream.Consume(CommandTreeToken.Kind.String)?.Value; + } + } + else + { + value = stream.Consume(CommandTreeToken.Kind.String)?.Value; + } } else { diff --git a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeToken.cs b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeToken.cs index 189792e48..8f300723f 100644 --- a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeToken.cs +++ b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeToken.cs @@ -6,7 +6,12 @@ internal sealed class CommandTreeToken public int Position { get; } public string Value { get; } public string Representation { get; } - public bool IsGrouped { get; set; } + public bool IsGrouped { get; set; } + + /// + /// Gets or sets a value indicating whether a separater was encountered immediately before the . + /// + public bool HadSeparator { get; set; } public enum Kind { diff --git a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenizer.cs b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenizer.cs index c37303b06..900692848 100644 --- a/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenizer.cs +++ b/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenizer.cs @@ -29,7 +29,14 @@ public static CommandTreeTokenizerResult Tokenize(IEnumerable args) var context = new CommandTreeTokenizerContext(); foreach (var arg in args) - { + { + if (string.IsNullOrEmpty(arg)) + { + // Null strings in the args array are still represented as tokens + tokens.Add(new CommandTreeToken(CommandTreeToken.Kind.String, position, string.Empty, string.Empty)); + continue; + } + var start = position; var reader = new TextBuffer(previousReader, arg); @@ -48,39 +55,30 @@ public static CommandTreeTokenizerResult Tokenize(IEnumerable args) } private static int ParseToken(CommandTreeTokenizerContext context, TextBuffer reader, int position, int start, List tokens) - { - while (reader.Peek() != -1) - { - if (reader.ReachedEnd) - { - position += reader.Position - start; - break; - } - - var character = reader.Peek(); - - // Eat whitespace - if (char.IsWhiteSpace(character)) - { - reader.Consume(); - continue; - } - - if (character == '-') - { - // Option - tokens.AddRange(ScanOptions(context, reader)); - } - else - { - // Command or argument - tokens.Add(ScanString(context, reader)); - } - - // Flush remaining tokens - context.FlushRemaining(); - } - + { + if (!reader.ReachedEnd && reader.Peek() == '-') + { + // Option + tokens.AddRange(ScanOptions(context, reader)); + } + else + { + // Command or argument + while (reader.Peek() != -1) + { + if (reader.ReachedEnd) + { + position += reader.Position - start; + break; + } + + tokens.Add(ScanString(context, reader)); + + // Flush remaining tokens + context.FlushRemaining(); + } + } + return position; } @@ -89,15 +87,6 @@ private static CommandTreeToken ScanString( TextBuffer reader, char[]? stop = null) { - if (reader.TryPeek(out var character)) - { - // Is this a quoted string? - if (character == '\"') - { - return ScanQuotedString(context, reader); - } - } - var position = reader.Position; var builder = new StringBuilder(); while (!reader.ReachedEnd) @@ -113,48 +102,8 @@ private static CommandTreeToken ScanString( builder.Append(current); } - var value = builder.ToString(); - return new CommandTreeToken(CommandTreeToken.Kind.String, position, value.Trim(), value); - } - - private static CommandTreeToken ScanQuotedString(CommandTreeTokenizerContext context, TextBuffer reader) - { - var position = reader.Position; - - context.FlushRemaining(); - reader.Consume('\"'); - - var builder = new StringBuilder(); - var terminated = false; - while (!reader.ReachedEnd) - { - var character = reader.Peek(); - if (character == '\"') - { - terminated = true; - reader.Read(); - break; - } - - builder.Append(reader.Read()); - } - - if (!terminated) - { - var unterminatedQuote = builder.ToString(); - var token = new CommandTreeToken(CommandTreeToken.Kind.String, position, unterminatedQuote, $"\"{unterminatedQuote}"); - throw CommandParseException.UnterminatedQuote(reader.Original, token); - } - - var quotedString = builder.ToString(); - - // Add to the context - context.AddRemaining(quotedString); - - return new CommandTreeToken( - CommandTreeToken.Kind.String, - position, quotedString, - quotedString); + var value = builder.ToString(); + return new CommandTreeToken(CommandTreeToken.Kind.String, position, value, value); } private static IEnumerable ScanOptions(CommandTreeTokenizerContext context, TextBuffer reader) @@ -166,7 +115,7 @@ private static IEnumerable ScanOptions(CommandTreeTokenizerCon reader.Consume('-'); context.AddRemaining('-'); - if (!reader.TryPeek(out var character)) + if (!reader.TryPeek(out var character) || character == ' ') { var token = new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position, "-", "-"); throw CommandParseException.OptionHasNoName(reader.Original, token); @@ -200,8 +149,10 @@ private static IEnumerable ScanOptions(CommandTreeTokenizerCon var token = new CommandTreeToken(CommandTreeToken.Kind.String, reader.Position, "=", "="); throw CommandParseException.OptionValueWasExpected(reader.Original, token); } - - result.Add(ScanString(context, reader)); + + var tokenValue = ScanString(context, reader); + tokenValue.HadSeparator = true; + result.Add(tokenValue); } } @@ -235,12 +186,38 @@ private static IEnumerable ScanShortOptions(CommandTreeTokeniz ? new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position, value, $"-{value}") : new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position + result.Count, value, value)); } - else + else if (result.Count == 0 && char.IsDigit(current)) { + // We require short options to be named with letters. Short options that start with a number + // ("-1", "-2ab", "-3..7") may actually mean values (either for options or arguments) and will + // be tokenized as strings. This block handles parsing those cases, but we only allow this + // when the digit is the first character in the token (i.e. "-a1" is always an error), hence the + // result.Count == 0 check above. + string value = string.Empty; + + while (!reader.ReachedEnd) + { + char c = reader.Peek(); + + if (char.IsWhiteSpace(c)) + { + break; + } + + value += c.ToString(CultureInfo.InvariantCulture); + reader.Read(); + } + + value = "-" + value; // Prefix with the minus sign that we originally thought to mean a short option + result.Add(new CommandTreeToken(CommandTreeToken.Kind.String, position, value, value)); + } + else + { // Create a token representing the short option. - var tokenPosition = position + 1 + result.Count; - var represntation = current.ToString(CultureInfo.InvariantCulture); - var token = new CommandTreeToken(CommandTreeToken.Kind.ShortOption, tokenPosition, represntation, represntation); + var representation = current.ToString(CultureInfo.InvariantCulture); + var tokenPosition = position + 1 + result.Count; + var token = new CommandTreeToken(CommandTreeToken.Kind.ShortOption, tokenPosition, representation, representation); + throw CommandParseException.InvalidShortOptionName(reader.Original, token); } } @@ -271,7 +248,7 @@ private static CommandTreeToken ScanLongOption(CommandTreeTokenizerContext conte var name = ScanString(context, reader, new[] { '=', ':' }); // Perform validation of the name. - if (name.Value.Length == 0) + if (name.Value == " ") { throw CommandParseException.LongOptionNameIsMissing(reader, position); } diff --git a/src/Spectre.Console.Cli/Spectre.Console.Cli.csproj b/src/Spectre.Console.Cli/Spectre.Console.Cli.csproj index 42fe92248..658dd16a8 100644 --- a/src/Spectre.Console.Cli/Spectre.Console.Cli.csproj +++ b/src/Spectre.Console.Cli/Spectre.Console.Cli.csproj @@ -1,7 +1,7 @@ - net6.0;netstandard2.0 + net7.0;net6.0;netstandard2.0 enable true SA1633 @@ -17,18 +17,18 @@ - + + 3.0.0 + False + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - 3.0.0 - False - - diff --git a/src/Spectre.Console.ImageSharp/CanvasImage.cs b/src/Spectre.Console.ImageSharp/CanvasImage.cs index 999ad91e4..aba8b016d 100644 --- a/src/Spectre.Console.ImageSharp/CanvasImage.cs +++ b/src/Spectre.Console.ImageSharp/CanvasImage.cs @@ -71,7 +71,7 @@ public CanvasImage(Stream data) } /// - protected override Measurement Measure(RenderContext context, int maxWidth) + protected override Measurement Measure(RenderOptions options, int maxWidth) { if (PixelWidth < 0) { @@ -88,7 +88,7 @@ protected override Measurement Measure(RenderContext context, int maxWidth) } /// - protected override IEnumerable Render(RenderContext context, int maxWidth) + protected override IEnumerable Render(RenderOptions options, int maxWidth) { var image = Image; @@ -138,6 +138,6 @@ protected override IEnumerable Render(RenderContext context, int maxWid } } - return ((IRenderable)canvas).Render(context, maxWidth); + return ((IRenderable)canvas).Render(options, maxWidth); } } \ No newline at end of file diff --git a/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj b/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj index 5564af283..8b57cac4f 100644 --- a/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj +++ b/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj @@ -1,7 +1,7 @@ - net6.0;netstandard2.0 + net7.0;net6.0;netstandard2.0 enable true A library that extends Spectre.Console with ImageSharp superpowers. diff --git a/src/Spectre.Console.Testing/Spectre.Console.Testing.csproj b/src/Spectre.Console.Testing/Spectre.Console.Testing.csproj index 851b890d5..c182429f9 100644 --- a/src/Spectre.Console.Testing/Spectre.Console.Testing.csproj +++ b/src/Spectre.Console.Testing/Spectre.Console.Testing.csproj @@ -1,7 +1,7 @@ - net6.0;netstandard2.0 + net7.0;net6.0;netstandard2.0 false enable true diff --git a/src/Spectre.Console.Testing/TestCapabilities.cs b/src/Spectre.Console.Testing/TestCapabilities.cs index 1064bd5a9..d4f702b87 100644 --- a/src/Spectre.Console.Testing/TestCapabilities.cs +++ b/src/Spectre.Console.Testing/TestCapabilities.cs @@ -27,11 +27,17 @@ public sealed class TestCapabilities : IReadOnlyCapabilities public bool Unicode { get; set; } /// - /// Creates a with the same capabilities as this instace. + /// Creates a with the same capabilities as this instace. /// - /// A with the same capabilities as this instace. - public RenderContext CreateRenderContext() + /// The console. + /// A with the same capabilities as this instace. + public RenderOptions CreateRenderContext(IAnsiConsole console) { - return new RenderContext(this); + if (console is null) + { + throw new ArgumentNullException(nameof(console)); + } + + return RenderOptions.Create(console, this); } } \ No newline at end of file diff --git a/src/Spectre.Console.Testing/TestConsoleExtensions.cs b/src/Spectre.Console.Testing/TestConsoleExtensions.cs index c2397d0c6..545414c9a 100644 --- a/src/Spectre.Console.Testing/TestConsoleExtensions.cs +++ b/src/Spectre.Console.Testing/TestConsoleExtensions.cs @@ -52,6 +52,31 @@ public static TestConsole Width(this TestConsole console, int width) return console; } + /// + /// Sets the console height. + /// + /// The console. + /// The console height. + /// The same instance so that multiple calls can be chained. + public static TestConsole Height(this TestConsole console, int width) + { + console.Profile.Height = width; + return console; + } + + /// + /// Sets the console size. + /// + /// The console. + /// The console size. + /// The same instance so that multiple calls can be chained. + public static TestConsole Size(this TestConsole console, Size size) + { + console.Profile.Width = size.Width; + console.Profile.Height = size.Height; + return console; + } + /// /// Turns on emitting of VT/ANSI sequences. /// diff --git a/src/Spectre.Console.v3.ncrunchsolution b/src/Spectre.Console.v3.ncrunchsolution new file mode 100644 index 000000000..10420ac91 --- /dev/null +++ b/src/Spectre.Console.v3.ncrunchsolution @@ -0,0 +1,6 @@ + + + True + True + + \ No newline at end of file diff --git a/src/Spectre.Console/Extensions/AlignExtensions.cs b/src/Spectre.Console/Extensions/AlignExtensions.cs new file mode 100644 index 000000000..f2c700eea --- /dev/null +++ b/src/Spectre.Console/Extensions/AlignExtensions.cs @@ -0,0 +1,106 @@ +namespace Spectre.Console.Extensions; + +/// +/// Contains extension methods for . +/// +public static class AlignExtensions +{ + /// + /// Sets the width. + /// + /// The object. + /// The width, or null for no explicit width. + /// The same instance so that multiple calls can be chained. + public static Align Width(this Align align, int? width) + { + if (align is null) + { + throw new ArgumentNullException(nameof(align)); + } + + align.Width = width; + return align; + } + + /// + /// Sets the height. + /// + /// The object. + /// The height, or null for no explicit height. + /// The same instance so that multiple calls can be chained. + public static Align Height(this Align align, int? height) + { + if (align is null) + { + throw new ArgumentNullException(nameof(align)); + } + + align.Height = height; + return align; + } + + /// + /// Sets the vertical alignment. + /// + /// The object. + /// The vertical alignment, or null for no vertical alignment. + /// The same instance so that multiple calls can be chained. + public static Align VerticalAlignment(this Align align, VerticalAlignment? vertical) + { + if (align is null) + { + throw new ArgumentNullException(nameof(align)); + } + + align.Vertical = vertical; + return align; + } + + /// + /// Sets the object to be top aligned. + /// + /// The object. + /// The same instance so that multiple calls can be chained. + public static Align TopAligned(this Align align) + { + if (align is null) + { + throw new ArgumentNullException(nameof(align)); + } + + align.Vertical = Console.VerticalAlignment.Top; + return align; + } + + /// + /// Sets the object to be middle aligned. + /// + /// The object. + /// The same instance so that multiple calls can be chained. + public static Align MiddleAligned(this Align align) + { + if (align is null) + { + throw new ArgumentNullException(nameof(align)); + } + + align.Vertical = Console.VerticalAlignment.Middle; + return align; + } + + /// + /// Sets the object to be bottom aligned. + /// + /// The object. + /// The same instance so that multiple calls can be chained. + public static Align BottomAligned(this Align align) + { + if (align is null) + { + throw new ArgumentNullException(nameof(align)); + } + + align.Vertical = Console.VerticalAlignment.Bottom; + return align; + } +} diff --git a/src/Spectre.Console/Extensions/AlignableExtensions.cs b/src/Spectre.Console/Extensions/AlignableExtensions.cs index 4efc934ac..8039f313f 100644 --- a/src/Spectre.Console/Extensions/AlignableExtensions.cs +++ b/src/Spectre.Console/Extensions/AlignableExtensions.cs @@ -77,4 +77,4 @@ public static T RightAligned(this T obj) obj.Alignment = Justify.Right; return obj; } -} \ No newline at end of file +} diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Screen.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Screen.cs index f41ba468f..e562cba0b 100644 --- a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Screen.cs +++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Screen.cs @@ -27,19 +27,18 @@ public static void AlternateScreen(this IAnsiConsole console, Action action) throw new NotSupportedException("Alternate buffers are not supported by your terminal."); } - console.ExclusivityMode.Run(() => - { - // Switch to alternate screen - console.Write(new ControlCode("\u001b[?1049h\u001b[H")); + // Switch to alternate screen + console.Write(new ControlCode("\u001b[?1049h\u001b[H")); + try + { // Execute custom action action(); - + } + finally + { // Switch back to primary screen console.Write(new ControlCode("\u001b[?1049l")); - - // Dummy result - return null; - }); + } } } \ No newline at end of file diff --git a/src/Spectre.Console/Extensions/EnumerableExtensions.cs b/src/Spectre.Console/Extensions/EnumerableExtensions.cs index ba43319e6..b41d4c7bc 100644 --- a/src/Spectre.Console/Extensions/EnumerableExtensions.cs +++ b/src/Spectre.Console/Extensions/EnumerableExtensions.cs @@ -110,7 +110,7 @@ public static IEnumerable SelectIndex(this IEnumerable s return source.Select((value, index) => func(value, index)); } -#if !NET5_0_OR_GREATER +#if !NET6_0_OR_GREATER public static IEnumerable<(TFirst First, TSecond Second)> Zip( this IEnumerable source, IEnumerable first) { diff --git a/src/Spectre.Console/Extensions/HasJustificationExtensions.cs b/src/Spectre.Console/Extensions/HasJustificationExtensions.cs new file mode 100644 index 000000000..63b8c4df7 --- /dev/null +++ b/src/Spectre.Console/Extensions/HasJustificationExtensions.cs @@ -0,0 +1,80 @@ +namespace Spectre.Console; + +/// +/// Contains extension methods for . +/// +public static class HasJustificationExtensions +{ + /// + /// Sets the justification for an object. + /// + /// The type that can be justified. + /// The alignable object. + /// The alignment. + /// The same instance so that multiple calls can be chained. + public static T Justify(this T obj, Justify? alignment) + where T : class, IHasJustification + { + if (obj is null) + { + throw new System.ArgumentNullException(nameof(obj)); + } + + obj.Justification = alignment; + return obj; + } + + /// + /// Sets the object to be left justified. + /// + /// The type that can be justified. + /// The alignable object. + /// The same instance so that multiple calls can be chained. + public static T LeftJustified(this T obj) + where T : class, IHasJustification + { + if (obj is null) + { + throw new System.ArgumentNullException(nameof(obj)); + } + + obj.Justification = Console.Justify.Left; + return obj; + } + + /// + /// Sets the object to be centered. + /// + /// The type that can be justified. + /// The alignable object. + /// The same instance so that multiple calls can be chained. + public static T Centered(this T obj) + where T : class, IHasJustification + { + if (obj is null) + { + throw new System.ArgumentNullException(nameof(obj)); + } + + obj.Justification = Console.Justify.Center; + return obj; + } + + /// + /// Sets the object to be right justified. + /// + /// The type that can be justified. + /// The alignable object. + /// The same instance so that multiple calls can be chained. + public static T RightJustified(this T obj) + where T : class, IHasJustification + { + if (obj is null) + { + throw new System.ArgumentNullException(nameof(obj)); + } + + obj.Justification = Console.Justify.Right; + return obj; + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Extensions/LayoutExtensions.cs b/src/Spectre.Console/Extensions/LayoutExtensions.cs new file mode 100644 index 000000000..6acea7b17 --- /dev/null +++ b/src/Spectre.Console/Extensions/LayoutExtensions.cs @@ -0,0 +1,58 @@ +namespace Spectre.Console; + +/// +/// Contains extension methods for . +/// +public static class LayoutExtensions +{ + /// + /// Sets the ratio of the layout. + /// + /// The layout. + /// The ratio. + /// The same instance so that multiple calls can be chained. + public static Layout Ratio(this Layout layout, int ratio) + { + if (layout is null) + { + throw new ArgumentNullException(nameof(layout)); + } + + layout.Ratio = ratio; + return layout; + } + + /// + /// Sets the size of the layout. + /// + /// The layout. + /// The size. + /// The same instance so that multiple calls can be chained. + public static Layout Size(this Layout layout, int size) + { + if (layout is null) + { + throw new ArgumentNullException(nameof(layout)); + } + + layout.Size = size; + return layout; + } + + /// + /// Sets the minimum width of the layout. + /// + /// The layout. + /// The size. + /// The same instance so that multiple calls can be chained. + public static Layout MinimumSize(this Layout layout, int size) + { + if (layout is null) + { + throw new ArgumentNullException(nameof(layout)); + } + + layout.MinimumSize = size; + return layout; + } +} diff --git a/src/Spectre.Console/Extensions/PanelExtensions.cs b/src/Spectre.Console/Extensions/PanelExtensions.cs index cc957e404..d83746503 100644 --- a/src/Spectre.Console/Extensions/PanelExtensions.cs +++ b/src/Spectre.Console/Extensions/PanelExtensions.cs @@ -24,7 +24,7 @@ public static Panel Header(this Panel panel, string text, Justify? alignment = n throw new ArgumentNullException(nameof(text)); } - alignment ??= panel.Header?.Alignment; + alignment ??= panel.Header?.Justification; return Header(panel, new PanelHeader(text, alignment)); } @@ -44,7 +44,7 @@ public static Panel HeaderAlignment(this Panel panel, Justify alignment) if (panel.Header != null) { // Update existing style - panel.Header.Alignment = alignment; + panel.Header.Justification = alignment; } else { diff --git a/src/Spectre.Console/Extensions/RenderOptionsExtensions.cs b/src/Spectre.Console/Extensions/RenderOptionsExtensions.cs new file mode 100644 index 000000000..6e501dab1 --- /dev/null +++ b/src/Spectre.Console/Extensions/RenderOptionsExtensions.cs @@ -0,0 +1,10 @@ +namespace Spectre.Console; + +internal static class RenderOptionsExtensions +{ + public static BoxBorder GetSafeBorder(this RenderOptions options, T border) + where T : IHasBoxBorder, IHasBorder + { + return BoxExtensions.GetSafeBorder(border.Border, !options.Unicode && border.UseSafeBorder); + } +} diff --git a/src/Spectre.Console/Extensions/RenderableExtensions.cs b/src/Spectre.Console/Extensions/RenderableExtensions.cs index 9e72be9fe..4761b2a03 100644 --- a/src/Spectre.Console/Extensions/RenderableExtensions.cs +++ b/src/Spectre.Console/Extensions/RenderableExtensions.cs @@ -23,13 +23,13 @@ public static IEnumerable GetSegments(this IRenderable renderable, IAns throw new ArgumentNullException(nameof(renderable)); } - var context = new RenderContext(console.Profile.Capabilities); + var context = RenderOptions.Create(console, console.Profile.Capabilities); var renderables = console.Pipeline.Process(context, new[] { renderable }); return GetSegments(console, context, renderables); } - private static IEnumerable GetSegments(IAnsiConsole console, RenderContext options, IEnumerable renderables) + private static IEnumerable GetSegments(IAnsiConsole console, RenderOptions options, IEnumerable renderables) { var result = new List(); foreach (var renderable in renderables) diff --git a/src/Spectre.Console/Extensions/StringBuilderExtensions.cs b/src/Spectre.Console/Extensions/StringBuilderExtensions.cs index 49b236786..cba13aa6c 100644 --- a/src/Spectre.Console/Extensions/StringBuilderExtensions.cs +++ b/src/Spectre.Console/Extensions/StringBuilderExtensions.cs @@ -26,7 +26,7 @@ public static StringBuilder AppendWithStyle(this StringBuilder builder, Style? s public static void AppendSpan(this StringBuilder builder, ReadOnlySpan span) { // NetStandard 2 lacks the override for StringBuilder to add the span. We'll need to convert the span - // to a string for it, but for .NET 5.0 or newer we'll use the override. + // to a string for it, but for .NET 6.0 or newer we'll use the override. #if NETSTANDARD2_0 builder.Append(span.ToString()); #else diff --git a/src/Spectre.Console/Extensions/VisibilityExtensions.cs b/src/Spectre.Console/Extensions/VisibilityExtensions.cs new file mode 100644 index 000000000..38bc04bf1 --- /dev/null +++ b/src/Spectre.Console/Extensions/VisibilityExtensions.cs @@ -0,0 +1,43 @@ +namespace Spectre.Console; + +/// +/// Contains extension methods for . +/// +public static class VisibilityExtensions +{ + /// + /// Marks the specified object as being invisible. + /// + /// An object implementing . + /// The object to hide. + /// The same instance so that multiple calls can be chained. + public static T Invisible(this T obj) + where T : class, IHasVisibility + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + obj.IsVisible = false; + return obj; + } + + /// + /// Marks the specified object as being visible. + /// + /// An object implementing . + /// The object to show. + /// The same instance so that multiple calls can be chained. + public static T Visible(this T obj) + where T : class, IHasVisibility + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + obj.IsVisible = true; + return obj; + } +} \ No newline at end of file diff --git a/src/Spectre.Console/HorizontalAlignment.cs b/src/Spectre.Console/HorizontalAlignment.cs new file mode 100644 index 000000000..e8f6c21da --- /dev/null +++ b/src/Spectre.Console/HorizontalAlignment.cs @@ -0,0 +1,22 @@ +namespace Spectre.Console; + +/// +/// Represents horizontal alignment. +/// +public enum HorizontalAlignment +{ + /// + /// Left aligned. + /// + Left, + + /// + /// Centered. + /// + Center, + + /// + /// Right aligned. + /// + Right, +} diff --git a/src/Spectre.Console/IAlignable.cs b/src/Spectre.Console/IAlignable.cs index 1f4f0c0a7..6207bfa1f 100644 --- a/src/Spectre.Console/IAlignable.cs +++ b/src/Spectre.Console/IAlignable.cs @@ -9,4 +9,4 @@ public interface IAlignable /// Gets or sets the alignment. /// Justify? Alignment { get; set; } -} \ No newline at end of file +} diff --git a/src/Spectre.Console/IHasJustification.cs b/src/Spectre.Console/IHasJustification.cs new file mode 100644 index 000000000..72b75dba4 --- /dev/null +++ b/src/Spectre.Console/IHasJustification.cs @@ -0,0 +1,12 @@ +namespace Spectre.Console; + +/// +/// Represents something that has justification. +/// +public interface IHasJustification +{ + /// + /// Gets or sets the justification. + /// + Justify? Justification { get; set; } +} \ No newline at end of file diff --git a/src/Spectre.Console/IHasVisibility.cs b/src/Spectre.Console/IHasVisibility.cs new file mode 100644 index 000000000..e6bb2e3b2 --- /dev/null +++ b/src/Spectre.Console/IHasVisibility.cs @@ -0,0 +1,13 @@ +namespace Spectre.Console; + +/// +/// Represents something that can be hidden. +/// +public interface IHasVisibility +{ + /// + /// Gets or sets a value indicating whether or not the object should + /// be visible or not. + /// + bool IsVisible { get; set; } +} \ No newline at end of file diff --git a/src/Spectre.Console/IRenderable.cs b/src/Spectre.Console/IRenderable.cs index af27d38aa..6e5b2815c 100644 --- a/src/Spectre.Console/IRenderable.cs +++ b/src/Spectre.Console/IRenderable.cs @@ -8,16 +8,16 @@ public interface IRenderable /// /// Measures the renderable object. /// - /// The render context. + /// The render options. /// The maximum allowed width. /// The minimum and maximum width of the object. - Measurement Measure(RenderContext context, int maxWidth); + Measurement Measure(RenderOptions options, int maxWidth); /// /// Renders the object. /// - /// The render context. + /// The render options. /// The maximum allowed width. /// A collection of segments. - IEnumerable Render(RenderContext context, int maxWidth); + IEnumerable Render(RenderOptions options, int maxWidth); } \ No newline at end of file diff --git a/src/Spectre.Console/Internal/Aligner.cs b/src/Spectre.Console/Internal/Aligner.cs index 92c7ff304..760c7b2f8 100644 --- a/src/Spectre.Console/Internal/Aligner.cs +++ b/src/Spectre.Console/Internal/Aligner.cs @@ -89,4 +89,51 @@ public static void Align(T segments, Justify? alignment, int maxWidth) throw new NotSupportedException("Unknown alignment"); } } + + public static void AlignHorizontally(T segments, HorizontalAlignment alignment, int maxWidth) + where T : List + { + var width = Segment.CellCount(segments); + if (width >= maxWidth) + { + return; + } + + switch (alignment) + { + case HorizontalAlignment.Left: + { + var diff = maxWidth - width; + segments.Add(Segment.Padding(diff)); + break; + } + + case HorizontalAlignment.Right: + { + var diff = maxWidth - width; + segments.Insert(0, Segment.Padding(diff)); + break; + } + + case HorizontalAlignment.Center: + { + // Left side. + var diff = (maxWidth - width) / 2; + segments.Insert(0, Segment.Padding(diff)); + + // Right side + segments.Add(Segment.Padding(diff)); + var remainder = (maxWidth - width) % 2; + if (remainder != 0) + { + segments.Add(Segment.Padding(remainder)); + } + + break; + } + + default: + throw new NotSupportedException("Unknown alignment"); + } + } } \ No newline at end of file diff --git a/src/Spectre.Console/Internal/Colors/ColorSystemDetector.cs b/src/Spectre.Console/Internal/Colors/ColorSystemDetector.cs index 67d8c712c..82ee90fca 100644 --- a/src/Spectre.Console/Internal/Colors/ColorSystemDetector.cs +++ b/src/Spectre.Console/Internal/Colors/ColorSystemDetector.cs @@ -64,8 +64,8 @@ private static bool GetWindowsVersionInformation(out int major, out int build) return false; } -#if NET5_0_OR_GREATER - // The reason we're not always using this, is because it will return wrong values on other runtimes than .NET 5+ +#if NET6_0_OR_GREATER + // The reason we're not always using this, is because it will return wrong values on other runtimes than .NET 6+ // See https://docs.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/5.0/environment-osversion-returns-correct-version var version = Environment.OSVersion.Version; major = version.Major; diff --git a/src/Spectre.Console/Internal/IRatioResolvable.cs b/src/Spectre.Console/Internal/IRatioResolvable.cs new file mode 100644 index 000000000..83d89d09a --- /dev/null +++ b/src/Spectre.Console/Internal/IRatioResolvable.cs @@ -0,0 +1,22 @@ +namespace Spectre.Console; + +/// +/// Represents something that can be used to resolve ratios. +/// +internal interface IRatioResolvable +{ + /// + /// Gets the ratio. + /// + int Ratio { get; } + + /// + /// Gets the size. + /// + int? Size { get; } + + /// + /// Gets the minimum size. + /// + int MinimumSize { get; } +} diff --git a/src/Spectre.Console/Internal/Polyfill/IsExternalInit.cs b/src/Spectre.Console/Internal/Polyfill/IsExternalInit.cs new file mode 100644 index 000000000..e152620ce --- /dev/null +++ b/src/Spectre.Console/Internal/Polyfill/IsExternalInit.cs @@ -0,0 +1,15 @@ +#if NETSTANDARD2_0 +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit + { + } +} +#endif \ No newline at end of file diff --git a/src/Spectre.Console/Internal/Ratio.cs b/src/Spectre.Console/Internal/Ratio.cs index a0f3fc94d..5865b7239 100644 --- a/src/Spectre.Console/Internal/Ratio.cs +++ b/src/Spectre.Console/Internal/Ratio.cs @@ -5,6 +5,78 @@ namespace Spectre.Console; internal static class Ratio { + public static List Resolve(int total, IEnumerable edges) + { + static (int Div, float Mod) DivMod(float x, float y) + { + return ((int)(x / y), x % y); + } + + static int? GetEdgeWidth(IRatioResolvable edge) + { + if (edge.Size != null && edge.Size < edge.MinimumSize) + { + return edge.MinimumSize; + } + + return edge.Size; + } + + var sizes = edges.Select(x => GetEdgeWidth(x)).ToArray(); + + while (sizes.Any(s => s == null)) + { + // Get all edges and map them back to their index. + // Ignore edges which have a explicit size. + var flexibleEdges = sizes.Zip(edges, (a, b) => (Size: a, Edge: b)) + .Enumerate() + .Select(x => (x.Index, x.Item.Size, x.Item.Edge)) + .Where(x => x.Size == null) + .ToList(); + + // Get the remaining space + var remaining = total - sizes.Select(size => size ?? 0).Sum(); + if (remaining <= 0) + { + // No more room for flexible edges. + return sizes + .Zip(edges, (size, edge) => (Size: size, Edge: edge)) + .Select(zip => zip.Size ?? zip.Edge.MinimumSize) + .Select(size => size > 0 ? size : 1) + .ToList(); + } + + var portion = (float)remaining / flexibleEdges.Sum(x => Math.Max(1, x.Edge.Ratio)); + + var invalidate = false; + foreach (var (index, size, edge) in flexibleEdges) + { + if (portion * edge.Ratio <= edge.MinimumSize) + { + sizes[index] = edge.MinimumSize; + + // New fixed size will invalidate calculations, + // so we need to repeat the process + invalidate = true; + break; + } + } + + if (!invalidate) + { + var remainder = 0f; + foreach (var flexibleEdge in flexibleEdges) + { + var (div, mod) = DivMod((portion * flexibleEdge.Edge.Ratio) + remainder, 1); + remainder = mod; + sizes[flexibleEdge.Index] = div; + } + } + } + + return sizes.Select(x => x ?? 1).ToList(); + } + public static List Reduce(int total, List ratios, List maximums, List values) { ratios = ratios.Zip(maximums, (a, b) => (ratio: a, max: b)).Select(a => a.max > 0 ? a.ratio : 0).ToList(); diff --git a/src/Spectre.Console/Internal/Text/Encoding/HtmlEncoder.cs b/src/Spectre.Console/Internal/Text/Encoding/HtmlEncoder.cs index d021e2684..31f36a07f 100644 --- a/src/Spectre.Console/Internal/Text/Encoding/HtmlEncoder.cs +++ b/src/Spectre.Console/Internal/Text/Encoding/HtmlEncoder.cs @@ -4,7 +4,7 @@ internal sealed class HtmlEncoder : IAnsiConsoleEncoder { public string Encode(IAnsiConsole console, IEnumerable renderables) { - var context = new RenderContext(new EncoderCapabilities(ColorSystem.TrueColor)); + var context = RenderOptions.Create(console, new EncoderCapabilities(ColorSystem.TrueColor)); var builder = new StringBuilder(); builder.Append("
\n");
diff --git a/src/Spectre.Console/Internal/Text/Encoding/TextEncoder.cs b/src/Spectre.Console/Internal/Text/Encoding/TextEncoder.cs
index ac2bc6173..2570699a5 100644
--- a/src/Spectre.Console/Internal/Text/Encoding/TextEncoder.cs
+++ b/src/Spectre.Console/Internal/Text/Encoding/TextEncoder.cs
@@ -4,7 +4,7 @@ internal sealed class TextEncoder : IAnsiConsoleEncoder
 {
     public string Encode(IAnsiConsole console, IEnumerable renderables)
     {
-        var context = new RenderContext(new EncoderCapabilities(ColorSystem.TrueColor));
+        var context = RenderOptions.Create(console, new EncoderCapabilities(ColorSystem.TrueColor));
         var builder = new StringBuilder();
 
         foreach (var renderable in renderables)
diff --git a/src/Spectre.Console/Internal/Text/Markup/MarkupTokenizer.cs b/src/Spectre.Console/Internal/Text/Markup/MarkupTokenizer.cs
index 38de6e9c9..8f4106b5b 100644
--- a/src/Spectre.Console/Internal/Text/Markup/MarkupTokenizer.cs
+++ b/src/Spectre.Console/Internal/Text/Markup/MarkupTokenizer.cs
@@ -122,34 +122,55 @@ private bool ReadMarkup()
         var encounteredClosing = false;
         while (!_reader.Eof)
         {
+            var currentStylePartCanContainMarkup =
+                builder.ToString()
+                .Split(' ')
+                .Last()
+                .StartsWith("link=", StringComparison.OrdinalIgnoreCase);
             current = _reader.Peek();
 
-            if (current == ']' && !encounteredOpening)
+            if (currentStylePartCanContainMarkup)
             {
-                if (encounteredClosing)
+                switch (current)
                 {
-                    builder.Append(_reader.Read());
-                    encounteredClosing = false;
-                    continue;
+                    case ']' when !encounteredOpening:
+                        if (encounteredClosing)
+                        {
+                            builder.Append(_reader.Read());
+                            encounteredClosing = false;
+                            continue;
+                        }
+
+                        _reader.Read();
+                        encounteredClosing = true;
+                        continue;
+
+                    case '[' when !encounteredClosing:
+                        if (encounteredOpening)
+                        {
+                            builder.Append(_reader.Read());
+                            encounteredOpening = false;
+                            continue;
+                        }
+
+                        _reader.Read();
+                        encounteredOpening = true;
+                        continue;
                 }
-
-                _reader.Read();
-                encounteredClosing = true;
-                continue;
             }
-
-            if (current == '[' && !encounteredClosing)
+            else
             {
-                if (encounteredOpening)
+                switch (current)
                 {
-                    builder.Append(_reader.Read());
-                    encounteredOpening = false;
-                    continue;
+                    case ']':
+                        _reader.Read();
+                        encounteredClosing = true;
+                        break;
+                    case '[':
+                        _reader.Read();
+                        encounteredOpening = true;
+                        break;
                 }
-
-                _reader.Read();
-                encounteredOpening = true;
-                continue;
             }
 
             if (encounteredClosing)
diff --git a/src/Spectre.Console/Internal/TypeConverterHelper.cs b/src/Spectre.Console/Internal/TypeConverterHelper.cs
index 901b308ba..03c393eb9 100644
--- a/src/Spectre.Console/Internal/TypeConverterHelper.cs
+++ b/src/Spectre.Console/Internal/TypeConverterHelper.cs
@@ -4,14 +4,20 @@ internal static class TypeConverterHelper
 {
     public static string ConvertToString(T input)
     {
-        return GetTypeConverter().ConvertToInvariantString(input);
+        var result = GetTypeConverter().ConvertToInvariantString(input);
+        if (result == null)
+        {
+            throw new InvalidOperationException("Could not convert input to a string");
+        }
+
+        return result;
     }
 
-    public static bool TryConvertFromString(string input, [MaybeNull] out T result)
+    public static bool TryConvertFromString(string input, [MaybeNull] out T? result)
     {
         try
         {
-            result = (T)GetTypeConverter().ConvertFromInvariantString(input);
+            result = (T?)GetTypeConverter().ConvertFromInvariantString(input);
             return true;
         }
         catch
@@ -21,7 +27,7 @@ public static bool TryConvertFromString(string input, [MaybeNull] out T resul
         }
     }
 
-    public static bool TryConvertFromStringWithCulture(string input, CultureInfo? info, [MaybeNull] out T result)
+    public static bool TryConvertFromStringWithCulture(string input, CultureInfo? info, [MaybeNull] out T? result)
     {
         try
         {
@@ -31,7 +37,7 @@ public static bool TryConvertFromStringWithCulture(string input, CultureInfo?
             }
             else
             {
-                result = (T)GetTypeConverter().ConvertFromString(null!, info, input);
+                result = (T?)GetTypeConverter().ConvertFromString(null!, info, input);
             }
 
             return true;
diff --git a/src/Spectre.Console/Justify.cs b/src/Spectre.Console/Justify.cs
index dc814374b..a4578de7c 100644
--- a/src/Spectre.Console/Justify.cs
+++ b/src/Spectre.Console/Justify.cs
@@ -6,12 +6,12 @@ namespace Spectre.Console;
 public enum Justify
 {
     /// 
-    /// Left aligned.
+    /// Left justified.
     /// 
     Left = 0,
 
     /// 
-    /// Right aligned.
+    /// Right justified.
     /// 
     Right = 1,
 
diff --git a/src/Spectre.Console/Live/LiveDisplayRenderer.cs b/src/Spectre.Console/Live/LiveDisplayRenderer.cs
index 3a95df1d8..a9a1a522e 100644
--- a/src/Spectre.Console/Live/LiveDisplayRenderer.cs
+++ b/src/Spectre.Console/Live/LiveDisplayRenderer.cs
@@ -41,7 +41,7 @@ public void Completed(bool autoclear)
         }
     }
 
-    public IEnumerable Process(RenderContext context, IEnumerable renderables)
+    public IEnumerable Process(RenderOptions options, IEnumerable renderables)
     {
         lock (_context.Lock)
         {
diff --git a/src/Spectre.Console/Live/LiveRenderable.cs b/src/Spectre.Console/Live/LiveRenderable.cs
index 915254060..40107f9cd 100644
--- a/src/Spectre.Console/Live/LiveRenderable.cs
+++ b/src/Spectre.Console/Live/LiveRenderable.cs
@@ -67,7 +67,7 @@ public IRenderable RestoreCursor()
         }
     }
 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
         lock (_lock)
         {
@@ -75,10 +75,10 @@ protected override IEnumerable Render(RenderContext context, int maxWid
 
             if (_renderable != null)
             {
-                var segments = _renderable.Render(context, maxWidth);
+                var segments = _renderable.Render(options, maxWidth);
                 var lines = Segment.SplitLines(segments);
 
-                var shape = SegmentShape.Calculate(context, lines);
+                var shape = SegmentShape.Calculate(options, lines);
                 if (shape.Height > _console.Profile.Height)
                 {
                     if (Overflow == VerticalOverflow.Crop)
@@ -97,12 +97,12 @@ protected override IEnumerable Render(RenderContext context, int maxWid
                             lines.RemoveRange(0, start);
                         }
 
-                        shape = SegmentShape.Calculate(context, lines);
+                        shape = SegmentShape.Calculate(options, lines);
                     }
                     else if (Overflow == VerticalOverflow.Ellipsis)
                     {
                         var ellipsisText = _console.Profile.Capabilities.Unicode ? "…" : "...";
-                        var ellipsis = new SegmentLine(((IRenderable)new Markup($"[yellow]{ellipsisText}[/]")).Render(context, maxWidth));
+                        var ellipsis = new SegmentLine(((IRenderable)new Markup($"[yellow]{ellipsisText}[/]")).Render(options, maxWidth));
 
                         if (OverflowCropping == VerticalOverflowCropping.Bottom)
                         {
@@ -120,14 +120,14 @@ protected override IEnumerable Render(RenderContext context, int maxWid
                             lines.Insert(0, ellipsis);
                         }
 
-                        shape = SegmentShape.Calculate(context, lines);
+                        shape = SegmentShape.Calculate(options, lines);
                     }
 
                     DidOverflow = true;
                 }
 
                 _shape = _shape == null ? shape : _shape.Value.Inflate(shape);
-                _shape.Value.Apply(context, ref lines);
+                _shape.Value.Apply(options, ref lines);
 
                 foreach (var (_, _, last, line) in lines.Enumerate())
                 {
diff --git a/src/Spectre.Console/Live/Progress/Columns/DownloadedColumn.cs b/src/Spectre.Console/Live/Progress/Columns/DownloadedColumn.cs
index fa8e496c1..61629860f 100644
--- a/src/Spectre.Console/Live/Progress/Columns/DownloadedColumn.cs
+++ b/src/Spectre.Console/Live/Progress/Columns/DownloadedColumn.cs
@@ -11,7 +11,7 @@ public sealed class DownloadedColumn : ProgressColumn
     public CultureInfo? Culture { get; set; }
 
     /// 
-    public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
+    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
     {
         var total = new FileSize(task.MaxValue);
 
diff --git a/src/Spectre.Console/Live/Progress/Columns/ElapsedTimeColumn.cs b/src/Spectre.Console/Live/Progress/Columns/ElapsedTimeColumn.cs
index a005be6cb..a29fdfbcc 100644
--- a/src/Spectre.Console/Live/Progress/Columns/ElapsedTimeColumn.cs
+++ b/src/Spectre.Console/Live/Progress/Columns/ElapsedTimeColumn.cs
@@ -14,7 +14,7 @@ public sealed class ElapsedTimeColumn : ProgressColumn
     public Style Style { get; set; } = new Style(foreground: Color.Blue);
 
     /// 
-    public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
+    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
     {
         var elapsed = task.ElapsedTime;
         if (elapsed == null)
@@ -31,7 +31,7 @@ public override IRenderable Render(RenderContext context, ProgressTask task, Tim
     }
 
     /// 
-    public override int? GetColumnWidth(RenderContext context)
+    public override int? GetColumnWidth(RenderOptions options)
     {
         return 8;
     }
diff --git a/src/Spectre.Console/Live/Progress/Columns/PercentageColumn.cs b/src/Spectre.Console/Live/Progress/Columns/PercentageColumn.cs
index f0bf59712..0dbef1c2c 100644
--- a/src/Spectre.Console/Live/Progress/Columns/PercentageColumn.cs
+++ b/src/Spectre.Console/Live/Progress/Columns/PercentageColumn.cs
@@ -16,15 +16,15 @@ public sealed class PercentageColumn : ProgressColumn
     public Style CompletedStyle { get; set; } = new Style(foreground: Color.Green);
 
     /// 
-    public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
+    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
     {
         var percentage = (int)task.Percentage;
         var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain;
-        return new Text($"{percentage}%", style).RightAligned();
+        return new Text($"{percentage}%", style).RightJustified();
     }
 
     /// 
-    public override int? GetColumnWidth(RenderContext context)
+    public override int? GetColumnWidth(RenderOptions options)
     {
         return 4;
     }
diff --git a/src/Spectre.Console/Live/Progress/Columns/ProgressBarColumn.cs b/src/Spectre.Console/Live/Progress/Columns/ProgressBarColumn.cs
index 7b0b018b8..16c7c1909 100644
--- a/src/Spectre.Console/Live/Progress/Columns/ProgressBarColumn.cs
+++ b/src/Spectre.Console/Live/Progress/Columns/ProgressBarColumn.cs
@@ -31,7 +31,7 @@ public sealed class ProgressBarColumn : ProgressColumn
     public Style IndeterminateStyle { get; set; } = ProgressBar.DefaultPulseStyle;
 
     /// 
-    public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
+    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
     {
         return new ProgressBar
         {
diff --git a/src/Spectre.Console/Live/Progress/Columns/RemainingTimeColumn.cs b/src/Spectre.Console/Live/Progress/Columns/RemainingTimeColumn.cs
index 49aeae56b..3ce467ab2 100644
--- a/src/Spectre.Console/Live/Progress/Columns/RemainingTimeColumn.cs
+++ b/src/Spectre.Console/Live/Progress/Columns/RemainingTimeColumn.cs
@@ -14,7 +14,7 @@ public sealed class RemainingTimeColumn : ProgressColumn
     public Style Style { get; set; } = new Style(foreground: Color.Blue);
 
     /// 
-    public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
+    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
     {
         var remaining = task.RemainingTime;
         if (remaining == null)
@@ -31,7 +31,7 @@ public override IRenderable Render(RenderContext context, ProgressTask task, Tim
     }
 
     /// 
-    public override int? GetColumnWidth(RenderContext context)
+    public override int? GetColumnWidth(RenderOptions options)
     {
         return 8;
     }
diff --git a/src/Spectre.Console/Live/Progress/Columns/SpinnerColumn.cs b/src/Spectre.Console/Live/Progress/Columns/SpinnerColumn.cs
index 429d48466..425de14cb 100644
--- a/src/Spectre.Console/Live/Progress/Columns/SpinnerColumn.cs
+++ b/src/Spectre.Console/Live/Progress/Columns/SpinnerColumn.cs
@@ -95,9 +95,9 @@ public SpinnerColumn(Spinner spinner)
     }
 
     /// 
-    public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
+    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
     {
-        var useAscii = !context.Unicode && _spinner.IsUnicode;
+        var useAscii = !options.Unicode && _spinner.IsUnicode;
         var spinner = useAscii ? Spinner.Known.Ascii : _spinner ?? Spinner.Known.Default;
 
         if (!task.IsStarted)
@@ -123,24 +123,24 @@ public override IRenderable Render(RenderContext context, ProgressTask task, Tim
     }
 
     /// 
-    public override int? GetColumnWidth(RenderContext context)
+    public override int? GetColumnWidth(RenderOptions options)
     {
-        return GetMaxWidth(context);
+        return GetMaxWidth(options);
     }
 
-    private int GetMaxWidth(RenderContext context)
+    private int GetMaxWidth(RenderOptions options)
     {
         lock (_lock)
         {
             if (_maxWidth == null)
             {
-                var useAscii = !context.Unicode && _spinner.IsUnicode;
+                var useAscii = !options.Unicode && _spinner.IsUnicode;
                 var spinner = useAscii ? Spinner.Known.Ascii : _spinner ?? Spinner.Known.Default;
 
                 _maxWidth = Math.Max(
                     Math.Max(
-                    ((IRenderable)new Markup(PendingText ?? " ")).Measure(context, int.MaxValue).Max,
-                    ((IRenderable)new Markup(CompletedText ?? " ")).Measure(context, int.MaxValue).Max),
+                    ((IRenderable)new Markup(PendingText ?? " ")).Measure(options, int.MaxValue).Max,
+                    ((IRenderable)new Markup(CompletedText ?? " ")).Measure(options, int.MaxValue).Max),
                     spinner.Frames.Max(frame => Cell.GetCellLength(frame)));
             }
 
diff --git a/src/Spectre.Console/Live/Progress/Columns/TaskDescriptionColumn.cs b/src/Spectre.Console/Live/Progress/Columns/TaskDescriptionColumn.cs
index b6ffb0d06..4e3c39ff5 100644
--- a/src/Spectre.Console/Live/Progress/Columns/TaskDescriptionColumn.cs
+++ b/src/Spectre.Console/Live/Progress/Columns/TaskDescriptionColumn.cs
@@ -14,9 +14,9 @@ public sealed class TaskDescriptionColumn : ProgressColumn
     public Justify Alignment { get; set; } = Justify.Right;
 
     /// 
-    public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
+    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
     {
         var text = task.Description?.RemoveNewLines()?.Trim();
-        return new Markup(text ?? string.Empty).Overflow(Overflow.Ellipsis).Alignment(Alignment);
+        return new Markup(text ?? string.Empty).Overflow(Overflow.Ellipsis).Justify(Alignment);
     }
 }
\ No newline at end of file
diff --git a/src/Spectre.Console/Live/Progress/Columns/TransferSpeedColumn.cs b/src/Spectre.Console/Live/Progress/Columns/TransferSpeedColumn.cs
index 7e6e7832e..84f2743f0 100644
--- a/src/Spectre.Console/Live/Progress/Columns/TransferSpeedColumn.cs
+++ b/src/Spectre.Console/Live/Progress/Columns/TransferSpeedColumn.cs
@@ -11,7 +11,7 @@ public sealed class TransferSpeedColumn : ProgressColumn
     public CultureInfo? Culture { get; set; }
 
     /// 
-    public override IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime)
+    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
     {
         if (task.Speed == null)
         {
diff --git a/src/Spectre.Console/Live/Progress/ProgressColumn.cs b/src/Spectre.Console/Live/Progress/ProgressColumn.cs
index 2aa593d4c..c10538e39 100644
--- a/src/Spectre.Console/Live/Progress/ProgressColumn.cs
+++ b/src/Spectre.Console/Live/Progress/ProgressColumn.cs
@@ -13,18 +13,18 @@ public abstract class ProgressColumn
     /// 
     /// Gets a renderable representing the column.
     /// 
-    /// The render context.
+    /// The render options.
     /// The task.
     /// The elapsed time since last call.
     /// A renderable representing the column.
-    public abstract IRenderable Render(RenderContext context, ProgressTask task, TimeSpan deltaTime);
+    public abstract IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime);
 
     /// 
     /// Gets the width of the column.
     /// 
-    /// The context.
+    /// The render options.
     /// The width of the column, or null to calculate.
-    public virtual int? GetColumnWidth(RenderContext context)
+    public virtual int? GetColumnWidth(RenderOptions options)
     {
         return null;
     }
diff --git a/src/Spectre.Console/Live/Progress/ProgressRenderer.cs b/src/Spectre.Console/Live/Progress/ProgressRenderer.cs
index 598ce5a70..5840b20c2 100644
--- a/src/Spectre.Console/Live/Progress/ProgressRenderer.cs
+++ b/src/Spectre.Console/Live/Progress/ProgressRenderer.cs
@@ -13,5 +13,5 @@ public virtual void Completed(bool clear)
     }
 
     public abstract void Update(ProgressContext context);
-    public abstract IEnumerable Process(RenderContext context, IEnumerable renderables);
+    public abstract IEnumerable Process(RenderOptions options, IEnumerable renderables);
 }
\ No newline at end of file
diff --git a/src/Spectre.Console/Live/Progress/Renderers/DefaultProgressRenderer.cs b/src/Spectre.Console/Live/Progress/Renderers/DefaultProgressRenderer.cs
index 97154020c..8155751e0 100644
--- a/src/Spectre.Console/Live/Progress/Renderers/DefaultProgressRenderer.cs
+++ b/src/Spectre.Console/Live/Progress/Renderers/DefaultProgressRenderer.cs
@@ -64,7 +64,7 @@ public override void Update(ProgressContext context)
                 _stopwatch.Start();
             }
 
-            var renderContext = new RenderContext(_console.Profile.Capabilities);
+            var renderContext = RenderOptions.Create(_console, _console.Profile.Capabilities);
 
             var delta = _stopwatch.Elapsed - _lastUpdate;
             _lastUpdate = _stopwatch.Elapsed;
@@ -105,7 +105,7 @@ public override void Update(ProgressContext context)
         }
     }
 
-    public override IEnumerable Process(RenderContext context, IEnumerable renderables)
+    public override IEnumerable Process(RenderOptions options, IEnumerable renderables)
     {
         lock (_lock)
         {
diff --git a/src/Spectre.Console/Live/Progress/Renderers/FallbackProgressRenderer.cs b/src/Spectre.Console/Live/Progress/Renderers/FallbackProgressRenderer.cs
index 90c54b380..e40f54ca5 100644
--- a/src/Spectre.Console/Live/Progress/Renderers/FallbackProgressRenderer.cs
+++ b/src/Spectre.Console/Live/Progress/Renderers/FallbackProgressRenderer.cs
@@ -58,7 +58,7 @@ public override void Update(ProgressContext context)
         }
     }
 
-    public override IEnumerable Process(RenderContext context, IEnumerable renderables)
+    public override IEnumerable Process(RenderOptions options, IEnumerable renderables)
     {
         lock (_lock)
         {
diff --git a/src/Spectre.Console/Live/Progress/Renderers/FallbackStatusRenderer.cs b/src/Spectre.Console/Live/Progress/Renderers/FallbackStatusRenderer.cs
index f8f445e1b..69e025ca9 100644
--- a/src/Spectre.Console/Live/Progress/Renderers/FallbackStatusRenderer.cs
+++ b/src/Spectre.Console/Live/Progress/Renderers/FallbackStatusRenderer.cs
@@ -34,7 +34,7 @@ public override void Update(ProgressContext context)
         }
     }
 
-    public override IEnumerable Process(RenderContext context, IEnumerable renderables)
+    public override IEnumerable Process(RenderOptions options, IEnumerable renderables)
     {
         lock (_lock)
         {
diff --git a/src/Spectre.Console/Prompts/List/ListPromptRenderHook.cs b/src/Spectre.Console/Prompts/List/ListPromptRenderHook.cs
index 53fa21f11..090a2f6df 100644
--- a/src/Spectre.Console/Prompts/List/ListPromptRenderHook.cs
+++ b/src/Spectre.Console/Prompts/List/ListPromptRenderHook.cs
@@ -32,7 +32,7 @@ public void Refresh()
         _console.Write(new ControlCode(string.Empty));
     }
 
-    public IEnumerable Process(RenderContext context, IEnumerable renderables)
+    public IEnumerable Process(RenderOptions options, IEnumerable renderables)
     {
         lock (_lock)
         {
diff --git a/src/Spectre.Console/Region.cs b/src/Spectre.Console/Region.cs
new file mode 100644
index 000000000..a5aeb05de
--- /dev/null
+++ b/src/Spectre.Console/Region.cs
@@ -0,0 +1,43 @@
+namespace Spectre.Console;
+
+/// 
+/// Represents a region.
+/// 
+[DebuggerDisplay("[X={X,nq}, Y={Y,nq}, W={Width,nq}, H={Height,nq}]")]
+public struct Region
+{
+    /// 
+    /// Gets the x-coordinate.
+    /// 
+    public int X { get; }
+
+    /// 
+    /// Gets the y-coordinate.
+    /// 
+    public int Y { get; }
+
+    /// 
+    /// Gets the width.
+    /// 
+    public int Width { get; }
+
+    /// 
+    /// Gets the height.
+    /// 
+    public int Height { get; }
+
+    /// 
+    /// Initializes a new instance of the  struct.
+    /// 
+    /// The x-coordinate.
+    /// The y-coordinate.
+    /// The width.
+    /// The height.
+    public Region(int x, int y, int width, int height)
+    {
+        X = x;
+        Y = y;
+        Width = width;
+        Height = height;
+    }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console/Rendering/IRenderHook.cs b/src/Spectre.Console/Rendering/IRenderHook.cs
index 417a5004a..49c92d6af 100644
--- a/src/Spectre.Console/Rendering/IRenderHook.cs
+++ b/src/Spectre.Console/Rendering/IRenderHook.cs
@@ -8,8 +8,8 @@ public interface IRenderHook
     /// 
     /// Processes the specified renderables.
     /// 
-    /// The render context.
+    /// The render options.
     /// The renderables to process.
     /// The processed renderables.
-    IEnumerable Process(RenderContext context, IEnumerable renderables);
+    IEnumerable Process(RenderOptions options, IEnumerable renderables);
 }
\ No newline at end of file
diff --git a/src/Spectre.Console/Rendering/JustInTimeRenderable.cs b/src/Spectre.Console/Rendering/JustInTimeRenderable.cs
index dd85e40a3..f30b3704a 100644
--- a/src/Spectre.Console/Rendering/JustInTimeRenderable.cs
+++ b/src/Spectre.Console/Rendering/JustInTimeRenderable.cs
@@ -10,13 +10,13 @@ public abstract class JustInTimeRenderable : Renderable
     private IRenderable? _rendered;
 
     /// 
-    protected sealed override Measurement Measure(RenderContext context, int maxWidth)
+    protected sealed override Measurement Measure(RenderOptions context, int maxWidth)
     {
         return GetInner().Measure(context, maxWidth);
     }
 
     /// 
-    protected sealed override IEnumerable Render(RenderContext context, int width)
+    protected sealed override IEnumerable Render(RenderOptions context, int width)
     {
         return GetInner().Render(context, width);
     }
diff --git a/src/Spectre.Console/Rendering/RenderContext.cs b/src/Spectre.Console/Rendering/RenderContext.cs
deleted file mode 100644
index c8d15f8d5..000000000
--- a/src/Spectre.Console/Rendering/RenderContext.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-namespace Spectre.Console.Rendering;
-
-/// 
-/// Represents a render context.
-/// 
-public sealed class RenderContext
-{
-    private readonly IReadOnlyCapabilities _capabilities;
-
-    /// 
-    /// Gets the current color system.
-    /// 
-    public ColorSystem ColorSystem => _capabilities.ColorSystem;
-
-    /// 
-    /// Gets a value indicating whether or not VT/Ansi codes are supported.
-    /// 
-    public bool Ansi => _capabilities.Ansi;
-
-    /// 
-    /// Gets a value indicating whether or not unicode is supported.
-    /// 
-    public bool Unicode => _capabilities.Unicode;
-
-    /// 
-    /// Gets the current justification.
-    /// 
-    public Justify? Justification { get; }
-
-    /// 
-    /// Gets a value indicating whether the context want items to render without
-    /// line breaks and return a single line where applicable.
-    /// 
-    internal bool SingleLine { get; }
-
-    /// 
-    /// Initializes a new instance of the  class.
-    /// 
-    /// The capabilities.
-    /// The justification.
-    public RenderContext(IReadOnlyCapabilities capabilities, Justify? justification = null)
-        : this(capabilities, justification, false)
-    {
-    }
-
-    private RenderContext(IReadOnlyCapabilities capabilities, Justify? justification = null, bool singleLine = false)
-    {
-        _capabilities = capabilities ?? throw new ArgumentNullException(nameof(capabilities));
-
-        Justification = justification;
-        SingleLine = singleLine;
-    }
-
-    /// 
-    /// Creates a new context with the specified justification.
-    /// 
-    /// The justification.
-    /// A new  instance.
-    public RenderContext WithJustification(Justify? justification)
-    {
-        return new RenderContext(_capabilities, justification, SingleLine);
-    }
-
-    /// 
-    /// Creates a new context that tell  instances
-    /// to not care about splitting things in new lines. Whether or not to
-    /// comply to the request is up to the item being rendered.
-    /// 
-    /// 
-    /// Use with care since this has the potential to mess things up.
-    /// Only use this kind of context with items that you know about.
-    /// 
-    /// A new  instance.
-    internal RenderContext WithSingleLine()
-    {
-        return new RenderContext(_capabilities, Justification, true);
-    }
-}
\ No newline at end of file
diff --git a/src/Spectre.Console/Rendering/RenderOptions.cs b/src/Spectre.Console/Rendering/RenderOptions.cs
new file mode 100644
index 000000000..3dcf04a5a
--- /dev/null
+++ b/src/Spectre.Console/Rendering/RenderOptions.cs
@@ -0,0 +1,63 @@
+namespace Spectre.Console.Rendering;
+
+/// 
+/// Represents render options.
+/// 
+/// The capabilities.
+/// The console size.
+public record class RenderOptions(IReadOnlyCapabilities Capabilities, Size ConsoleSize)
+{
+    /// 
+    /// Gets the current color system.
+    /// 
+    public ColorSystem ColorSystem => Capabilities.ColorSystem;
+
+    /// 
+    /// Gets a value indicating whether or not VT/Ansi codes are supported.
+    /// 
+    public bool Ansi => Capabilities.Ansi;
+
+    /// 
+    /// Gets a value indicating whether or not unicode is supported.
+    /// 
+    public bool Unicode => Capabilities.Unicode;
+
+    /// 
+    /// Gets the current justification.
+    /// 
+    public Justify? Justification { get; init; }
+
+    /// 
+    /// Gets the requested height.
+    /// 
+    public int? Height { get; init; }
+
+    /// 
+    /// Gets a value indicating whether the context want items to render without
+    /// line breaks and return a single line where applicable.
+    /// 
+    internal bool SingleLine { get; init; }
+
+    /// 
+    /// Creates a  instance from a .
+    /// 
+    /// The console.
+    /// The capabilities, or null to use the provided console's capabilities.
+    /// A  representing the provided .
+    public static RenderOptions Create(IAnsiConsole console, IReadOnlyCapabilities? capabilities = null)
+    {
+        if (console is null)
+        {
+            throw new ArgumentNullException(nameof(console));
+        }
+
+        return new RenderOptions(
+            capabilities ?? console.Profile.Capabilities,
+            new Size(console.Profile.Width, console.Profile.Height))
+        {
+            Justification = null,
+            Height = null,
+            SingleLine = false,
+        };
+    }
+}
diff --git a/src/Spectre.Console/Rendering/RenderPipeline.cs b/src/Spectre.Console/Rendering/RenderPipeline.cs
index 367fffd2b..6865c1b12 100644
--- a/src/Spectre.Console/Rendering/RenderPipeline.cs
+++ b/src/Spectre.Console/Rendering/RenderPipeline.cs
@@ -44,17 +44,17 @@ public void Detach(IRenderHook hook)
     /// 
     /// Processes the specified renderables.
     /// 
-    /// The render context.
+    /// The render options.
     /// The renderables to process.
     /// The processed renderables.
-    public IEnumerable Process(RenderContext context, IEnumerable renderables)
+    public IEnumerable Process(RenderOptions options, IEnumerable renderables)
     {
         lock (_lock)
         {
             var current = renderables;
             for (var index = _hooks.Count - 1; index >= 0; index--)
             {
-                current = _hooks[index].Process(context, current);
+                current = _hooks[index].Process(options, current);
             }
 
             return current;
diff --git a/src/Spectre.Console/Rendering/Renderable.cs b/src/Spectre.Console/Rendering/Renderable.cs
index 2b22f5d19..1234053ec 100644
--- a/src/Spectre.Console/Rendering/Renderable.cs
+++ b/src/Spectre.Console/Rendering/Renderable.cs
@@ -7,25 +7,25 @@ public abstract class Renderable : IRenderable
 {
     /// 
     [DebuggerStepThrough]
-    Measurement IRenderable.Measure(RenderContext context, int maxWidth)
+    Measurement IRenderable.Measure(RenderOptions options, int maxWidth)
     {
-        return Measure(context, maxWidth);
+        return Measure(options, maxWidth);
     }
 
     /// 
     [DebuggerStepThrough]
-    IEnumerable IRenderable.Render(RenderContext context, int maxWidth)
+    IEnumerable IRenderable.Render(RenderOptions options, int maxWidth)
     {
-        return Render(context, maxWidth);
+        return Render(options, maxWidth);
     }
 
     /// 
     /// Measures the renderable object.
     /// 
-    /// The render context.
+    /// The render options.
     /// The maximum allowed width.
     /// The minimum and maximum width of the object.
-    protected virtual Measurement Measure(RenderContext context, int maxWidth)
+    protected virtual Measurement Measure(RenderOptions options, int maxWidth)
     {
         return new Measurement(maxWidth, maxWidth);
     }
@@ -33,8 +33,8 @@ protected virtual Measurement Measure(RenderContext context, int maxWidth)
     /// 
     /// Renders the object.
     /// 
-    /// The render context.
+    /// The render options.
     /// The maximum allowed width.
     /// A collection of segments.
-    protected abstract IEnumerable Render(RenderContext context, int maxWidth);
+    protected abstract IEnumerable Render(RenderOptions options, int maxWidth);
 }
\ No newline at end of file
diff --git a/src/Spectre.Console/Rendering/Segment.cs b/src/Spectre.Console/Rendering/Segment.cs
index 53bd2f9a5..9f36a772c 100644
--- a/src/Spectre.Console/Rendering/Segment.cs
+++ b/src/Spectre.Console/Rendering/Segment.cs
@@ -201,8 +201,9 @@ public static List SplitLines(IEnumerable segments)
     /// 
     /// The segments to split into lines.
     /// The maximum width.
+    /// The height (if any).
     /// A list of lines.
-    public static List SplitLines(IEnumerable segments, int maxWidth)
+    public static List SplitLines(IEnumerable segments, int maxWidth, int? height = null)
     {
         if (segments is null)
         {
@@ -294,6 +295,25 @@ public static List SplitLines(IEnumerable segments, int ma
             lines.Add(line);
         }
 
+        // Got a height specified?
+        if (height != null)
+        {
+            if (lines.Count >= height)
+            {
+                // Remove lines
+                lines.RemoveRange(height.Value, lines.Count - height.Value);
+            }
+            else
+            {
+                // Add lines
+                var missing = height - lines.Count;
+                for (var i = 0; i < missing; i++)
+                {
+                    lines.Add(new SegmentLine());
+                }
+            }
+        }
+
         return lines;
     }
 
@@ -549,6 +569,21 @@ internal static List> MakeSameHeight(int cellHeight, List MakeWidth(int expectedWidth, List lines)
+    {
+        foreach (var line in lines)
+        {
+            var width = line.CellCount();
+            if (width < expectedWidth)
+            {
+                var diff = expectedWidth - width;
+                line.Add(new Segment(new string(' ', diff)));
+            }
+        }
+
+        return lines;
+    }
+
     internal static List SplitSegment(string text, int maxCellLength)
     {
         var list = new List();
diff --git a/src/Spectre.Console/Rendering/SegmentShape.cs b/src/Spectre.Console/Rendering/SegmentShape.cs
index 6b45c383d..fd8c7ff09 100644
--- a/src/Spectre.Console/Rendering/SegmentShape.cs
+++ b/src/Spectre.Console/Rendering/SegmentShape.cs
@@ -11,13 +11,8 @@ public SegmentShape(int width, int height)
         Height = height;
     }
 
-    public static SegmentShape Calculate(RenderContext context, List lines)
+    public static SegmentShape Calculate(RenderOptions options, List lines)
     {
-        if (context is null)
-        {
-            throw new ArgumentNullException(nameof(context));
-        }
-
         if (lines is null)
         {
             throw new ArgumentNullException(nameof(lines));
@@ -36,7 +31,7 @@ public SegmentShape Inflate(SegmentShape other)
             Math.Max(Height, other.Height));
     }
 
-    public void Apply(RenderContext context, ref List lines)
+    public void Apply(RenderOptions options, ref List lines)
     {
         foreach (var line in lines)
         {
diff --git a/src/Spectre.Console/Size.cs b/src/Spectre.Console/Size.cs
new file mode 100644
index 000000000..5a074c850
--- /dev/null
+++ b/src/Spectre.Console/Size.cs
@@ -0,0 +1,29 @@
+namespace Spectre.Console;
+
+/// 
+/// Represents a size.
+/// 
+[DebuggerDisplay("{Width,nq}x{Height,nq}")]
+public struct Size
+{
+    /// 
+    /// Gets the width.
+    /// 
+    public int Width { get; }
+
+    /// 
+    /// Gets the height.
+    /// 
+    public int Height { get; }
+
+    /// 
+    /// Initializes a new instance of the  struct.
+    /// 
+    /// The width.
+    /// The height.
+    public Size(int width, int height)
+    {
+        Width = width;
+        Height = height;
+    }
+}
diff --git a/src/Spectre.Console/Spectre.Console.csproj b/src/Spectre.Console/Spectre.Console.csproj
index ac3494f41..102ef074c 100644
--- a/src/Spectre.Console/Spectre.Console.csproj
+++ b/src/Spectre.Console/Spectre.Console.csproj
@@ -1,7 +1,7 @@
 
 
   
-    net6.0;netstandard2.0
+    net7.0;net6.0;netstandard2.0
     enable
     true
     SA1633
@@ -17,22 +17,25 @@
 
   
     
-    
-    
-    
-      all
-      runtime; build; native; contentfiles; analyzers; buildtransitive
-    
     
       all
     
   
 
-  
+  
     3.0.0
     False
   
 
+  
+    
+    
+    
+      all
+      runtime; build; native; contentfiles; analyzers; buildtransitive
+    
+  
+
   
     $(DefineConstants)TRACE;WCWIDTH_VISIBILITY_INTERNAL
   
diff --git a/src/Spectre.Console/VerticalAlignment.cs b/src/Spectre.Console/VerticalAlignment.cs
new file mode 100644
index 000000000..dfffb770f
--- /dev/null
+++ b/src/Spectre.Console/VerticalAlignment.cs
@@ -0,0 +1,22 @@
+namespace Spectre.Console;
+
+/// 
+/// Represents vertical alignment.
+/// 
+public enum VerticalAlignment
+{
+    /// 
+    /// Top aligned.
+    /// 
+    Top,
+
+    /// 
+    /// Middle aligned.
+    /// 
+    Middle,
+
+    /// 
+    /// Bottom aligned.
+    /// 
+    Bottom,
+}
diff --git a/src/Spectre.Console/Widgets/Align.cs b/src/Spectre.Console/Widgets/Align.cs
new file mode 100644
index 000000000..725b784ea
--- /dev/null
+++ b/src/Spectre.Console/Widgets/Align.cs
@@ -0,0 +1,146 @@
+namespace Spectre.Console;
+
+/// 
+/// Represents a renderable used to align content.
+/// 
+public sealed class Align : Renderable
+{
+    private readonly IRenderable _renderable;
+
+    /// 
+    /// Gets or sets the horizontal alignment.
+    /// 
+    public HorizontalAlignment Horizontal { get; set; } = HorizontalAlignment.Left;
+
+    /// 
+    /// Gets or sets the vertical alignment.
+    /// 
+    public VerticalAlignment? Vertical { get; set; }
+
+    /// 
+    /// Gets or sets the width.
+    /// 
+    public int? Width { get; set; }
+
+    /// 
+    /// Gets or sets the height.
+    /// 
+    public int? Height { get; set; }
+
+    /// 
+    /// Initializes a new instance of the  class.
+    /// 
+    /// The renderable to align.
+    /// The horizontal alignment.
+    /// The vertical alignment, or null if none.
+    public Align(IRenderable renderable, HorizontalAlignment horizontal, VerticalAlignment? vertical = null)
+    {
+        _renderable = renderable ?? throw new ArgumentNullException(nameof(renderable));
+
+        Horizontal = horizontal;
+        Vertical = vertical;
+    }
+
+    /// 
+    /// Initializes a new instance of the  class that is left aligned.
+    /// 
+    /// The  to align.
+    /// The vertical alignment, or null if none.
+    /// A new  object.
+    public static Align Left(IRenderable renderable, VerticalAlignment? vertical = null)
+    {
+        return new Align(renderable, HorizontalAlignment.Left, vertical);
+    }
+
+    /// 
+    /// Initializes a new instance of the  class that is center aligned.
+    /// 
+    /// The  to align.
+    /// The vertical alignment, or null if none.
+    /// A new  object.
+    public static Align Center(IRenderable renderable, VerticalAlignment? vertical = null)
+    {
+        return new Align(renderable, HorizontalAlignment.Center, vertical);
+    }
+
+    /// 
+    /// Initializes a new instance of the  class that is right aligned.
+    /// 
+    /// The  to align.
+    /// The vertical alignment, or null if none.
+    /// A new  object.
+    public static Align Right(IRenderable renderable, VerticalAlignment? vertical = null)
+    {
+        return new Align(renderable, HorizontalAlignment.Right, vertical);
+    }
+
+    /// 
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
+    {
+        var rendered = _renderable.Render(options with { Height = null }, maxWidth);
+        var lines = Segment.SplitLines(rendered);
+
+        var width = Math.Min(Width ?? maxWidth, maxWidth);
+        var height = Height ?? options.Height;
+
+        var blank = new SegmentLine(new[] { new Segment(new string(' ', width)) });
+
+        // Align vertically
+        if (Vertical != null && height != null)
+        {
+            switch (Vertical)
+            {
+                case VerticalAlignment.Top:
+                    {
+                        var diff = height - lines.Count;
+                        for (var i = 0; i < diff; i++)
+                        {
+                            lines.Add(blank);
+                        }
+
+                        break;
+                    }
+
+                case VerticalAlignment.Middle:
+                    {
+                        var top = (height - lines.Count) / 2;
+                        var bottom = height - top - lines.Count;
+
+                        for (var i = 0; i < top; i++)
+                        {
+                            lines.Insert(0, blank);
+                        }
+
+                        for (var i = 0; i < bottom; i++)
+                        {
+                            lines.Add(blank);
+                        }
+
+                        break;
+                    }
+
+                case VerticalAlignment.Bottom:
+                    {
+                        var diff = height - lines.Count;
+                        for (var i = 0; i < diff; i++)
+                        {
+                            lines.Insert(0, blank);
+                        }
+
+                        break;
+                    }
+
+                default:
+                    throw new NotSupportedException("Unknown vertical alignment");
+            }
+        }
+
+        // Align horizontally
+        foreach (var line in lines)
+        {
+            Aligner.AlignHorizontally(line, Horizontal, width);
+        }
+
+        return new SegmentLineEnumerator(lines);
+    }
+}
diff --git a/src/Spectre.Console/Widgets/Calendar.cs b/src/Spectre.Console/Widgets/Calendar.cs
index 3f4ece6e6..6e2bcd974 100644
--- a/src/Spectre.Console/Widgets/Calendar.cs
+++ b/src/Spectre.Console/Widgets/Calendar.cs
@@ -107,6 +107,7 @@ public Style? HeaderStyle
     }
 
     /// 
+    [Obsolete("Use the Align widget instead. This property will be removed in a later release.")]
     public Justify? Alignment
     {
         get => _alignment;
@@ -162,6 +163,7 @@ protected override IRenderable Build()
     {
         var culture = Culture ?? CultureInfo.InvariantCulture;
 
+#pragma warning disable CS0618 // Type or member is obsolete
         var table = new Table
         {
             Border = _border,
@@ -169,6 +171,7 @@ protected override IRenderable Build()
             BorderStyle = _borderStyle,
             Alignment = _alignment,
         };
+#pragma warning restore CS0618 // Type or member is obsolete
 
         if (ShowHeader)
         {
diff --git a/src/Spectre.Console/Widgets/Canvas.cs b/src/Spectre.Console/Widgets/Canvas.cs
index 2718a4982..cdabca053 100644
--- a/src/Spectre.Console/Widgets/Canvas.cs
+++ b/src/Spectre.Console/Widgets/Canvas.cs
@@ -70,7 +70,7 @@ public Canvas SetPixel(int x, int y, Color color)
     }
 
     /// 
-    protected override Measurement Measure(RenderContext context, int maxWidth)
+    protected override Measurement Measure(RenderOptions options, int maxWidth)
     {
         if (PixelWidth < 0)
         {
@@ -88,7 +88,7 @@ protected override Measurement Measure(RenderContext context, int maxWidth)
     }
 
     /// 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
         if (PixelWidth < 0)
         {
diff --git a/src/Spectre.Console/Widgets/Charts/BarChart.cs b/src/Spectre.Console/Widgets/Charts/BarChart.cs
index 457116b56..b6367b794 100644
--- a/src/Spectre.Console/Widgets/Charts/BarChart.cs
+++ b/src/Spectre.Console/Widgets/Charts/BarChart.cs
@@ -52,14 +52,14 @@ public BarChart()
     }
 
     /// 
-    protected override Measurement Measure(RenderContext context, int maxWidth)
+    protected override Measurement Measure(RenderOptions options, int maxWidth)
     {
         var width = Math.Min(Width ?? maxWidth, maxWidth);
         return new Measurement(width, width);
     }
 
     /// 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
         var width = Math.Min(Width ?? maxWidth, maxWidth);
         var maxValue = Math.Max(MaxValue ?? 0d, Data.Max(item => item.Value));
@@ -72,7 +72,7 @@ protected override IEnumerable Render(RenderContext context, int maxWid
 
         if (!string.IsNullOrWhiteSpace(Label))
         {
-            grid.AddRow(Text.Empty, new Markup(Label).Alignment(LabelAlignment));
+            grid.AddRow(Text.Empty, new Markup(Label).Justify(LabelAlignment));
         }
 
         foreach (var item in Data)
@@ -93,6 +93,6 @@ protected override IEnumerable Render(RenderContext context, int maxWid
                 });
         }
 
-        return ((IRenderable)grid).Render(context, width);
+        return ((IRenderable)grid).Render(options, width);
     }
 }
\ No newline at end of file
diff --git a/src/Spectre.Console/Widgets/Charts/BreakdownBar.cs b/src/Spectre.Console/Widgets/Charts/BreakdownBar.cs
index 4db0bc491..fa1e5f931 100644
--- a/src/Spectre.Console/Widgets/Charts/BreakdownBar.cs
+++ b/src/Spectre.Console/Widgets/Charts/BreakdownBar.cs
@@ -11,13 +11,13 @@ public BreakdownBar(List data)
         _data = data ?? throw new ArgumentNullException(nameof(data));
     }
 
-    protected override Measurement Measure(RenderContext context, int maxWidth)
+    protected override Measurement Measure(RenderOptions options, int maxWidth)
     {
         var width = Math.Min(Width ?? maxWidth, maxWidth);
         return new Measurement(width, width);
     }
 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
         var width = Math.Min(Width ?? maxWidth, maxWidth);
 
diff --git a/src/Spectre.Console/Widgets/Charts/BreakdownChart.cs b/src/Spectre.Console/Widgets/Charts/BreakdownChart.cs
index b7f973c35..85067c904 100644
--- a/src/Spectre.Console/Widgets/Charts/BreakdownChart.cs
+++ b/src/Spectre.Console/Widgets/Charts/BreakdownChart.cs
@@ -3,7 +3,7 @@ namespace Spectre.Console;
 /// 
 /// A renderable breakdown chart.
 /// 
-public sealed class BreakdownChart : Renderable, IHasCulture
+public sealed class BreakdownChart : Renderable, IHasCulture, IExpandable
 {
     /// 
     /// Gets the breakdown chart data.
@@ -43,6 +43,13 @@ public sealed class BreakdownChart : Renderable, IHasCulture
     /// Defaults to invariant culture.
     public CultureInfo? Culture { get; set; }
 
+    /// 
+    /// Gets or sets a value indicating whether or not the object should
+    /// expand to the available space. If false, the object's
+    /// width will be auto calculated.
+    /// 
+    public bool Expand { get; set; } = true;
+
     /// 
     /// Initializes a new instance of the  class.
     /// 
@@ -53,14 +60,14 @@ public BreakdownChart()
     }
 
     /// 
-    protected override Measurement Measure(RenderContext context, int maxWidth)
+    protected override Measurement Measure(RenderOptions options, int maxWidth)
     {
         var width = Math.Min(Width ?? maxWidth, maxWidth);
         return new Measurement(width, width);
     }
 
     /// 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
         var width = Math.Min(Width ?? maxWidth, maxWidth);
 
@@ -90,6 +97,11 @@ protected override IEnumerable Render(RenderContext context, int maxWid
             });
         }
 
-        return ((IRenderable)grid).Render(context, width);
+        if (!Expand)
+        {
+            grid.Collapse();
+        }
+
+        return ((IRenderable)grid).Render(options, width);
     }
 }
\ No newline at end of file
diff --git a/src/Spectre.Console/Widgets/Charts/BreakdownTags.cs b/src/Spectre.Console/Widgets/Charts/BreakdownTags.cs
index 669660aaa..a74734555 100644
--- a/src/Spectre.Console/Widgets/Charts/BreakdownTags.cs
+++ b/src/Spectre.Console/Widgets/Charts/BreakdownTags.cs
@@ -14,13 +14,13 @@ public BreakdownTags(List data)
         _data = data ?? throw new ArgumentNullException(nameof(data));
     }
 
-    protected override Measurement Measure(RenderContext context, int maxWidth)
+    protected override Measurement Measure(RenderOptions options, int maxWidth)
     {
         var width = Math.Min(Width ?? maxWidth, maxWidth);
         return new Measurement(width, width);
     }
 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
         var culture = Culture ?? CultureInfo.InvariantCulture;
 
@@ -29,13 +29,13 @@ protected override IEnumerable Render(RenderContext context, int maxWid
         {
             var panel = new Panel(GetTag(item, culture));
             panel.Inline = true;
-            panel.Padding = new Padding(0, 0);
+            panel.Padding = new Padding(0, 0, 2, 0);
             panel.NoBorder();
 
             panels.Add(panel);
         }
 
-        foreach (var segment in ((IRenderable)new Columns(panels).Padding(0, 0)).Render(context, maxWidth))
+        foreach (var segment in ((IRenderable)new Columns(panels).Padding(0, 0)).Render(options, maxWidth))
         {
             yield return segment;
         }
diff --git a/src/Spectre.Console/Widgets/Columns.cs b/src/Spectre.Console/Widgets/Columns.cs
index d6bb8de7c..de20d35c0 100644
--- a/src/Spectre.Console/Widgets/Columns.cs
+++ b/src/Spectre.Console/Widgets/Columns.cs
@@ -55,11 +55,11 @@ public Columns(IEnumerable items)
     }
 
     /// 
-    protected override Measurement Measure(RenderContext context, int maxWidth)
+    protected override Measurement Measure(RenderOptions options, int maxWidth)
     {
         var maxPadding = Math.Max(Padding.GetLeftSafe(), Padding.GetRightSafe());
 
-        var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray();
+        var itemWidths = _items.Select(item => item.Measure(options, maxWidth).Max).ToArray();
         var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding);
         if (columnCount == 0)
         {
@@ -83,11 +83,11 @@ protected override Measurement Measure(RenderContext context, int maxWidth)
     }
 
     /// 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
         var maxPadding = Math.Max(Padding.GetLeftSafe(), Padding.GetRightSafe());
 
-        var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray();
+        var itemWidths = _items.Select(item => item.Measure(options, maxWidth).Max).ToArray();
         var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding);
         if (columnCount == 0)
         {
@@ -121,7 +121,7 @@ protected override IEnumerable Render(RenderContext context, int maxWid
             table.AddRow(_items.Skip(start).Take(columnCount).ToArray());
         }
 
-        return ((IRenderable)table).Render(context, maxWidth);
+        return ((IRenderable)table).Render(options, maxWidth);
     }
 
     // Algorithm borrowed from https://github.com/willmcgugan/rich/blob/master/rich/columns.py
diff --git a/src/Spectre.Console/Widgets/ControlCode.cs b/src/Spectre.Console/Widgets/ControlCode.cs
index fa7b46e40..b83d83323 100644
--- a/src/Spectre.Console/Widgets/ControlCode.cs
+++ b/src/Spectre.Console/Widgets/ControlCode.cs
@@ -9,14 +9,14 @@ public ControlCode(string control)
         _segment = Segment.Control(control);
     }
 
-    protected override Measurement Measure(RenderContext context, int maxWidth)
+    protected override Measurement Measure(RenderOptions options, int maxWidth)
     {
         return new Measurement(0, 0);
     }
 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
-        if (context.Ansi)
+        if (options.Ansi)
         {
             yield return _segment;
         }
diff --git a/src/Spectre.Console/Widgets/Figlet/FigletFontParser.cs b/src/Spectre.Console/Widgets/Figlet/FigletFontParser.cs
index 0ec352bdc..fc03993e1 100644
--- a/src/Spectre.Console/Widgets/Figlet/FigletFontParser.cs
+++ b/src/Spectre.Console/Widgets/Figlet/FigletFontParser.cs
@@ -5,14 +5,20 @@ internal static class FigletFontParser
     public static FigletFont Parse(string source)
     {
         var lines = source.SplitLines();
-        var header = ParseHeader(lines.FirstOrDefault());
 
-        var characters = new List();
+        var headerLine = lines.FirstOrDefault();
+        if (headerLine == null)
+        {
+            throw new InvalidOperationException("Could not read header line");
+        }
+
+        var header = ParseHeader(headerLine);
 
         var index = 32;
         var indexOverridden = false;
         var hasOverriddenIndex = false;
         var buffer = new List();
+        var characters = new List();
 
         foreach (var line in lines.Skip(header.CommentLines + 1))
         {
diff --git a/src/Spectre.Console/Widgets/Figlet/FigletText.cs b/src/Spectre.Console/Widgets/Figlet/FigletText.cs
index f17d6a534..ce9f73f59 100644
--- a/src/Spectre.Console/Widgets/Figlet/FigletText.cs
+++ b/src/Spectre.Console/Widgets/Figlet/FigletText.cs
@@ -3,7 +3,7 @@ namespace Spectre.Console;
 /// 
 /// Represents text rendered with a FIGlet font.
 /// 
-public sealed class FigletText : Renderable, IAlignable
+public sealed class FigletText : Renderable, IHasJustification
 {
     private readonly FigletFont _font;
     private readonly string _text;
@@ -14,7 +14,14 @@ public sealed class FigletText : Renderable, IAlignable
     public Color? Color { get; set; }
 
     /// 
-    public Justify? Alignment { get; set; }
+    public Justify? Justification { get; set; }
+
+    /// 
+    /// Gets or sets a value indicating whether or not
+    /// the right side should be padded.
+    /// 
+    /// Defaults to false.
+    public bool Pad { get; set; }
 
     /// 
     /// Initializes a new instance of the  class.
@@ -37,10 +44,10 @@ public FigletText(FigletFont font, string text)
     }
 
     /// 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
         var style = new Style(Color ?? Console.Color.Default);
-        var alignment = Alignment ?? Justify.Left;
+        var alignment = Justification ?? Console.Justify.Left;
 
         foreach (var row in GetRows(maxWidth))
         {
@@ -49,25 +56,29 @@ protected override IEnumerable Render(RenderContext context, int maxWid
                 var line = new Segment(string.Concat(row.Select(x => x.Lines[index])), style);
 
                 var lineWidth = line.CellCount();
-                if (alignment == Justify.Left)
+                if (alignment == Console.Justify.Left)
                 {
                     yield return line;
 
-                    if (lineWidth < maxWidth)
+                    if (lineWidth < maxWidth && Pad)
                     {
                         yield return Segment.Padding(maxWidth - lineWidth);
                     }
                 }
-                else if (alignment == Justify.Center)
+                else if (alignment == Console.Justify.Center)
                 {
                     var left = (maxWidth - lineWidth) / 2;
                     var right = left + ((maxWidth - lineWidth) % 2);
 
                     yield return Segment.Padding(left);
                     yield return line;
-                    yield return Segment.Padding(right);
+
+                    if (Pad)
+                    {
+                        yield return Segment.Padding(right);
+                    }
                 }
-                else if (alignment == Justify.Right)
+                else if (alignment == Console.Justify.Right)
                 {
                     if (lineWidth < maxWidth)
                     {
diff --git a/src/Spectre.Console/Widgets/Grid.cs b/src/Spectre.Console/Widgets/Grid.cs
index 448ef1b98..7a14798c1 100644
--- a/src/Spectre.Console/Widgets/Grid.cs
+++ b/src/Spectre.Console/Widgets/Grid.cs
@@ -30,6 +30,7 @@ public bool Expand
     }
 
     /// 
+    [Obsolete("Use the Align widget instead. This property will be removed in a later release.")]
     public Justify? Alignment
     {
         get => _alignment;
diff --git a/src/Spectre.Console/Widgets/Layout/Layout.cs b/src/Spectre.Console/Widgets/Layout/Layout.cs
new file mode 100644
index 000000000..f3bc0c4d3
--- /dev/null
+++ b/src/Spectre.Console/Widgets/Layout/Layout.cs
@@ -0,0 +1,311 @@
+namespace Spectre.Console;
+
+/// 
+/// Represents a renderable to divide a fixed height into rows or columns.
+/// 
+public sealed class Layout : Renderable, IRatioResolvable, IHasVisibility
+{
+    private LayoutSplitter _splitter;
+    private Layout[] _children;
+    private IRenderable _renderable;
+    private int _ratio;
+    private int _minimumSize;
+    private int? _size;
+
+    /// 
+    /// Gets or sets the name.
+    /// 
+    public string? Name { get; set; }
+
+    /// 
+    /// Gets or sets the ratio.
+    /// 
+    /// 
+    /// Defaults to 1.
+    /// Must be greater than 0.
+    /// 
+    public int Ratio
+    {
+        get => _ratio;
+        set
+        {
+            if (value < 1)
+            {
+                throw new InvalidOperationException("Ratio must be equal to or greater than 1");
+            }
+
+            _ratio = value;
+        }
+    }
+
+    /// 
+    /// Gets or sets the minimum width.
+    /// 
+    /// 
+    /// Defaults to 1.
+    /// Must be greater than 0.
+    /// 
+    public int MinimumSize
+    {
+        get => _minimumSize;
+        set
+        {
+            if (value < 1)
+            {
+                throw new InvalidOperationException("Minimum size must be equal to or greater than 1");
+            }
+
+            _minimumSize = value;
+        }
+    }
+
+    /// 
+    /// Gets or sets the width.
+    /// 
+    /// 
+    /// Defaults to null.
+    /// Must be greater than 0.
+    /// 
+    public int? Size
+    {
+        get => _size;
+        set
+        {
+            if (value < 1)
+            {
+                throw new InvalidOperationException("Size must be equal to or greater than 1");
+            }
+
+            _size = value;
+        }
+    }
+
+    /// 
+    /// Gets or sets a value indicating whether or not the layout should
+    /// be visible or not.
+    /// 
+    /// Defaults to true.
+    public bool IsVisible { get; set; } = true;
+
+    /// 
+    /// Gets the splitter used for this layout.
+    /// 
+    internal LayoutSplitter Splitter => _splitter;
+
+    /// 
+    /// Gets the  associated with this layout.
+    /// 
+    internal IRenderable Renderable => _renderable;
+
+    /// 
+    /// Gets a child layout by it's name.
+    /// 
+    /// The layout name.
+    /// The specified child .
+    public Layout this[string name]
+    {
+        get => GetLayout(name);
+    }
+
+    /// 
+    /// Initializes a new instance of the  class.
+    /// 
+    /// The layout name.
+    public Layout(string name)
+        : this(name, null)
+    {
+    }
+
+    /// 
+    /// Initializes a new instance of the  class.
+    /// 
+    /// The renderable.
+    public Layout(IRenderable renderable)
+        : this(null, renderable)
+    {
+    }
+
+    /// 
+    /// Initializes a new instance of the  class.
+    /// 
+    /// The layout name.
+    /// The renderable.
+    public Layout(string? name = null, IRenderable? renderable = null)
+    {
+        _splitter = LayoutSplitter.Null;
+        _children = Array.Empty();
+        _renderable = renderable ?? new LayoutPlaceholder(this);
+        _ratio = 1;
+        _size = null;
+
+        Name = name;
+    }
+
+    /// 
+    /// Gets a child layout by it's name.
+    /// 
+    /// The layout name.
+    /// The specified child .
+    public Layout GetLayout(string name)
+    {
+        if (string.IsNullOrEmpty(name))
+        {
+            throw new ArgumentException($"'{nameof(name)}' cannot be null or empty.", nameof(name));
+        }
+
+        var stack = new Stack();
+        stack.Push(this);
+
+        while (stack.Count > 0)
+        {
+            var current = stack.Pop();
+            if (name.Equals(current.Name, StringComparison.OrdinalIgnoreCase))
+            {
+                return current;
+            }
+
+            foreach (var layout in current.GetChildren())
+            {
+                stack.Push(layout);
+            }
+        }
+
+        throw new InvalidOperationException($"Could not find layout '{name}'");
+    }
+
+    /// 
+    /// Splits the layout into rows.
+    /// 
+    /// The layout to split into rows.
+    /// The same instance so that multiple calls can be chained.
+    public Layout SplitRows(params Layout[] children)
+    {
+        Split(LayoutSplitter.Row, children);
+        return this;
+    }
+
+    /// 
+    /// Splits the layout into columns.
+    /// 
+    /// The layout to split into columns.
+    /// The same instance so that multiple calls can be chained.
+    public Layout SplitColumns(params Layout[] children)
+    {
+        Split(LayoutSplitter.Column, children);
+        return this;
+    }
+
+    /// 
+    /// Updates the containing .
+    /// 
+    /// The renderable to use for this layout.
+    /// /// The same instance so that multiple calls can be chained.
+    public Layout Update(IRenderable renderable)
+    {
+        _renderable = renderable ?? new LayoutPlaceholder(this);
+        return this;
+    }
+
+    /// 
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
+    {
+        var height = options.Height ?? options.ConsoleSize.Height;
+        var map = MakeRenderMap(options, maxWidth);
+
+        var layoutLines = new List();
+        layoutLines.AddRange(Enumerable.Range(0, height).Select(x => new SegmentLine()));
+
+        foreach (var (region, lines) in map.Values.Select(x => (x.Region, x.Render)))
+        {
+            foreach (var line in layoutLines
+                .Skip(region.Y)
+                .Take(region.Y + region.Height)
+                .Enumerate().Select(x => (Index: x.Index + region.Y, Line: x.Item))
+                .Zip(lines, (first, second) => (first.Index, Line: second)))
+            {
+                layoutLines[line.Index].AddRange(line.Line);
+            }
+        }
+
+        // Return all the segments in all the lines
+        foreach (var (_, _, last, line) in layoutLines.Enumerate())
+        {
+            foreach (var segment in line)
+            {
+                yield return segment;
+            }
+
+            if (!last)
+            {
+                yield return Segment.LineBreak;
+            }
+        }
+    }
+
+    private IEnumerable GetChildren(bool visibleOnly = false)
+    {
+        return visibleOnly ? _children.Where(c => c.IsVisible) : _children;
+    }
+
+    private bool HasChildren(bool visibleOnly = false)
+    {
+        return visibleOnly ? _children.Any(c => c.IsVisible) : _children.Any();
+    }
+
+    private void Split(LayoutSplitter splitter, Layout[] layouts)
+    {
+        if (_children.Length > 0)
+        {
+            throw new InvalidOperationException("Cannot split the same layout twice");
+        }
+
+        _splitter = splitter ?? throw new ArgumentNullException(nameof(splitter));
+        _children = layouts ?? throw new ArgumentNullException(nameof(layouts));
+    }
+
+    private Dictionary MakeRenderMap(RenderOptions options, int maxWidth)
+    {
+        var result = new Dictionary();
+
+        var renderWidth = maxWidth;
+        var renderHeight = options.Height ?? options.ConsoleSize.Height;
+        var regionMap = MakeRegionMap(maxWidth, renderHeight);
+
+        foreach (var (layout, region) in regionMap.Where(x => !x.Layout.HasChildren(visibleOnly: true)))
+        {
+            var segments = layout.Renderable.Render(options with { Height = region.Height }, region.Width);
+
+            var lines = Segment.SplitLines(segments, region.Width, region.Height);
+            lines = Segment.MakeWidth(region.Width, lines);
+
+            result[layout] = new LayoutRender(region, lines);
+        }
+
+        return result;
+    }
+
+    private IEnumerable<(Layout Layout, Region Region)> MakeRegionMap(int width, int height)
+    {
+        var stack = new Stack<(Layout Layout, Region Region)>();
+        stack.Push((this, new Region(0, 0, width, height)));
+
+        var result = new List<(Layout Layout, Region Region)>();
+
+        while (stack.Count > 0)
+        {
+            var current = stack.Pop();
+            result.Add(current);
+
+            if (current.Layout.HasChildren(visibleOnly: true))
+            {
+                foreach (var childAndRegion in current.Layout.Splitter
+                    .Divide(current.Region, current.Layout.GetChildren(visibleOnly: true)))
+                {
+                    stack.Push(childAndRegion);
+                }
+            }
+        }
+
+        return result.ReverseEnumerable();
+    }
+}
diff --git a/src/Spectre.Console/Widgets/Layout/LayoutPlaceholder.cs b/src/Spectre.Console/Widgets/Layout/LayoutPlaceholder.cs
new file mode 100644
index 000000000..46b959aab
--- /dev/null
+++ b/src/Spectre.Console/Widgets/Layout/LayoutPlaceholder.cs
@@ -0,0 +1,31 @@
+namespace Spectre.Console;
+
+internal sealed class LayoutPlaceholder : Renderable
+{
+    public Layout Layout { get; }
+
+    public LayoutPlaceholder(Layout layout)
+    {
+        Layout = layout ?? throw new ArgumentNullException(nameof(layout));
+    }
+
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
+    {
+        var width = maxWidth;
+        var height = options.Height ?? options.ConsoleSize.Height;
+        var title = Layout.Name != null
+            ? $"{Layout.Name} ({width} x {height})"
+            : $"{width} x {height}";
+
+        var panel = new Panel(
+            Align.Center(new Text("Placeholder"), VerticalAlignment.Middle))
+        {
+            Width = maxWidth,
+            Height = options.Height ?? options.ConsoleSize.Height,
+            Header = new PanelHeader(title),
+            Border = BoxBorder.Rounded,
+        };
+
+        return ((IRenderable)panel).Render(options, maxWidth);
+    }
+}
diff --git a/src/Spectre.Console/Widgets/Layout/LayoutRender.cs b/src/Spectre.Console/Widgets/Layout/LayoutRender.cs
new file mode 100644
index 000000000..a608be798
--- /dev/null
+++ b/src/Spectre.Console/Widgets/Layout/LayoutRender.cs
@@ -0,0 +1,14 @@
+namespace Spectre.Console;
+
+[DebuggerDisplay("{Region,nq}")]
+internal sealed class LayoutRender
+{
+    public Region Region { get; }
+    public List Render { get; }
+
+    public LayoutRender(Region region, List render)
+    {
+        Region = region;
+        Render = render ?? throw new ArgumentNullException(nameof(render));
+    }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console/Widgets/Layout/LayoutSplitter.cs b/src/Spectre.Console/Widgets/Layout/LayoutSplitter.cs
new file mode 100644
index 000000000..6be913cd0
--- /dev/null
+++ b/src/Spectre.Console/Widgets/Layout/LayoutSplitter.cs
@@ -0,0 +1,48 @@
+namespace Spectre.Console;
+
+internal abstract class LayoutSplitter
+{
+    public static LayoutSplitter Column { get; } = new ColumnSplitter();
+    public static LayoutSplitter Row { get; } = new RowSplitter();
+    public static LayoutSplitter Null { get; } = new NullSplitter();
+
+    public abstract IEnumerable<(Layout Child, Region Region)> Divide(Region region, IEnumerable layouts);
+
+    private sealed class NullSplitter : LayoutSplitter
+    {
+        public override IEnumerable<(Layout Child, Region Region)> Divide(Region region, IEnumerable layouts)
+        {
+            yield break;
+        }
+    }
+
+    private sealed class ColumnSplitter : LayoutSplitter
+    {
+        public override IEnumerable<(Layout Child, Region Region)> Divide(Region region, IEnumerable children)
+        {
+            var widths = Ratio.Resolve(region.Width, children);
+            var offset = 0;
+
+            foreach (var (child, childWidth) in children.Zip(widths, (child, width) => (child, width)))
+            {
+                yield return (child, new Region(region.X + offset, region.Y, childWidth, region.Height));
+                offset += childWidth;
+            }
+        }
+    }
+
+    private sealed class RowSplitter : LayoutSplitter
+    {
+        public override IEnumerable<(Layout Child, Region Region)> Divide(Region region, IEnumerable children)
+        {
+            var heights = Ratio.Resolve(region.Height, children);
+            var offset = 0;
+
+            foreach (var (child, childHeight) in children.Zip(heights, (child, height) => (child, height)))
+            {
+                yield return (child, new Region(region.X, region.Y + offset, region.Width, childHeight));
+                offset += childHeight;
+            }
+        }
+    }
+}
diff --git a/src/Spectre.Console/Widgets/Markup.cs b/src/Spectre.Console/Widgets/Markup.cs
index 7b233762c..5e43809e2 100644
--- a/src/Spectre.Console/Widgets/Markup.cs
+++ b/src/Spectre.Console/Widgets/Markup.cs
@@ -4,15 +4,15 @@ namespace Spectre.Console;
 /// A renderable piece of markup text.
 /// 
 [SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
-public sealed class Markup : Renderable, IAlignable, IOverflowable
+public sealed class Markup : Renderable, IHasJustification, IOverflowable
 {
     private readonly Paragraph _paragraph;
 
     /// 
-    public Justify? Alignment
+    public Justify? Justification
     {
-        get => _paragraph.Alignment;
-        set => _paragraph.Alignment = value;
+        get => _paragraph.Justification;
+        set => _paragraph.Justification = value;
     }
 
     /// 
@@ -43,15 +43,15 @@ public Markup(string text, Style? style = null)
     }
 
     /// 
-    protected override Measurement Measure(RenderContext context, int maxWidth)
+    protected override Measurement Measure(RenderOptions options, int maxWidth)
     {
-        return ((IRenderable)_paragraph).Measure(context, maxWidth);
+        return ((IRenderable)_paragraph).Measure(options, maxWidth);
     }
 
     /// 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
-        return ((IRenderable)_paragraph).Render(context, maxWidth);
+        return ((IRenderable)_paragraph).Render(options, maxWidth);
     }
 
     /// 
diff --git a/src/Spectre.Console/Widgets/Padder.cs b/src/Spectre.Console/Widgets/Padder.cs
index 9f2d376df..eeecdf1bc 100644
--- a/src/Spectre.Console/Widgets/Padder.cs
+++ b/src/Spectre.Console/Widgets/Padder.cs
@@ -29,10 +29,10 @@ public Padder(IRenderable child, Padding? padding = null)
     }
 
     /// 
-    protected override Measurement Measure(RenderContext context, int maxWidth)
+    protected override Measurement Measure(RenderOptions options, int maxWidth)
     {
         var paddingWidth = Padding?.GetWidth() ?? 0;
-        var measurement = _child.Measure(context, maxWidth - paddingWidth);
+        var measurement = _child.Measure(options, maxWidth - paddingWidth);
 
         return new Measurement(
             measurement.Min + paddingWidth,
@@ -40,14 +40,14 @@ protected override Measurement Measure(RenderContext context, int maxWidth)
     }
 
     /// 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
         var paddingWidth = Padding?.GetWidth() ?? 0;
         var childWidth = maxWidth - paddingWidth;
 
         if (!Expand)
         {
-            var measurement = _child.Measure(context, maxWidth - paddingWidth);
+            var measurement = _child.Measure(options, maxWidth - paddingWidth);
             childWidth = measurement.Max;
         }
 
@@ -66,7 +66,7 @@ protected override IEnumerable Render(RenderContext context, int maxWid
             result.Add(Segment.LineBreak);
         }
 
-        var child = _child.Render(context, maxWidth - paddingWidth);
+        var child = _child.Render(options, maxWidth - paddingWidth);
         foreach (var line in Segment.SplitLines(child))
         {
             // Left padding
diff --git a/src/Spectre.Console/Widgets/Panel.cs b/src/Spectre.Console/Widgets/Panel.cs
index 0c263cb62..05d41d459 100644
--- a/src/Spectre.Console/Widgets/Panel.cs
+++ b/src/Spectre.Console/Widgets/Panel.cs
@@ -35,6 +35,16 @@ public sealed class Panel : Renderable, IHasBoxBorder, IHasBorder, IExpandable,
     /// 
     public PanelHeader? Header { get; set; }
 
+    /// 
+    /// Gets or sets the width of the panel.
+    /// 
+    public int? Width { get; set; }
+
+    /// 
+    /// Gets or sets the height of the panel.
+    /// 
+    public int? Height { get; set; }
+
     /// 
     /// Gets or sets a value indicating whether or not the panel is inlined.
     /// 
@@ -59,54 +69,73 @@ public Panel(IRenderable content)
     }
 
     /// 
-    protected override Measurement Measure(RenderContext context, int maxWidth)
+    protected override Measurement Measure(RenderOptions options, int maxWidth)
     {
         var child = new Padder(_child, Padding);
-        var childWidth = ((IRenderable)child).Measure(context, maxWidth);
+        return Measure(options, maxWidth, child);
+    }
+
+    private Measurement Measure(RenderOptions options, int maxWidth, IRenderable child)
+    {
+        var edgeWidth = (options.GetSafeBorder(this) is not NoBoxBorder) ? EdgeWidth : 0;
+        var childWidth = child.Measure(options, maxWidth - edgeWidth);
+
+        if (Width != null)
+        {
+            var width = Width.Value - edgeWidth;
+            if (width > childWidth.Max)
+            {
+                childWidth = new Measurement(
+                    childWidth.Min,
+                    width);
+            }
+        }
+
         return new Measurement(
-            childWidth.Min + EdgeWidth,
-            childWidth.Max + EdgeWidth);
+            childWidth.Min + edgeWidth,
+            childWidth.Max + edgeWidth);
     }
 
     /// 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
-        var edgeWidth = EdgeWidth;
-
-        var border = BoxExtensions.GetSafeBorder(Border, !context.Unicode && UseSafeBorder);
+        var border = options.GetSafeBorder(this);
         var borderStyle = BorderStyle ?? Style.Plain;
 
-        var showBorder = true;
-        if (border is NoBoxBorder)
-        {
-            showBorder = false;
-            edgeWidth = 0;
-        }
+        var showBorder = border is not NoBoxBorder;
+        var edgeWidth = showBorder ? EdgeWidth : 0;
 
         var child = new Padder(_child, Padding);
-        var childWidth = maxWidth - edgeWidth;
+        var width = Measure(options, maxWidth, child);
+
+        var panelWidth = Math.Min(!Expand ? width.Max : maxWidth, maxWidth);
+        var innerWidth = panelWidth - edgeWidth;
+
+        var height = Height != null
+            ? Height - 2
+            : options.Height != null
+                ? options.Height - 2
+                : null;
 
         if (!Expand)
         {
-            var measurement = ((IRenderable)child).Measure(context, maxWidth - edgeWidth);
-            childWidth = measurement.Max;
+            // Set the height to the explicit height (or null)
+            // if the panel isn't expandable.
+            height = Height != null ? Height - 2 : null;
         }
 
-        var panelWidth = childWidth + edgeWidth;
-        panelWidth = Math.Min(panelWidth, maxWidth);
-        childWidth = panelWidth - edgeWidth;
-
+        // Start building the panel
         var result = new List();
 
+        // Panel top
         if (showBorder)
         {
-            // Panel top
-            AddTopBorder(result, context, border, borderStyle, panelWidth);
+            AddTopBorder(result, options, border, borderStyle, panelWidth);
         }
 
         // Split the child segments into lines.
-        var childSegments = ((IRenderable)child).Render(context, childWidth);
-        foreach (var (_, _, last, line) in Segment.SplitLines(childSegments, childWidth).Enumerate())
+        var childSegments = ((IRenderable)child).Render(options with { Height = height }, innerWidth);
+        foreach (var (_, _, last, line) in Segment.SplitLines(childSegments, innerWidth, height).Enumerate())
         {
             if (line.Count == 1 && line[0].IsWhiteSpace)
             {
@@ -125,9 +154,9 @@ protected override IEnumerable Render(RenderContext context, int maxWid
 
             // Do we need to pad the panel?
             var length = line.Sum(segment => segment.CellCount());
-            if (length < childWidth)
+            if (length < innerWidth)
             {
-                var diff = childWidth - length;
+                var diff = innerWidth - length;
                 content.Add(Segment.Padding(diff));
             }
 
@@ -170,7 +199,7 @@ protected override IEnumerable Render(RenderContext context, int maxWid
     }
 
     private void AddTopBorder(
-        List result, RenderContext context, BoxBorder border,
+        List result, RenderOptions options, BoxBorder border,
         Style borderStyle, int panelWidth)
     {
         var rule = new Rule
@@ -180,14 +209,14 @@ private void AddTopBorder(
             TitlePadding = 1,
             TitleSpacing = 0,
             Title = Header?.Text,
-            Alignment = Header?.Alignment ?? Justify.Left,
+            Justification = Header?.Justification ?? Justify.Left,
         };
 
         // Top left border
         result.Add(new Segment(border.GetPart(BoxBorderPart.TopLeft), borderStyle));
 
         // Top border (and header text if specified)
-        result.AddRange(((IRenderable)rule).Render(context, panelWidth - 2).Where(x => !x.IsLineBreak));
+        result.AddRange(((IRenderable)rule).Render(options, panelWidth - 2).Where(x => !x.IsLineBreak));
 
         // Top right border
         result.Add(new Segment(border.GetPart(BoxBorderPart.TopRight), borderStyle));
diff --git a/src/Spectre.Console/Widgets/PanelHeader.cs b/src/Spectre.Console/Widgets/PanelHeader.cs
index 951dcbdc5..97cdd3308 100644
--- a/src/Spectre.Console/Widgets/PanelHeader.cs
+++ b/src/Spectre.Console/Widgets/PanelHeader.cs
@@ -3,7 +3,7 @@ namespace Spectre.Console;
 /// 
 /// Represents a panel header.
 /// 
-public sealed class PanelHeader : IAlignable
+public sealed class PanelHeader : IHasJustification
 {
     /// 
     /// Gets the panel header text.
@@ -13,7 +13,7 @@ public sealed class PanelHeader : IAlignable
     /// 
     /// Gets or sets the panel header alignment.
     /// 
-    public Justify? Alignment { get; set; }
+    public Justify? Justification { get; set; }
 
     /// 
     /// Initializes a new instance of the  class.
@@ -23,7 +23,7 @@ public sealed class PanelHeader : IAlignable
     public PanelHeader(string text, Justify? alignment = null)
     {
         Text = text ?? throw new ArgumentNullException(nameof(text));
-        Alignment = alignment;
+        Justification = alignment;
     }
 
     /// 
@@ -57,7 +57,7 @@ public PanelHeader SetStyle(string style)
     /// The same instance so that multiple calls can be chained.
     public PanelHeader SetAlignment(Justify alignment)
     {
-        Alignment = alignment;
+        Justification = alignment;
         return this;
     }
 }
\ No newline at end of file
diff --git a/src/Spectre.Console/Widgets/Paragraph.cs b/src/Spectre.Console/Widgets/Paragraph.cs
index 5ac2f8ed9..96012d4bc 100644
--- a/src/Spectre.Console/Widgets/Paragraph.cs
+++ b/src/Spectre.Console/Widgets/Paragraph.cs
@@ -5,14 +5,14 @@ namespace Spectre.Console;
 /// of the paragraph can have individual styling.
 /// 
 [DebuggerDisplay("{_text,nq}")]
-public sealed class Paragraph : Renderable, IAlignable, IOverflowable
+public sealed class Paragraph : Renderable, IHasJustification, IOverflowable
 {
     private readonly List _lines;
 
     /// 
     /// Gets or sets the alignment of the whole paragraph.
     /// 
-    public Justify? Alignment { get; set; }
+    public Justify? Justification { get; set; }
 
     /// 
     /// Gets or sets the text overflow strategy.
@@ -115,7 +115,7 @@ public Paragraph Append(string text, Style? style = null)
     }
 
     /// 
-    protected override Measurement Measure(RenderContext context, int maxWidth)
+    protected override Measurement Measure(RenderOptions options, int maxWidth)
     {
         if (_lines.Count == 0)
         {
@@ -129,11 +129,11 @@ protected override Measurement Measure(RenderContext context, int maxWidth)
     }
 
     /// 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
-        if (context is null)
+        if (options is null)
         {
-            throw new ArgumentNullException(nameof(context));
+            throw new ArgumentNullException(nameof(options));
         }
 
         if (_lines.Count == 0)
@@ -141,13 +141,13 @@ protected override IEnumerable Render(RenderContext context, int maxWid
             return Array.Empty();
         }
 
-        var lines = context.SingleLine
+        var lines = options.SingleLine
             ? new List(_lines)
             : SplitLines(maxWidth);
 
         // Justify lines
-        var justification = context.Justification ?? Alignment ?? Justify.Left;
-        if (justification != Justify.Left)
+        var justification = options.Justification ?? Justification ?? Console.Justify.Left;
+        if (justification != Console.Justify.Left)
         {
             foreach (var line in lines)
             {
@@ -155,7 +155,7 @@ protected override IEnumerable Render(RenderContext context, int maxWid
             }
         }
 
-        if (context.SingleLine)
+        if (options.SingleLine)
         {
             // Return the first line
             return lines[0].Where(segment => !segment.IsLineBreak);
diff --git a/src/Spectre.Console/Widgets/ProgressBar.cs b/src/Spectre.Console/Widgets/ProgressBar.cs
index 67ef339d0..6b59a420d 100644
--- a/src/Spectre.Console/Widgets/ProgressBar.cs
+++ b/src/Spectre.Console/Widgets/ProgressBar.cs
@@ -23,13 +23,13 @@ internal sealed class ProgressBar : Renderable, IHasCulture
 
     internal static Style DefaultPulseStyle { get; } = new Style(foreground: Color.DodgerBlue1, background: Color.Grey23);
 
-    protected override Measurement Measure(RenderContext context, int maxWidth)
+    protected override Measurement Measure(RenderOptions options, int maxWidth)
     {
         var width = Math.Min(Width ?? maxWidth, maxWidth);
         return new Measurement(4, width);
     }
 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
         var width = Math.Min(Width ?? maxWidth, maxWidth);
         var completedBarCount = Math.Min(MaxValue, Math.Max(0, Value));
@@ -37,7 +37,7 @@ protected override IEnumerable Render(RenderContext context, int maxWid
 
         if (IsIndeterminate && !isCompleted)
         {
-            foreach (var segment in RenderIndeterminate(context, width))
+            foreach (var segment in RenderIndeterminate(options, width))
             {
                 yield return segment;
             }
@@ -45,7 +45,7 @@ protected override IEnumerable Render(RenderContext context, int maxWid
             yield break;
         }
 
-        var bar = !context.Unicode ? AsciiBar : UnicodeBar;
+        var bar = !options.Unicode ? AsciiBar : UnicodeBar;
         var style = isCompleted ? FinishedStyle : CompletedStyle;
         var barCount = Math.Max(0, (int)(width * (completedBarCount / MaxValue)));
 
@@ -84,29 +84,29 @@ protected override IEnumerable Render(RenderContext context, int maxWid
                 }
             }
 
-            var legacy = context.ColorSystem == ColorSystem.NoColors || context.ColorSystem == ColorSystem.Legacy;
+            var legacy = options.ColorSystem == ColorSystem.NoColors || options.ColorSystem == ColorSystem.Legacy;
             var remainingToken = ShowRemaining && !legacy ? bar : ' ';
             yield return new Segment(new string(remainingToken, diff), RemainingStyle);
         }
     }
 
-    private IEnumerable RenderIndeterminate(RenderContext context, int width)
+    private IEnumerable RenderIndeterminate(RenderOptions options, int width)
     {
-        var bar = context.Unicode ? UnicodeBar.ToString() : AsciiBar.ToString();
+        var bar = options.Unicode ? UnicodeBar.ToString() : AsciiBar.ToString();
         var style = IndeterminateStyle ?? DefaultPulseStyle;
 
         IEnumerable GetPulseSegments()
         {
             // For 1-bit and 3-bit colors, fall back to
             // a simpler versions with only two colors.
-            if (context.ColorSystem == ColorSystem.NoColors ||
-                context.ColorSystem == ColorSystem.Legacy)
+            if (options.ColorSystem == ColorSystem.NoColors ||
+                options.ColorSystem == ColorSystem.Legacy)
             {
                 // First half of the pulse
                 var segments = Enumerable.Repeat(new Segment(bar, new Style(style.Foreground)), PULSESIZE / 2);
 
                 // Second half of the pulse
-                var legacy = context.ColorSystem == ColorSystem.NoColors || context.ColorSystem == ColorSystem.Legacy;
+                var legacy = options.ColorSystem == ColorSystem.NoColors || options.ColorSystem == ColorSystem.Legacy;
                 var bar2 = legacy ? " " : bar;
                 segments = segments.Concat(Enumerable.Repeat(new Segment(bar2, new Style(style.Background)), PULSESIZE - (PULSESIZE / 2)));
 
diff --git a/src/Spectre.Console/Widgets/Rows.cs b/src/Spectre.Console/Widgets/Rows.cs
index 32f88cf3c..953944071 100644
--- a/src/Spectre.Console/Widgets/Rows.cs
+++ b/src/Spectre.Console/Widgets/Rows.cs
@@ -29,7 +29,7 @@ public Rows(IEnumerable children)
     }
 
     /// 
-    protected override Measurement Measure(RenderContext context, int maxWidth)
+    protected override Measurement Measure(RenderOptions options, int maxWidth)
     {
         if (Expand)
         {
@@ -37,7 +37,7 @@ protected override Measurement Measure(RenderContext context, int maxWidth)
         }
         else
         {
-            var measurements = _children.Select(c => c.Measure(context, maxWidth));
+            var measurements = _children.Select(c => c.Measure(options, maxWidth));
             return new Measurement(
                 measurements.Min(c => c.Min),
                 measurements.Min(c => c.Max));
@@ -45,13 +45,13 @@ protected override Measurement Measure(RenderContext context, int maxWidth)
     }
 
     /// 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
         var result = new List();
 
         foreach (var child in _children)
         {
-            var segments = child.Render(context, maxWidth);
+            var segments = child.Render(options, maxWidth);
             foreach (var (_, _, last, segment) in segments.Enumerate())
             {
                 result.Add(segment);
diff --git a/src/Spectre.Console/Widgets/Rule.cs b/src/Spectre.Console/Widgets/Rule.cs
index 210318c09..0bcef99da 100644
--- a/src/Spectre.Console/Widgets/Rule.cs
+++ b/src/Spectre.Console/Widgets/Rule.cs
@@ -3,7 +3,7 @@ namespace Spectre.Console;
 /// 
 /// A renderable horizontal rule.
 /// 
-public sealed class Rule : Renderable, IAlignable, IHasBoxBorder
+public sealed class Rule : Renderable, IHasJustification, IHasBoxBorder
 {
     /// 
     /// Gets or sets the rule title markup text.
@@ -16,9 +16,9 @@ public sealed class Rule : Renderable, IAlignable, IHasBoxBorder
     public Style? Style { get; set; }
 
     /// 
-    /// Gets or sets the rule's title alignment.
+    /// Gets or sets the rule's title justification.
     /// 
-    public Justify? Alignment { get; set; }
+    public Justify? Justification { get; set; }
 
     /// 
     public BoxBorder Border { get; set; } = BoxBorder.Square;
@@ -43,17 +43,17 @@ public Rule(string title)
     }
 
     /// 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
         var extraLength = (2 * TitlePadding) + (2 * TitleSpacing);
 
         if (Title == null || maxWidth <= extraLength)
         {
-            return GetLineWithoutTitle(context, maxWidth);
+            return GetLineWithoutTitle(options, maxWidth);
         }
 
         // Get the title and make sure it fits.
-        var title = GetTitleSegments(context, Title, maxWidth - extraLength);
+        var title = GetTitleSegments(options, Title, maxWidth - extraLength);
         if (Segment.CellCount(title) > maxWidth - extraLength)
         {
             // Truncate the title
@@ -61,11 +61,11 @@ protected override IEnumerable Render(RenderContext context, int maxWid
             if (!title.Any())
             {
                 // We couldn't fit the title at all.
-                return GetLineWithoutTitle(context, maxWidth);
+                return GetLineWithoutTitle(options, maxWidth);
             }
         }
 
-        var (left, right) = GetLineSegments(context, maxWidth, title);
+        var (left, right) = GetLineSegments(options, maxWidth, title);
 
         var segments = new List();
         segments.Add(left);
@@ -76,9 +76,9 @@ protected override IEnumerable Render(RenderContext context, int maxWid
         return segments;
     }
 
-    private IEnumerable GetLineWithoutTitle(RenderContext context, int maxWidth)
+    private IEnumerable GetLineWithoutTitle(RenderOptions options, int maxWidth)
     {
-        var border = Border.GetSafeBorder(safe: !context.Unicode);
+        var border = Border.GetSafeBorder(safe: !options.Unicode);
         var text = border.GetPart(BoxBorderPart.Top).Repeat(maxWidth);
 
         return new[]
@@ -88,21 +88,21 @@ private IEnumerable GetLineWithoutTitle(RenderContext context, int maxW
         };
     }
 
-    private IEnumerable GetTitleSegments(RenderContext context, string title, int width)
+    private IEnumerable GetTitleSegments(RenderOptions options, string title, int width)
     {
         title = title.NormalizeNewLines().ReplaceExact("\n", " ").Trim();
         var markup = new Markup(title, Style);
-        return ((IRenderable)markup).Render(context.WithSingleLine(), width);
+        return ((IRenderable)markup).Render(options with { SingleLine = true }, width);
     }
 
-    private (Segment Left, Segment Right) GetLineSegments(RenderContext context, int width, IEnumerable title)
+    private (Segment Left, Segment Right) GetLineSegments(RenderOptions options, int width, IEnumerable title)
     {
         var titleLength = Segment.CellCount(title);
 
-        var border = Border.GetSafeBorder(safe: !context.Unicode);
+        var border = Border.GetSafeBorder(safe: !options.Unicode);
         var borderPart = border.GetPart(BoxBorderPart.Top);
 
-        var alignment = Alignment ?? Justify.Center;
+        var alignment = Justification ?? Justify.Center;
         if (alignment == Justify.Left)
         {
             var left = new Segment(borderPart.Repeat(TitlePadding) + new string(' ', TitleSpacing), Style ?? Style.Plain);
diff --git a/src/Spectre.Console/Widgets/Table/Table.cs b/src/Spectre.Console/Widgets/Table/Table.cs
index a22dd639e..c0d83e186 100644
--- a/src/Spectre.Console/Widgets/Table/Table.cs
+++ b/src/Spectre.Console/Widgets/Table/Table.cs
@@ -59,6 +59,7 @@ public sealed class Table : Renderable, IHasTableBorder, IExpandable, IAlignable
     public TableTitle? Caption { get; set; }
 
     /// 
+    [Obsolete("Use the Align widget instead. This property will be removed in a later release.")]
     public Justify? Alignment { get; set; }
 
     // Whether this is a grid or not.
@@ -100,14 +101,14 @@ public Table AddColumn(TableColumn column)
     }
 
     /// 
-    protected override Measurement Measure(RenderContext context, int maxWidth)
+    protected override Measurement Measure(RenderOptions options, int maxWidth)
     {
-        if (context is null)
+        if (options is null)
         {
-            throw new ArgumentNullException(nameof(context));
+            throw new ArgumentNullException(nameof(options));
         }
 
-        var measurer = new TableMeasurer(this, context);
+        var measurer = new TableMeasurer(this, options);
 
         // Calculate the total cell width
         var totalCellWidth = measurer.CalculateTotalCellWidth(maxWidth);
@@ -120,14 +121,14 @@ protected override Measurement Measure(RenderContext context, int maxWidth)
     }
 
     /// 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
-        if (context is null)
+        if (options is null)
         {
-            throw new ArgumentNullException(nameof(context));
+            throw new ArgumentNullException(nameof(options));
         }
 
-        var measurer = new TableMeasurer(this, context);
+        var measurer = new TableMeasurer(this, options);
 
         // Calculate the column and table width
         var totalCellWidth = measurer.CalculateTotalCellWidth(maxWidth);
@@ -139,7 +140,7 @@ protected override IEnumerable Render(RenderContext context, int maxWid
 
         // Render the table
         return TableRenderer.Render(
-            new TableRendererContext(this, context, rows, tableWidth, maxWidth),
+            new TableRendererContext(this, options, rows, tableWidth, maxWidth),
             columnWidths);
     }
 
diff --git a/src/Spectre.Console/Widgets/Table/TableAccessor.cs b/src/Spectre.Console/Widgets/Table/TableAccessor.cs
index 2979a0d14..3dda3eea6 100644
--- a/src/Spectre.Console/Widgets/Table/TableAccessor.cs
+++ b/src/Spectre.Console/Widgets/Table/TableAccessor.cs
@@ -4,12 +4,12 @@ internal abstract class TableAccessor
 {
     private readonly Table _table;
 
-    public RenderContext Options { get; }
+    public RenderOptions Options { get; }
     public IReadOnlyList Columns => _table.Columns;
     public virtual IReadOnlyList Rows => _table.Rows;
     public bool Expand => _table.Expand || _table.Width != null;
 
-    protected TableAccessor(Table table, RenderContext options)
+    protected TableAccessor(Table table, RenderOptions options)
     {
         _table = table ?? throw new ArgumentNullException(nameof(table));
         Options = options ?? throw new ArgumentNullException(nameof(options));
diff --git a/src/Spectre.Console/Widgets/Table/TableMeasurer.cs b/src/Spectre.Console/Widgets/Table/TableMeasurer.cs
index d7a26cd65..d436a08df 100644
--- a/src/Spectre.Console/Widgets/Table/TableMeasurer.cs
+++ b/src/Spectre.Console/Widgets/Table/TableMeasurer.cs
@@ -8,7 +8,7 @@ internal sealed class TableMeasurer : TableAccessor
     private readonly TableBorder _border;
     private readonly bool _padRightCell;
 
-    public TableMeasurer(Table table, RenderContext options)
+    public TableMeasurer(Table table, RenderOptions options)
         : base(table, options)
     {
         _explicitWidth = table.Width;
diff --git a/src/Spectre.Console/Widgets/Table/TableRenderer.cs b/src/Spectre.Console/Widgets/Table/TableRenderer.cs
index fadade9b8..467fe616c 100644
--- a/src/Spectre.Console/Widgets/Table/TableRenderer.cs
+++ b/src/Spectre.Console/Widgets/Table/TableRenderer.cs
@@ -26,7 +26,7 @@ public static List Render(TableRendererContext context, List colum
             foreach (var (columnIndex, _, _, (rowWidth, cell)) in columnWidths.Zip(row).Enumerate())
             {
                 var justification = context.Columns[columnIndex].Alignment;
-                var childContext = context.Options.WithJustification(justification);
+                var childContext = context.Options with { Justification = justification };
 
                 var lines = Segment.SplitLines(cell.Render(childContext, rowWidth));
                 cellHeight = Math.Max(cellHeight, lines.Count);
@@ -159,7 +159,7 @@ private static IEnumerable RenderAnnotation(TableRendererContext contex
         }
 
         var paragraph = new Markup(header.Text, header.Style ?? defaultStyle)
-            .Alignment(Justify.Center)
+            .Justify(Justify.Center)
             .Overflow(Overflow.Ellipsis);
 
         // Render the paragraphs
diff --git a/src/Spectre.Console/Widgets/Table/TableRendererContext.cs b/src/Spectre.Console/Widgets/Table/TableRendererContext.cs
index 17e98c7d9..b54980248 100644
--- a/src/Spectre.Console/Widgets/Table/TableRendererContext.cs
+++ b/src/Spectre.Console/Widgets/Table/TableRendererContext.cs
@@ -31,9 +31,12 @@ internal sealed class TableRendererContext : TableAccessor
     public bool PadRightCell => _table.PadRightCell;
     public TableTitle? Title => _table.Title;
     public TableTitle? Caption => _table.Caption;
+
+#pragma warning disable CS0618 // Type or member is obsolete
     public Justify? Alignment => _table.Alignment;
+#pragma warning restore CS0618 // Type or member is obsolete
 
-    public TableRendererContext(Table table, RenderContext options, IEnumerable rows, int tableWidth, int maxWidth)
+    public TableRendererContext(Table table, RenderOptions options, IEnumerable rows, int tableWidth, int maxWidth)
         : base(table, options)
     {
         _table = table ?? throw new ArgumentNullException(nameof(table));
diff --git a/src/Spectre.Console/Widgets/Table/TableRowCollection.cs b/src/Spectre.Console/Widgets/Table/TableRowCollection.cs
index 11c093863..b8f2f93bf 100644
--- a/src/Spectre.Console/Widgets/Table/TableRowCollection.cs
+++ b/src/Spectre.Console/Widgets/Table/TableRowCollection.cs
@@ -58,7 +58,7 @@ public int Add(IEnumerable columns)
         {
             var row = CreateRow(columns);
             _list.Add(row);
-            return _list.IndexOf(row);
+            return _list.Count - 1;
         }
     }
 
@@ -107,8 +107,7 @@ public void Update(int row, int column, IRenderable cellData)
                 throw new IndexOutOfRangeException("Table row index cannot exceed the number of rows in the table.");
             }
 
-            var tableRow = _list.ElementAtOrDefault(row);
-
+            var tableRow = _list.ElementAt(row);
             var currentRenderables = tableRow.ToList();
 
             if (column < 0)
diff --git a/src/Spectre.Console/Widgets/Text.cs b/src/Spectre.Console/Widgets/Text.cs
index e84c2f3e7..d715730d3 100644
--- a/src/Spectre.Console/Widgets/Text.cs
+++ b/src/Spectre.Console/Widgets/Text.cs
@@ -5,7 +5,7 @@ namespace Spectre.Console;
 /// 
 [DebuggerDisplay("{_text,nq}")]
 [SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
-public sealed class Text : Renderable, IAlignable, IOverflowable
+public sealed class Text : Renderable, IHasJustification, IOverflowable
 {
     private readonly Paragraph _paragraph;
 
@@ -32,10 +32,10 @@ public Text(string text, Style? style = null)
     /// 
     /// Gets or sets the text alignment.
     /// 
-    public Justify? Alignment
+    public Justify? Justification
     {
-        get => _paragraph.Alignment;
-        set => _paragraph.Alignment = value;
+        get => _paragraph.Justification;
+        set => _paragraph.Justification = value;
     }
 
     /// 
@@ -58,14 +58,14 @@ public Overflow? Overflow
     public int Lines => _paragraph.Lines;
 
     /// 
-    protected override Measurement Measure(RenderContext context, int maxWidth)
+    protected override Measurement Measure(RenderOptions options, int maxWidth)
     {
-        return ((IRenderable)_paragraph).Measure(context, maxWidth);
+        return ((IRenderable)_paragraph).Measure(options, maxWidth);
     }
 
     /// 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
-        return ((IRenderable)_paragraph).Render(context, maxWidth);
+        return ((IRenderable)_paragraph).Render(options, maxWidth);
     }
 }
\ No newline at end of file
diff --git a/src/Spectre.Console/Widgets/TextPath.cs b/src/Spectre.Console/Widgets/TextPath.cs
index 2d03e1912..92c847170 100644
--- a/src/Spectre.Console/Widgets/TextPath.cs
+++ b/src/Spectre.Console/Widgets/TextPath.cs
@@ -3,7 +3,7 @@ namespace Spectre.Console;
 /// 
 /// Representation of a file system path.
 /// 
-public sealed class TextPath : IRenderable, IAlignable
+public sealed class TextPath : IRenderable, IHasJustification
 {
     private const string Ellipsis = "...";
     private const string UnicodeEllipsis = "…";
@@ -35,7 +35,7 @@ public sealed class TextPath : IRenderable, IAlignable
     /// 
     /// Gets or sets the alignment.
     /// 
-    public Justify? Alignment { get; set; }
+    public Justify? Justification { get; set; }
 
     /// 
     /// Initializes a new instance of the  class.
@@ -66,9 +66,9 @@ public TextPath(string path)
     }
 
     /// 
-    public Measurement Measure(RenderContext context, int maxWidth)
+    public Measurement Measure(RenderOptions options, int maxWidth)
     {
-        var fitted = Fit(context, maxWidth);
+        var fitted = Fit(options, maxWidth);
         var separatorCount = fitted.Length - 1;
         var length = fitted.Sum(f => f.Length) + separatorCount;
 
@@ -78,16 +78,14 @@ public Measurement Measure(RenderContext context, int maxWidth)
     }
 
     /// 
-    public IEnumerable Render(RenderContext context, int maxWidth)
+    public IEnumerable Render(RenderOptions options, int maxWidth)
     {
-        var alignment = Alignment ?? Justify.Left;
-
         var rootStyle = RootStyle ?? Style.Plain;
         var separatorStyle = SeparatorStyle ?? Style.Plain;
         var stemStyle = StemStyle ?? Style.Plain;
         var leafStyle = LeafStyle ?? Style.Plain;
 
-        var fitted = Fit(context, maxWidth);
+        var fitted = Fit(options, maxWidth);
         var parts = new List();
         foreach (var (_, first, last, item) in fitted.Enumerate())
         {
@@ -119,7 +117,7 @@ public IEnumerable Render(RenderContext context, int maxWidth)
         }
 
         // Align the result
-        Aligner.Align(parts, Alignment, maxWidth);
+        Aligner.Align(parts, Justification, maxWidth);
 
         // Insert a line break
         parts.Add(Segment.LineBreak);
@@ -127,7 +125,7 @@ public IEnumerable Render(RenderContext context, int maxWidth)
         return parts;
     }
 
-    private string[] Fit(RenderContext context, int maxWidth)
+    private string[] Fit(RenderOptions options, int maxWidth)
     {
         // No parts?
         if (_parts.Length == 0)
@@ -141,7 +139,7 @@ private string[] Fit(RenderContext context, int maxWidth)
             return _parts;
         }
 
-        var ellipsis = context.Unicode ? UnicodeEllipsis : Ellipsis;
+        var ellipsis = options.Unicode ? UnicodeEllipsis : Ellipsis;
         var ellipsisLength = Cell.GetCellLength(ellipsis);
 
         if (_parts.Length >= 2)
diff --git a/src/Spectre.Console/Widgets/Tree.cs b/src/Spectre.Console/Widgets/Tree.cs
index 9929a53f8..289884ed9 100644
--- a/src/Spectre.Console/Widgets/Tree.cs
+++ b/src/Spectre.Console/Widgets/Tree.cs
@@ -47,7 +47,7 @@ public Tree(string label)
     }
 
     /// 
-    protected override IEnumerable Render(RenderContext context, int maxWidth)
+    protected override IEnumerable Render(RenderOptions options, int maxWidth)
     {
         var result = new List();
         var visitedNodes = new HashSet();
@@ -56,7 +56,7 @@ protected override IEnumerable Render(RenderContext context, int maxWid
         stack.Push(new Queue(new[] { _root }));
 
         var levels = new List();
-        levels.Add(GetGuide(context, TreeGuidePart.Continue));
+        levels.Add(GetGuide(options, TreeGuidePart.Continue));
 
         while (stack.Count > 0)
         {
@@ -66,7 +66,7 @@ protected override IEnumerable Render(RenderContext context, int maxWid
                 levels.RemoveLast();
                 if (levels.Count > 0)
                 {
-                    levels.AddOrReplaceLast(GetGuide(context, TreeGuidePart.Fork));
+                    levels.AddOrReplaceLast(GetGuide(options, TreeGuidePart.Fork));
                 }
 
                 continue;
@@ -83,11 +83,11 @@ protected override IEnumerable Render(RenderContext context, int maxWid
 
             if (isLastChild)
             {
-                levels.AddOrReplaceLast(GetGuide(context, TreeGuidePart.End));
+                levels.AddOrReplaceLast(GetGuide(options, TreeGuidePart.End));
             }
 
             var prefix = levels.Skip(1).ToList();
-            var renderableLines = Segment.SplitLines(current.Renderable.Render(context, maxWidth - Segment.CellCount(prefix)));
+            var renderableLines = Segment.SplitLines(current.Renderable.Render(options, maxWidth - Segment.CellCount(prefix)));
 
             foreach (var (_, isFirstLine, _, line) in renderableLines.Enumerate())
             {
@@ -102,14 +102,14 @@ protected override IEnumerable Render(RenderContext context, int maxWid
                 if (isFirstLine && prefix.Count > 0)
                 {
                     var part = isLastChild ? TreeGuidePart.Space : TreeGuidePart.Continue;
-                    prefix.AddOrReplaceLast(GetGuide(context, part));
+                    prefix.AddOrReplaceLast(GetGuide(options, part));
                 }
             }
 
             if (current.Expanded && current.Nodes.Count > 0)
             {
-                levels.AddOrReplaceLast(GetGuide(context, isLastChild ? TreeGuidePart.Space : TreeGuidePart.Continue));
-                levels.Add(GetGuide(context, current.Nodes.Count == 1 ? TreeGuidePart.End : TreeGuidePart.Fork));
+                levels.AddOrReplaceLast(GetGuide(options, isLastChild ? TreeGuidePart.Space : TreeGuidePart.Continue));
+                levels.Add(GetGuide(options, current.Nodes.Count == 1 ? TreeGuidePart.End : TreeGuidePart.Fork));
 
                 stack.Push(new Queue(current.Nodes));
             }
@@ -118,9 +118,9 @@ protected override IEnumerable Render(RenderContext context, int maxWid
         return result;
     }
 
-    private Segment GetGuide(RenderContext context, TreeGuidePart part)
+    private Segment GetGuide(RenderOptions options, TreeGuidePart part)
     {
-        var guide = Guide.GetSafeTreeGuide(safe: !context.Unicode);
+        var guide = Guide.GetSafeTreeGuide(safe: !options.Unicode);
         return new Segment(guide.GetPart(part), Style ?? Style.Plain);
     }
 }
\ No newline at end of file
diff --git a/test/Directory.Build.props b/test/Directory.Build.props
index 88ea54502..7d0d62ad7 100644
--- a/test/Directory.Build.props
+++ b/test/Directory.Build.props
@@ -9,7 +9,7 @@
     
       All
     
-    
+    
       All
     
   
diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Parsing/CannotAssignValueToFlag/Test_1.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Parsing/CannotAssignValueToFlag/Test_1.Output.verified.txt
index 7def92f3a..609a7a654 100644
--- a/test/Spectre.Console.Cli.Tests/Expectations/Parsing/CannotAssignValueToFlag/Test_1.Output.verified.txt
+++ b/test/Spectre.Console.Cli.Tests/Expectations/Parsing/CannotAssignValueToFlag/Test_1.Output.verified.txt
@@ -1,4 +1,4 @@
 Error: Flags cannot be assigned a value.
 
-       dog --alive foo
+       dog --alive=indeterminate foo
            ^^^^^^^ Can't assign value
\ No newline at end of file
diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Parsing/CannotAssignValueToFlag/Test_2.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Parsing/CannotAssignValueToFlag/Test_2.Output.verified.txt
index b3381d2f7..8027f610f 100644
--- a/test/Spectre.Console.Cli.Tests/Expectations/Parsing/CannotAssignValueToFlag/Test_2.Output.verified.txt
+++ b/test/Spectre.Console.Cli.Tests/Expectations/Parsing/CannotAssignValueToFlag/Test_2.Output.verified.txt
@@ -1,4 +1,4 @@
 Error: Flags cannot be assigned a value.
 
-       dog -a foo
+       dog -a=indeterminate foo
            ^^ Can't assign value
\ No newline at end of file
diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Parsing/Quoted_Strings.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Parsing/Quoted_Strings.Output.verified.txt
deleted file mode 100644
index 1c0548318..000000000
--- a/test/Spectre.Console.Cli.Tests/Expectations/Parsing/Quoted_Strings.Output.verified.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-# Raw
-/c
-set && pause
\ No newline at end of file
diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Parsing/UnterminatedQuote/Test_1.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Parsing/UnterminatedQuote/Test_1.Output.verified.txt
deleted file mode 100644
index a6d70327d..000000000
--- a/test/Spectre.Console.Cli.Tests/Expectations/Parsing/UnterminatedQuote/Test_1.Output.verified.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-Error: Encountered unterminated quoted string 'Rufus'.
-
-       --name "Rufus
-              ^^^^^^ Did you forget the closing quotation mark?
\ No newline at end of file
diff --git a/test/Spectre.Console.Cli.Tests/Spectre.Console.Cli.Tests.csproj b/test/Spectre.Console.Cli.Tests/Spectre.Console.Cli.Tests.csproj
index 06e7fc722..6f6843471 100644
--- a/test/Spectre.Console.Cli.Tests/Spectre.Console.Cli.Tests.csproj
+++ b/test/Spectre.Console.Cli.Tests/Spectre.Console.Cli.Tests.csproj
@@ -1,8 +1,7 @@
 
 
   
-    net6.0;net48
-    net6.0
+    net7.0
   
 
   
@@ -14,10 +13,9 @@
     
     
     
-    
-    
-    
-    
+    
+    
+    
     
     
       all
@@ -31,15 +29,4 @@
     
   
 
-  
-    
-      $([System.String]::Copy('%(FileName)').Split('.')[0])
-      %(ParentFile).cs
-    
-    
-      $([System.String]::Copy('%(FileName)').Split('.')[0])
-      %(ParentFile).cs
-    
-  
-
 
diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Parsing.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Parsing.cs
index 3ee830538..c195ffdcf 100644
--- a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Parsing.cs
+++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Parsing.cs
@@ -15,17 +15,17 @@ public sealed class UnknownCommand
             public Task Should_Return_Correct_Text_When_Command_Is_Unknown()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(config =>
+                var app = new CommandAppTester();
+                app.Configure(config =>
                 {
                     config.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("cat", "14");
+                var result = app.Run("cat", "14");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
 
             [Fact]
@@ -33,17 +33,17 @@ public Task Should_Return_Correct_Text_When_Command_Is_Unknown()
             public Task Should_Return_Correct_Text_For_Unknown_Command_When_Current_Command_Has_No_Arguments()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("empty");
                 });
 
                 // When
-                var result = fixture.Run("empty", "other");
+                var result = app.Run("empty", "other");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
 
             [Fact]
@@ -51,8 +51,8 @@ public Task Should_Return_Correct_Text_For_Unknown_Command_When_Current_Command_
             public Task Should_Return_Correct_Text_With_Suggestion_When_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(config =>
+                var app = new CommandAppTester();
+                app.Configure(config =>
                 {
                     config.AddBranch("dog", a =>
                     {
@@ -61,10 +61,10 @@ public Task Should_Return_Correct_Text_With_Suggestion_When_Command_Followed_By_
                 });
 
                 // When
-                var result = fixture.Run("dog", "bat", "14");
+                var result = app.Run("dog", "bat", "14");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
 
             [Fact]
@@ -72,17 +72,17 @@ public Task Should_Return_Correct_Text_With_Suggestion_When_Command_Followed_By_
             public Task Should_Return_Correct_Text_With_Suggestion_When_Root_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(config =>
+                var app = new CommandAppTester();
+                app.Configure(config =>
                 {
                     config.AddCommand("cat");
                 });
 
                 // When
-                var result = fixture.Run("bat", "14");
+                var result = app.Run("bat", "14");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
 
             [Fact]
@@ -90,18 +90,18 @@ public Task Should_Return_Correct_Text_With_Suggestion_When_Root_Command_Followe
             public Task Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Root_Command_Is_Unknown_And_Distance_Is_Small()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.WithDefaultCommand>();
-                fixture.Configure(config =>
+                var app = new CommandAppTester();
+                app.SetDefaultCommand>();
+                app.Configure(config =>
                 {
                     config.AddCommand>("cat");
                 });
 
                 // When
-                var result = fixture.Run("bat");
+                var result = app.Run("bat");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
 
             [Fact]
@@ -109,9 +109,9 @@ public Task Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Roo
             public Task Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Command_Is_Unknown_And_Distance_Is_Small()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.WithDefaultCommand>();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.SetDefaultCommand>();
+                app.Configure(configurator =>
                 {
                     configurator.AddBranch("dog", a =>
                     {
@@ -120,10 +120,10 @@ public Task Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Com
                 });
 
                 // When
-                var result = fixture.Run("dog", "bat");
+                var result = app.Run("dog", "bat");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
 
             [Fact]
@@ -131,18 +131,18 @@ public Task Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Com
             public Task Should_Return_Correct_Text_With_Suggestion_When_Root_Command_After_Argument_Is_Unknown_And_Distance_Is_Small()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.WithDefaultCommand>();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.SetDefaultCommand>();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand>("bar");
                 });
 
                 // When
-                var result = fixture.Run("qux", "bat");
+                var result = app.Run("qux", "bat");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
 
             [Fact]
@@ -150,8 +150,8 @@ public Task Should_Return_Correct_Text_With_Suggestion_When_Root_Command_After_A
             public Task Should_Return_Correct_Text_With_Suggestion_When_Command_After_Argument_Is_Unknown_And_Distance_Is_Small()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddBranch("foo", a =>
                     {
@@ -160,10 +160,10 @@ public Task Should_Return_Correct_Text_With_Suggestion_When_Command_After_Argume
                 });
 
                 // When
-                var result = fixture.Run("foo", "qux", "bat");
+                var result = app.Run("foo", "qux", "bat");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
         }
 
@@ -176,17 +176,17 @@ public sealed class CannotAssignValueToFlag
             public Task Should_Return_Correct_Text_For_Long_Option()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", "--alive", "foo");
+                var result = app.Run("dog", "--alive=indeterminate", "foo");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
 
             [Fact]
@@ -194,17 +194,17 @@ public Task Should_Return_Correct_Text_For_Long_Option()
             public Task Should_Return_Correct_Text_For_Short_Option()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", "-a", "foo");
+                var result = app.Run("dog", "-a=indeterminate", "foo");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
         }
 
@@ -217,17 +217,17 @@ public sealed class NoValueForOption
             public Task Should_Return_Correct_Text_For_Long_Option()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", "--name");
+                var result = app.Run("dog", "--name");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
 
             [Fact]
@@ -235,17 +235,17 @@ public Task Should_Return_Correct_Text_For_Long_Option()
             public Task Should_Return_Correct_Text_For_Short_Option()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", "-n");
+                var result = app.Run("dog", "-n");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
         }
 
@@ -258,17 +258,17 @@ public sealed class NoMatchingArgument
             public Task Should_Return_Correct_Text()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("giraffe");
                 });
 
                 // When
-                var result = fixture.Run("giraffe", "foo", "bar", "baz");
+                var result = app.Run("giraffe", "foo", "bar", "baz");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
         }
 
@@ -281,17 +281,17 @@ public sealed class UnexpectedOption
             public Task Should_Return_Correct_Text_For_Long_Option()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("--foo");
+                var result = app.Run("--foo");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
 
             [Fact]
@@ -299,17 +299,17 @@ public Task Should_Return_Correct_Text_For_Long_Option()
             public Task Should_Return_Correct_Text_For_Short_Option()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("-f");
+                var result = app.Run("-f");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
         }
 
@@ -322,18 +322,18 @@ public sealed class UnknownOption
             public Task Should_Return_Correct_Text_For_Long_Option_If_Strict_Mode_Is_Enabled()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.UseStrictParsing();
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", "--unknown");
+                var result = app.Run("dog", "--unknown");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
 
             [Fact]
@@ -341,42 +341,19 @@ public Task Should_Return_Correct_Text_For_Long_Option_If_Strict_Mode_Is_Enabled
             public Task Should_Return_Correct_Text_For_Short_Option_If_Strict_Mode_Is_Enabled()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.UseStrictParsing();
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", "-u");
+                var result = app.Run("dog", "-u");
 
                 // Then
-                return Verifier.Verify(result);
-            }
-        }
-
-        [UsesVerify]
-        [ExpectationPath("UnterminatedQuote")]
-        public sealed class UnterminatedQuote
-        {
-            [Fact]
-            [Expectation("Test_1")]
-            public Task Should_Return_Correct_Text()
-            {
-                // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
-                {
-                    configurator.AddCommand("dog");
-                });
-
-                // When
-                var result = fixture.Run("--name", "\"Rufus");
-
-                // Then
-                return Verifier.Verify(result);
-            }
+                return Verifier.Verify(result.Output);
+            }
         }
 
         [UsesVerify]
@@ -388,17 +365,17 @@ public sealed class OptionWithoutName
             public Task Should_Return_Correct_Text_For_Short_Option()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", "-", " ");
+                var result = app.Run("dog", "-", " ");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
 
             [Fact]
@@ -406,17 +383,17 @@ public Task Should_Return_Correct_Text_For_Short_Option()
             public Task Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Equality_Separator()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", $"--foo=");
+                var result = app.Run("dog", $"--foo=");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
 
             [Fact]
@@ -424,17 +401,17 @@ public Task Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Equali
             public Task Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Colon_Separator()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", $"--foo:");
+                var result = app.Run("dog", $"--foo:");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
 
             [Fact]
@@ -442,17 +419,17 @@ public Task Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Colon_
             public Task Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Equality_Separator()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", $"-f=");
+                var result = app.Run("dog", $"-f=");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
 
             [Fact]
@@ -460,17 +437,17 @@ public Task Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Equal
             public Task Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Colon_Separator()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", $"-f:");
+                var result = app.Run("dog", $"-f:");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
         }
 
@@ -483,17 +460,17 @@ public sealed class InvalidShortOptionName
             public Task Should_Return_Correct_Text()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", $"-f0o");
+                var result = app.Run("dog", $"-f0o");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
         }
 
@@ -506,17 +483,17 @@ public sealed class LongOptionNameIsOneCharacter
             public Task Should_Return_Correct_Text()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", $"--f");
+                var result = app.Run("dog", $"--f");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
         }
 
@@ -529,17 +506,17 @@ public sealed class LongOptionNameIsMissing
             public Task Should_Return_Correct_Text()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", $"-- ");
+                var result = app.Run("dog", $"-- ");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
         }
 
@@ -552,17 +529,17 @@ public sealed class LongOptionNameStartWithDigit
             public Task Should_Return_Correct_Text()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", $"--1foo");
+                var result = app.Run("dog", $"--1foo");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
         }
 
@@ -575,17 +552,17 @@ public sealed class LongOptionNameContainSymbol
             public Task Should_Return_Correct_Text()
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", $"--f€oo");
+                var result = app.Run("dog", $"--f€oo");
 
                 // Then
-                return Verifier.Verify(result);
+                return Verifier.Verify(result.Output);
             }
 
             [Theory]
@@ -596,71 +573,18 @@ public Task Should_Return_Correct_Text()
             public void Should_Allow_Special_Symbols_In_Name(string option)
             {
                 // Given
-                var fixture = new Fixture();
-                fixture.Configure(configurator =>
+                var app = new CommandAppTester();
+                app.Configure(configurator =>
                 {
                     configurator.AddCommand("dog");
                 });
 
                 // When
-                var result = fixture.Run("dog", option);
+                var result = app.Run("dog", option);
 
                 // Then
-                result.ShouldBe("Error: Command 'dog' is missing required argument 'AGE'.");
-            }
-        }
-
-        [Fact]
-        [Expectation("Quoted_Strings")]
-        public Task Should_Parse_Quoted_Strings_Correctly()
-        {
-            // Given
-            var fixture = new Fixture();
-            fixture.Configure(configurator =>
-            {
-                configurator.AddCommand("foo");
-            });
-
-            // When
-            var result = fixture.Run("foo", "--", "/c", "\"set && pause\"");
-
-            // Then
-            return Verifier.Verify(result);
-        }
-    }
-
-    internal sealed class Fixture
-    {
-        private Action _appConfiguration = _ => { };
-        private Action _configuration;
-
-        public void WithDefaultCommand()
-            where T : class, ICommand
-        {
-            _appConfiguration = (app) => app.SetDefaultCommand();
-        }
-
-        public void Configure(Action action)
-        {
-            _configuration = action;
-        }
-
-        public string Run(params string[] args)
-        {
-            using (var console = new TestConsole())
-            {
-                var app = new CommandApp();
-                _appConfiguration?.Invoke(app);
-
-                app.Configure(_configuration);
-                app.Configure(c => c.ConfigureConsole(console));
-                app.Run(args);
-
-                return console.Output
-                    .NormalizeLineEndings()
-                    .TrimLines()
-                    .Trim();
-            }
+                result.Output.ShouldBe("Error: Command 'dog' is missing required argument 'AGE'.");
+            }
         }
     }
 }
diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Remaining.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Remaining.cs
index ba1b31a8a..542512365 100644
--- a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Remaining.cs
+++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Remaining.cs
@@ -62,9 +62,38 @@ public void Should_Register_Remaining_Raw_Arguments_With_Context()
             result.Context.Remaining.Raw[0].ShouldBe("--foo");
             result.Context.Remaining.Raw[1].ShouldBe("bar");
             result.Context.Remaining.Raw[2].ShouldBe("-bar");
-            result.Context.Remaining.Raw[3].ShouldBe("baz");
+            result.Context.Remaining.Raw[3].ShouldBe("\"baz\"");
             result.Context.Remaining.Raw[4].ShouldBe("qux");
             result.Context.Remaining.Raw[5].ShouldBe("foo bar baz qux");
         }
+
+        [Fact]
+        public void Should_Preserve_Quotes_Hyphen_Delimiters()
+        {
+            // Given
+            var app = new CommandAppTester();
+            app.Configure(config =>
+            {
+                config.PropagateExceptions();
+                config.AddBranch("animal", animal =>
+                {
+                    animal.AddCommand("dog");
+                });
+            });
+
+            // When
+            var result = app.Run(new[]
+            {
+                "animal", "4", "dog", "12", "--",
+                "/c", "\"set && pause\"",
+                "Name=\" -Rufus --' ",
+            });
+
+            // Then
+            result.Context.Remaining.Raw.Count.ShouldBe(3);
+            result.Context.Remaining.Raw[0].ShouldBe("/c");
+            result.Context.Remaining.Raw[1].ShouldBe("\"set && pause\"");
+            result.Context.Remaining.Raw[2].ShouldBe("Name=\" -Rufus --' ");
+        }
     }
 }
diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs
index 8175acef7..9021b3c9b 100644
--- a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs
+++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.cs
@@ -132,10 +132,43 @@ public void Should_Pass_Case_4()
             dog.IsAlive.ShouldBe(false);
             dog.Name.ShouldBe("Rufus");
         });
-    }
-
+    }
+
     [Fact]
     public void Should_Pass_Case_5()
+    {
+        // Given
+        var app = new CommandAppTester();
+        app.Configure(config =>
+        {
+            config.PropagateExceptions();
+            config.AddBranch("animal", animal =>
+            {
+                animal.AddCommand("dog");
+            });
+        });
+
+        // When
+        var result = app.Run(new[]
+        {
+            "animal", "--alive", "4", "dog", "--good-boy", "12",
+            "--name", "Rufus",
+        });
+
+        // Then
+        result.ExitCode.ShouldBe(0);
+        result.Settings.ShouldBeOfType().And(dog =>
+        {
+            dog.Legs.ShouldBe(4);
+            dog.Age.ShouldBe(12);
+            dog.GoodBoy.ShouldBe(true);
+            dog.IsAlive.ShouldBe(true);
+            dog.Name.ShouldBe("Rufus");
+        });
+    }
+
+    [Fact]
+    public void Should_Pass_Case_6()
     {
         // Given
         var app = new CommandAppTester();
@@ -164,7 +197,7 @@ public void Should_Pass_Case_5()
     }
 
     [Fact]
-    public void Should_Pass_Case_6()
+    public void Should_Pass_Case_7()
     {
         // Given
         var app = new CommandAppTester();
@@ -189,6 +222,38 @@ public void Should_Pass_Case_6()
         });
     }
 
+    [Fact]
+    public void Should_Preserve_Quotes_Hyphen_Delimiters_Spaces()
+    {
+        // Given
+        var app = new CommandAppTester();
+        app.Configure(config =>
+        {
+            config.PropagateExceptions();
+            config.AddCommand("dog");
+        });
+
+        // When
+        var result = app.Run(new[]
+        {
+            "dog", "12", "4",
+            "--name=\" -Rufus --' ",
+            "--",
+            "--order-by", "\"-size\"",
+            "--order-by", " ",
+            "--order-by", string.Empty,
+        });
+
+        // Then
+        result.ExitCode.ShouldBe(0);
+        result.Settings.ShouldBeOfType().And(dog =>
+        {
+            dog.Name.ShouldBe("\" -Rufus --' ");
+        });
+        result.Context.Remaining.Parsed.Count.ShouldBe(1);
+        result.Context.ShouldHaveRemainingArgument("order-by", values: new[] { "\"-size\"", " ", string.Empty });
+    }
+
     [Fact]
     public void Should_Be_Able_To_Use_Command_Alias()
     {
@@ -489,6 +554,181 @@ public void Should_Accept_Explicit_Boolan_Flag(string value, bool expected)
         {
             dog.IsAlive.ShouldBe(expected);
         });
+    }
+
+    [Fact]
+    public void Should_Set_Short_Option_Before_Argument()
+    {
+        // Given
+        var app = new CommandAppTester();
+        app.Configure(config =>
+        {
+            config.PropagateExceptions();
+            config.AddCommand("dog");
+        });
+
+        // When
+        var result = app.Run(new[] { "dog", "-a", "-n=Rufus", "4", "12", });
+
+        // Then
+        result.ExitCode.ShouldBe(0);
+        result.Settings.ShouldBeOfType().And(settings =>
+        {
+            settings.IsAlive.ShouldBeTrue();
+            settings.Name.ShouldBe("Rufus");
+            settings.Legs.ShouldBe(4);
+            settings.Age.ShouldBe(12);
+        });
+    }
+
+    [Theory]
+    [InlineData("true", true)]
+    [InlineData("True", true)]
+    [InlineData("false", false)]
+    [InlineData("False", false)]
+    public void Should_Set_Short_Option_With_Explicit_Boolan_Flag_Before_Argument(string value, bool expected)
+    {
+        // Given
+        var app = new CommandAppTester();
+        app.Configure(config =>
+        {
+            config.PropagateExceptions();
+            config.AddCommand("dog");
+        });
+
+        // When
+        var result = app.Run(new[] { "dog", "-a", value, "4", "12", });
+
+        // Then
+        result.ExitCode.ShouldBe(0);
+        result.Settings.ShouldBeOfType().And(settings =>
+        {
+            settings.IsAlive.ShouldBe(expected);
+            settings.Legs.ShouldBe(4);
+            settings.Age.ShouldBe(12);
+        });
+    }
+
+    [Fact]
+    public void Should_Set_Long_Option_Before_Argument()
+    {
+        // Given
+        var app = new CommandAppTester();
+        app.Configure(config =>
+        {
+            config.PropagateExceptions();
+            config.AddCommand("dog");
+        });
+
+        // When
+        var result = app.Run(new[] { "dog", "--alive", "--name=Rufus", "4", "12" });
+
+        // Then
+        result.ExitCode.ShouldBe(0);
+        result.Settings.ShouldBeOfType().And(settings =>
+        {
+            settings.IsAlive.ShouldBeTrue();
+            settings.Name.ShouldBe("Rufus");
+            settings.Legs.ShouldBe(4);
+            settings.Age.ShouldBe(12);
+        });
+    }
+
+    [Theory]
+    [InlineData("true", true)]
+    [InlineData("True", true)]
+    [InlineData("false", false)]
+    [InlineData("False", false)]
+    public void Should_Set_Long_Option_With_Explicit_Boolan_Flag_Before_Argument(string value, bool expected)
+    {
+        // Given
+        var app = new CommandAppTester();
+        app.Configure(config =>
+        {
+            config.PropagateExceptions();
+            config.AddCommand("dog");
+        });
+
+        // When
+        var result = app.Run(new[] { "dog", "--alive", value, "4", "12", });
+
+        // Then
+        result.ExitCode.ShouldBe(0);
+        result.Settings.ShouldBeOfType().And(settings =>
+        {
+            settings.IsAlive.ShouldBe(expected);
+            settings.Legs.ShouldBe(4);
+            settings.Age.ShouldBe(12);
+        });
+    }
+
+    [Theory]
+
+    // Long options
+    [InlineData("dog --alive 4 12 --name Rufus", 4, 12, false, true, "Rufus")]
+    [InlineData("dog --alive=true 4 12 --name Rufus", 4, 12, false, true, "Rufus")]
+    [InlineData("dog --alive:true 4 12 --name Rufus", 4, 12, false, true, "Rufus")]
+    [InlineData("dog --alive --good-boy 4 12 --name Rufus", 4, 12, true, true, "Rufus")]
+    [InlineData("dog --alive=true --good-boy=true 4 12 --name Rufus", 4, 12, true, true, "Rufus")]
+    [InlineData("dog --alive:true --good-boy:true 4 12 --name Rufus", 4, 12, true, true, "Rufus")]
+    [InlineData("dog --alive --good-boy --name Rufus 4 12", 4, 12, true, true, "Rufus")]
+    [InlineData("dog --alive=true --good-boy=true --name Rufus 4 12", 4, 12, true, true, "Rufus")]
+    [InlineData("dog --alive:true --good-boy:true --name Rufus 4 12", 4, 12, true, true, "Rufus")]
+
+    // Short options
+    [InlineData("dog -a 4 12 --name Rufus", 4, 12, false, true, "Rufus")]
+    [InlineData("dog -a=true 4 12 --name Rufus", 4, 12, false, true, "Rufus")]
+    [InlineData("dog -a:true 4 12 --name Rufus", 4, 12, false, true, "Rufus")]
+    [InlineData("dog -a --good-boy 4 12 --name Rufus", 4, 12, true, true, "Rufus")]
+    [InlineData("dog -a=true -g=true 4 12 --name Rufus", 4, 12, true, true, "Rufus")]
+    [InlineData("dog -a:true -g:true 4 12 --name Rufus", 4, 12, true, true, "Rufus")]
+    [InlineData("dog -a -g --name Rufus 4 12", 4, 12, true, true, "Rufus")]
+    [InlineData("dog -a=true -g=true --name Rufus 4 12", 4, 12, true, true, "Rufus")]
+    [InlineData("dog -a:true -g:true --name Rufus 4 12", 4, 12, true, true, "Rufus")]
+
+    // Switch around ordering of the options
+    [InlineData("dog --good-boy:true --name Rufus --alive:true 4 12", 4, 12, true, true, "Rufus")]
+    [InlineData("dog --name Rufus --alive:true --good-boy:true 4 12", 4, 12, true, true, "Rufus")]
+    [InlineData("dog --name Rufus --good-boy:true --alive:true 4 12", 4, 12, true, true, "Rufus")]
+
+    // Inject the command arguments in between the options
+    [InlineData("dog 4 12 --good-boy:true --name Rufus --alive:true", 4, 12, true, true, "Rufus")]
+    [InlineData("dog 4 --good-boy:true 12 --name Rufus --alive:true", 4, 12, true, true, "Rufus")]
+    [InlineData("dog --good-boy:true 4 12 --name Rufus --alive:true", 4, 12, true, true, "Rufus")]
+    [InlineData("dog --good-boy:true 4 --name Rufus 12 --alive:true", 4, 12, true, true, "Rufus")]
+    [InlineData("dog --name Rufus --alive:true 4 12 --good-boy:true", 4, 12, true, true, "Rufus")]
+    [InlineData("dog --name Rufus --alive:true 4 --good-boy:true 12", 4, 12, true, true, "Rufus")]
+
+    // Inject the command arguments in between the options (all flag values set to false)
+    [InlineData("dog 4 12 --good-boy:false --name Rufus --alive:false", 4, 12, false, false, "Rufus")]
+    [InlineData("dog 4 --good-boy:false 12 --name Rufus --alive:false", 4, 12, false, false, "Rufus")]
+    [InlineData("dog --good-boy:false 4 12 --name Rufus --alive:false", 4, 12, false, false, "Rufus")]
+    [InlineData("dog --good-boy:false 4 --name Rufus 12 --alive:false", 4, 12, false, false, "Rufus")]
+    [InlineData("dog --name Rufus --alive:false 4 12 --good-boy:false", 4, 12, false, false, "Rufus")]
+    [InlineData("dog --name Rufus --alive:false 4 --good-boy:false 12", 4, 12, false, false, "Rufus")]
+    public void Should_Set_Option_Before_Argument(string arguments, int legs, int age, bool goodBoy, bool isAlive, string name)
+    {
+        // Given
+        var app = new CommandAppTester();
+        app.Configure(config =>
+        {
+            config.PropagateExceptions();
+            config.AddCommand("dog");
+        });
+
+        // When
+        var result = app.Run(arguments.Split(' '));
+
+        // Then
+        result.ExitCode.ShouldBe(0);
+        result.Settings.ShouldBeOfType().And(settings =>
+        {
+            settings.Legs.ShouldBe(legs);
+            settings.Age.ShouldBe(age);
+            settings.GoodBoy.ShouldBe(goodBoy);
+            settings.IsAlive.ShouldBe(isAlive);
+            settings.Name.ShouldBe(name);
+        });
     }
 
     [Fact]
@@ -805,7 +1045,7 @@ public void Should_Register_Remaining_Raw_Arguments_With_Context()
             result.Context.Remaining.Raw[0].ShouldBe("--foo");
             result.Context.Remaining.Raw[1].ShouldBe("bar");
             result.Context.Remaining.Raw[2].ShouldBe("-bar");
-            result.Context.Remaining.Raw[3].ShouldBe("baz");
+            result.Context.Remaining.Raw[3].ShouldBe("\"baz\"");
             result.Context.Remaining.Raw[4].ShouldBe("qux");
         }
     }
diff --git a/test/Spectre.Console.Cli.Tests/Unit/Parsing/CommandTreeTokenizerTests.cs b/test/Spectre.Console.Cli.Tests/Unit/Parsing/CommandTreeTokenizerTests.cs
new file mode 100644
index 000000000..d968224d3
--- /dev/null
+++ b/test/Spectre.Console.Cli.Tests/Unit/Parsing/CommandTreeTokenizerTests.cs
@@ -0,0 +1,314 @@
+namespace Spectre.Console.Tests.Unit.Cli.Parsing;
+
+public class CommandTreeTokenizerTests
+{
+    public sealed class ScanString
+    {
+        [Theory]
+        [InlineData("")]
+        [InlineData(" ")]
+        [InlineData("  ")]
+        [InlineData("\t")]
+        [InlineData("\r\n\t")]
+        [InlineData("👋🏻")]
+        [InlineData("🐎👋🏻🔥❤️")]
+        [InlineData("\"🐎👋🏻🔥❤️\" is an emoji sequence")]
+        public void Should_Preserve_Edgecase_Inputs(string actualAndExpected)
+        {
+            // Given
+
+            // When
+            var result = CommandTreeTokenizer.Tokenize(new string[] { actualAndExpected });
+
+            // Then
+            result.Tokens.Count.ShouldBe(1);
+            result.Tokens[0].Value.ShouldBe(actualAndExpected);
+            result.Tokens[0].TokenKind.ShouldBe(CommandTreeToken.Kind.String);
+        }
+
+        [Theory]
+
+        // Double-quote handling
+        [InlineData("\"")]
+        [InlineData("\"\"")]
+        [InlineData("\"Rufus\"")]
+        [InlineData("\" Rufus\"")]
+        [InlineData("\"-R\"")]
+        [InlineData("\"-Rufus\"")]
+        [InlineData("\" -Rufus\"")]
+
+        // Single-quote handling
+        [InlineData("'")]
+        [InlineData("''")]
+        [InlineData("'Rufus'")]
+        [InlineData("' Rufus'")]
+        [InlineData("'-R'")]
+        [InlineData("'-Rufus'")]
+        [InlineData("' -Rufus'")]
+        public void Should_Preserve_Quotes(string actualAndExpected)
+        {
+            // Given
+
+            // When
+            var result = CommandTreeTokenizer.Tokenize(new string[] { actualAndExpected });
+
+            // Then
+            result.Tokens.Count.ShouldBe(1);
+            result.Tokens[0].Value.ShouldBe(actualAndExpected);
+            result.Tokens[0].TokenKind.ShouldBe(CommandTreeToken.Kind.String);
+        }
+
+        [Theory]
+        [InlineData("Rufus-")]
+        [InlineData("Rufus--")]
+        [InlineData("R-u-f-u-s")]
+        public void Should_Preserve_Hyphen_Delimiters(string actualAndExpected)
+        {
+            // Given
+
+            // When
+            var result = CommandTreeTokenizer.Tokenize(new string[] { actualAndExpected });
+
+            // Then
+            result.Tokens.Count.ShouldBe(1);
+            result.Tokens[0].Value.ShouldBe(actualAndExpected);
+            result.Tokens[0].TokenKind.ShouldBe(CommandTreeToken.Kind.String);
+        }
+
+        [Theory]
+        [InlineData(" Rufus")]
+        [InlineData("Rufus ")]
+        [InlineData(" Rufus ")]
+        public void Should_Preserve_Spaces(string actualAndExpected)
+        {
+            // Given
+
+            // When
+            var result = CommandTreeTokenizer.Tokenize(new string[] { actualAndExpected });
+
+            // Then
+            result.Tokens.Count.ShouldBe(1);
+            result.Tokens[0].Value.ShouldBe(actualAndExpected);
+            result.Tokens[0].TokenKind.ShouldBe(CommandTreeToken.Kind.String);
+        }
+
+        [Theory]
+        [InlineData(" \" -Rufus -- ")]
+        [InlineData("Name=\" -Rufus --' ")]
+        public void Should_Preserve_Quotes_Hyphen_Delimiters_Spaces(string actualAndExpected)
+        {
+            // Given
+
+            // When
+            var result = CommandTreeTokenizer.Tokenize(new string[] { actualAndExpected });
+
+            // Then
+            result.Tokens.Count.ShouldBe(1);
+            result.Tokens[0].Value.ShouldBe(actualAndExpected);
+            result.Tokens[0].TokenKind.ShouldBe(CommandTreeToken.Kind.String);
+        }
+    }
+
+    public sealed class ScanLongOption
+    {
+        [Theory]
+        [InlineData("--Name-", "Name-")]
+        [InlineData("--Name_", "Name_")]
+        public void Should_Allow_Hyphens_And_Underscores_In_Option_Name(string actual, string expected)
+        {
+            // Given
+
+            // When
+            var result = CommandTreeTokenizer.Tokenize(new string[] { actual });
+
+            // Then
+            result.Tokens.Count.ShouldBe(1);
+            result.Tokens[0].Value.ShouldBe(expected);
+            result.Tokens[0].TokenKind.ShouldBe(CommandTreeToken.Kind.LongOption);
+        }
+
+        [Theory]
+        [InlineData("-- ")]
+        [InlineData("--Name ")]
+        [InlineData("--Name\"")]
+        [InlineData("--Nam\"e")]
+        public void Should_Throw_On_Invalid_Option_Name(string actual)
+        {
+            // Given
+
+            // When
+            var result = Record.Exception(() => CommandTreeTokenizer.Tokenize(new string[] { actual }));
+
+            // Then
+            result.ShouldBeOfType().And(ex =>
+            {
+                ex.Message.ShouldBe("Invalid long option name.");
+            });
+        }
+    }
+
+    public sealed class ScanShortOptions
+    {
+        [Fact]
+        public void Should_Accept_Option_Without_Value()
+        {
+            // Given
+
+            // When
+            var result = CommandTreeTokenizer.Tokenize(new[] { "-a" });
+
+            // Then
+            result.Remaining.ShouldBeEmpty();
+            result.Tokens.ShouldHaveSingleItem();
+
+            var t = result.Tokens[0];
+            t.TokenKind.ShouldBe(CommandTreeToken.Kind.ShortOption);
+            t.IsGrouped.ShouldBe(false);
+            t.Position.ShouldBe(0);
+            t.Value.ShouldBe("a");
+            t.Representation.ShouldBe("-a");
+        }
+
+        [Theory]
+        [InlineData("-a:foo")]
+        [InlineData("-a=foo")]
+        public void Should_Accept_Option_With_Value(string param)
+        {
+            // Given
+
+            // When
+            var result = CommandTreeTokenizer.Tokenize(new[] { param });
+
+            // Then
+            result.Remaining.ShouldBeEmpty();
+            result.Tokens.Count.ShouldBe(2);
+
+            var t = result.Tokens.Consume();
+            t.TokenKind.ShouldBe(CommandTreeToken.Kind.ShortOption);
+            t.IsGrouped.ShouldBe(false);
+            t.Position.ShouldBe(0);
+            t.Value.ShouldBe("a");
+            t.Representation.ShouldBe("-a");
+
+            t = result.Tokens.Consume();
+            t.TokenKind.ShouldBe(CommandTreeToken.Kind.String);
+            t.IsGrouped.ShouldBe(false);
+            t.Position.ShouldBe(3);
+            t.Value.ShouldBe("foo");
+            t.Representation.ShouldBe("foo");
+        }
+
+        [Theory]
+
+        // Positive values
+        [InlineData("-a:1.5", null, "1.5")]
+        [InlineData("-a=1.5", null, "1.5")]
+        [InlineData("-a", "1.5", "1.5")]
+
+        // Negative values
+        [InlineData("-a:-1.5", null, "-1.5")]
+        [InlineData("-a=-1.5", null, "-1.5")]
+        [InlineData("-a", "-1.5", "-1.5")]
+        public void Should_Accept_Option_With_Numeric_Value(string firstArg, string secondArg, string expectedValue)
+        {
+            // Given
+            List args = new List();
+            args.Add(firstArg);
+            if (secondArg != null)
+            {
+                args.Add(secondArg);
+            }
+
+            // When
+            var result = CommandTreeTokenizer.Tokenize(args);
+
+            // Then
+            result.Remaining.ShouldBeEmpty();
+            result.Tokens.Count.ShouldBe(2);
+
+            var t = result.Tokens.Consume();
+            t.TokenKind.ShouldBe(CommandTreeToken.Kind.ShortOption);
+            t.IsGrouped.ShouldBe(false);
+            t.Position.ShouldBe(0);
+            t.Value.ShouldBe("a");
+            t.Representation.ShouldBe("-a");
+
+            t = result.Tokens.Consume();
+            t.TokenKind.ShouldBe(CommandTreeToken.Kind.String);
+            t.IsGrouped.ShouldBe(false);
+            t.Position.ShouldBe(3);
+            t.Value.ShouldBe(expectedValue);
+            t.Representation.ShouldBe(expectedValue);
+        }
+
+        [Fact]
+        public void Should_Accept_Option_With_Negative_Numeric_Prefixed_String_Value()
+        {
+            // Given
+
+            // When
+            var result = CommandTreeTokenizer.Tokenize(new[] { "-6..2 " });
+
+            // Then
+            result.Remaining.ShouldBeEmpty();
+            result.Tokens.ShouldHaveSingleItem();
+
+            var t = result.Tokens[0];
+            t.TokenKind.ShouldBe(CommandTreeToken.Kind.String);
+            t.IsGrouped.ShouldBe(false);
+            t.Position.ShouldBe(0);
+            t.Value.ShouldBe("-6..2");
+            t.Representation.ShouldBe("-6..2");
+        }
+
+        [Theory]
+        [InlineData("-N ", "N")]
+        public void Should_Remove_Trailing_Spaces_In_Option_Name(string actual, string expected)
+        {
+            // Given
+
+            // When
+            var result = CommandTreeTokenizer.Tokenize(new string[] { actual });
+
+            // Then
+            result.Tokens.Count.ShouldBe(1);
+            result.Tokens[0].Value.ShouldBe(expected);
+            result.Tokens[0].TokenKind.ShouldBe(CommandTreeToken.Kind.ShortOption);
+        }
+
+        [Theory]
+        [InlineData("-N-")]
+        [InlineData("-N\"")]
+        [InlineData("-a1")]
+        public void Should_Throw_On_Invalid_Option_Name(string actual)
+        {
+            // Given
+
+            // When
+            var result = Record.Exception(() => CommandTreeTokenizer.Tokenize(new string[] { actual }));
+
+            // Then
+            result.ShouldBeOfType().And(ex =>
+            {
+                ex.Message.ShouldBe("Short option does not have a valid name.");
+            });
+        }
+    }
+
+    [Theory]
+    [InlineData("-")]
+    [InlineData("- ")]
+    public void Should_Throw_On_Missing_Option_Name(string actual)
+    {
+        // Given
+
+        // When
+        var result = Record.Exception(() => CommandTreeTokenizer.Tokenize(new string[] { actual }));
+
+        // Then
+        result.ShouldBeOfType().And(ex =>
+        {
+            ex.Message.ShouldBe("Option does not have a name.");
+        });
+    }
+}
diff --git a/test/Spectre.Console.Cli.Tests/Utilities/ModuleInitializerAttribute.cs b/test/Spectre.Console.Cli.Tests/Utilities/ModuleInitializerAttribute.cs
index 6f93cb48c..5b84da221 100644
--- a/test/Spectre.Console.Cli.Tests/Utilities/ModuleInitializerAttribute.cs
+++ b/test/Spectre.Console.Cli.Tests/Utilities/ModuleInitializerAttribute.cs
@@ -1,4 +1,4 @@
-#if !NET5_0_OR_GREATER
+#if !NET6_0_OR_GREATER
 namespace System.Runtime.CompilerServices;
 
 [AttributeUsage(AttributeTargets.Method, Inherited = false)]
diff --git a/test/Spectre.Console.Cli.Tests/VerifyConfiguration.cs b/test/Spectre.Console.Cli.Tests/VerifyConfiguration.cs
index 770ec2bde..b3261c23f 100644
--- a/test/Spectre.Console.Cli.Tests/VerifyConfiguration.cs
+++ b/test/Spectre.Console.Cli.Tests/VerifyConfiguration.cs
@@ -5,6 +5,6 @@ public static class VerifyConfiguration
     [ModuleInitializer]
     public static void Init()
     {
-        VerifierSettings.DerivePathInfo(Expectations.Initialize);
+        Verifier.DerivePathInfo(Expectations.Initialize);
     }
 }
diff --git a/test/Spectre.Console.Tests/Expectations/Cli/Parsing/Quoted_Strings.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Cli/Parsing/Quoted_Strings.Output.verified.txt
deleted file mode 100644
index 1c0548318..000000000
--- a/test/Spectre.Console.Tests/Expectations/Cli/Parsing/Quoted_Strings.Output.verified.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-# Raw
-/c
-set && pause
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Cli/Parsing/UnterminatedQuote/Test_1.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Cli/Parsing/UnterminatedQuote/Test_1.Output.verified.txt
deleted file mode 100644
index a6d70327d..000000000
--- a/test/Spectre.Console.Tests/Expectations/Cli/Parsing/UnterminatedQuote/Test_1.Output.verified.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-Error: Encountered unterminated quoted string 'Rufus'.
-
-       --name "Rufus
-              ^^^^^^ Did you forget the closing quotation mark?
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Bottom.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Bottom.Output.verified.txt
new file mode 100644
index 000000000..74ab31e63
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Bottom.Output.verified.txt
@@ -0,0 +1,15 @@
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+            ┌──────────────┐            
+            │ Hello World! │            
+            └──────────────┘            
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Middle.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Middle.Output.verified.txt
new file mode 100644
index 000000000..33cc191a4
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Middle.Output.verified.txt
@@ -0,0 +1,15 @@
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+            ┌──────────────┐            
+            │ Hello World! │            
+            └──────────────┘            
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Top.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Top.Output.verified.txt
new file mode 100644
index 000000000..739003022
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Center_Top.Output.verified.txt
@@ -0,0 +1,15 @@
+            ┌──────────────┐            
+            │ Hello World! │            
+            └──────────────┘            
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Bottom.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Bottom.Output.verified.txt
new file mode 100644
index 000000000..5c9e9c013
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Bottom.Output.verified.txt
@@ -0,0 +1,15 @@
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+┌──────────────┐                        
+│ Hello World! │                        
+└──────────────┘                        
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Middle.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Middle.Output.verified.txt
new file mode 100644
index 000000000..f7beea556
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Middle.Output.verified.txt
@@ -0,0 +1,15 @@
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+┌──────────────┐                        
+│ Hello World! │                        
+└──────────────┘                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Top.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Top.Output.verified.txt
new file mode 100644
index 000000000..21d696cbb
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Left_Top.Output.verified.txt
@@ -0,0 +1,15 @@
+┌──────────────┐                        
+│ Hello World! │                        
+└──────────────┘                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Bottom.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Bottom.Output.verified.txt
new file mode 100644
index 000000000..d61cd86ea
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Bottom.Output.verified.txt
@@ -0,0 +1,15 @@
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                        ┌──────────────┐
+                        │ Hello World! │
+                        └──────────────┘
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Middle.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Middle.Output.verified.txt
new file mode 100644
index 000000000..90a5884e0
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Middle.Output.verified.txt
@@ -0,0 +1,15 @@
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                        ┌──────────────┐
+                        │ Hello World! │
+                        └──────────────┘
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Top.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Top.Output.verified.txt
new file mode 100644
index 000000000..a1742a857
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Align/Right_Top.Output.verified.txt
@@ -0,0 +1,15 @@
+                        ┌──────────────┐
+                        │ Hello World! │
+                        └──────────────┘
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
+                                        
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Load_Stream.Output_fontfile=poison.flf.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Load_Stream.Output_fontfile=poison.flf.verified.txt
index 1235447a0..7b6953771 100644
--- a/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Load_Stream.Output_fontfile=poison.flf.verified.txt
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Load_Stream.Output_fontfile=poison.flf.verified.txt
@@ -1,12 +1,12 @@
-                                                                                                                                                                                    
-@@@@@@@    @@@@@@   @@@@@@@  @@@@@@@   @@@  @@@  @@@     @@@  @@@  @@@   @@@@@@    @@@@@@      @@@  @@@  @@@@@@@@  @@@@@@@   @@@@@@@@                                               
-@@@@@@@@  @@@@@@@@  @@@@@@@  @@@@@@@@  @@@  @@@  @@@     @@@  @@@  @@@  @@@@@@@@  @@@@@@@      @@@  @@@  @@@@@@@@  @@@@@@@@  @@@@@@@@                                               
-@@!  @@@  @@!  @@@    @@!    @@!  @@@  @@!  @@!  !@@     @@!  @@!  @@!  @@!  @@@  !@@          @@!  @@@  @@!       @@!  @@@  @@!                                                    
-!@!  @!@  !@!  @!@    !@!    !@!  @!@  !@!  !@!  @!!     !@!  !@!  !@!  !@!  @!@  !@!          !@!  @!@  !@!       !@!  @!@  !@!                                                    
-@!@@!@!   @!@!@!@!    @!!    @!@!!@!   !!@  @!@@!@!      @!!  !!@  @!@  @!@!@!@!  !!@@!!       @!@!@!@!  @!!!:!    @!@!!@!   @!!!:!                                                 
-!!@!!!    !!!@!!!!    !!!    !!@!@!    !!!  !!@!!!       !@!  !!!  !@!  !!!@!!!!   !!@!!!      !!!@!!!!  !!!!!:    !!@!@!    !!!!!:                                                 
-!!:       !!:  !!!    !!:    !!: :!!   !!:  !!: :!!      !!:  !!:  !!:  !!:  !!!       !:!     !!:  !!!  !!:       !!: :!!   !!:                                                    
-:!:       :!:  !:!    :!:    :!:  !:!  :!:  :!:  !:!     :!:  :!:  :!:  :!:  !:!      !:!      :!:  !:!  :!:       :!:  !:!  :!:                                                    
- ::       ::   :::     ::    ::   :::   ::   ::  :::      :::: :: :::   ::   :::  :::: ::      ::   :::   :: ::::  ::   :::   :: ::::                                               
- :         :   : :     :      :   : :  :     :   :::       :: :  : :     :   : :  :: : :        :   : :  : :: ::    :   : :  : :: ::                                                
-                                                                                                                                                                                    
+                                                                                                                                       
+@@@@@@@    @@@@@@   @@@@@@@  @@@@@@@   @@@  @@@  @@@     @@@  @@@  @@@   @@@@@@    @@@@@@      @@@  @@@  @@@@@@@@  @@@@@@@   @@@@@@@@  
+@@@@@@@@  @@@@@@@@  @@@@@@@  @@@@@@@@  @@@  @@@  @@@     @@@  @@@  @@@  @@@@@@@@  @@@@@@@      @@@  @@@  @@@@@@@@  @@@@@@@@  @@@@@@@@  
+@@!  @@@  @@!  @@@    @@!    @@!  @@@  @@!  @@!  !@@     @@!  @@!  @@!  @@!  @@@  !@@          @@!  @@@  @@!       @@!  @@@  @@!       
+!@!  @!@  !@!  @!@    !@!    !@!  @!@  !@!  !@!  @!!     !@!  !@!  !@!  !@!  @!@  !@!          !@!  @!@  !@!       !@!  @!@  !@!       
+@!@@!@!   @!@!@!@!    @!!    @!@!!@!   !!@  @!@@!@!      @!!  !!@  @!@  @!@!@!@!  !!@@!!       @!@!@!@!  @!!!:!    @!@!!@!   @!!!:!    
+!!@!!!    !!!@!!!!    !!!    !!@!@!    !!!  !!@!!!       !@!  !!!  !@!  !!!@!!!!   !!@!!!      !!!@!!!!  !!!!!:    !!@!@!    !!!!!:    
+!!:       !!:  !!!    !!:    !!: :!!   !!:  !!: :!!      !!:  !!:  !!:  !!:  !!!       !:!     !!:  !!!  !!:       !!: :!!   !!:       
+:!:       :!:  !:!    :!:    :!:  !:!  :!:  :!:  !:!     :!:  :!:  :!:  :!:  !:!      !:!      :!:  !:!  :!:       :!:  !:!  :!:       
+ ::       ::   :::     ::    ::   :::   ::   ::  :::      :::: :: :::   ::   :::  :::: ::      ::   :::   :: ::::  ::   :::   :: ::::  
+ :         :   : :     :      :   : :  :     :   :::       :: :  : :     :   : :  :: : :        :   : :  : :: ::    :   : :  : :: ::   
+                                                                                                                                       
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Load_Stream.Output_fontfile=starwars.flf.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Load_Stream.Output_fontfile=starwars.flf.verified.txt
index 76aff34b6..e25eb5864 100644
--- a/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Load_Stream.Output_fontfile=starwars.flf.verified.txt
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Load_Stream.Output_fontfile=starwars.flf.verified.txt
@@ -1,7 +1,7 @@
-.______        ___      .___________..______       __   __  ___    ____    __    ____      ___           _______.    __    __   _______ .______       _______                       
-|   _  \      /   \     |           ||   _  \     |  | |  |/  /    \   \  /  \  /   /     /   \         /       |   |  |  |  | |   ____||   _  \     |   ____|                      
-|  |_)  |    /  ^  \    `---|  |----`|  |_)  |    |  | |  '  /      \   \/    \/   /     /  ^  \       |   (----`   |  |__|  | |  |__   |  |_)  |    |  |__                         
-|   ___/    /  /_\  \       |  |     |      /     |  | |    <        \            /     /  /_\  \       \   \       |   __   | |   __|  |      /     |   __|                        
-|  |       /  _____  \      |  |     |  |\  \----.|  | |  .  \        \    /\    /     /  _____  \  .----)   |      |  |  |  | |  |____ |  |\  \----.|  |____                       
-| _|      /__/     \__\     |__|     | _| `._____||__| |__|\__\        \__/  \__/     /__/     \__\ |_______/       |__|  |__| |_______|| _| `._____||_______|                      
-                                                                                                                                                                                    
+.______        ___      .___________..______       __   __  ___    ____    __    ____      ___           _______.    __    __   _______ .______       _______ 
+|   _  \      /   \     |           ||   _  \     |  | |  |/  /    \   \  /  \  /   /     /   \         /       |   |  |  |  | |   ____||   _  \     |   ____|
+|  |_)  |    /  ^  \    `---|  |----`|  |_)  |    |  | |  '  /      \   \/    \/   /     /  ^  \       |   (----`   |  |__|  | |  |__   |  |_)  |    |  |__   
+|   ___/    /  /_\  \       |  |     |      /     |  | |    <        \            /     /  /_\  \       \   \       |   __   | |   __|  |      /     |   __|  
+|  |       /  _____  \      |  |     |  |\  \----.|  | |  .  \        \    /\    /     /  _____  \  .----)   |      |  |  |  | |  |____ |  |\  \----.|  |____ 
+| _|      /__/     \__\     |__|     | _| `._____||__| |__|\__\        \__/  \__/     /__/     \__\ |_______/       |__|  |__| |_______|| _| `._____||_______|
+                                                                                                                                                              
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render.Output.verified.txt
index 3e813831b..a3bbd711c 100644
--- a/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render.Output.verified.txt
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render.Output.verified.txt
@@ -1,12 +1,12 @@
-  ____            _            _   _                                  
- |  _ \    __ _  | |_   _ __  (_) | | __   __      __   __ _   ___    
- | |_) |  / _` | | __| | '__| | | | |/ /   \ \ /\ / /  / _` | / __|   
- |  __/  | (_| | | |_  | |    | | |   <     \ V  V /  | (_| | \__ \   
- |_|      \__,_|  \__| |_|    |_| |_|\_\     \_/\_/    \__,_| |___/   
-                                                                      
-  _                                                                   
- | |__     ___   _ __    ___                                          
- | '_ \   / _ \ | '__|  / _ \                                         
- | | | | |  __/ | |    |  __/                                         
- |_| |_|  \___| |_|     \___|                                         
-                                                                      
+  ____            _            _   _                                 
+ |  _ \    __ _  | |_   _ __  (_) | | __   __      __   __ _   ___   
+ | |_) |  / _` | | __| | '__| | | | |/ /   \ \ /\ / /  / _` | / __|  
+ |  __/  | (_| | | |_  | |    | | |   <     \ V  V /  | (_| | \__ \  
+ |_|      \__,_|  \__| |_|    |_| |_|\_\     \_/\_/    \__,_| |___/  
+                                                                     
+  _                          
+ | |__     ___   _ __    ___ 
+ | '_ \   / _ \ | '__|  / _ \
+ | | | | |  __/ | |    |  __/
+ |_| |_|  \___| |_|     \___|
+                             
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Centered.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Centered.Output.verified.txt
index af87b25f1..13236c45a 100644
--- a/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Centered.Output.verified.txt
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Centered.Output.verified.txt
@@ -1,6 +1,6 @@
-          ____                          _                        ____                                 _                 
-         / ___|   _ __     ___    ___  | |_   _ __    ___       / ___|   ___    _ __    ___    ___   | |   ___          
-         \___ \  | '_ \   / _ \  / __| | __| | '__|  / _ \     | |      / _ \  | '_ \  / __|  / _ \  | |  / _ \         
-          ___) | | |_) | |  __/ | (__  | |_  | |    |  __/  _  | |___  | (_) | | | | | \__ \ | (_) | | | |  __/         
-         |____/  | .__/   \___|  \___|  \__| |_|     \___| (_)  \____|  \___/  |_| |_| |___/  \___/  |_|  \___|         
-                 |_|                                                                                                    
+          ____                          _                        ____                                 _        
+         / ___|   _ __     ___    ___  | |_   _ __    ___       / ___|   ___    _ __    ___    ___   | |   ___ 
+         \___ \  | '_ \   / _ \  / __| | __| | '__|  / _ \     | |      / _ \  | '_ \  / __|  / _ \  | |  / _ \
+          ___) | | |_) | |  __/ | (__  | |_  | |    |  __/  _  | |___  | (_) | | | | | \__ \ | (_) | | | |  __/
+         |____/  | .__/   \___|  \___|  \__| |_|     \___| (_)  \____|  \___/  |_| |_| |___/  \___/  |_|  \___|
+                 |_|                                                                                           
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_LeftAligned.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_LeftAligned.Output.verified.txt
index 702c7865f..21bfd2bcb 100644
--- a/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_LeftAligned.Output.verified.txt
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_LeftAligned.Output.verified.txt
@@ -1,6 +1,6 @@
-  ____                          _                        ____                                 _                         
- / ___|   _ __     ___    ___  | |_   _ __    ___       / ___|   ___    _ __    ___    ___   | |   ___                  
- \___ \  | '_ \   / _ \  / __| | __| | '__|  / _ \     | |      / _ \  | '_ \  / __|  / _ \  | |  / _ \                 
-  ___) | | |_) | |  __/ | (__  | |_  | |    |  __/  _  | |___  | (_) | | | | | \__ \ | (_) | | | |  __/                 
- |____/  | .__/   \___|  \___|  \__| |_|     \___| (_)  \____|  \___/  |_| |_| |___/  \___/  |_|  \___|                 
-         |_|                                                                                                            
+  ____                          _                        ____                                 _        
+ / ___|   _ __     ___    ___  | |_   _ __    ___       / ___|   ___    _ __    ___    ___   | |   ___ 
+ \___ \  | '_ \   / _ \  / __| | __| | '__|  / _ \     | |      / _ \  | '_ \  / __|  / _ \  | |  / _ \
+  ___) | | |_) | |  __/ | (__  | |_  | |    |  __/  _  | |___  | (_) | | | | | \__ \ | (_) | | | |  __/
+ |____/  | .__/   \___|  \___|  \__| |_|     \___| (_)  \____|  \___/  |_| |_| |___/  \___/  |_|  \___|
+         |_|                                                                                           
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Wrapped.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Wrapped.Output.verified.txt
index 7fa6180c3..7e9272dad 100644
--- a/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Wrapped.Output.verified.txt
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Wrapped.Output.verified.txt
@@ -1,12 +1,12 @@
-  ____                          _                        ____         
+  ____                          _                        ____         
  / ___|   _ __     ___    ___  | |_   _ __    ___       / ___|   ___  
  \___ \  | '_ \   / _ \  / __| | __| | '__|  / _ \     | |      / _ \ 
   ___) | | |_) | |  __/ | (__  | |_  | |    |  __/  _  | |___  | (_) |
  |____/  | .__/   \___|  \___|  \__| |_|     \___| (_)  \____|  \___/ 
          |_|                                                          
-                        _                                             
-  _ __    ___    ___   | |   ___                                      
- | '_ \  / __|  / _ \  | |  / _ \                                     
- | | | | \__ \ | (_) | | | |  __/                                     
- |_| |_| |___/  \___/  |_|  \___|                                     
-                                                                      
+                        _        
+  _ __    ___    ___   | |   ___ 
+ | '_ \  / __|  / _ \  | |  / _ \
+ | | | | \__ \ | (_) | | | |  __/
+ |_| |_| |___/  \___/  |_|  \___|
+                                 
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Empty_Layout.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Empty_Layout.Output.verified.txt
new file mode 100644
index 000000000..5caaedbf9
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Empty_Layout.Output.verified.txt
@@ -0,0 +1,15 @@
+╭─40 x 15──────────────────────────────╮
+│                                      │
+│                                      │
+│                                      │
+│                                      │
+│                                      │
+│                                      │
+│             Placeholder              │
+│                                      │
+│                                      │
+│                                      │
+│                                      │
+│                                      │
+│                                      │
+╰──────────────────────────────────────╯
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Fallback_Layout.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Fallback_Layout.Output.verified.txt
new file mode 100644
index 000000000..0df2d7970
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Fallback_Layout.Output.verified.txt
@@ -0,0 +1,15 @@
+╭─T1 (40 x 7)──────────────────────────╮
+│                                      │
+│                                      │
+│             Placeholder              │
+│                                      │
+│                                      │
+╰──────────────────────────────────────╯
+╭─T2 (40 x 8)──────────────────────────╮
+│                                      │
+│                                      │
+│             Placeholder              │
+│                                      │
+│                                      │
+│                                      │
+╰──────────────────────────────────────╯
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout.Output.verified.txt
new file mode 100644
index 000000000..614b3a14d
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout.Output.verified.txt
@@ -0,0 +1,15 @@
+╔══════════════════════════════════════╗
+║ Hello                                ║
+║                                      ║
+║                                      ║
+║                                      ║
+║                                      ║
+║                                      ║
+║                                      ║
+║                                      ║
+║                                      ║
+║                                      ║
+║                                      ║
+║                                      ║
+║                                      ║
+╚══════════════════════════════════════╝
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Columns.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Columns.Output.verified.txt
new file mode 100644
index 000000000..5fea3f19e
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Columns.Output.verified.txt
@@ -0,0 +1,15 @@
+╭─Left (20 x 15)───╮╭─Right (20 x 15)──╮
+│                  ││                  │
+│                  ││                  │
+│                  ││                  │
+│                  ││                  │
+│                  ││                  │
+│                  ││                  │
+│   Placeholder    ││   Placeholder    │
+│                  ││                  │
+│                  ││                  │
+│                  ││                  │
+│                  ││                  │
+│                  ││                  │
+│                  ││                  │
+╰──────────────────╯╰──────────────────╯
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Columns.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Columns.Output.verified.txt
new file mode 100644
index 000000000..9c7084a15
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Columns.Output.verified.txt
@@ -0,0 +1,15 @@
+╭─L1…────╮╭─L2…────╮╭─R1…────╮╭─R2…────╮
+│        ││        ││        ││        │
+│        ││        ││        ││        │
+│        ││        ││        ││        │
+│        ││        ││        ││        │
+│        ││        ││        ││        │
+│ Placeh ││ Placeh ││ Placeh ││ Placeh │
+│ older  ││ older  ││ older  ││ older  │
+│        ││        ││        ││        │
+│        ││        ││        ││        │
+│        ││        ││        ││        │
+│        ││        ││        ││        │
+│        ││        ││        ││        │
+│        ││        ││        ││        │
+╰────────╯╰────────╯╰────────╯╰────────╯
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Rows.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Rows.Output.verified.txt
new file mode 100644
index 000000000..bb5f7d988
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Rows.Output.verified.txt
@@ -0,0 +1,15 @@
+╭─T1 (40 x 3)──────────────────────────╮
+│             Placeholder              │
+╰──────────────────────────────────────╯
+╭─T2 (40 x 4)──────────────────────────╮
+│             Placeholder              │
+│                                      │
+╰──────────────────────────────────────╯
+╭─B1 (40 x 4)──────────────────────────╮
+│             Placeholder              │
+│                                      │
+╰──────────────────────────────────────╯
+╭─B2 (40 x 4)──────────────────────────╮
+│             Placeholder              │
+│                                      │
+╰──────────────────────────────────────╯
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Rows_And_Columns.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Rows_And_Columns.Output.verified.txt
new file mode 100644
index 000000000..258288ba0
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Nested_Rows_And_Columns.Output.verified.txt
@@ -0,0 +1,15 @@
+╭─A (20 x 3)───────╮╭─B (20 x 3)───────╮
+│   Placeholder    ││   Placeholder    │
+╰──────────────────╯╰──────────────────╯
+╭─C (20 x 4)───────╮╭─D (20 x 4)───────╮
+│   Placeholder    ││   Placeholder    │
+│                  ││                  │
+╰──────────────────╯╰──────────────────╯
+╭─E (20 x 4)───────╮╭─F (20 x 4)───────╮
+│   Placeholder    ││   Placeholder    │
+│                  ││                  │
+╰──────────────────╯╰──────────────────╯
+╭─G (20 x 4)───────╮╭─H (20 x 4)───────╮
+│   Placeholder    ││   Placeholder    │
+│                  ││                  │
+╰──────────────────╯╰──────────────────╯
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Minimum_Size.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Minimum_Size.Output.verified.txt
new file mode 100644
index 000000000..5a21d7440
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Minimum_Size.Output.verified.txt
@@ -0,0 +1,15 @@
+╭─Left (30 x 15)─────────────╮╭─Right…─╮
+│                            ││        │
+│                            ││        │
+│                            ││        │
+│                            ││        │
+│                            ││        │
+│                            ││ Placeh │
+│        Placeholder         ││ older  │
+│                            ││        │
+│                            ││        │
+│                            ││        │
+│                            ││        │
+│                            ││        │
+│                            ││        │
+╰────────────────────────────╯╰────────╯
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Ratio.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Ratio.Output.verified.txt
new file mode 100644
index 000000000..5a21d7440
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Ratio.Output.verified.txt
@@ -0,0 +1,15 @@
+╭─Left (30 x 15)─────────────╮╭─Right…─╮
+│                            ││        │
+│                            ││        │
+│                            ││        │
+│                            ││        │
+│                            ││        │
+│                            ││ Placeh │
+│        Placeholder         ││ older  │
+│                            ││        │
+│                            ││        │
+│                            ││        │
+│                            ││        │
+│                            ││        │
+│                            ││        │
+╰────────────────────────────╯╰────────╯
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Size.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Size.Output.verified.txt
new file mode 100644
index 000000000..6ffd3098d
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Respect_To_Size.Output.verified.txt
@@ -0,0 +1,15 @@
+╭─Left (28 x 15)───────────╮╭─Right…───╮
+│                          ││          │
+│                          ││          │
+│                          ││          │
+│                          ││          │
+│                          ││          │
+│                          ││ Placehol │
+│       Placeholder        ││   der    │
+│                          ││          │
+│                          ││          │
+│                          ││          │
+│                          ││          │
+│                          ││          │
+│                          ││          │
+╰──────────────────────────╯╰──────────╯
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Rows.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Rows.Output.verified.txt
new file mode 100644
index 000000000..12a6f9d57
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_With_Rows.Output.verified.txt
@@ -0,0 +1,15 @@
+╭─Top (40 x 7)─────────────────────────╮
+│                                      │
+│                                      │
+│             Placeholder              │
+│                                      │
+│                                      │
+╰──────────────────────────────────────╯
+╭─Bottom (40 x 8)──────────────────────╮
+│                                      │
+│                                      │
+│             Placeholder              │
+│                                      │
+│                                      │
+│                                      │
+╰──────────────────────────────────────╯
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_Without_Invisible_Children.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_Without_Invisible_Children.Output.verified.txt
new file mode 100644
index 000000000..9468df94b
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Layout/Render_Layout_Without_Invisible_Children.Output.verified.txt
@@ -0,0 +1,15 @@
+╭─B (40 x 3)───────────────────────────╮
+│             Placeholder              │
+╰──────────────────────────────────────╯
+╭─C (20 x 4)───────╮╭─D (20 x 4)───────╮
+│   Placeholder    ││   Placeholder    │
+│                  ││                  │
+╰──────────────────╯╰──────────────────╯
+╭─E (20 x 4)───────╮╭─F (20 x 4)───────╮
+│   Placeholder    ││   Placeholder    │
+│                  ││                  │
+╰──────────────────╯╰──────────────────╯
+╭─G (40 x 4)───────────────────────────╮
+│             Placeholder              │
+│                                      │
+╰──────────────────────────────────────╯
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Height.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Height.Output.verified.txt
new file mode 100644
index 000000000..554308f7d
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Height.Output.verified.txt
@@ -0,0 +1,25 @@
+┌───────────────────┐
+│ Hello World       │
+│ Hello Hello Hello │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+│                   │
+└───────────────────┘
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width.Output.verified.txt
new file mode 100644
index 000000000..dc133c3d5
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width.Output.verified.txt
@@ -0,0 +1,3 @@
+┌───────────────────────┐
+│ Hello World           │
+└───────────────────────┘
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width_Height.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width_Height.Output.verified.txt
new file mode 100644
index 000000000..2f85980cc
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width_Height.Output.verified.txt
@@ -0,0 +1,25 @@
+┌────────────────────────────────────────────────┐
+│ Hello World                                    │
+│ Hello Hello Hello                              │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+│                                                │
+└────────────────────────────────────────────────┘
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width_MaxWidth.Output.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width_MaxWidth.Output.verified.txt
new file mode 100644
index 000000000..21ceda978
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Panel/Render_Width_MaxWidth.Output.verified.txt
@@ -0,0 +1,3 @@
+┌──────────────────┐
+│ Hello World      │
+└──────────────────┘
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_Centered.Align_Widget.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_Centered.Align_Widget.verified.txt
new file mode 100644
index 000000000..62617a039
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_Centered.Align_Widget.verified.txt
@@ -0,0 +1,6 @@
+                          ┌────────┬────────┬───────┐                           
+                          │ Foo    │ Bar    │ Baz   │                           
+                          ├────────┼────────┼───────┤                           
+                          │ Qux    │ Corgi  │ Waldo │                           
+                          │ Grault │ Garply │ Fred  │                           
+                          └────────┴────────┴───────┘                           
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_LeftAligned.Align_Widget.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_LeftAligned.Align_Widget.verified.txt
new file mode 100644
index 000000000..6edca8f6e
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_LeftAligned.Align_Widget.verified.txt
@@ -0,0 +1,6 @@
+┌────────┬────────┬───────┐                                                     
+│ Foo    │ Bar    │ Baz   │                                                     
+├────────┼────────┼───────┤                                                     
+│ Qux    │ Corgi  │ Waldo │                                                     
+│ Grault │ Garply │ Fred  │                                                     
+└────────┴────────┴───────┘                                                     
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_RightAligned.Align_Widget.verified.txt b/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_RightAligned.Align_Widget.verified.txt
new file mode 100644
index 000000000..72a459e51
--- /dev/null
+++ b/test/Spectre.Console.Tests/Expectations/Widgets/Table/Render_RightAligned.Align_Widget.verified.txt
@@ -0,0 +1,6 @@
+                                                     ┌────────┬────────┬───────┐
+                                                     │ Foo    │ Bar    │ Baz   │
+                                                     ├────────┼────────┼───────┤
+                                                     │ Qux    │ Corgi  │ Waldo │
+                                                     │ Grault │ Garply │ Fred  │
+                                                     └────────┴────────┴───────┘
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Spectre.Console.Tests.csproj b/test/Spectre.Console.Tests/Spectre.Console.Tests.csproj
index c450b0245..4d7481040 100644
--- a/test/Spectre.Console.Tests/Spectre.Console.Tests.csproj
+++ b/test/Spectre.Console.Tests/Spectre.Console.Tests.csproj
@@ -1,8 +1,7 @@
 
 
   
-    net6.0;net48
-    net6.0
+    net7.0
   
 
   
@@ -18,10 +17,9 @@
     
     
     
-    
-    
-    
-    
+    
+    
+    
     
     
       all
diff --git a/test/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs b/test/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs
index c00bde333..01bbfa6ce 100644
--- a/test/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs
+++ b/test/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs
@@ -154,5 +154,25 @@ public void Should_Not_Get_Confused_When_Mixing_Escaped_And_Unescaped()
             // Then
             console.Output.ShouldBe("[grey][white]");
         }
+
+        [Theory]
+        [InlineData("[white][[[/][white]]][/]", "[]")]
+        [InlineData("[white][[[/]", "[")]
+        [InlineData("[white]]][/]", "]")]
+        [InlineData("[black on white link=https://www.gooole.com/q=]]]Search for a bracket[/]", "Search for a bracket")]
+        [InlineData("[link=https://www.gooole.com/q=]] black on white]Search for a bracket[/]", "Search for a bracket")]
+        [InlineData("[link]https://www.gooole.com/q=]][/]", "https://www.gooole.com/q=]")]
+        public void Should_Not_Fail_As_In_GH1024(string markup, string expected)
+        {
+            // Given
+            var console = new TestConsole();
+            console.EmitAnsiSequences = false;
+
+            // When
+            console.Markup(markup);
+
+            // Then
+            console.Output.ShouldBe(expected);
+        }
     }
 }
diff --git a/test/Spectre.Console.Tests/Unit/Live/Progress/ProgressColumnFixture.cs b/test/Spectre.Console.Tests/Unit/Live/Progress/ProgressColumnFixture.cs
index 4b10877b7..9a2fac258 100644
--- a/test/Spectre.Console.Tests/Unit/Live/Progress/ProgressColumnFixture.cs
+++ b/test/Spectre.Console.Tests/Unit/Live/Progress/ProgressColumnFixture.cs
@@ -16,7 +16,7 @@ public ProgressColumnFixture(double completed, double total)
     public string Render()
     {
         var console = new TestConsole();
-        var context = new RenderContext(console.Profile.Capabilities);
+        var context = RenderOptions.Create(console, console.Profile.Capabilities);
         console.Write(Column.Render(context, Task, TimeSpan.Zero));
         return console.Output;
     }
diff --git a/test/Spectre.Console.Tests/Unit/Rendering/RenderHookTests.cs b/test/Spectre.Console.Tests/Unit/Rendering/RenderHookTests.cs
index 219a13d74..6be28da46 100644
--- a/test/Spectre.Console.Tests/Unit/Rendering/RenderHookTests.cs
+++ b/test/Spectre.Console.Tests/Unit/Rendering/RenderHookTests.cs
@@ -4,7 +4,7 @@ public sealed class RenderHookTests
 {
     private sealed class HelloRenderHook : IRenderHook
     {
-        public IEnumerable Process(RenderContext context, IEnumerable renderables)
+        public IEnumerable Process(RenderOptions options, IEnumerable renderables)
         {
             return new IRenderable[] { new Text("Hello\n") }.Concat(renderables);
         }
diff --git a/test/Spectre.Console.Tests/Unit/StyleTests.cs b/test/Spectre.Console.Tests/Unit/StyleTests.cs
index 5213ed9ae..e75ce2095 100644
--- a/test/Spectre.Console.Tests/Unit/StyleTests.cs
+++ b/test/Spectre.Console.Tests/Unit/StyleTests.cs
@@ -302,7 +302,7 @@ public void Should_Return_Error_If_Color_Number_Is_Invalid(string style, string
         [InlineData("rgb(255)", "Invalid RGB color 'rgb(255)'.")]
         [InlineData("rgb(255,255)", "Invalid RGB color 'rgb(255,255)'.")]
         [InlineData("rgb(255,255,255", "Invalid RGB color 'rgb(255,255,255'.")]
-        [InlineData("rgb(A,B,C)", "Invalid RGB color 'rgb(A,B,C)'. Input string was not in a correct format.")]
+        [InlineData("rgb(A,B,C)", "Invalid RGB color 'rgb(A,B,C)'.")]
         public void Should_Return_Error_If_Rgb_Color_Is_Invalid(string style, string expected)
         {
             // Given, When
@@ -310,7 +310,7 @@ public void Should_Return_Error_If_Rgb_Color_Is_Invalid(string style, string exp
 
             // Then
             result.ShouldNotBeNull();
-            result.Message.ShouldBe(expected);
+            result.Message.ShouldStartWith(expected);
         }
     }
 
diff --git a/test/Spectre.Console.Tests/Unit/Widgets/AlignTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/AlignTests.cs
new file mode 100644
index 000000000..e3f292303
--- /dev/null
+++ b/test/Spectre.Console.Tests/Unit/Widgets/AlignTests.cs
@@ -0,0 +1,155 @@
+using Spectre.Console.Extensions;
+
+namespace Spectre.Console.Tests.Unit;
+
+[UsesVerify]
+[ExpectationPath("Widgets/Align")]
+public sealed class AlignTests
+{
+    [UsesVerify]
+    public sealed class Left
+    {
+        [Fact]
+        [Expectation("Left_Top")]
+        public Task Should_Render_Panel_Left_Aligned_At_Top()
+        {
+            // Given
+            var console = new TestConsole().Size(new Size(40, 15));
+            var align = Align.Left(new Panel("Hello World!"), VerticalAlignment.Top).Height(15);
+
+            // When
+            console.Write(align);
+
+            // Then
+            return Verifier.Verify(console.Output);
+        }
+
+        [Fact]
+        [Expectation("Left_Middle")]
+        public Task Should_Render_Panel_Left_Aligned_At_Middle()
+        {
+            // Given
+            var console = new TestConsole().Size(new Size(40, 15));
+            var align = Align.Left(new Panel("Hello World!"), VerticalAlignment.Middle).Height(15);
+
+            // When
+            console.Write(align);
+
+            // Then
+            return Verifier.Verify(console.Output);
+        }
+
+        [Fact]
+        [Expectation("Left_Bottom")]
+        public Task Should_Render_Panel_Left_Aligned_At_Bottom()
+        {
+            // Given
+            var console = new TestConsole().Size(new Size(40, 15));
+            var align = Align.Left(new Panel("Hello World!"), VerticalAlignment.Bottom).Height(15);
+
+            // When
+            console.Write(align);
+
+            // Then
+            return Verifier.Verify(console.Output);
+        }
+    }
+
+    [UsesVerify]
+    public sealed class Center
+    {
+        [Fact]
+        [Expectation("Center_Top")]
+        public Task Should_Render_Panel_Center_Aligned_At_Top()
+        {
+            // Given
+            var console = new TestConsole().Size(new Size(40, 15));
+            var align = Align.Center(new Panel("Hello World!"), VerticalAlignment.Top).Height(15);
+
+            // When
+            console.Write(align);
+
+            // Then
+            return Verifier.Verify(console.Output);
+        }
+
+        [Fact]
+        [Expectation("Center_Middle")]
+        public Task Should_Render_Panel_Center_Aligned_At_Middle()
+        {
+            // Given
+            var console = new TestConsole().Size(new Size(40, 15));
+            var align = Align.Center(new Panel("Hello World!"), VerticalAlignment.Middle).Height(15);
+
+            // When
+            console.Write(align);
+
+            // Then
+            return Verifier.Verify(console.Output);
+        }
+
+        [Fact]
+        [Expectation("Center_Bottom")]
+        public Task Should_Render_Panel_Center_Aligned_At_Bottom()
+        {
+            // Given
+            var console = new TestConsole().Size(new Size(40, 15));
+            var align = Align.Center(new Panel("Hello World!"), VerticalAlignment.Bottom).Height(15);
+
+            // When
+            console.Write(align);
+
+            // Then
+            return Verifier.Verify(console.Output);
+        }
+    }
+
+    [UsesVerify]
+    public sealed class Right
+    {
+        [Fact]
+        [Expectation("Right_Top")]
+        public Task Should_Render_Panel_Right_Aligned_At_Top()
+        {
+            // Given
+            var console = new TestConsole().Size(new Size(40, 15));
+            var align = Align.Right(new Panel("Hello World!"), VerticalAlignment.Top).Height(15);
+
+            // When
+            console.Write(align);
+
+            // Then
+            return Verifier.Verify(console.Output);
+        }
+
+        [Fact]
+        [Expectation("Right_Middle")]
+        public Task Should_Render_Panel_Right_Aligned_At_Middle()
+        {
+            // Given
+            var console = new TestConsole().Size(new Size(40, 15));
+            var align = Align.Right(new Panel("Hello World!"), VerticalAlignment.Middle).Height(15);
+
+            // When
+            console.Write(align);
+
+            // Then
+            return Verifier.Verify(console.Output);
+        }
+
+        [Fact]
+        [Expectation("Right_Bottom")]
+        public Task Should_Render_Panel_Right_Aligned_At_Bottom()
+        {
+            // Given
+            var console = new TestConsole().Size(new Size(40, 15));
+            var align = Align.Right(new Panel("Hello World!"), VerticalAlignment.Bottom).Height(15);
+
+            // When
+            console.Write(align);
+
+            // Then
+            return Verifier.Verify(console.Output);
+        }
+    }
+}
diff --git a/test/Spectre.Console.Tests/Unit/Widgets/FigletTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/FigletTests.cs
index 845390146..b2831769a 100644
--- a/test/Spectre.Console.Tests/Unit/Widgets/FigletTests.cs
+++ b/test/Spectre.Console.Tests/Unit/Widgets/FigletTests.cs
@@ -60,7 +60,7 @@ public async Task Should_Render_Left_Aligned_Text_Correctly()
         // Given
         var console = new TestConsole().Width(120);
         var text = new FigletText(FigletFont.Default, "Spectre.Console")
-            .Alignment(Justify.Left);
+            .Justify(Justify.Left);
 
         // When
         console.Write(text);
@@ -76,7 +76,7 @@ public async Task Should_Render_Centered_Text_Correctly()
         // Given
         var console = new TestConsole().Width(120);
         var text = new FigletText(FigletFont.Default, "Spectre.Console")
-            .Alignment(Justify.Center);
+            .Justify(Justify.Center);
 
         // When
         console.Write(text);
@@ -92,7 +92,7 @@ public async Task Should_Render_Right_Aligned_Text_Correctly()
         // Given
         var console = new TestConsole().Width(120);
         var text = new FigletText(FigletFont.Default, "Spectre.Console")
-            .Alignment(Justify.Right);
+            .Justify(Justify.Right);
 
         // When
         console.Write(text);
diff --git a/test/Spectre.Console.Tests/Unit/Widgets/LayoutTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/LayoutTests.cs
new file mode 100644
index 000000000..fc0017e7b
--- /dev/null
+++ b/test/Spectre.Console.Tests/Unit/Widgets/LayoutTests.cs
@@ -0,0 +1,268 @@
+namespace Spectre.Console.Tests.Unit;
+
+[UsesVerify]
+[ExpectationPath("Widgets/Layout")]
+public sealed class LayoutTests
+{
+    [Fact]
+    [Expectation("Render_Empty_Layout")]
+    public Task Should_Render_Empty_Layout()
+    {
+        // Given
+        var console = new TestConsole().Size(new Size(40, 15));
+        var layout = new Layout();
+
+        // When
+        console.Write(layout);
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
+    [Fact]
+    [Expectation("Render_Layout")]
+    public Task Should_Render_Empty_Layout_With_Renderable()
+    {
+        // Given
+        var console = new TestConsole().Size(new Size(40, 15));
+        var layout = new Layout().Update(new Panel("Hello").DoubleBorder().Expand());
+
+        // When
+        console.Write(layout);
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
+    [Fact]
+    [Expectation("Render_Layout_With_Columns")]
+    public Task Should_Render_Layout_With_Columns()
+    {
+        // Given
+        var console = new TestConsole().Size(new Size(40, 15));
+        var layout = new Layout()
+            .SplitColumns(
+                new Layout("Left"),
+                new Layout("Right"));
+
+        // When
+        console.Write(layout);
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
+    [Fact]
+    [Expectation("Render_Layout_With_Rows")]
+    public Task Should_Render_Layout_With_Rows()
+    {
+        // Given
+        var console = new TestConsole().Size(new Size(40, 15));
+        var layout = new Layout()
+            .SplitRows(
+                new Layout("Top"),
+                new Layout("Bottom"));
+
+        // When
+        console.Write(layout);
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
+    [Fact]
+    [Expectation("Render_Layout_With_Nested_Columns")]
+    public Task Should_Render_Layout_With_Nested_Columns()
+    {
+        // Given
+        var console = new TestConsole().Size(new Size(40, 15));
+        var layout = new Layout()
+            .SplitColumns(
+                new Layout("Left")
+                    .SplitColumns(
+                        new Layout("L1"),
+                        new Layout("L2")),
+                new Layout("Right")
+                    .SplitColumns(
+                        new Layout("R1"),
+                        new Layout("R2")));
+
+        // When
+        console.Write(layout);
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
+    [Fact]
+    [Expectation("Render_Layout_With_Nested_Rows")]
+    public Task Should_Render_Layout_With_Nested_Rows()
+    {
+        // Given
+        var console = new TestConsole().Size(new Size(40, 15));
+        var layout = new Layout()
+            .SplitRows(
+                new Layout("Top")
+                    .SplitRows(
+                        new Layout("T1"),
+                        new Layout("T2")),
+                new Layout("Bottom")
+                    .SplitRows(
+                        new Layout("B1"),
+                        new Layout("B2")));
+
+        // When
+        console.Write(layout);
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
+    [Fact]
+    [Expectation("Render_Layout_With_Nested_Rows_And_Columns")]
+    public Task Should_Render_Layout_With_Nested_Rows_And_Columns()
+    {
+        // Given
+        var console = new TestConsole().Size(new Size(40, 15));
+        var layout = new Layout()
+            .SplitRows(
+                new Layout("Top")
+                    .SplitRows(
+                        new Layout("T1")
+                            .SplitColumns(
+                                new Layout("A"),
+                                new Layout("B")),
+                        new Layout("T2")
+                            .SplitColumns(
+                                new Layout("C"),
+                                new Layout("D"))),
+                new Layout("Bottom")
+                    .SplitRows(
+                        new Layout("B1")
+                            .SplitColumns(
+                                new Layout("E"),
+                                new Layout("F")),
+                        new Layout("B2")
+                            .SplitColumns(
+                                    new Layout("G"),
+                                    new Layout("H"))));
+
+        // When
+        console.Write(layout);
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
+    [Fact]
+    [Expectation("Render_Layout_Without_Invisible_Children")]
+    public Task Should_Render_Layout_Without_Invisible_Children()
+    {
+        // Given
+        var console = new TestConsole().Size(new Size(40, 15));
+        var layout = new Layout()
+            .SplitRows(
+                new Layout("Top")
+                    .SplitRows(
+                        new Layout("T1")
+                            .SplitColumns(
+                                new Layout("A").Invisible(),
+                                new Layout("B")),
+                        new Layout("T2")
+                            .SplitColumns(
+                                new Layout("C"),
+                                new Layout("D"))),
+                new Layout("Bottom")
+                    .SplitRows(
+                        new Layout("B1")
+                            .SplitColumns(
+                                new Layout("E"),
+                                new Layout("F")),
+                        new Layout("B2")
+                            .SplitColumns(
+                                    new Layout("G"),
+                                    new Layout("H").Invisible())));
+
+        // When
+        console.Write(layout);
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
+    [Fact]
+    [Expectation("Render_Layout_With_Respect_To_Ratio")]
+    public Task Should_Render_Layout_With_Respect_To_Ratio()
+    {
+        // Given
+        var console = new TestConsole().Size(new Size(40, 15));
+        var layout = new Layout()
+            .SplitColumns(
+                new Layout("Left").Ratio(3),
+                new Layout("Right"));
+
+        // When
+        console.Write(layout);
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
+    [Fact]
+    [Expectation("Render_Layout_With_Respect_To_Size")]
+    public Task Should_Render_Layout_With_Respect_To_Size()
+    {
+        // Given
+        var console = new TestConsole().Size(new Size(40, 15));
+        var layout = new Layout()
+            .SplitColumns(
+                new Layout("Left").Size(28),
+                new Layout("Right"));
+
+        // When
+        console.Write(layout);
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
+    [Fact]
+    [Expectation("Render_Layout_With_Respect_To_Minimum_Size")]
+    public Task Should_Render_Layout_With_Respect_To_Minimum_Size()
+    {
+        // Given
+        var console = new TestConsole().Size(new Size(40, 15));
+        var layout = new Layout()
+            .SplitColumns(
+                new Layout("Left").Size(28).MinimumSize(30),
+                new Layout("Right"));
+
+        // When
+        console.Write(layout);
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
+    [Fact]
+    [Expectation("Render_Fallback_Layout")]
+    public Task Should_Fall_Back_To_Parent_Layout_If_All_Children_Are_Invisible()
+    {
+        // Given
+        var console = new TestConsole().Size(new Size(40, 15));
+        var layout = new Layout()
+            .SplitRows(
+                new Layout("T1").SplitColumns(
+                    new Layout("A").Invisible(),
+                    new Layout("B").Invisible()),
+                new Layout("T2").SplitColumns(
+                    new Layout("C").Invisible(),
+                    new Layout("D").Invisible()));
+
+        // When
+        console.Write(layout);
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+}
diff --git a/test/Spectre.Console.Tests/Unit/Widgets/PanelTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/PanelTests.cs
index 73a2ca920..377c94f35 100644
--- a/test/Spectre.Console.Tests/Unit/Widgets/PanelTests.cs
+++ b/test/Spectre.Console.Tests/Unit/Widgets/PanelTests.cs
@@ -81,7 +81,7 @@ public Task Should_Render_Panel_With_Left_Aligned_Header()
         // When
         console.Write(new Panel("Hello World")
         {
-            Header = new PanelHeader("Greeting").LeftAligned(),
+            Header = new PanelHeader("Greeting").LeftJustified(),
             Expand = true,
         });
 
@@ -117,7 +117,7 @@ public Task Should_Render_Panel_With_Right_Aligned_Header()
         // When
         console.Write(new Panel("Hello World")
         {
-            Header = new PanelHeader("Greeting").RightAligned(),
+            Header = new PanelHeader("Greeting").RightJustified(),
             Expand = true,
         });
 
@@ -204,6 +204,76 @@ public Task Should_Expand_Panel_If_Enabled()
         return Verifier.Verify(console.Output);
     }
 
+    [Fact]
+    [Expectation("Render_Width")]
+    public Task Should_Render_To_Specified_Width()
+    {
+        // Given
+        var console = new TestConsole();
+
+        // When
+        console.Write(new Panel(new Text("Hello World"))
+        {
+            Width = 25,
+        });
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
+    [Fact]
+    [Expectation("Render_Width_MaxWidth")]
+    public Task Should_Use_Max_Width_If_Specified_Width_Is_Too_Large()
+    {
+        // Given
+        var console = new TestConsole();
+        console.Profile.Width = 20;
+
+        // When
+        console.Write(new Panel(new Text("Hello World"))
+        {
+            Width = 25,
+        });
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
+    [Fact]
+    [Expectation("Render_Height")]
+    public Task Should_Render_To_Specified_Height()
+    {
+        // Given
+        var console = new TestConsole();
+
+        // When
+        console.Write(new Panel(new Text("Hello World\nHello Hello Hello"))
+        {
+            Height = 25,
+        });
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
+    [Fact]
+    [Expectation("Render_Width_Height")]
+    public Task Should_Render_To_Specified_Width_And_Height()
+    {
+        // Given
+        var console = new TestConsole();
+
+        // When
+        console.Write(new Panel("Hello World\nHello Hello Hello")
+        {
+            Width = 50,
+            Height = 25,
+        });
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
     [Fact]
     [Expectation("Render_Child_RightAligned")]
     public Task Should_Justify_Child_To_Right_Correctly()
@@ -213,7 +283,7 @@ public Task Should_Justify_Child_To_Right_Correctly()
 
         // When
         console.Write(
-            new Panel(new Text("Hello World").RightAligned())
+            new Panel(new Text("Hello World").RightJustified())
             {
                 Expand = true,
             });
diff --git a/test/Spectre.Console.Tests/Unit/Widgets/RuleTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/RuleTests.cs
index fbff7b516..fada3fd6d 100644
--- a/test/Spectre.Console.Tests/Unit/Widgets/RuleTests.cs
+++ b/test/Spectre.Console.Tests/Unit/Widgets/RuleTests.cs
@@ -70,7 +70,7 @@ public Task Should_Render_Default_Rule_With_Title_Left_Aligned()
         // When
         console.Write(new Rule("Hello World")
         {
-            Alignment = Justify.Left,
+            Justification = Justify.Left,
         });
 
         // Then
@@ -87,7 +87,7 @@ public Task Should_Render_Default_Rule_With_Title_Right_Aligned()
         // When
         console.Write(new Rule("Hello World")
         {
-            Alignment = Justify.Right,
+            Justification = Justify.Right,
         });
 
         // Then
diff --git a/test/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs
index 2716fbb1f..a02765811 100644
--- a/test/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs
+++ b/test/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs
@@ -187,7 +187,9 @@ public Task Should_Left_Align_Table_Correctly()
         // Given
         var console = new TestConsole();
         var table = new Table();
+#pragma warning disable CS0618 // Type or member is obsolete
         table.Alignment = Justify.Left;
+#pragma warning restore CS0618 // Type or member is obsolete
         table.AddColumns("Foo", "Bar", "Baz");
         table.AddRow("Qux", "Corgi", "Waldo");
         table.AddRow("Grault", "Garply", "Fred");
@@ -199,6 +201,24 @@ public Task Should_Left_Align_Table_Correctly()
         return Verifier.Verify(console.Output);
     }
 
+    [Fact]
+    [Expectation("Render_LeftAligned", "Align_Widget")]
+    public Task Should_Left_Align_Table_Correctly_When_Wrapped_In_Align_Widget()
+    {
+        // Given
+        var console = new TestConsole();
+        var table = new Table();
+        table.AddColumns("Foo", "Bar", "Baz");
+        table.AddRow("Qux", "Corgi", "Waldo");
+        table.AddRow("Grault", "Garply", "Fred");
+
+        // When
+        console.Write(Align.Left(table));
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
     [Fact]
     [Expectation("Render_Centered")]
     public Task Should_Center_Table_Correctly()
@@ -206,7 +226,9 @@ public Task Should_Center_Table_Correctly()
         // Given
         var console = new TestConsole();
         var table = new Table();
+#pragma warning disable CS0618 // Type or member is obsolete
         table.Alignment = Justify.Center;
+#pragma warning restore CS0618 // Type or member is obsolete
         table.AddColumns("Foo", "Bar", "Baz");
         table.AddRow("Qux", "Corgi", "Waldo");
         table.AddRow("Grault", "Garply", "Fred");
@@ -218,6 +240,24 @@ public Task Should_Center_Table_Correctly()
         return Verifier.Verify(console.Output);
     }
 
+    [Fact]
+    [Expectation("Render_Centered", "Align_Widget")]
+    public Task Should_Center_Table_Correctly_When_Wrapped_In_Align_Widget()
+    {
+        // Given
+        var console = new TestConsole();
+        var table = new Table();
+        table.AddColumns("Foo", "Bar", "Baz");
+        table.AddRow("Qux", "Corgi", "Waldo");
+        table.AddRow("Grault", "Garply", "Fred");
+
+        // When
+        console.Write(Align.Center(table));
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
     [Fact]
     [Expectation("Render_RightAligned")]
     public Task Should_Right_Align_Table_Correctly()
@@ -225,7 +265,9 @@ public Task Should_Right_Align_Table_Correctly()
         // Given
         var console = new TestConsole();
         var table = new Table();
+#pragma warning disable CS0618 // Type or member is obsolete
         table.Alignment = Justify.Right;
+#pragma warning restore CS0618 // Type or member is obsolete
         table.AddColumns("Foo", "Bar", "Baz");
         table.AddRow("Qux", "Corgi", "Waldo");
         table.AddRow("Grault", "Garply", "Fred");
@@ -237,6 +279,24 @@ public Task Should_Right_Align_Table_Correctly()
         return Verifier.Verify(console.Output);
     }
 
+    [Fact]
+    [Expectation("Render_RightAligned", "Align_Widget")]
+    public Task Should_Right_Align_Table_Correctly_When_Wrapped_In_Align_Widget()
+    {
+        // Given
+        var console = new TestConsole();
+        var table = new Table();
+        table.AddColumns("Foo", "Bar", "Baz");
+        table.AddRow("Qux", "Corgi", "Waldo");
+        table.AddRow("Grault", "Garply", "Fred");
+
+        // When
+        console.Write(Align.Right(table));
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
+
     [Fact]
     [Expectation("Render_Nested")]
     public Task Should_Render_Table_Nested_In_Panels_Correctly()
@@ -380,7 +440,7 @@ public Task Should_Not_Draw_Tables_That_Are_Impossible_To_Draw()
         table.AddColumn(new TableColumn(new Panel("[u]DEF[/]").BorderColor(Color.Green)));
         table.AddColumn(new TableColumn(new Panel("[u]GHI[/]").BorderColor(Color.Blue)));
         table.AddRow(new Text("Hello").Centered(), new Markup("[red]World[/]"), Text.Empty);
-        table.AddRow(second, new Text("Whaat"), new Text("Lol").RightAligned());
+        table.AddRow(second, new Text("Whaat"), new Text("Lol").RightJustified());
         table.AddRow(new Markup("[blue]Hej[/]"), new Markup("[yellow]Världen[/]"), Text.Empty);
 
         // When
diff --git a/test/Spectre.Console.Tests/Unit/Widgets/TextPathTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/TextPathTests.cs
index 0f984be7d..d7e5513c7 100644
--- a/test/Spectre.Console.Tests/Unit/Widgets/TextPathTests.cs
+++ b/test/Spectre.Console.Tests/Unit/Widgets/TextPathTests.cs
@@ -77,7 +77,7 @@ public void Should_Right_Align_Correctly()
         var console = new TestConsole().Width(40);
 
         // When
-        console.Write(new TextPath("C:/My documents/Bar/Baz.txt").RightAligned());
+        console.Write(new TextPath("C:/My documents/Bar/Baz.txt").RightJustified());
 
         // Then
         console.Output.TrimEnd('\n')
diff --git a/test/Spectre.Console.Tests/Unit/Widgets/TextTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/TextTests.cs
index cf08e2347..6b0bdfdb5 100644
--- a/test/Spectre.Console.Tests/Unit/Widgets/TextTests.cs
+++ b/test/Spectre.Console.Tests/Unit/Widgets/TextTests.cs
@@ -42,11 +42,13 @@ public void Should_Return_The_Number_Of_Lines(string input, int expected)
     public void Should_Consider_The_Longest_Word_As_Minimum_Width()
     {
         // Given
+        var console = new TestConsole();
         var caps = new TestCapabilities { Unicode = true };
         var text = new Text("Foo Bar Baz\nQux\nLol mobile");
 
         // When
-        var result = ((IRenderable)text).Measure(caps.CreateRenderContext(), 80);
+        var result = ((IRenderable)text).Measure(
+            caps.CreateRenderContext(console), 80);
 
         // Then
         result.Min.ShouldBe(6);
@@ -56,11 +58,13 @@ public void Should_Consider_The_Longest_Word_As_Minimum_Width()
     public void Should_Consider_The_Longest_Line_As_Maximum_Width()
     {
         // Given
+        var console = new TestConsole();
         var caps = new TestCapabilities { Unicode = true };
         var text = new Text("Foo Bar Baz\nQux\nLol mobile");
 
         // When
-        var result = ((IRenderable)text).Measure(caps.CreateRenderContext(), 80);
+        var result = ((IRenderable)text).Measure(
+            caps.CreateRenderContext(console), 80);
 
         // Then
         result.Max.ShouldBe(11);
diff --git a/test/Spectre.Console.Tests/Utilities/ModuleInitializerAttribute.cs b/test/Spectre.Console.Tests/Utilities/ModuleInitializerAttribute.cs
index 6f93cb48c..5b84da221 100644
--- a/test/Spectre.Console.Tests/Utilities/ModuleInitializerAttribute.cs
+++ b/test/Spectre.Console.Tests/Utilities/ModuleInitializerAttribute.cs
@@ -1,4 +1,4 @@
-#if !NET5_0_OR_GREATER
+#if !NET6_0_OR_GREATER
 namespace System.Runtime.CompilerServices;
 
 [AttributeUsage(AttributeTargets.Method, Inherited = false)]
diff --git a/test/Spectre.Console.Tests/VerifyConfiguration.cs b/test/Spectre.Console.Tests/VerifyConfiguration.cs
index 770ec2bde..b3261c23f 100644
--- a/test/Spectre.Console.Tests/VerifyConfiguration.cs
+++ b/test/Spectre.Console.Tests/VerifyConfiguration.cs
@@ -5,6 +5,6 @@ public static class VerifyConfiguration
     [ModuleInitializer]
     public static void Init()
     {
-        VerifierSettings.DerivePathInfo(Expectations.Initialize);
+        Verifier.DerivePathInfo(Expectations.Initialize);
     }
 }