From fa852165548624b11b25c18d2e92160c8e5c4b32 Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Fri, 7 Aug 2020 22:24:38 +0200 Subject: [PATCH] Add fallback for unicode borders --- src/Sample/Program.cs | 2 + .../Fixtures/PlainConsole.cs | 10 +++-- .../Unit/Composition/BorderTests.cs | 17 +++++--- src/Spectre.Console/Capabilities.cs | 8 +++- src/Spectre.Console/Composition/Border.cs | 13 +++++- .../Composition/Borders/NoBorder.cs | 6 ++- src/Spectre.Console/Composition/Grid.cs | 9 ++-- .../Composition/IRenderable.cs | 11 +++-- src/Spectre.Console/Composition/Panel.cs | 42 +++++++++++-------- .../Composition/RenderContext.cs | 37 ++++++++++++++++ .../Composition/Table.Calculations.cs | 9 ++-- src/Spectre.Console/Composition/Table.cs | 32 ++++++++++---- src/Spectre.Console/Composition/Text.cs | 7 ++-- .../ConsoleExtensions.Rendering.cs | 4 +- 14 files changed, 149 insertions(+), 58 deletions(-) create mode 100644 src/Spectre.Console/Composition/RenderContext.cs diff --git a/src/Sample/Program.cs b/src/Sample/Program.cs index 09f99df..f129d13 100644 --- a/src/Sample/Program.cs +++ b/src/Sample/Program.cs @@ -13,6 +13,7 @@ namespace Sample AnsiConsole.WriteLine("Hello World!"); AnsiConsole.Reset(); AnsiConsole.MarkupLine("Capabilities: [yellow underline]{0}[/]", AnsiConsole.Capabilities); + AnsiConsole.MarkupLine("Encoding: [yellow underline]{0}[/]", AnsiConsole.Console.Encoding.EncodingName); AnsiConsole.MarkupLine("Width=[yellow]{0}[/], Height=[yellow]{1}[/]", AnsiConsole.Width, AnsiConsole.Height); AnsiConsole.MarkupLine("[white on red]Good[/] [red]bye[/]!"); AnsiConsole.WriteLine(); @@ -51,6 +52,7 @@ namespace Sample console.ResetColors(); console.ResetDecoration(); console.MarkupLine("Capabilities: [yellow underline]{0}[/]", console.Capabilities); + console.MarkupLine("Encoding: [yellow underline]{0}[/]", AnsiConsole.Console.Encoding.EncodingName); console.MarkupLine("Width=[yellow]{0}[/], Height=[yellow]{1}[/]", console.Width, console.Height); console.MarkupLine("[white on red]Good[/] [red]bye[/]!"); console.WriteLine(); diff --git a/src/Spectre.Console.Tests/Fixtures/PlainConsole.cs b/src/Spectre.Console.Tests/Fixtures/PlainConsole.cs index 4d52aa1..e1d6dad 100644 --- a/src/Spectre.Console.Tests/Fixtures/PlainConsole.cs +++ b/src/Spectre.Console.Tests/Fixtures/PlainConsole.cs @@ -7,7 +7,7 @@ namespace Spectre.Console.Tests { public sealed class PlainConsole : IAnsiConsole, IDisposable { - public Capabilities Capabilities => throw new NotSupportedException(); + public Capabilities Capabilities { get; } public Encoding Encoding { get; } public int Width { get; } @@ -21,11 +21,15 @@ namespace Spectre.Console.Tests public string Output => Writer.ToString().TrimEnd('\n'); public IReadOnlyList Lines => Output.Split(new char[] { '\n' }); - public PlainConsole(int width = 80, int height = 9000, Encoding encoding = null) + public PlainConsole( + int width = 80, int height = 9000, Encoding encoding = null, + bool supportsAnsi = true, ColorSystem colorSystem = ColorSystem.Standard, + bool legacyConsole = false) { + Capabilities = new Capabilities(supportsAnsi, colorSystem, legacyConsole); + Encoding = encoding ?? Encoding.UTF8; Width = width; Height = height; - Encoding = encoding ?? Encoding.UTF8; Writer = new StringWriter(); } diff --git a/src/Spectre.Console.Tests/Unit/Composition/BorderTests.cs b/src/Spectre.Console.Tests/Unit/Composition/BorderTests.cs index bfebaea..701e121 100644 --- a/src/Spectre.Console.Tests/Unit/Composition/BorderTests.cs +++ b/src/Spectre.Console.Tests/Unit/Composition/BorderTests.cs @@ -10,13 +10,18 @@ namespace Spectre.Console.Tests.Unit.Composition public sealed class TheGetBorderMethod { [Theory] - [InlineData(BorderKind.Ascii, typeof(AsciiBorder))] - [InlineData(BorderKind.Square, typeof(SquareBorder))] - [InlineData(BorderKind.Rounded, typeof(RoundedBorder))] - public void Should_Return_Correct_Border_For_Specified_Kind(BorderKind kind, Type expected) + [InlineData(BorderKind.None, false, typeof(NoBorder))] + [InlineData(BorderKind.Ascii, false, typeof(AsciiBorder))] + [InlineData(BorderKind.Square, false, typeof(SquareBorder))] + [InlineData(BorderKind.Rounded, false, typeof(RoundedBorder))] + [InlineData(BorderKind.None, true, typeof(NoBorder))] + [InlineData(BorderKind.Ascii, true, typeof(AsciiBorder))] + [InlineData(BorderKind.Square, true, typeof(SquareBorder))] + [InlineData(BorderKind.Rounded, true, typeof(SquareBorder))] + public void Should_Return_Correct_Border_For_Specified_Kind(BorderKind kind, bool safe, Type expected) { // Given, When - var result = Border.GetBorder(kind); + var result = Border.GetBorder(kind, safe); // Then result.ShouldBeOfType(expected); @@ -26,7 +31,7 @@ namespace Spectre.Console.Tests.Unit.Composition public void Should_Throw_If_Unknown_Border_Kind_Is_Specified() { // Given, When - var result = Record.Exception(() => Border.GetBorder((BorderKind)int.MaxValue)); + var result = Record.Exception(() => Border.GetBorder((BorderKind)int.MaxValue, false)); // Then result.ShouldBeOfType(); diff --git a/src/Spectre.Console/Capabilities.cs b/src/Spectre.Console/Capabilities.cs index 8c47677..be0db18 100644 --- a/src/Spectre.Console/Capabilities.cs +++ b/src/Spectre.Console/Capabilities.cs @@ -25,7 +25,13 @@ namespace Spectre.Console /// public bool LegacyConsole { get; } - internal Capabilities(bool supportsAnsi, ColorSystem colorSystem, bool legacyConsole) + /// + /// Initializes a new instance of the class. + /// + /// Whether or not ANSI escape sequences are supported. + /// The color system that is supported. + /// Whether or not this is a legacy console. + public Capabilities(bool supportsAnsi, ColorSystem colorSystem, bool legacyConsole) { SupportsAnsi = supportsAnsi; ColorSystem = colorSystem; diff --git a/src/Spectre.Console/Composition/Border.cs b/src/Spectre.Console/Composition/Border.cs index 3e999f2..3ccdfb8 100644 --- a/src/Spectre.Console/Composition/Border.cs +++ b/src/Spectre.Console/Composition/Border.cs @@ -20,6 +20,11 @@ namespace Spectre.Console.Composition { BorderKind.Rounded, new RoundedBorder() }, }; + private static readonly Dictionary _safeLookup = new Dictionary + { + { BorderKind.Rounded, BorderKind.Square }, + }; + /// /// Initializes a new instance of the class. /// @@ -32,9 +37,15 @@ namespace Spectre.Console.Composition /// Gets a represented by the specified . /// /// The kind of border to get. + /// Whether or not to get a "safe" border that can be rendered in a legacy console. /// A instance representing the specified . - public static Border GetBorder(BorderKind kind) + public static Border GetBorder(BorderKind kind, bool safe) { + if (safe && _safeLookup.TryGetValue(kind, out var safeKind)) + { + kind = safeKind; + } + if (!_borders.TryGetValue(kind, out var border)) { throw new InvalidOperationException("Unknown border kind"); diff --git a/src/Spectre.Console/Composition/Borders/NoBorder.cs b/src/Spectre.Console/Composition/Borders/NoBorder.cs index 121336c..8a2cd16 100644 --- a/src/Spectre.Console/Composition/Borders/NoBorder.cs +++ b/src/Spectre.Console/Composition/Borders/NoBorder.cs @@ -1,7 +1,11 @@ namespace Spectre.Console.Composition { - internal sealed class NoBorder : Border + /// + /// Represents an invisible border. + /// + public sealed class NoBorder : Border { + /// protected override string GetBoxPart(BorderPart part) { return " "; diff --git a/src/Spectre.Console/Composition/Grid.cs b/src/Spectre.Console/Composition/Grid.cs index 590755b..99c8f0a 100644 --- a/src/Spectre.Console/Composition/Grid.cs +++ b/src/Spectre.Console/Composition/Grid.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; using Spectre.Console.Composition; namespace Spectre.Console @@ -25,15 +24,15 @@ namespace Spectre.Console } /// - public Measurement Measure(Encoding encoding, int maxWidth) + public Measurement Measure(RenderContext context, int maxWidth) { - return ((IRenderable)_table).Measure(encoding, maxWidth); + return ((IRenderable)_table).Measure(context, maxWidth); } /// - public IEnumerable Render(Encoding encoding, int width) + public IEnumerable Render(RenderContext context, int width) { - return ((IRenderable)_table).Render(encoding, width); + return ((IRenderable)_table).Render(context, width); } /// diff --git a/src/Spectre.Console/Composition/IRenderable.cs b/src/Spectre.Console/Composition/IRenderable.cs index 545c1cb..d12b409 100644 --- a/src/Spectre.Console/Composition/IRenderable.cs +++ b/src/Spectre.Console/Composition/IRenderable.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Text; namespace Spectre.Console.Composition { @@ -11,17 +10,17 @@ namespace Spectre.Console.Composition /// /// Measures the renderable object. /// - /// The encoding to use. + /// The render context. /// The maximum allowed width. /// The minimum and maximum width of the object. - Measurement Measure(Encoding encoding, int maxWidth); + Measurement Measure(RenderContext context, int maxWidth); /// /// Renders the object. /// - /// The encoding to use. - /// The width of the render area. + /// The render context. + /// The maximum allowed width. /// A collection of segments. - IEnumerable Render(Encoding encoding, int width); + IEnumerable Render(RenderContext context, int maxWidth); } } diff --git a/src/Spectre.Console/Composition/Panel.cs b/src/Spectre.Console/Composition/Panel.cs index 0041593..2d44885 100644 --- a/src/Spectre.Console/Composition/Panel.cs +++ b/src/Spectre.Console/Composition/Panel.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using System.Text; using Spectre.Console.Composition; namespace Spectre.Console @@ -13,7 +12,14 @@ namespace Spectre.Console private readonly IRenderable _child; private readonly bool _fit; private readonly Justify _content; - private readonly Border _border; + private readonly BorderKind _border; + + /// + /// Gets or sets a value indicating whether or not to use + /// a "safe" border on legacy consoles that might not be able + /// to render non-ASCII characters. Defaults to true. + /// + public bool SafeBorder { get; set; } = true; /// /// Initializes a new instance of the class. @@ -31,47 +37,49 @@ namespace Spectre.Console _child = child ?? throw new System.ArgumentNullException(nameof(child)); _fit = fit; _content = content; - _border = Border.GetBorder(border); + _border = border; } /// - Measurement IRenderable.Measure(Encoding encoding, int maxWidth) + Measurement IRenderable.Measure(RenderContext context, int maxWidth) { - var childWidth = _child.Measure(encoding, maxWidth); + var childWidth = _child.Measure(context, maxWidth); return new Measurement(childWidth.Min + 4, childWidth.Max + 4); } /// - IEnumerable IRenderable.Render(Encoding encoding, int width) + IEnumerable IRenderable.Render(RenderContext context, int width) { + var border = Border.GetBorder(_border, (context.LegacyConsole || !context.Unicode) && SafeBorder); + var childWidth = width - 4; if (!_fit) { - var measurement = _child.Measure(encoding, width - 2); + var measurement = _child.Measure(context, width - 2); childWidth = measurement.Max; } var result = new List(); var panelWidth = childWidth + 2; - result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopLeft))); - result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop, panelWidth))); - result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopRight))); + result.Add(new Segment(border.GetPart(BorderPart.HeaderTopLeft))); + result.Add(new Segment(border.GetPart(BorderPart.HeaderTop, panelWidth))); + result.Add(new Segment(border.GetPart(BorderPart.HeaderTopRight))); result.Add(new Segment("\n")); // Render the child. - var childSegments = _child.Render(encoding, childWidth); + var childSegments = _child.Render(context, childWidth); // Split the child segments into lines. var lines = Segment.SplitLines(childSegments, childWidth); foreach (var line in lines) { - result.Add(new Segment(_border.GetPart(BorderPart.CellLeft))); + result.Add(new Segment(border.GetPart(BorderPart.CellLeft))); result.Add(new Segment(" ")); // Left padding var content = new List(); - var length = line.Sum(segment => segment.CellLength(encoding)); + var length = line.Sum(segment => segment.CellLength(context.Encoding)); if (length < childWidth) { // Justify right side @@ -116,13 +124,13 @@ namespace Spectre.Console result.AddRange(content); result.Add(new Segment(" ")); - result.Add(new Segment(_border.GetPart(BorderPart.CellRight))); + result.Add(new Segment(border.GetPart(BorderPart.CellRight))); result.Add(new Segment("\n")); } - result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomLeft))); - result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom, panelWidth))); - result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomRight))); + result.Add(new Segment(border.GetPart(BorderPart.FooterBottomLeft))); + result.Add(new Segment(border.GetPart(BorderPart.FooterBottom, panelWidth))); + result.Add(new Segment(border.GetPart(BorderPart.FooterBottomRight))); result.Add(new Segment("\n")); return result; diff --git a/src/Spectre.Console/Composition/RenderContext.cs b/src/Spectre.Console/Composition/RenderContext.cs new file mode 100644 index 0000000..97194a1 --- /dev/null +++ b/src/Spectre.Console/Composition/RenderContext.cs @@ -0,0 +1,37 @@ +using System.Text; + +namespace Spectre.Console.Composition +{ + /// + /// Represents a render context. + /// + public sealed class RenderContext + { + /// + /// Gets the console's output encoding. + /// + public Encoding Encoding { get; } + + /// + /// Gets a value indicating whether or not this a legacy console (i.e. cmd.exe). + /// + public bool LegacyConsole { get; } + + /// + /// Gets a value indicating whether or not unicode is supported. + /// + public bool Unicode { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The console's output encoding. + /// A value indicating whether or not this a legacy console (i.e. cmd.exe). + public RenderContext(Encoding encoding, bool legacyConsole) + { + Encoding = encoding ?? throw new System.ArgumentNullException(nameof(encoding)); + LegacyConsole = legacyConsole; + Unicode = Encoding == Encoding.UTF8 || Encoding == Encoding.Unicode; + } + } +} diff --git a/src/Spectre.Console/Composition/Table.Calculations.cs b/src/Spectre.Console/Composition/Table.Calculations.cs index f140cf3..c161af2 100644 --- a/src/Spectre.Console/Composition/Table.Calculations.cs +++ b/src/Spectre.Console/Composition/Table.Calculations.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using Spectre.Console.Composition; using Spectre.Console.Internal; @@ -15,9 +14,9 @@ namespace Spectre.Console // Calculate the widths of each column, including padding, not including borders. // Ported from Rich by Will McGugan, licensed under MIT. // https://github.com/willmcgugan/rich/blob/527475837ebbfc427530b3ee0d4d0741d2d0fc6d/rich/table.py#L394 - private List CalculateColumnWidths(Encoding encoding, int maxWidth) + private List CalculateColumnWidths(RenderContext options, int maxWidth) { - var width_ranges = _columns.Select(column => MeasureColumn(column, encoding, maxWidth)); + var width_ranges = _columns.Select(column => MeasureColumn(column, options, maxWidth)); var widths = width_ranges.Select(range => range.Max).ToList(); var tableWidth = widths.Sum(); @@ -132,7 +131,7 @@ namespace Spectre.Console return widths; } - private (int Min, int Max) MeasureColumn(TableColumn column, Encoding encoding, int maxWidth) + private (int Min, int Max) MeasureColumn(TableColumn column, RenderContext options, int maxWidth) { // Predetermined width? if (column.Width != null) @@ -148,7 +147,7 @@ namespace Spectre.Console var maxWidths = new List(); foreach (var row in rows) { - var measure = ((IRenderable)row).Measure(encoding, maxWidth); + var measure = ((IRenderable)row).Measure(options, maxWidth); minWidths.Add(measure.Min); maxWidths.Add(measure.Max); } diff --git a/src/Spectre.Console/Composition/Table.cs b/src/Spectre.Console/Composition/Table.cs index f7a2433..8e40cd4 100644 --- a/src/Spectre.Console/Composition/Table.cs +++ b/src/Spectre.Console/Composition/Table.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using Spectre.Console.Composition; using Spectre.Console.Internal; @@ -47,6 +46,13 @@ namespace Spectre.Console /// public int? Width { get; set; } = null; + /// + /// Gets or sets a value indicating whether or not to use + /// a "safe" border on legacy consoles that might not be able + /// to render non-ASCII characters. Defaults to true. + /// + public bool SafeBorder { get; set; } = true; + /// /// Initializes a new instance of the class. /// @@ -123,8 +129,13 @@ namespace Spectre.Console } /// - Measurement IRenderable.Measure(Encoding encoding, int maxWidth) + Measurement IRenderable.Measure(RenderContext context, int maxWidth) { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + if (Width != null) { maxWidth = Math.Min(Width.Value, maxWidth); @@ -132,7 +143,7 @@ namespace Spectre.Console maxWidth -= GetExtraWidth(includePadding: true); - var measurements = _columns.Select(column => MeasureColumn(column, encoding, maxWidth)).ToList(); + var measurements = _columns.Select(column => MeasureColumn(column, context, maxWidth)).ToList(); var min = measurements.Sum(x => x.Min) + GetExtraWidth(includePadding: true); var max = Width ?? measurements.Sum(x => x.Max) + GetExtraWidth(includePadding: true); @@ -140,9 +151,14 @@ namespace Spectre.Console } /// - IEnumerable IRenderable.Render(Encoding encoding, int width) + IEnumerable IRenderable.Render(RenderContext context, int width) { - var border = Composition.Border.GetBorder(Border); + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var border = Composition.Border.GetBorder(Border, (context.LegacyConsole || !context.Unicode) && SafeBorder); var showBorder = Border != BorderKind.None; var hideBorder = Border == BorderKind.None; @@ -156,7 +172,7 @@ namespace Spectre.Console maxWidth -= GetExtraWidth(includePadding: true); // Calculate the column and table widths - var columnWidths = CalculateColumnWidths(encoding, maxWidth); + var columnWidths = CalculateColumnWidths(context, maxWidth); // Update the table width. width = columnWidths.Sum() + GetExtraWidth(includePadding: false); @@ -181,7 +197,7 @@ namespace Spectre.Console var cells = new List>(); foreach (var (rowWidth, cell) in columnWidths.Zip(row, (f, s) => (f, s))) { - var lines = Segment.SplitLines(((IRenderable)cell).Render(encoding, rowWidth)); + var lines = Segment.SplitLines(((IRenderable)cell).Render(context, rowWidth)); cellHeight = Math.Max(cellHeight, lines.Count); cells.Add(lines); } @@ -234,7 +250,7 @@ namespace Spectre.Console result.AddRange(cell[cellRowIndex]); // Pad cell content right - var length = cell[cellRowIndex].Sum(segment => segment.CellLength(encoding)); + var length = cell[cellRowIndex].Sum(segment => segment.CellLength(context.Encoding)); if (length < columnWidths[cellIndex]) { result.Add(new Segment(new string(' ', columnWidths[cellIndex] - length))); diff --git a/src/Spectre.Console/Composition/Text.cs b/src/Spectre.Console/Composition/Text.cs index da892f3..b05e0f6 100644 --- a/src/Spectre.Console/Composition/Text.cs +++ b/src/Spectre.Console/Composition/Text.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Text; using Spectre.Console.Composition; using Spectre.Console.Internal; @@ -98,9 +97,9 @@ namespace Spectre.Console } /// - Measurement IRenderable.Measure(Encoding encoding, int maxWidth) + Measurement IRenderable.Measure(RenderContext context, int maxWidth) { - var lines = Segment.SplitLines(((IRenderable)this).Render(encoding, maxWidth)); + var lines = Segment.SplitLines(((IRenderable)this).Render(context, maxWidth)); if (lines.Count == 0) { return new Measurement(0, maxWidth); @@ -113,7 +112,7 @@ namespace Spectre.Console } /// - IEnumerable IRenderable.Render(Encoding encoding, int width) + IEnumerable IRenderable.Render(RenderContext context, int width) { if (string.IsNullOrWhiteSpace(_text)) { diff --git a/src/Spectre.Console/ConsoleExtensions.Rendering.cs b/src/Spectre.Console/ConsoleExtensions.Rendering.cs index 9ad1e04..36d7a31 100644 --- a/src/Spectre.Console/ConsoleExtensions.Rendering.cs +++ b/src/Spectre.Console/ConsoleExtensions.Rendering.cs @@ -26,7 +26,9 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(renderable)); } - foreach (var segment in renderable.Render(console.Encoding, console.Width)) + var options = new RenderContext(console.Encoding, console.Capabilities.LegacyConsole); + + foreach (var segment in renderable.Render(options, console.Width)) { if (!segment.Style.Equals(Style.Plain)) {