From 9c567602826232b4cca5f37785329b2068e08dc0 Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Wed, 22 Apr 2026 16:28:32 +0200 Subject: [PATCH 01/25] Ensure redirected output works as expected Closes #2095 --- .../Unit/AnsiConsoleTests.cs | 17 ++++++++++++ .../Backends/Ansi/AnsiConsoleBackend.cs | 26 ++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs index b985bc3d3..b808a1d39 100644 --- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs +++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs @@ -159,4 +159,21 @@ public void Should_Not_Throw_If_Exception_Has_No_StackTrace() Should.NotThrow(When); } } + + [Fact] + public void Should_Write_To_New_Output_If_Redirected() + { + // Given + var console = new TestConsole(); + var redirected = new StringWriter(); + + // When + console.Write("ABC"); + console.Profile.Out = new AnsiConsoleOutput(redirected); + console.Write("DEF"); + + // Then + console.Output.ShouldBe("ABC"); + redirected.ToString().ShouldBe("DEF"); + } } \ No newline at end of file diff --git a/src/Spectre.Console/Internal/Backends/Ansi/AnsiConsoleBackend.cs b/src/Spectre.Console/Internal/Backends/Ansi/AnsiConsoleBackend.cs index 93d7ce8e8..9e9dec844 100644 --- a/src/Spectre.Console/Internal/Backends/Ansi/AnsiConsoleBackend.cs +++ b/src/Spectre.Console/Internal/Backends/Ansi/AnsiConsoleBackend.cs @@ -3,7 +3,8 @@ namespace Spectre.Console; internal sealed class AnsiConsoleBackend : IAnsiConsoleBackend { private readonly IAnsiConsole _console; - private readonly AnsiWriter _writer; + private AnsiWriter _writer; + private IAnsiConsoleOutput _cachedOutput; public IAnsiConsoleCursor Cursor { get; } public Capabilities Capabilities => _console.Profile.Capabilities; @@ -11,6 +12,7 @@ internal sealed class AnsiConsoleBackend : IAnsiConsoleBackend public AnsiConsoleBackend(IAnsiConsole console) { _console = console ?? throw new ArgumentNullException(nameof(console)); + _cachedOutput = _console.Profile.Out; _writer = new AnsiWriter(_console.Profile.Out.Writer, _console.Profile.Capabilities); Cursor = new AnsiConsoleCursor(this); @@ -18,8 +20,11 @@ public AnsiConsoleBackend(IAnsiConsole console) public void Clear(bool home) { - Write(w => w.EraseInDisplay(2)); - Write(w => w.ClearScrollback()); + Write(w => + { + w.EraseInDisplay(2); + w.ClearScrollback(); + }); if (home) { @@ -29,11 +34,26 @@ public void Clear(bool home) public void Write(IRenderable renderable) { + EnsureWriter(); _writer.Write(_console, renderable); } public void Write(Action action) { + EnsureWriter(); action(_writer); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureWriter() + { + // Output has not changed? + if (ReferenceEquals(_cachedOutput, _console.Profile.Out)) + { + return; + } + + _cachedOutput = _console.Profile.Out; + _writer = new AnsiWriter(_console.Profile.Out.Writer, _console.Profile.Capabilities); + } } \ No newline at end of file From 264d90395abed7ea1ba04d6b05597753fcf25552 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:34:59 +0000 Subject: [PATCH 02/25] chore: Update dotnet monorepo --- global.json | 2 +- src/Directory.Packages.props | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/global.json b/global.json index 6107e0568..bcdee6486 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "$schema": "http://json.schemastore.org/global", "sdk": { - "version": "10.0.202", + "version": "10.0.203", "rollForward": "latestFeature" } } diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 4f3101bda..f67ff608f 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -6,7 +6,7 @@ - + @@ -14,11 +14,11 @@ - + - + From e056e380d69f203b8d6a218f10920b7bcdc3e7ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:07:56 +0000 Subject: [PATCH 03/25] chore: Update dependency Microsoft.NET.Test.Sdk to 18.5.0 --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index f67ff608f..22e70d623 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -5,7 +5,7 @@ - + From 9aeb9e7018758d75625d4026cbd83f106f34c033 Mon Sep 17 00:00:00 2001 From: Merkle Groot Date: Sat, 25 Apr 2026 03:31:15 -0400 Subject: [PATCH 04/25] Fix #1638: Add missing text prompt suffix --- .../Text/ClearOnFinish.Output.verified.txt | 2 +- .../Text/Issue_1638.Output.verified.txt | 2 ++ .../Unit/Prompts/TextPromptTests.cs | 16 ++++++++++++ src/Spectre.Console/Prompts/TextPrompt.cs | 25 ++++++++++++++++--- 4 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 src/Spectre.Console.Tests/Expectations/Prompts/Text/Issue_1638.Output.verified.txt diff --git a/src/Spectre.Console.Tests/Expectations/Prompts/Text/ClearOnFinish.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Prompts/Text/ClearOnFinish.Output.verified.txt index 9b0e676c7..ba451889b 100644 --- a/src/Spectre.Console.Tests/Expectations/Prompts/Text/ClearOnFinish.Output.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Prompts/Text/ClearOnFinish.Output.verified.txt @@ -1,3 +1,3 @@ -Enter a value ************ +Enter a value: ************  \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/Prompts/Text/Issue_1638.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Prompts/Text/Issue_1638.Output.verified.txt new file mode 100644 index 000000000..b75128d3a --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Prompts/Text/Issue_1638.Output.verified.txt @@ -0,0 +1,2 @@ +no default, with suffix: input + diff --git a/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs b/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs index 0f09591e7..09d0eb983 100644 --- a/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs +++ b/src/Spectre.Console.Tests/Unit/Prompts/TextPromptTests.cs @@ -319,6 +319,22 @@ public Task Should_Not_Append_Questionmark_Or_Colon_If_No_Choices_Are_Set() return Verifier.Verify(console.Output); } + [Fact] + [Expectation("Issue_1638")] + public Task Should_Append_Colon_When_No_Default_Value_Is_Set() + { + // Given + var console = new TestConsole(); + console.Input.PushTextWithEnter("input"); + + // When + console.Prompt( + new TextPrompt("no default, with suffix")); + + // Then + return Verifier.Verify(console.Output); + } + [Fact] [Expectation("DefaultValueStyleNotSet")] public Task Uses_default_style_for_default_value_if_no_style_is_set() diff --git a/src/Spectre.Console/Prompts/TextPrompt.cs b/src/Spectre.Console/Prompts/TextPrompt.cs index 4dbcf71b0..bc6f90344 100644 --- a/src/Spectre.Console/Prompts/TextPrompt.cs +++ b/src/Spectre.Console/Prompts/TextPrompt.cs @@ -219,10 +219,10 @@ private void WritePrompt(IAnsiConsole console) var builder = new StringBuilder(); builder.Append(_prompt.TrimEnd()); - var appendSuffix = false; + var hasPromptDetails = false; if (ShowChoices && Choices.Count > 0) { - appendSuffix = true; + hasPromptDetails = true; var converter = Converter ?? TypeConverterHelper.ConvertToString; var choices = string.Join("/", Choices.Select(choice => converter(choice))); var choicesStyle = ChoicesStyle?.ToMarkup() ?? "blue"; @@ -231,7 +231,7 @@ private void WritePrompt(IAnsiConsole console) if (ShowDefaultValue && DefaultValue != null) { - appendSuffix = true; + hasPromptDetails = true; var converter = Converter ?? TypeConverterHelper.ConvertToString; var defaultValueStyle = DefaultValueStyle?.ToMarkup() ?? "green"; var defaultValue = converter(DefaultValue.Value); @@ -244,7 +244,7 @@ private void WritePrompt(IAnsiConsole console) } var markup = builder.ToString().Trim(); - if (appendSuffix) + if (ShouldAppendColon(markup, hasPromptDetails)) { markup += ":"; } @@ -252,6 +252,23 @@ private void WritePrompt(IAnsiConsole console) console.Markup(markup + " "); } + /// + /// A colon should be appended when prompt details are rendered, or when a plain prompt does not already end with punctuation. + /// + /// The prompt markup. + /// Whether the prompt includes choices or a default value. + /// Whether a colon should be appended. + private static bool ShouldAppendColon(string markup, bool hasPromptDetails) + { + if (hasPromptDetails) + { + return true; + } + + var prompt = Markup.Remove(markup).TrimEnd(); + return prompt.Length > 0 && char.IsLetterOrDigit(prompt[^1]); + } + /// /// Clears the prompt line when enabled. /// From 58429081d62d5a7a446dc33f22967037159c3b1b Mon Sep 17 00:00:00 2001 From: Grant Totinov Date: Sat, 25 Apr 2026 15:52:50 +0300 Subject: [PATCH 05/25] Fix Align measure to respect explicitly set width (#2101) --- .gitignore | 1 + .../Expectations/Public_API.Output.verified.txt | 1 + .../Unit/Widgets/AlignTests.cs | 17 +++++++++++++++++ src/Spectre.Console/Widgets/Align.cs | 8 ++++++++ 4 files changed, 27 insertions(+) diff --git a/.gitignore b/.gitignore index 0f148ac9e..0fa3df2b7 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ src/.idea/**/uiDesigner.xml [Dd]ebug/ [Rr]elease/ x64/ +*.lscache *_i.c *_p.c *.ilk diff --git a/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt index d3f347d32..b9012bb2e 100644 --- a/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt @@ -7,6 +7,7 @@ public Spectre.Console.HorizontalAlignment Horizontal { get; set; } public Spectre.Console.VerticalAlignment? Vertical { get; set; } public int? Width { get; set; } + protected override Spectre.Console.Rendering.Measurement Measure(Spectre.Console.Rendering.RenderOptions options, int maxWidth) { } protected override System.Collections.Generic.IEnumerable Render(Spectre.Console.Rendering.RenderOptions options, int maxWidth) { } public static Spectre.Console.Align Center(Spectre.Console.Rendering.IRenderable renderable, Spectre.Console.VerticalAlignment? vertical = default) { } public static Spectre.Console.Align Left(Spectre.Console.Rendering.IRenderable renderable, Spectre.Console.VerticalAlignment? vertical = default) { } diff --git a/src/Spectre.Console.Tests/Unit/Widgets/AlignTests.cs b/src/Spectre.Console.Tests/Unit/Widgets/AlignTests.cs index 5400bafd1..35715b4a3 100644 --- a/src/Spectre.Console.Tests/Unit/Widgets/AlignTests.cs +++ b/src/Spectre.Console.Tests/Unit/Widgets/AlignTests.cs @@ -146,4 +146,21 @@ public Task Should_Render_Panel_Right_Aligned_At_Bottom() return Verifier.Verify(console.Output); } } + public sealed class Measure + { + [Fact] + public void Should_Return_Explicit_Width_When_Set() + { + // Given + var console = new TestConsole().Size(new Size(40, 15)); + var align = Align.Left(new Panel("Hello World!")).Width(20); + + // When + var measurement = ((IRenderable)align) + .Measure(RenderOptions.Create(console), 40); + + // Then + Assert.Equal(20, measurement.Max); + } + } } \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/Align.cs b/src/Spectre.Console/Widgets/Align.cs index 95e92cf7f..1e23ebd5e 100644 --- a/src/Spectre.Console/Widgets/Align.cs +++ b/src/Spectre.Console/Widgets/Align.cs @@ -74,6 +74,14 @@ public static Align Right(IRenderable renderable, VerticalAlignment? vertical = return new Align(renderable, HorizontalAlignment.Right, vertical); } + /// + protected override Measurement Measure(RenderOptions options, int maxWidth) + { + var width = Math.Min(Width ?? maxWidth, maxWidth); + var measurement = _renderable.Measure(options with { Height = null }, width); + return new Measurement(Math.Min(measurement.Min, width), width); + } + /// protected override IEnumerable Render(RenderOptions options, int maxWidth) { From dbbed60c76ad73d5961d4e4bab370e650ff5617f Mon Sep 17 00:00:00 2001 From: James Newell <144143381+james-newell-forge@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:30:34 -0400 Subject: [PATCH 06/25] Option to exclude vertical padding for live progress renderer (#2100) --- .../Public_API.Output.verified.txt | 4 ++ .../Unit/Live/Progress/ProgressTests.cs | 56 +++++++++++++++++++ src/Spectre.Console/Live/LiveRenderable.cs | 5 +- src/Spectre.Console/Live/Progress/Progress.cs | 45 ++++++++++++++- .../Renderers/DefaultProgressRenderer.cs | 6 +- 5 files changed, 112 insertions(+), 4 deletions(-) diff --git a/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt index b9012bb2e..252b1cdad 100644 --- a/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt @@ -2531,6 +2531,7 @@ public Progress(Spectre.Console.IAnsiConsole console, System.TimeProvider? timeProvider = null) { } public bool AutoClear { get; set; } public bool AutoRefresh { get; set; } + public bool ExcludeVerticalPadding { get; set; } public bool HideCompleted { get; set; } public System.TimeSpan RefreshRate { get; set; } public System.Func, Spectre.Console.Rendering.IRenderable> RenderHook { get; set; } @@ -2581,7 +2582,10 @@ public static Spectre.Console.Progress AutoClear(this Spectre.Console.Progress progress, bool enabled) { } public static Spectre.Console.Progress AutoRefresh(this Spectre.Console.Progress progress, bool enabled) { } public static Spectre.Console.Progress Columns(this Spectre.Console.Progress progress, params Spectre.Console.ProgressColumn[] columns) { } + public static Spectre.Console.Progress ExcludeVerticalPadding(this Spectre.Console.Progress progress) { } + public static Spectre.Console.Progress ExcludeVerticalPadding(this Spectre.Console.Progress progress, bool enabled) { } public static Spectre.Console.Progress HideCompleted(this Spectre.Console.Progress progress, bool enabled) { } + public static Spectre.Console.Progress IncludeVerticalPadding(this Spectre.Console.Progress progress) { } public static Spectre.Console.Progress UseRenderHook(this Spectre.Console.Progress progress, System.Func, Spectre.Console.Rendering.IRenderable> renderHook) { } } public sealed class ProgressTask : System.IProgress diff --git a/src/Spectre.Console.Tests/Unit/Live/Progress/ProgressTests.cs b/src/Spectre.Console.Tests/Unit/Live/Progress/ProgressTests.cs index 89fcaf916..894ff16cb 100644 --- a/src/Spectre.Console.Tests/Unit/Live/Progress/ProgressTests.cs +++ b/src/Spectre.Console.Tests/Unit/Live/Progress/ProgressTests.cs @@ -671,4 +671,60 @@ public void Should_Include_StartTime_In_Speed_Calculation() .ShouldNotBeNull() .ShouldBeGreaterThan(0); } + + [Fact] + public void Should_Exclude_Vertical_Padding() + { + // Given + var console = new TestConsole() + .Width(10) + .Interactive() + .EmitAnsiSequences(); + + var progress = new Progress(console) + .Columns(new ProgressBarColumn()) + .AutoRefresh(false) + .AutoClear(false) + .ExcludeVerticalPadding(); + + // When + progress.Start(ctx => ctx.AddTask("foo")); + + // Then + console.Output + .NormalizeLineEndings() + .ShouldBe( + "\e[?25l" + // Hide cursor + "\e[38;5;8m━━━━━━━━━━\e[0m\n" + // Task + "\e[?25h"); // show cursor + } + + [Fact] + public void Should_Include_Vertical_Padding() + { + // Given + var console = new TestConsole() + .Width(10) + .Interactive() + .EmitAnsiSequences(); + + var progress = new Progress(console) + .Columns(new ProgressBarColumn()) + .AutoRefresh(false) + .AutoClear(false) + .IncludeVerticalPadding(); + + // When + progress.Start(ctx => ctx.AddTask("foo")); + + // Then + console.Output + .NormalizeLineEndings() + .ShouldBe( + "\e[?25l" + // Hide cursor + " \n" + // Top padding + "\e[38;5;8m━━━━━━━━━━\e[0m\n" + // Task + " \n" + // Bottom padding + "\e[?25h"); // show cursor + } } \ No newline at end of file diff --git a/src/Spectre.Console/Live/LiveRenderable.cs b/src/Spectre.Console/Live/LiveRenderable.cs index 72ef12e7f..e106082a9 100644 --- a/src/Spectre.Console/Live/LiveRenderable.cs +++ b/src/Spectre.Console/Live/LiveRenderable.cs @@ -63,7 +63,10 @@ public IRenderable PositionCursor(RenderOptions options) return ControlCode.Create(options.Capabilities, w => { w.Write("\r"); // More efficient than CHA (CSI 1 G)? - w.CursorUp(linesToMoveUp); + if (linesToMoveUp > 0) + { + w.CursorUp(linesToMoveUp); + } }); } } diff --git a/src/Spectre.Console/Live/Progress/Progress.cs b/src/Spectre.Console/Live/Progress/Progress.cs index b10121e72..72e680130 100644 --- a/src/Spectre.Console/Live/Progress/Progress.cs +++ b/src/Spectre.Console/Live/Progress/Progress.cs @@ -33,6 +33,13 @@ public sealed class Progress /// public bool HideCompleted { get; set; } + /// + /// Gets or sets a value indicating whether or not to exclude the + /// vertical padding in the display. + /// Defaults to false. + /// + public bool ExcludeVerticalPadding { get; set; } + /// /// Gets or sets the refresh rate if AutoRefresh is enabled. /// Defaults to 10 times/second. @@ -160,7 +167,7 @@ private ProgressRenderer CreateRenderer() if (interactive) { var columns = new List(Columns); - return new DefaultProgressRenderer(_console, columns, RefreshRate, HideCompleted, RenderHook); + return new DefaultProgressRenderer(_console, columns, RefreshRate, HideCompleted, ExcludeVerticalPadding, RenderHook); } else { @@ -259,4 +266,40 @@ public static Progress HideCompleted(this Progress progress, bool enabled) return progress; } + + /// + /// Sets whether or not to exclude the vertical padding in the display. + /// If enabled, the progress display will not include vertical padding. + /// + /// The instance. + /// Whether or not to exclude the vertical padding. + /// The same instance so that multiple calls can be chained. + public static Progress ExcludeVerticalPadding(this Progress progress, bool enabled) + { + ArgumentNullException.ThrowIfNull(progress); + + progress.ExcludeVerticalPadding = enabled; + + return progress; + } + + /// + /// Excludes the vertical padding in the display. + /// + /// The instance. + /// The same instance so that multiple calls can be chained. + public static Progress ExcludeVerticalPadding(this Progress progress) + { + return progress.ExcludeVerticalPadding(true); + } + + /// + /// Includes the vertical padding in the display. + /// + /// The instance. + /// The same instance so that multiple calls can be chained. + public static Progress IncludeVerticalPadding(this Progress progress) + { + return progress.ExcludeVerticalPadding(false); + } } \ 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 9871e238e..0fa358820 100644 --- a/src/Spectre.Console/Live/Progress/Renderers/DefaultProgressRenderer.cs +++ b/src/Spectre.Console/Live/Progress/Renderers/DefaultProgressRenderer.cs @@ -8,12 +8,13 @@ internal sealed class DefaultProgressRenderer : ProgressRenderer private readonly object _lock; private readonly Stopwatch _stopwatch; private readonly bool _hideCompleted; + private readonly bool _excludeVerticalPadding; private readonly Func, IRenderable> _renderHook; private TimeSpan _lastUpdate; public override TimeSpan RefreshRate { get; } - public DefaultProgressRenderer(IAnsiConsole console, List columns, TimeSpan refreshRate, bool hideCompleted, Func, IRenderable> renderHook) + public DefaultProgressRenderer(IAnsiConsole console, List columns, TimeSpan refreshRate, bool hideCompleted, bool excludeVerticalPadding, Func, IRenderable> renderHook) { _console = console ?? throw new ArgumentNullException(nameof(console)); _columns = columns ?? throw new ArgumentNullException(nameof(columns)); @@ -22,6 +23,7 @@ public DefaultProgressRenderer(IAnsiConsole console, List column _stopwatch = new Stopwatch(); _lastUpdate = TimeSpan.Zero; _hideCompleted = hideCompleted; + _excludeVerticalPadding = excludeVerticalPadding; _renderHook = renderHook; RefreshRate = refreshRate; @@ -110,7 +112,7 @@ public override void Update(ProgressContext context) layout.AddRow(grid); - _live.SetRenderable(new Padder(_renderHook(layout, tasks), new Padding(0, 1))); + _live.SetRenderable(_excludeVerticalPadding ? _renderHook(layout, tasks) : new Padder(_renderHook(layout, tasks), new Padding(0, 1))); } } From 2d51cb2eef8818343f03f23bf26e692ff935581a Mon Sep 17 00:00:00 2001 From: Merkle Groot <40680812+merklegroot@users.noreply.github.com> Date: Tue, 28 Apr 2026 08:03:24 -0400 Subject: [PATCH 07/25] Don't emit \e[0N for zero steps in any cursor movement (#2104) Prevents terminal inconsistencies with \e[0A \e[0B \e[0C \e[0D etc. Applies the same guard to CursorUp/Down/Forward/Backward/NextLine/PreviousLine. Fixes #2076 --- .../AnsiWriterTests.cs | 130 ++++++++++++++++++ src/Spectre.Console.Ansi/AnsiWriter.cs | 40 ++++++ 2 files changed, 170 insertions(+) diff --git a/src/Spectre.Console.Ansi.Tests/AnsiWriterTests.cs b/src/Spectre.Console.Ansi.Tests/AnsiWriterTests.cs index 8739cebdd..887af4849 100644 --- a/src/Spectre.Console.Ansi.Tests/AnsiWriterTests.cs +++ b/src/Spectre.Console.Ansi.Tests/AnsiWriterTests.cs @@ -77,6 +77,19 @@ public void Should_Write_Correct_Ansi() // Then fixture.Output.ShouldBe("\e[4D"); } + + [Fact] + public void Should_Not_Write_Ansi_For_Zero_Steps() + { + // Given + var fixture = new AnsiFixture(); + + // When + fixture.Writer.CursorLeft(0); + + // Then + fixture.Output.ShouldBeEmpty(); + } } public sealed class CursorBackward @@ -93,6 +106,19 @@ public void Should_Write_Correct_Ansi() // Then fixture.Output.ShouldBe("\e[4D"); } + + [Fact] + public void Should_Not_Write_Ansi_For_Zero_Steps() + { + // Given + var fixture = new AnsiFixture(); + + // When + fixture.Writer.CursorBackward(0); + + // Then + fixture.Output.ShouldBeEmpty(); + } } public sealed class CursorRight @@ -109,6 +135,19 @@ public void Should_Write_Correct_Ansi() // Then fixture.Output.ShouldBe("\e[4C"); } + + [Fact] + public void Should_Not_Write_Ansi_For_Zero_Steps() + { + // Given + var fixture = new AnsiFixture(); + + // When + fixture.Writer.CursorRight(0); + + // Then + fixture.Output.ShouldBeEmpty(); + } } public sealed class CursorForward @@ -125,6 +164,19 @@ public void Should_Write_Correct_Ansi() // Then fixture.Output.ShouldBe("\e[4C"); } + + [Fact] + public void Should_Not_Write_Ansi_For_Zero_Steps() + { + // Given + var fixture = new AnsiFixture(); + + // When + fixture.Writer.CursorForward(0); + + // Then + fixture.Output.ShouldBeEmpty(); + } } public sealed class CursorDown @@ -141,6 +193,19 @@ public void Should_Write_Correct_Ansi() // Then fixture.Output.ShouldBe("\e[4B"); } + + [Fact] + public void Should_Not_Write_Ansi_For_Zero_Steps() + { + // Given + var fixture = new AnsiFixture(); + + // When + fixture.Writer.CursorDown(0); + + // Then + fixture.Output.ShouldBeEmpty(); + } } public sealed class CursorUp @@ -157,6 +222,19 @@ public void Should_Write_Correct_Ansi() // Then fixture.Output.ShouldBe("\e[4A"); } + + [Fact] + public void Should_Not_Write_Ansi_For_Zero_Steps() + { + // Given + var fixture = new AnsiFixture(); + + // When + fixture.Writer.CursorUp(0); + + // Then + fixture.Output.ShouldBeEmpty(); + } } public sealed class CursorPosition @@ -391,6 +469,19 @@ public void Should_Write_Correct_Ansi() // Then fixture.Output.ShouldBe("\e[4Z"); } + + [Fact] + public void Should_Not_Write_Ansi_For_Zero_Tabs() + { + // Given + var fixture = new AnsiFixture(); + + // When + fixture.Writer.CursorBackwardTabulation(0); + + // Then + fixture.Output.ShouldBeEmpty(); + } } public sealed class CursorHorizontalTabulation @@ -407,6 +498,19 @@ public void Should_Write_Correct_Ansi() // Then fixture.Output.ShouldBe("\e[4I"); } + + [Fact] + public void Should_Not_Write_Ansi_For_Zero_Tabs() + { + // Given + var fixture = new AnsiFixture(); + + // When + fixture.Writer.CursorHorizontalTabulation(0); + + // Then + fixture.Output.ShouldBeEmpty(); + } } public sealed class CursorNextLine @@ -423,6 +527,19 @@ public void Should_Write_Correct_Ansi() // Then fixture.Output.ShouldBe("\e[4E"); } + + [Fact] + public void Should_Not_Write_Ansi_For_Zero_Lines() + { + // Given + var fixture = new AnsiFixture(); + + // When + fixture.Writer.CursorNextLine(0); + + // Then + fixture.Output.ShouldBeEmpty(); + } } public sealed class CursorPreviousLine @@ -439,6 +556,19 @@ public void Should_Write_Correct_Ansi() // Then fixture.Output.ShouldBe("\e[4F"); } + + [Fact] + public void Should_Not_Write_Ansi_For_Zero_Lines() + { + // Given + var fixture = new AnsiFixture(); + + // When + fixture.Writer.CursorPreviousLine(0); + + // Then + fixture.Output.ShouldBeEmpty(); + } } public sealed class Index diff --git a/src/Spectre.Console.Ansi/AnsiWriter.cs b/src/Spectre.Console.Ansi/AnsiWriter.cs index 53f303461..41c5a6c7d 100644 --- a/src/Spectre.Console.Ansi/AnsiWriter.cs +++ b/src/Spectre.Console.Ansi/AnsiWriter.cs @@ -351,6 +351,11 @@ public AnsiWriter CursorHome() /// The same instance so that multiple calls can be chained. public AnsiWriter CursorUp(int steps) { + if (steps == 0) + { + return this; + } + return WriteCsi(steps, 'A'); } @@ -367,6 +372,11 @@ public AnsiWriter CursorUp(int steps) /// The same instance so that multiple calls can be chained. public AnsiWriter CursorDown(int steps) { + if (steps == 0) + { + return this; + } + return WriteCsi(steps, 'B'); } @@ -397,6 +407,11 @@ public AnsiWriter CursorRight(int steps) /// The same instance so that multiple calls can be chained. public AnsiWriter CursorForward(int steps) { + if (steps == 0) + { + return this; + } + return WriteCsi(steps, 'C'); } @@ -427,6 +442,11 @@ public AnsiWriter CursorLeft(int steps) /// The same instance so that multiple calls can be chained. public AnsiWriter CursorBackward(int steps) { + if (steps == 0) + { + return this; + } + return WriteCsi(steps, 'D'); } @@ -607,6 +627,11 @@ public AnsiWriter ClearScrollback() /// The same instance so that multiple calls can be chained. public AnsiWriter CursorBackwardTabulation(int tabs = 1) { + if (tabs == 0) + { + return this; + } + return WriteCsi(tabs, 'Z'); } @@ -621,6 +646,11 @@ public AnsiWriter CursorBackwardTabulation(int tabs = 1) /// The same instance so that multiple calls can be chained. public AnsiWriter CursorHorizontalTabulation(int tabs = 1) { + if (tabs == 0) + { + return this; + } + return WriteCsi(tabs, 'I'); } @@ -635,6 +665,11 @@ public AnsiWriter CursorHorizontalTabulation(int tabs = 1) /// The same instance so that multiple calls can be chained. public AnsiWriter CursorNextLine(int lines = 1) { + if (lines == 0) + { + return this; + } + return WriteCsi(lines, 'E'); } @@ -649,6 +684,11 @@ public AnsiWriter CursorNextLine(int lines = 1) /// The same instance so that multiple calls can be chained. public AnsiWriter CursorPreviousLine(int lines = 1) { + if (lines == 0) + { + return this; + } + return WriteCsi(lines, 'F'); } From 68a8815121cc3b22410d330dfb7b898b8896005c Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Mon, 4 May 2026 00:12:49 +0200 Subject: [PATCH 08/25] ConfirmationPrompt: Allow submission without Enter key Add a new `RequireEnter` property to `ConfirmationPrompt` which defaults to `true`. Setting it to `false` makes the prompt accept input without the user pressing Enter. Closes #2106 --- .../Public_API.Output.verified.txt | 2 + .../Unit/Prompts/ConfirmationPromptTests.cs | 68 +++++++++++++++++++ .../Extensions/AnsiConsoleExtensions.Input.cs | 9 ++- .../Prompts/ConfirmationPrompt.cs | 46 ++++++++++++- src/Spectre.Console/Prompts/TextPrompt.cs | 20 +++--- .../Prompts/TextPromptExtensions.cs | 9 +++ .../Prompts/TextPromptInputHandler.cs | 7 ++ 7 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 src/Spectre.Console.Tests/Unit/Prompts/ConfirmationPromptTests.cs create mode 100644 src/Spectre.Console/Prompts/TextPromptInputHandler.cs diff --git a/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt index 252b1cdad..30a28a284 100644 --- a/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt @@ -379,6 +379,7 @@ public Spectre.Console.Style? DefaultValueStyle { get; set; } public string InvalidChoiceMessage { get; set; } public char No { get; set; } + public bool RequireEnter { get; set; } public bool ShowChoices { get; set; } public bool ShowDefaultValue { get; set; } public char Yes { get; set; } @@ -393,6 +394,7 @@ public static Spectre.Console.ConfirmationPrompt HideDefaultValue(this Spectre.Console.ConfirmationPrompt obj) { } public static Spectre.Console.ConfirmationPrompt InvalidChoiceMessage(this Spectre.Console.ConfirmationPrompt obj, string message) { } public static Spectre.Console.ConfirmationPrompt No(this Spectre.Console.ConfirmationPrompt obj, char character) { } + public static Spectre.Console.ConfirmationPrompt RequireEnter(this Spectre.Console.ConfirmationPrompt obj, bool require = true) { } public static Spectre.Console.ConfirmationPrompt ShowChoices(this Spectre.Console.ConfirmationPrompt obj) { } public static Spectre.Console.ConfirmationPrompt ShowChoices(this Spectre.Console.ConfirmationPrompt obj, bool show) { } public static Spectre.Console.ConfirmationPrompt ShowDefaultValue(this Spectre.Console.ConfirmationPrompt obj) { } diff --git a/src/Spectre.Console.Tests/Unit/Prompts/ConfirmationPromptTests.cs b/src/Spectre.Console.Tests/Unit/Prompts/ConfirmationPromptTests.cs new file mode 100644 index 000000000..eca32eb52 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Prompts/ConfirmationPromptTests.cs @@ -0,0 +1,68 @@ +namespace Spectre.Console.Tests.Unit; + +public sealed class ConfirmationTextPromptTests +{ + [Theory] + [InlineData("y", true)] + [InlineData("n", false)] + public void Should_Require_Enter_By_Default( + string input, bool expected) + { + // Given + var console = new TestConsole(); + console.Input.PushTextWithEnter(input); + + // When + var result = console.Prompt( + new ConfirmationPrompt("Accept?") + .Yes('y') + .No('n')); + + // Then + result.ShouldBe(expected); + console.Input.IsKeyAvailable().ShouldBeFalse(); + } + + [Theory] + [InlineData('y', true)] + [InlineData('n', false)] + public void Should_Not_Require_Enter_If_RequireEnter_Is_Set_To_False( + char input, bool expected) + { + // Given + var console = new TestConsole(); + console.Input.PushCharacter(input); + + // When + var result = console.Prompt( + new ConfirmationPrompt("Accept?") + .RequireEnter(false) + .Yes('y') + .No('n')); + + // Then + result.ShouldBe(expected); + } + + [Fact] + public void Should_Ignore_Invalid_Input() + { + // Given + var console = new TestConsole(); + console.Input.PushCharacter('a'); + console.Input.PushCharacter('b'); + console.Input.PushCharacter('c'); + console.Input.PushCharacter('y'); + + // When + var result = console.Prompt( + new ConfirmationPrompt("Accept?") + .RequireEnter(false) + .Yes('y') + .No('n')); + + // Then + result.ShouldBe(true); + console.Input.IsKeyAvailable().ShouldBeFalse(); + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs index 5f7575940..758804cfe 100644 --- a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs +++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs @@ -5,7 +5,8 @@ namespace Spectre.Console; /// public static partial class AnsiConsoleExtensions { - internal static async Task ReadLine(this IAnsiConsole console, Style? style, bool secret, char? mask, IEnumerable? items = null, CancellationToken cancellationToken = default, string? initialInput = null) + internal static async Task ReadLine(this IAnsiConsole console, Style? style, bool secret, char? mask, + IEnumerable? items = null, string? initialInput = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(console); @@ -98,7 +99,8 @@ internal static async Task ReadLine(this IAnsiConsole console, Style? st } } - private static string AutoComplete(List autocomplete, string text, AutoCompleteDirection autoCompleteDirection) + private static string AutoComplete(List autocomplete, string text, + AutoCompleteDirection autoCompleteDirection) { var found = autocomplete.Find(i => i == text); var replace = string.Empty; @@ -126,7 +128,8 @@ private static string AutoComplete(List autocomplete, string text, AutoC return replace; } - private static string GetAutocompleteValue(AutoCompleteDirection autoCompleteDirection, IList autocomplete, string found) + private static string GetAutocompleteValue(AutoCompleteDirection autoCompleteDirection, IList autocomplete, + string found) { var foundAutocompleteIndex = autocomplete.IndexOf(found); var index = autoCompleteDirection switch diff --git a/src/Spectre.Console/Prompts/ConfirmationPrompt.cs b/src/Spectre.Console/Prompts/ConfirmationPrompt.cs index 10563bbd5..78ce47562 100644 --- a/src/Spectre.Console/Prompts/ConfirmationPrompt.cs +++ b/src/Spectre.Console/Prompts/ConfirmationPrompt.cs @@ -40,15 +40,23 @@ public sealed class ConfirmationPrompt : IPrompt public bool ShowDefaultValue { get; set; } = true; /// - /// Gets or sets the style in which the default value is displayed. Defaults to green when . + /// Gets or sets the style in which the default value is displayed. + /// Defaults to green when . /// public Style? DefaultValueStyle { get; set; } /// - /// Gets or sets the style in which the list of choices is displayed. Defaults to blue when . + /// Gets or sets the style in which the list of choices is displayed. + /// Defaults to blue when . /// public Style? ChoicesStyle { get; set; } + /// + /// Gets or sets a value indicating whether or not the + /// enter key is required after input + /// + public bool RequireEnter { get; set; } = true; + /// /// Gets or sets the string comparer to use when comparing user input /// against Yes/No choices. @@ -86,6 +94,7 @@ public async Task ShowAsync(IAnsiConsole console, CancellationToken cancel .ShowDefaultValue(ShowDefaultValue) .DefaultValue(DefaultValue ? Yes : No) .DefaultValueStyle(DefaultValueStyle) + .UseInputHandler(RequireEnter ? null : SingleKeyInputHandler) .AddChoice(Yes) .AddChoice(No); @@ -93,6 +102,25 @@ public async Task ShowAsync(IAnsiConsole console, CancellationToken cancel return comparer.Compare(Yes.ToString(), result.ToString()) == 0; } + + private static async Task SingleKeyInputHandler( + IAnsiConsole console, Style? style, bool secret, + char? mask, IEnumerable? items = null, + string? initialInput = null, + CancellationToken cancellationToken = default) + { + var key = await console.Input.ReadKeyAsync(true, cancellationToken); + if (key != null) + { + var character = key.Value.KeyChar; + if (char.IsLetter(character) || char.IsNumber(character)) + { + return character.ToString(); + } + } + + return string.Empty; + } } /// @@ -237,4 +265,18 @@ public static ConfirmationPrompt No(this ConfirmationPrompt obj, char character) obj.No = character; return obj; } + + /// + /// Sets whether or not the enter key is required after input. + /// + /// The confirmation prompt. + /// Whether or not the enter key is required after input. + /// The same instance so that multiple calls can be chained. + public static ConfirmationPrompt RequireEnter(this ConfirmationPrompt obj, bool require = true) + { + ArgumentNullException.ThrowIfNull(obj); + + obj.RequireEnter = require; + return obj; + } } \ No newline at end of file diff --git a/src/Spectre.Console/Prompts/TextPrompt.cs b/src/Spectre.Console/Prompts/TextPrompt.cs index bc6f90344..a085f066c 100644 --- a/src/Spectre.Console/Prompts/TextPrompt.cs +++ b/src/Spectre.Console/Prompts/TextPrompt.cs @@ -96,10 +96,8 @@ public sealed class TextPrompt : IPrompt, IHasCulture /// public Style? ChoicesStyle { get; set; } - /// - /// Gets or sets the default value. - /// internal DefaultPromptValue? DefaultValue { get; set; } + internal TextPromptInputHandler? InputHandler { get; set; } /// /// Initializes a new instance of the class. @@ -134,6 +132,7 @@ public async Task ShowAsync(IAnsiConsole console, CancellationToken cancellat var converter = Converter ?? TypeConverterHelper.ConvertToString; var choices = Choices.Select(choice => converter(choice)).ToList(); var choiceMap = Choices.ToDictionary(choice => converter(choice), choice => choice, _comparer); + var inputhandler = InputHandler ?? AnsiConsoleExtensions.ReadLine; WritePrompt(console); @@ -142,15 +141,17 @@ public async Task ShowAsync(IAnsiConsole console, CancellationToken cancellat string input; if (EditableDefaultValue && DefaultValue != null) { - input = await console.ReadLine(promptStyle, IsSecret, Mask, choices, cancellationToken, converter(DefaultValue.Value)).ConfigureAwait(false); + input = await inputhandler(console, promptStyle, IsSecret, Mask, choices, + converter(DefaultValue.Value), cancellationToken) + .ConfigureAwait(false); } else { - input = await console.ReadLine(promptStyle, IsSecret, Mask, choices, cancellationToken).ConfigureAwait(false); + input = await inputhandler(console, promptStyle, IsSecret, Mask, choices, + null, cancellationToken) + .ConfigureAwait(false); } - - // Nothing entered? if (string.IsNullOrWhiteSpace(input)) { @@ -187,7 +188,8 @@ public async Task ShowAsync(IAnsiConsole console, CancellationToken cancellat continue; } } - else if (!TypeConverterHelper.TryConvertFromStringWithCulture(input, Culture, out result) || result == null) + else if (!TypeConverterHelper.TryConvertFromStringWithCulture(input, Culture, out result) || + result == null) { console.MarkupLine(ValidationErrorMessage); WritePrompt(console); @@ -310,4 +312,4 @@ private bool ValidateResult(T value, [NotNullWhen(false)] out string? message) message = null; return true; } -} +} \ No newline at end of file diff --git a/src/Spectre.Console/Prompts/TextPromptExtensions.cs b/src/Spectre.Console/Prompts/TextPromptExtensions.cs index e0e9c4662..79d7ce6dc 100644 --- a/src/Spectre.Console/Prompts/TextPromptExtensions.cs +++ b/src/Spectre.Console/Prompts/TextPromptExtensions.cs @@ -333,4 +333,13 @@ public static TextPrompt ClearOnFinish(this TextPrompt obj, bool clear obj.ClearOnFinish = clear; return obj; } + + internal static TextPrompt UseInputHandler( + this TextPrompt obj, TextPromptInputHandler? inputHandler) + { + ArgumentNullException.ThrowIfNull(obj); + + obj.InputHandler = inputHandler; + return obj; + } } \ No newline at end of file diff --git a/src/Spectre.Console/Prompts/TextPromptInputHandler.cs b/src/Spectre.Console/Prompts/TextPromptInputHandler.cs new file mode 100644 index 000000000..df3b93af4 --- /dev/null +++ b/src/Spectre.Console/Prompts/TextPromptInputHandler.cs @@ -0,0 +1,7 @@ +namespace Spectre.Console; + +internal delegate Task TextPromptInputHandler( + IAnsiConsole console, Style? style, bool secret, + char? mask, IEnumerable? items = null, + string? initialInput = null, + CancellationToken cancellationToken = default); \ No newline at end of file From 9e11f2c5007f0628bb569badeaf1af67b07b92d8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 22:20:58 +0000 Subject: [PATCH 09/25] chore: Update dependency Microsoft.NET.Test.Sdk to 18.5.1 --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 22e70d623..c59e597cb 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -5,7 +5,7 @@ - + From ac45c72f44b1cfdee5620cb6a2c9fe4f77b0e38c Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Tue, 31 Mar 2026 14:42:42 +0200 Subject: [PATCH 10/25] Adds two new layout modes for FigletText Now supports fitting (kerning) as well as smushing (both universal and from font rules). Closes #1448 --- src/Spectre.Console.Tests/Data/banner.flf | 2204 +++++++++++++++++ .../Public_API.Output.verified.txt | 13 + .../Figlet/Render_Fitted.Output.verified.txt | 6 + .../Figlet/Render_Single.Output.verified.txt | 8 + .../Figlet/Render_Smushed.Output.verified.txt | 8 + ...al.Output_fontfile=poison.flf.verified.txt | 24 + ....Output_fontfile=starwars.flf.verified.txt | 14 + .../Spectre.Console.Tests.csproj | 2 + .../Unit/Widgets/FigletTests.cs | 117 + .../Extensions/Bcl/StringBuilderExtensions.cs | 21 +- .../Widgets/Figlet/FigletFont.cs | 71 +- .../Widgets/Figlet/FigletFontParser.cs | 12 +- .../Widgets/Figlet/FigletHeader.cs | 1 + .../Widgets/Figlet/FigletLayoutMode.cs | 25 + .../Widgets/Figlet/FigletText.cs | 361 ++- 15 files changed, 2846 insertions(+), 41 deletions(-) create mode 100644 src/Spectre.Console.Tests/Data/banner.flf create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Fitted.Output.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Single.Output.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Smushed.Output.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Smushed_Universal.Output_fontfile=poison.flf.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Smushed_Universal.Output_fontfile=starwars.flf.verified.txt create mode 100644 src/Spectre.Console/Widgets/Figlet/FigletLayoutMode.cs diff --git a/src/Spectre.Console.Tests/Data/banner.flf b/src/Spectre.Console.Tests/Data/banner.flf new file mode 100644 index 000000000..6ca2de96a --- /dev/null +++ b/src/Spectre.Console.Tests/Data/banner.flf @@ -0,0 +1,2204 @@ +flf2a$ 8 6 59 15 10 0 24463 153 +Big by Glenn Chappell 4/93 -- based on Standard +Includes ISO Latin-1 +Greek characters by Bruce Jakeway +figlet release 2.2 -- November 1996 +Permission is hereby given to modify this font, as long as the +modifier's name is placed on a comment line. + +Modified by Paul Burton 12/96 to include new parameter +supported by FIGlet and FIGWin. May also be slightly modified for better use +of new full-width/kern/smush alternatives, but default output is NOT changed. + $@ + $@ + $@ + $@ + $@ + $@ + $@ + $@@ + _ @ + | |@ + | |@ + | |@ + |_|@ + (_)@ + @ + @@ + _ _ @ + ( | )@ + V V @ + $ @ + $ @ + $ @ + @ + @@ + _ _ @ + _| || |_ @ + |_ __ _|@ + _| || |_ @ + |_ __ _|@ + |_||_| @ + @ + @@ + _ @ + | | @ + / __)@ + \__ \@ + ( /@ + |_| @ + @ + @@ + _ __@ + (_) / /@ + / / @ + / / @ + / / _ @ + /_/ (_)@ + @ + @@ + @ + ___ @ + ( _ ) @ + / _ \/\@ + | (_> <@ + \___/\/@ + @ + @@ + _ @ + ( )@ + |/ @ + $ @ + $ @ + $ @ + @ + @@ + __@ + / /@ + | | @ + | | @ + | | @ + | | @ + \_\@ + @@ + __ @ + \ \ @ + | |@ + | |@ + | |@ + | |@ + /_/ @ + @@ + _ @ + /\| |/\ @ + \ ` ' / @ + |_ _|@ + / , . \ @ + \/|_|\/ @ + @ + @@ + @ + _ @ + _| |_ @ + |_ _|@ + |_| @ + $ @ + @ + @@ + @ + @ + @ + @ + _ @ + ( )@ + |/ @ + @@ + @ + @ + ______ @ + |______|@ + $ @ + $ @ + @ + @@ + @ + @ + @ + @ + _ @ + (_)@ + @ + @@ + __@ + / /@ + / / @ + / / @ + / / @ + /_/ @ + @ + @@ + ___ @ + / _ \ @ + | | | |@ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ + __ @ + /_ |@ + | |@ + | |@ + | |@ + |_|@ + @ + @@ + ___ @ + |__ \ @ + $) |@ + / / @ + / /_ @ + |____|@ + @ + @@ + ____ @ + |___ \ @ + __) |@ + |__ < @ + ___) |@ + |____/ @ + @ + @@ + _ _ @ + | || | @ + | || |_ @ + |__ _|@ + | | @ + |_| @ + @ + @@ + _____ @ + | ____|@ + | |__ @ + |___ \ @ + ___) |@ + |____/ @ + @ + @@ + __ @ + / / @ + / /_ @ + | '_ \ @ + | (_) |@ + \___/ @ + @ + @@ + ______ @ + |____ |@ + $/ / @ + / / @ + / / @ + /_/ @ + @ + @@ + ___ @ + / _ \ @ + | (_) |@ + > _ < @ + | (_) |@ + \___/ @ + @ + @@ + ___ @ + / _ \ @ + | (_) |@ + \__, |@ + / / @ + /_/ @ + @ + @@ + @ + _ @ + (_)@ + $ @ + _ @ + (_)@ + @ + @@ + @ + _ @ + (_)@ + $ @ + _ @ + ( )@ + |/ @ + @@ + __@ + / /@ + / / @ + < < @ + \ \ @ + \_\@ + @ + @@ + @ + ______ @ + |______|@ + ______ @ + |______|@ + @ + @ + @@ + __ @ + \ \ @ + \ \ @ + > >@ + / / @ + /_/ @ + @ + @@ + ___ @ + |__ \ @ + ) |@ + / / @ + |_| @ + (_) @ + @ + @@ + @ + ____ @ + / __ \ @ + / / _` |@ + | | (_| |@ + \ \__,_|@ + \____/ @ + @@ + @ + /\ @ + / \ @ + / /\ \ @ + / ____ \ @ + /_/ \_\@ + @ + @@ + ____ @ + | _ \ @ + | |_) |@ + | _ < @ + | |_) |@ + |____/ @ + @ + @@ + _____ @ + / ____|@ + | | $ @ + | | $ @ + | |____ @ + \_____|@ + @ + @@ + _____ @ + | __ \ @ + | | | |@ + | | | |@ + | |__| |@ + |_____/ @ + @ + @@ + ______ @ + | ____|@ + | |__ @ + | __| @ + | |____ @ + |______|@ + @ + @@ + ______ @ + | ____|@ + | |__ @ + | __| @ + | | @ + |_| @ + @ + @@ + _____ @ + / ____|@ + | | __ @ + | | |_ |@ + | |__| |@ + \_____|@ + @ + @@ + _ _ @ + | | | |@ + | |__| |@ + | __ |@ + | | | |@ + |_| |_|@ + @ + @@ + _____ @ + |_ _|@ + | | @ + | | @ + _| |_ @ + |_____|@ + @ + @@ + _ @ + | |@ + | |@ + _ | |@ + | |__| |@ + \____/ @ + @ + @@ + _ __@ + | |/ /@ + | ' / @ + | < @ + | . \ @ + |_|\_\@ + @ + @@ + _ @ + | | @ + | | @ + | | @ + | |____ @ + |______|@ + @ + @@ + __ __ @ + | \/ |@ + | \ / |@ + | |\/| |@ + | | | |@ + |_| |_|@ + @ + @@ + _ _ @ + | \ | |@ + | \| |@ + | . ` |@ + | |\ |@ + |_| \_|@ + @ + @@ + ____ @ + / __ \ @ + | | | |@ + | | | |@ + | |__| |@ + \____/ @ + @ + @@ + _____ @ + | __ \ @ + | |__) |@ + | ___/ @ + | | @ + |_| @ + @ + @@ + ____ @ + / __ \ @ + | | | |@ + | | | |@ + | |__| |@ + \___\_\@ + @ + @@ + _____ @ + | __ \ @ + | |__) |@ + | _ / @ + | | \ \ @ + |_| \_\@ + @ + @@ + _____ @ + / ____|@ + | (___ @ + \___ \ @ + ____) |@ + |_____/ @ + @ + @@ + _______ @ + |__ __|@ + | | @ + | | @ + | | @ + |_| @ + @ + @@ + _ _ @ + | | | |@ + | | | |@ + | | | |@ + | |__| |@ + \____/ @ + @ + @@ + __ __@ + \ \ / /@ + \ \ / / @ + \ \/ / @ + \ / @ + \/ @ + @ + @@ + __ __@ + \ \ / /@ + \ \ /\ / / @ + \ \/ \/ / @ + \ /\ / @ + \/ \/ @ + @ + @@ + __ __@ + \ \ / /@ + \ V / @ + > < @ + / . \ @ + /_/ \_\@ + @ + @@ + __ __@ + \ \ / /@ + \ \_/ / @ + \ / @ + | | @ + |_| @ + @ + @@ + ______@ + |___ /@ + $/ / @ + / / @ + / /__ @ + /_____|@ + @ + @@ + ___ @ + | _|@ + | | @ + | | @ + | | @ + | |_ @ + |___|@ + @@ + __ @ + \ \ @ + \ \ @ + \ \ @ + \ \ @ + \_\@ + @ + @@ + ___ @ + |_ |@ + | |@ + | |@ + | |@ + _| |@ + |___|@ + @@ + /\ @ + |/\|@ + $ @ + $ @ + $ @ + $ @ + @ + @@ + @ + @ + @ + @ + @ + $ @ + ______ @ + |______|@@ + _ @ + ( )@ + \|@ + $ @ + $ @ + $ @ + @ + @@ + @ + @ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ + _ @ + | | @ + | |__ @ + | '_ \ @ + | |_) |@ + |_.__/ @ + @ + @@ + @ + @ + ___ @ + / __|@ + | (__ @ + \___|@ + @ + @@ + _ @ + | |@ + __| |@ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ + @ + @ + ___ @ + / _ \@ + | __/@ + \___|@ + @ + @@ + __ @ + / _|@ + | |_ @ + | _|@ + | | @ + |_| @ + @ + @@ + @ + @ + __ _ @ + / _` |@ + | (_| |@ + \__, |@ + __/ |@ + |___/ @@ + _ @ + | | @ + | |__ @ + | '_ \ @ + | | | |@ + |_| |_|@ + @ + @@ + _ @ + (_)@ + _ @ + | |@ + | |@ + |_|@ + @ + @@ + _ @ + (_)@ + _ @ + | |@ + | |@ + | |@ + _/ |@ + |__/ @@ + _ @ + | | @ + | | __@ + | |/ /@ + | < @ + |_|\_\@ + @ + @@ + _ @ + | |@ + | |@ + | |@ + | |@ + |_|@ + @ + @@ + @ + @ + _ __ ___ @ + | '_ ` _ \ @ + | | | | | |@ + |_| |_| |_|@ + @ + @@ + @ + @ + _ __ @ + | '_ \ @ + | | | |@ + |_| |_|@ + @ + @@ + @ + @ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @ + @@ + @ + @ + _ __ @ + | '_ \ @ + | |_) |@ + | .__/ @ + | | @ + |_| @@ + @ + @ + __ _ @ + / _` |@ + | (_| |@ + \__, |@ + | |@ + |_|@@ + @ + @ + _ __ @ + | '__|@ + | | @ + |_| @ + @ + @@ + @ + @ + ___ @ + / __|@ + \__ \@ + |___/@ + @ + @@ + _ @ + | | @ + | |_ @ + | __|@ + | |_ @ + \__|@ + @ + @@ + @ + @ + _ _ @ + | | | |@ + | |_| |@ + \__,_|@ + @ + @@ + @ + @ + __ __@ + \ \ / /@ + \ V / @ + \_/ @ + @ + @@ + @ + @ + __ __@ + \ \ /\ / /@ + \ V V / @ + \_/\_/ @ + @ + @@ + @ + @ + __ __@ + \ \/ /@ + > < @ + /_/\_\@ + @ + @@ + @ + @ + _ _ @ + | | | |@ + | |_| |@ + \__, |@ + __/ |@ + |___/ @@ + @ + @ + ____@ + |_ /@ + / / @ + /___|@ + @ + @@ + __@ + / /@ + | | @ + / / @ + \ \ @ + | | @ + \_\@ + @@ + _ @ + | |@ + | |@ + | |@ + | |@ + | |@ + | |@ + |_|@@ + __ @ + \ \ @ + | | @ + \ \@ + / /@ + | | @ + /_/ @ + @@ + /\/|@ + |/\/ @ + $ @ + $ @ + $ @ + $ @ + @ + @@ + _ _ @ + (_)_(_) @ + / \ @ + / _ \ @ + / ___ \ @ + /_/ \_\@ + @ + @@ + _ _ @ + (_)_(_)@ + / _ \ @ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ + _ _ @ + (_) (_)@ + | | | |@ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ + _ _ @ + (_) (_)@ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ + _ _ @ + (_) (_)@ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @ + @@ + _ _ @ + (_) (_)@ + _ _ @ + | | | |@ + | |_| |@ + \__,_|@ + @ + @@ + ___ @ + / _ \ @ + | | ) |@ + | |< < @ + | | ) |@ + | ||_/ @ + |_| @ + @@ +160 NO-BREAK SPACE + $@ + $@ + $@ + $@ + $@ + $@ + $@ + $@@ +161 INVERTED EXCLAMATION MARK + _ @ + (_)@ + | |@ + | |@ + | |@ + |_|@ + @ + @@ +162 CENT SIGN + @ + _ @ + | | @ + / __)@ + | (__ @ + \ )@ + |_| @ + @@ +163 POUND SIGN + ___ @ + / ,_\ @ + _| |_ @ + |__ __| @ + | |____ @ + (_,_____|@ + @ + @@ +164 CURRENCY SIGN + @ + /\___/\@ + \ _ /@ + | (_) |@ + / ___ \@ + \/ \/@ + @ + @@ +165 YEN SIGN + __ __ @ + \ \ / / @ + _\ V /_ @ + |___ ___|@ + |___ ___|@ + |_| @ + @ + @@ +166 BROKEN BAR + _ @ + | |@ + | |@ + |_|@ + _ @ + | |@ + | |@ + |_|@@ +167 SECTION SIGN + __ @ + _/ _)@ + / \ \ @ + \ \\ \@ + \ \_/@ + (__/ @ + @ + @@ +168 DIAERESIS + _ _ @ + (_) (_)@ + $ $ @ + $ $ @ + $ $ @ + $ $ @ + @ + @@ +169 COPYRIGHT SIGN + ________ @ + / ____ \ @ + / / ___| \ @ + | | | |@ + | | |___ |@ + \ \____| / @ + \________/ @ + @@ +170 FEMININE ORDINAL INDICATOR + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + |_____|@ + $ @ + @ + @@ +171 LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + ____@ + / / /@ + / / / @ + < < < @ + \ \ \ @ + \_\_\@ + @ + @@ +172 NOT SIGN + @ + @ + ______ @ + |____ |@ + |_|@ + $ @ + @ + @@ +173 SOFT HYPHEN + @ + @ + _____ @ + |_____|@ + $ @ + $ @ + @ + @@ +174 REGISTERED SIGN + ________ @ + / ____ \ @ + / | _ \ \ @ + | | |_) | |@ + | | _ < |@ + \ |_| \_\ / @ + \________/ @ + @@ +175 MACRON + ______ @ + |______|@ + $ @ + $ @ + $ @ + $ @ + @ + @@ +176 DEGREE SIGN + __ @ + / \ @ + | () |@ + \__/ @ + $ @ + $ @ + @ + @@ +177 PLUS-MINUS SIGN + _ @ + _| |_ @ + |_ _|@ + |_| @ + _____ @ + |_____|@ + @ + @@ +178 SUPERSCRIPT TWO + ___ @ + |_ )@ + / / @ + /___|@ + $ @ + $ @ + @ + @@ +179 SUPERSCRIPT THREE + ____@ + |__ /@ + |_ \@ + |___/@ + $ @ + $ @ + @ + @@ +180 ACUTE ACCENT + __@ + /_/@ + $ @ + $ @ + $ @ + $ @ + @ + @@ +181 MICRO SIGN + @ + @ + _ _ @ + | | | |@ + | |_| |@ + | ._,_|@ + | | @ + |_| @@ +182 PILCROW SIGN + ______ @ + / |@ + | (| || |@ + \__ || |@ + | || |@ + |_||_|@ + @ + @@ +183 MIDDLE DOT + @ + @ + _ @ + (_)@ + $ @ + $ @ + @ + @@ +184 CEDILLA + @ + @ + @ + @ + @ + _ @ + )_)@ + @@ +185 SUPERSCRIPT ONE + _ @ + / |@ + | |@ + |_|@ + $ @ + $ @ + @ + @@ +186 MASCULINE ORDINAL INDICATOR + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + |_____|@ + $ @ + @ + @@ +187 RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + ____ @ + \ \ \ @ + \ \ \ @ + > > >@ + / / / @ + /_/_/ @ + @ + @@ +188 VULGAR FRACTION ONE QUARTER + _ __ @ + / | / / @ + | |/ / _ @ + |_/ / | | @ + / /|_ _|@ + /_/ |_| @ + @ + @@ +189 VULGAR FRACTION ONE HALF + _ __ @ + / | / / @ + | |/ /__ @ + |_/ /_ )@ + / / / / @ + /_/ /___|@ + @ + @@ +190 VULGAR FRACTION THREE QUARTERS + ____ __ @ + |__ / / / @ + |_ \/ / _ @ + |___/ / | | @ + / /|_ _|@ + /_/ |_| @ + @ + @@ +191 INVERTED QUESTION MARK + _ @ + (_) @ + | | @ + / / @ + | (__ @ + \___|@ + @ + @@ +192 LATIN CAPITAL LETTER A WITH GRAVE + __ @ + \_\ @ + / \ @ + / _ \ @ + / ___ \ @ + /_/ \_\@ + @ + @@ +193 LATIN CAPITAL LETTER A WITH ACUTE + __ @ + /_/ @ + / \ @ + / _ \ @ + / ___ \ @ + /_/ \_\@ + @ + @@ +194 LATIN CAPITAL LETTER A WITH CIRCUMFLEX + //\ @ + |/_\| @ + / \ @ + / _ \ @ + / ___ \ @ + /_/ \_\@ + @ + @@ +195 LATIN CAPITAL LETTER A WITH TILDE + /\/| @ + |/\/ @ + / \ @ + / _ \ @ + / ___ \ @ + /_/ \_\@ + @ + @@ +196 LATIN CAPITAL LETTER A WITH DIAERESIS + _ _ @ + (_)_(_) @ + / \ @ + / _ \ @ + / ___ \ @ + /_/ \_\@ + @ + @@ +197 LATIN CAPITAL LETTER A WITH RING ABOVE + _ @ + (o) @ + / \ @ + / _ \ @ + / ___ \ @ + /_/ \_\@ + @ + @@ +198 LATIN CAPITAL LETTER AE + _______ @ + / ____|@ + / |__ @ + / /| __| @ + / ___ |____ @ + /_/ |______|@ + @ + @@ +199 LATIN CAPITAL LETTER C WITH CEDILLA + _____ @ + / ____|@ + | | $ @ + | | $ @ + | |____ @ + \_____|@ + )_) @ + @@ +200 LATIN CAPITAL LETTER E WITH GRAVE + __ @ + _\_\_ @ + | ____|@ + | _| @ + | |___ @ + |_____|@ + @ + @@ +201 LATIN CAPITAL LETTER E WITH ACUTE + __ @ + _/_/_ @ + | ____|@ + | _| @ + | |___ @ + |_____|@ + @ + @@ +202 LATIN CAPITAL LETTER E WITH CIRCUMFLEX + //\ @ + |/ \| @ + | ____|@ + | _| @ + | |___ @ + |_____|@ + @ + @@ +203 LATIN CAPITAL LETTER E WITH DIAERESIS + _ _ @ + (_) (_)@ + | ____|@ + | _| @ + | |___ @ + |_____|@ + @ + @@ +204 LATIN CAPITAL LETTER I WITH GRAVE + __ @ + \_\ @ + |_ _|@ + | | @ + | | @ + |___|@ + @ + @@ +205 LATIN CAPITAL LETTER I WITH ACUTE + __ @ + /_/ @ + |_ _|@ + | | @ + | | @ + |___|@ + @ + @@ +206 LATIN CAPITAL LETTER I WITH CIRCUMFLEX + //\ @ + |/_\|@ + |_ _|@ + | | @ + | | @ + |___|@ + @ + @@ +207 LATIN CAPITAL LETTER I WITH DIAERESIS + _ _ @ + (_)_(_)@ + |_ _| @ + | | @ + | | @ + |___| @ + @ + @@ +208 LATIN CAPITAL LETTER ETH + _____ @ + | __ \ @ + _| |_ | |@ + |__ __|| |@ + | |__| |@ + |_____/ @ + @ + @@ +209 LATIN CAPITAL LETTER N WITH TILDE + /\/| @ + |/\/_ @ + | \ | |@ + | \| |@ + | |\ |@ + |_| \_|@ + @ + @@ +210 LATIN CAPITAL LETTER O WITH GRAVE + __ @ + \_\ @ + / _ \ @ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +211 LATIN CAPITAL LETTER O WITH ACUTE + __ @ + /_/ @ + / _ \ @ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +212 LATIN CAPITAL LETTER O WITH CIRCUMFLEX + //\ @ + |/_\| @ + / _ \ @ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +213 LATIN CAPITAL LETTER O WITH TILDE + /\/| @ + |/\/ @ + / _ \ @ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +214 LATIN CAPITAL LETTER O WITH DIAERESIS + _ _ @ + (_)_(_)@ + / _ \ @ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +215 MULTIPLICATION SIGN + @ + @ + /\/\@ + > <@ + \/\/@ + $ @ + @ + @@ +216 LATIN CAPITAL LETTER O WITH STROKE + _____ @ + / __// @ + | | // |@ + | |//| |@ + | //_| |@ + //___/ @ + @ + @@ +217 LATIN CAPITAL LETTER U WITH GRAVE + __ @ + _\_\_ @ + | | | |@ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +218 LATIN CAPITAL LETTER U WITH ACUTE + __ @ + _/_/_ @ + | | | |@ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +219 LATIN CAPITAL LETTER U WITH CIRCUMFLEX + //\ @ + |/ \| @ + | | | |@ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +220 LATIN CAPITAL LETTER U WITH DIAERESIS + _ _ @ + (_) (_)@ + | | | |@ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +221 LATIN CAPITAL LETTER Y WITH ACUTE + __ @ + __/_/__@ + \ \ / /@ + \ V / @ + | | @ + |_| @ + @ + @@ +222 LATIN CAPITAL LETTER THORN + _ @ + | |___ @ + | __ \ @ + | |__) |@ + | ___/ @ + |_| @ + @ + @@ +223 LATIN SMALL LETTER SHARP S + ___ @ + / _ \ @ + | | ) |@ + | |< < @ + | | ) |@ + | ||_/ @ + |_| @ + @@ +224 LATIN SMALL LETTER A WITH GRAVE + __ @ + \_\ @ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ +225 LATIN SMALL LETTER A WITH ACUTE + __ @ + /_/ @ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ +226 LATIN SMALL LETTER A WITH CIRCUMFLEX + //\ @ + |/ \| @ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ +227 LATIN SMALL LETTER A WITH TILDE + /\/| @ + |/\/ @ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ +228 LATIN SMALL LETTER A WITH DIAERESIS + _ _ @ + (_) (_)@ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ +229 LATIN SMALL LETTER A WITH RING ABOVE + __ @ + (()) @ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @ + @@ +230 LATIN SMALL LETTER AE + @ + @ + __ ____ @ + / _` _ \@ + | (_| __/@ + \__,____|@ + @ + @@ +231 LATIN SMALL LETTER C WITH CEDILLA + @ + @ + ___ @ + / __|@ + | (__ @ + \___|@ + )_) @ + @@ +232 LATIN SMALL LETTER E WITH GRAVE + __ @ + \_\ @ + ___ @ + / _ \@ + | __/@ + \___|@ + @ + @@ +233 LATIN SMALL LETTER E WITH ACUTE + __ @ + /_/ @ + ___ @ + / _ \@ + | __/@ + \___|@ + @ + @@ +234 LATIN SMALL LETTER E WITH CIRCUMFLEX + //\ @ + |/ \|@ + ___ @ + / _ \@ + | __/@ + \___|@ + @ + @@ +235 LATIN SMALL LETTER E WITH DIAERESIS + _ _ @ + (_) (_)@ + ___ @ + / _ \ @ + | __/ @ + \___| @ + @ + @@ +236 LATIN SMALL LETTER I WITH GRAVE + __ @ + \_\@ + _ @ + | |@ + | |@ + |_|@ + @ + @@ +237 LATIN SMALL LETTER I WITH ACUTE + __@ + /_/@ + _ @ + | |@ + | |@ + |_|@ + @ + @@ +238 LATIN SMALL LETTER I WITH CIRCUMFLEX + //\ @ + |/ \|@ + _ @ + | | @ + | | @ + |_| @ + @ + @@ +239 LATIN SMALL LETTER I WITH DIAERESIS + _ _ @ + (_) (_)@ + _ @ + | | @ + | | @ + |_| @ + @ + @@ +240 LATIN SMALL LETTER ETH + /\/\ @ + > < @ + \/\ \ @ + / _` |@ + | (_) |@ + \___/ @ + @ + @@ +241 LATIN SMALL LETTER N WITH TILDE + /\/| @ + |/\/ @ + _ __ @ + | '_ \ @ + | | | |@ + |_| |_|@ + @ + @@ +242 LATIN SMALL LETTER O WITH GRAVE + __ @ + \_\ @ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @ + @@ +243 LATIN SMALL LETTER O WITH ACUTE + __ @ + /_/ @ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @ + @@ +244 LATIN SMALL LETTER O WITH CIRCUMFLEX + //\ @ + |/ \| @ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @ + @@ +245 LATIN SMALL LETTER O WITH TILDE + /\/| @ + |/\/ @ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @ + @@ +246 LATIN SMALL LETTER O WITH DIAERESIS + _ _ @ + (_) (_)@ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @ + @@ +247 DIVISION SIGN + _ @ + (_) @ + _______ @ + |_______|@ + _ @ + (_) @ + @ + @@ +248 LATIN SMALL LETTER O WITH STROKE + @ + @ + ____ @ + / _//\ @ + | (//) |@ + \//__/ @ + @ + @@ +249 LATIN SMALL LETTER U WITH GRAVE + __ @ + \_\ @ + _ _ @ + | | | |@ + | |_| |@ + \__,_|@ + @ + @@ +250 LATIN SMALL LETTER U WITH ACUTE + __ @ + /_/ @ + _ _ @ + | | | |@ + | |_| |@ + \__,_|@ + @ + @@ +251 LATIN SMALL LETTER U WITH CIRCUMFLEX + //\ @ + |/ \| @ + _ _ @ + | | | |@ + | |_| |@ + \__,_|@ + @ + @@ +252 LATIN SMALL LETTER U WITH DIAERESIS + _ _ @ + (_) (_)@ + _ _ @ + | | | |@ + | |_| |@ + \__,_|@ + @ + @@ +253 LATIN SMALL LETTER Y WITH ACUTE + __ @ + /_/ @ + _ _ @ + | | | |@ + | |_| |@ + \__, |@ + __/ |@ + |___/ @@ +254 LATIN SMALL LETTER THORN + _ @ + | | @ + | |__ @ + | '_ \ @ + | |_) |@ + | .__/ @ + | | @ + |_| @@ +255 LATIN SMALL LETTER Y WITH DIAERESIS + _ _ @ + (_) (_)@ + _ _ @ + | | | |@ + | |_| |@ + \__, |@ + __/ |@ + |___/ @@ +0x02BC MODIFIER LETTER APOSTROPHE + @ + @ + ))@ + @ + @ + @ + @ + @@ +0x02BD MODIFIER LETTER REVERSED COMMA + @ + @ + ((@ + @ + @ + @ + @ + @@ +0x037A GREEK YPOGEGRAMMENI + @ + @ + @ + @ + @ + @ + @ + ||@@ +0x0387 GREEK ANO TELEIA + @ + $ @ + _ @ + (_)@ + @ + $ @ + @ + @@ +0x0391 GREEK CAPITAL LETTER ALPHA + ___ @ + / _ \ @ + | |_| |@ + | _ |@ + | | | |@ + |_| |_|@ + @ + @@ +0x0392 GREEK CAPITAL LETTER BETA + ____ @ + | _ \ @ + | |_) )@ + | _ ( @ + | |_) )@ + |____/ @ + @ + @@ +0x0393 GREEK CAPITAL LETTER GAMMA + _____ @ + | ___)@ + | |$ @ + | |$ @ + | | @ + |_| @ + @ + @@ +0x0394 GREEK CAPITAL LETTER DELTA + @ + /\ @ + / \ @ + / /\ \ @ + / /__\ \ @ + /________\@ + @ + @@ +0x0395 GREEK CAPITAL LETTER EPSILON + _____ @ + | ___)@ + | |_ @ + | _) @ + | |___ @ + |_____)@ + @ + @@ +0x0396 GREEK CAPITAL LETTER ZETA + ______@ + (___ /@ + / / @ + / / @ + / /__ @ + /_____)@ + @ + @@ +0x0397 GREEK CAPITAL LETTER ETA + _ _ @ + | | | |@ + | |_| |@ + | _ |@ + | | | |@ + |_| |_|@ + @ + @@ +0x0398 GREEK CAPITAL LETTER THETA + ____ @ + / __ \ @ + | |__| |@ + | __ |@ + | |__| |@ + \____/ @ + @ + @@ +0x0399 GREEK CAPITAL LETTER IOTA + ___ @ + ( )@ + | | @ + | | @ + | | @ + (___)@ + @ + @@ +0x039A GREEK CAPITAL LETTER KAPPA + _ __@ + | | / /@ + | |/ / @ + | < @ + | |\ \ @ + |_| \_\@ + @ + @@ +0x039B GREEK CAPITAL LETTER LAMDA + @ + /\ @ + / \ @ + / /\ \ @ + / / \ \ @ + /_/ \_\@ + @ + @@ +0x039C GREEK CAPITAL LETTER MU + __ __ @ + | \ / |@ + | v |@ + | |\_/| |@ + | | | |@ + |_| |_|@ + @ + @@ +0x039D GREEK CAPITAL LETTER NU + _ _ @ + | \ | |@ + | \| |@ + | |@ + | |\ |@ + |_| \_|@ + @ + @@ +0x039E GREEK CAPITAL LETTER XI + _____ @ + (_____)@ + ___ @ + (___) @ + _____ @ + (_____)@ + @ + @@ +0x039F GREEK CAPITAL LETTER OMICRON + ___ @ + / _ \ @ + | | | |@ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +0x03A0 GREEK CAPITAL LETTER PI + _______ @ + ( _ )@ + | | | | @ + | | | | @ + | | | | @ + |_| |_| @ + @ + @@ +0x03A1 GREEK CAPITAL LETTER RHO + ____ @ + | _ \ @ + | |_) )@ + | __/ @ + | | @ + |_| @ + @ + @@ +0x03A3 GREEK CAPITAL LETTER SIGMA + ______ @ + \ ___)@ + \ \ @ + > > @ + / /__ @ + /_____)@ + @ + @@ +0x03A4 GREEK CAPITAL LETTER TAU + _____ @ + (_ _)@ + | | @ + | | @ + | | @ + |_| @ + @ + @@ +0x03A5 GREEK CAPITAL LETTER UPSILON + __ __ @ + (_ \ / _)@ + \ v / @ + | | @ + | | @ + |_| @ + @ + @@ +0x03A6 GREEK CAPITAL LETTER PHI + _ @ + _| |_ @ + / \ @ + ( (| |) )@ + \_ _/ @ + |_| @ + @ + @@ +0x03A7 GREEK CAPITAL LETTER CHI + __ __@ + \ \ / /@ + \ v / @ + > < @ + / ^ \ @ + /_/ \_\@ + @ + @@ +0x03A8 GREEK CAPITAL LETTER PSI + _ _ _ @ + | || || |@ + | \| |/ |@ + \_ _/ @ + | | @ + |_| @ + @ + @@ +0x03A9 GREEK CAPITAL LETTER OMEGA + ____ @ + / __ \ @ + | | | | @ + | | | | @ + _\ \/ /_ @ + (___||___)@ + @ + @@ +0x03B1 GREEK SMALL LETTER ALPHA + @ + @ + __ __@ + / \/ /@ + ( () < @ + \__/\_\@ + @ + @@ +0x03B2 GREEK SMALL LETTER BETA + ___ @ + / _ \ @ + | |_) )@ + | _ < @ + | |_) )@ + | __/ @ + | | @ + |_| @@ +0x03B3 GREEK SMALL LETTER GAMMA + @ + @ + _ _ @ + ( \ / )@ + \ v / @ + | | @ + | | @ + |_| @@ +0x03B4 GREEK SMALL LETTER DELTA + __ @ + / _) @ + \ \ @ + / _ \ @ + ( (_) )@ + \___/ @ + @ + @@ +0x03B5 GREEK SMALL LETTER EPSILON + @ + @ + ___ @ + / __)@ + > _) @ + \___)@ + @ + @@ +0x03B6 GREEK SMALL LETTER ZETA + _____ @ + \__ ) @ + / / @ + / / @ + | |__ @ + \__ \ @ + ) )@ + (_/ @@ +0x03B7 GREEK SMALL LETTER ETA + @ + @ + _ __ @ + | '_ \ @ + | | | |@ + |_| | |@ + | |@ + |_|@@ +0x03B8 GREEK SMALL LETTER THETA + ___ @ + / _ \ @ + | |_| |@ + | _ |@ + | |_| |@ + \___/ @ + @ + @@ +0x03B9 GREEK SMALL LETTER IOTA + @ + @ + _ @ + | | @ + | | @ + \_)@ + @ + @@ +0x03BA GREEK SMALL LETTER KAPPA + @ + @ + _ __@ + | |/ /@ + | < @ + |_|\_\@ + @ + @@ +0x03BB GREEK SMALL LETTER LAMDA + __ @ + \ \ @ + \ \ @ + > \ @ + / ^ \ @ + /_/ \_\@ + @ + @@ +0x03BC GREEK SMALL LETTER MU + @ + @ + _ _ @ + | | | |@ + | |_| |@ + | ._,_|@ + | | @ + |_| @@ +0x03BD GREEK SMALL LETTER NU + @ + @ + _ __@ + | |/ /@ + | / / @ + |__/ @ + @ + @@ +0x03BE GREEK SMALL LETTER XI + \=\__ @ + > __) @ + ( (_ @ + > _) @ + ( (__ @ + \__ \ @ + ) )@ + (_/ @@ +0x03BF GREEK SMALL LETTER OMICRON + @ + @ + ___ @ + / _ \ @ + ( (_) )@ + \___/ @ + @ + @@ +0x03C0 GREEK SMALL LETTER PI + @ + @ + ______ @ + ( __ )@ + | || | @ + |_||_| @ + @ + @@ +0x03C1 GREEK SMALL LETTER RHO + @ + @ + ___ @ + / _ \ @ + | |_) )@ + | __/ @ + | | @ + |_| @@ +0x03C2 GREEK SMALL LETTER FINAL SIGMA + @ + @ + ____ @ + / ___)@ + ( (__ @ + \__ \ @ + _) )@ + (__/ @@ +0x03C3 GREEK SMALL LETTER SIGMA + @ + @ + ____ @ + / ._)@ + ( () ) @ + \__/ @ + @ + @@ +0x03C4 GREEK SMALL LETTER TAU + @ + @ + ___ @ + ( )@ + | | @ + \_)@ + @ + @@ +0x03C5 GREEK SMALL LETTER UPSILON + @ + @ + _ _ @ + | | | |@ + | |_| |@ + \___/ @ + @ + @@ +0x03C6 GREEK SMALL LETTER PHI + _ @ + | | @ + _| |_ @ + / \ @ + ( (| |) )@ + \_ _/ @ + | | @ + |_| @@ +0x03C7 GREEK SMALL LETTER CHI + @ + @ + __ __@ + \ \ / /@ + \ v / @ + > < @ + / ^ \ @ + /_/ \_\@@ +0x03C8 GREEK SMALL LETTER PSI + @ + @ + _ _ _ @ + | || || |@ + | \| |/ |@ + \_ _/ @ + | | @ + |_| @@ +0x03C9 GREEK SMALL LETTER OMEGA + @ + @ + __ __ @ + / / _ \ \ @ + | |_/ \_| |@ + \___^___/ @ + @ + @@ +0x03D1 GREEK THETA SYMBOL + ___ @ + / _ \ @ + ( (_| |_ @ + _ \ _ _)@ + | |___| | @ + \_____/ @ + @ + @@ +0x03D5 GREEK PHI SYMBOL + @ + @ + _ __ @ + | | / \ @ + | || || )@ + \_ _/ @ + | | @ + |_| @@ +0x03D6 GREEK PI SYMBOL + @ + @ + _________ @ + ( _____ )@ + | |_/ \_| |@ + \___^___/ @ + @ + @@ +-0x0005 +alpha = a, beta = b, gamma = g, delta = d, epsilon = e @ +zeta = z, eta = h, theta = q, iota = i, lamda = l, mu = m@ +nu = n, xi = x, omicron = o, pi = p, rho = r, sigma = s @ +phi = f, chi = c, psi = y, omega = w, final sigma = V @ + pi symbol = v, theta symbol = J, phi symbol = j @ + middle dot = :, ypogegrammeni = _ @ + rough breathing = (, smooth breathing = ) @ + acute accent = ', grave accent = `, dialytika = ^ @@ diff --git a/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt index 30a28a284..98944f91e 100644 --- a/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt +++ b/src/Spectre.Console.Tests/Expectations/Public_API.Output.verified.txt @@ -1944,25 +1944,38 @@ { public int Baseline { get; } public int Count { get; } + public char Hardblank { get; } public int Height { get; } public int MaxWidth { get; } + public int SmushingRules { get; } public static Spectre.Console.FigletFont Default { get; } public static Spectre.Console.FigletFont Load(System.IO.Stream stream) { } public static Spectre.Console.FigletFont Load(string path) { } public static Spectre.Console.FigletFont Parse(string source) { } } + public enum FigletLayoutMode + { + FullSize = 0, + Fitted = 1, + Smushed = 2, + } public sealed class FigletText : Spectre.Console.Rendering.Renderable, Spectre.Console.IHasJustification { public FigletText(string text) { } public FigletText(Spectre.Console.FigletFont font, string text) { } public Spectre.Console.Color? Color { get; set; } public Spectre.Console.Justify? Justification { get; set; } + public Spectre.Console.FigletLayoutMode LayoutMode { get; set; } public bool Pad { get; set; } protected override System.Collections.Generic.IEnumerable Render(Spectre.Console.Rendering.RenderOptions options, int maxWidth) { } } public static class FigletTextExtensions { public static Spectre.Console.FigletText Color(this Spectre.Console.FigletText text, Spectre.Console.Color? color) { } + public static Spectre.Console.FigletText Fitted(this Spectre.Console.FigletText text) { } + public static Spectre.Console.FigletText FullSize(this Spectre.Console.FigletText text) { } + public static Spectre.Console.FigletText LayoutMode(this Spectre.Console.FigletText text, Spectre.Console.FigletLayoutMode mode) { } + public static Spectre.Console.FigletText Smushed(this Spectre.Console.FigletText text) { } } public enum FileSizeBase { diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Fitted.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Fitted.Output.verified.txt new file mode 100644 index 000000000..773f99180 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Fitted.Output.verified.txt @@ -0,0 +1,6 @@ + ____ _ ____ _ + / ___| _ __ ___ ___ | |_ _ __ ___ / ___| ___ _ __ ___ ___ | | ___ + \___ \ | '_ \ / _ \ / __|| __|| '__|/ _ \ | | / _ \ | '_ \ / __| / _ \ | | / _ \ + ___) || |_) || __/| (__ | |_ | | | __/ _| |___| (_) || | | |\__ \| (_) || || __/ + |____/ | .__/ \___| \___| \__||_| \___|(_)\____|\___/ |_| |_||___/ \___/ |_| \___| + |_| diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Single.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Single.Output.verified.txt new file mode 100644 index 000000000..133678028 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Single.Output.verified.txt @@ -0,0 +1,8 @@ + _____ + | __ \ + | |__) | + | ___/ + | | + |_| + + diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Smushed.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Smushed.Output.verified.txt new file mode 100644 index 000000000..4511c352c --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Smushed.Output.verified.txt @@ -0,0 +1,8 @@ + _____ _ _____ _ + / ____| | | / ____| | | + | (___ _ __ ___ ___| |_ _ __ ___ | | ___ _ __ ___ ___ | | ___ + \___ \| '_ \ / _ \/ __| __| '__/ _ | | / _ \| '_ \/ __|/ _ \| |/ _ \ + ____) | |_) | __/ (__| |_| | | __/| |___| (_) | | | \__ \ (_) | | __/ + |_____/| .__/ \___|\___|\__|_| \___(_)_____\___/|_| |_|___/\___/|_|\___| + | | + |_| diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Smushed_Universal.Output_fontfile=poison.flf.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Smushed_Universal.Output_fontfile=poison.flf.verified.txt new file mode 100644 index 000000000..0547c5e86 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Smushed_Universal.Output_fontfile=poison.flf.verified.txt @@ -0,0 +1,24 @@ + + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@ @@@@@@@@@ +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@ @@@@@@@@@ +!@@ @@! @@@@! !@@ @@! @@! @@@@! !@@ @@! @@@@!@!@@!@@ +!@! !@! @!!@! !@! !@! !@! @!!@! !@! !@! @!!@!!@!@!@! +!!@@!! @!@@!@!@!!!:! !@! @!! @!@!!@!@!!!:! !@! @!@ !@@!@ !!@!!@@!! + !!@!!!!!@!!! !!!!!: !!! !!! !!@!@! !!!!!: !!! !@! !!!@! !!!!!@!!! + !:!!: !!: :!! !!: !!: :!!!!: :!! !!: !!!!: !!! !:! + !:!:!: :!: :!: :!: :!: !::!: :!:!: :!: !::!: !:! !:! +:::: :: :: :: ::::::: ::: :: :: ::::: ::::::::: ::::::: :::: ::::: :: +:: : : : : :: :: :: :: : : : : : :: ::::::: :: :: : ::: ::: : : + + + @@@@@@@@@ @@@@@@@@ +@@@@@@@@@@ @@@@@@@@ +@@! @@@@! @@! +!@! @!!@! !@! +@!@ !@@!! @!!!:! +!@! !!!!! !!!!!: +!!: !!!!: !!: +:!: !:!:!: :!: +::::: :::: :::::: :::: + : : :: :: : : :: :: + diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Smushed_Universal.Output_fontfile=starwars.flf.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Smushed_Universal.Output_fontfile=starwars.flf.verified.txt new file mode 100644 index 000000000..01dcf9482 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Smushed_Universal.Output_fontfile=starwars.flf.verified.txt @@ -0,0 +1,14 @@ + _______..______ _______ ______ .___________..______ _______ ______ ______ .__ __. + / || _ \| ____/ || || _ \ | ____| / | / __ \| \ | | + | (----`| |_) | |__ | ,----'`---| |----`| |_) | | |__ | ,----'| | | | \| | + \ \ | ___/| __|| | | | | / | __| | | | | | | . ` | +.----) | | | | |___| `----. | | | |\ \----.| |____ _| `----.| `--' | |\ | +|_______/ | _| |_______\______| |__| | _| `._____||_______(__)______| \______/|__| \__| + + _______. ______ __ _______ + / | / __ \| | | ____| + | (----`| | | | | | |__ + \ \ | | | | | | __| +.----) | | `--' | `----.| |____ +|_______/ \______/|_______||_______| + diff --git a/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj b/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj index 2d8c45c05..152e10fa8 100644 --- a/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj +++ b/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj @@ -12,6 +12,8 @@ + + diff --git a/src/Spectre.Console.Tests/Unit/Widgets/FigletTests.cs b/src/Spectre.Console.Tests/Unit/Widgets/FigletTests.cs index 666170ee8..b3d98f250 100644 --- a/src/Spectre.Console.Tests/Unit/Widgets/FigletTests.cs +++ b/src/Spectre.Console.Tests/Unit/Widgets/FigletTests.cs @@ -37,6 +37,22 @@ public async Task Should_Render_Text_Correctly() await Verifier.Verify(console.Output); } + [Fact] + [Expectation("Render_Single")] + public async Task Should_Render_Single_Character() + { + // Given + var console = new TestConsole().Width(120); + var font = FigletFont.Load(EmbeddedResourceReader.LoadResourceStream("Spectre.Console.Tests/Data/banner.flf")); + var text = new FigletText(font, "P"); + + // When + console.Write(text); + + // Then + await Verifier.Verify(console.Output); + } + [Fact] [Expectation("Render_Wrapped")] public async Task Should_Render_Wrapped_Text_Correctly() @@ -99,4 +115,105 @@ public async Task Should_Render_Right_Aligned_Text_Correctly() // Then await Verifier.Verify(console.Output); } + + [Fact] + [Expectation("Render_Fitted")] + public async Task Should_Render_Fitted_Text_Correctly() + { + // Given + var console = new TestConsole().Width(120); + var text = new FigletText(FigletFont.Default, "Spectre.Console") + { + LayoutMode = FigletLayoutMode.Fitted, + }; + + // When + console.Write(text); + + // Then + await Verifier.Verify(console.Output); + } + + [Theory] + [InlineData("starwars.flf")] + [InlineData("poison.flf")] + [Expectation("Render_Smushed_Universal")] + public async Task Should_Render_Smushed_Text_Correctly_Using_Universal_Smushing_Rules(string fontfile) + { + // Given + var console = new TestConsole().Width(120); + var font = FigletFont.Load(EmbeddedResourceReader.LoadResourceStream($"Spectre.Console.Tests/Data/{fontfile}")); + var text = new FigletText(font, "Spectre.Console") + { + LayoutMode = FigletLayoutMode.Smushed, + }; + + // When + console.Write(text); + + // Then + await Verifier.Verify(console.Output) + .UseParameters(fontfile); + } + + [Fact] + [Expectation("Render_Smushed")] + public async Task Should_Render_Smushed_Text_Correctly_Using_Font_Smushing_Rules() + { + // Given + var console = new TestConsole().Width(120); + var font = FigletFont.Load(EmbeddedResourceReader.LoadResourceStream("Spectre.Console.Tests/Data/banner.flf")); + var text = new FigletText(font, "Spectre.Console") + { + LayoutMode = FigletLayoutMode.Smushed, + }; + + // When + console.Write(text); + + // Then + await Verifier.Verify(console.Output); + } + + [Theory] + [InlineData(FigletLayoutMode.FullSize)] + [InlineData(FigletLayoutMode.Fitted)] + [InlineData(FigletLayoutMode.Smushed)] + public void Should_Render_Empty_String(FigletLayoutMode mode) + { + // Given + var console = new TestConsole().Width(120); + var font = FigletFont.Load(EmbeddedResourceReader.LoadResourceStream("Spectre.Console.Tests/Data/banner.flf")); + var text = new FigletText(font, string.Empty) + { + LayoutMode = mode, + }; + + // When + console.Write(text); + + // Then + console.Output.ShouldBeEmpty(); + } + + [Theory] + [InlineData(FigletLayoutMode.FullSize)] + [InlineData(FigletLayoutMode.Fitted)] + [InlineData(FigletLayoutMode.Smushed)] + public void Should_Not_Render_Non_Existent_Glyph(FigletLayoutMode mode) + { + // Given + var console = new TestConsole().Width(120); + var font = FigletFont.Load(EmbeddedResourceReader.LoadResourceStream("Spectre.Console.Tests/Data/banner.flf")); + var text = new FigletText(font, "😄") + { + LayoutMode = mode, + }; + + // When + console.Write(text); + + // Then + console.Output.ShouldBeEmpty(); + } } \ No newline at end of file diff --git a/src/Spectre.Console/Extensions/Bcl/StringBuilderExtensions.cs b/src/Spectre.Console/Extensions/Bcl/StringBuilderExtensions.cs index 54fe1242c..bf9bb8395 100644 --- a/src/Spectre.Console/Extensions/Bcl/StringBuilderExtensions.cs +++ b/src/Spectre.Console/Extensions/Bcl/StringBuilderExtensions.cs @@ -14,10 +14,10 @@ public static StringBuilder AppendWithStyle(this StringBuilder builder, Style? s if (style != null) { return builder.Append('[') - .Append(style.Value.ToMarkup()) - .Append(']') - .Append(value.EscapeMarkup()) - .Append("[/]"); + .Append(style.Value.ToMarkup()) + .Append(']') + .Append(value.EscapeMarkup()) + .Append("[/]"); } return builder.Append(value); @@ -33,4 +33,17 @@ public static void AppendSpan(this StringBuilder builder, ReadOnlySpan spa builder.Append(span); #endif } + + public static int CountTrailing(this StringBuilder current, char[] characters) + { + var trailing = 0; + for (var index = current.Length - 1; + index >= 0 && characters.Any(c => current[index] == c); + index--) + { + trailing++; + } + + return trailing; + } } \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/Figlet/FigletFont.cs b/src/Spectre.Console/Widgets/Figlet/FigletFont.cs index 975352c3d..a82b39f23 100644 --- a/src/Spectre.Console/Widgets/Figlet/FigletFont.cs +++ b/src/Spectre.Console/Widgets/Figlet/FigletFont.cs @@ -1,7 +1,7 @@ namespace Spectre.Console; /// -/// Represents a FIGlet font. +/// Represents a Figlet font. /// public sealed class FigletFont { @@ -31,7 +31,20 @@ public sealed class FigletFont public int MaxWidth { get; } /// - /// Gets the default FIGlet font. + /// Gets the hardblank character used in this font's glyph definitions. + /// Hardblanks are rendered as spaces in output but are treated as opaque + /// (non-space) characters during fitting and smushing. + /// + public char Hardblank { get; } + + /// + /// Gets the horizontal smushing rules encoded as bit flags (bits 0–5, values 1–32). + /// A value of 0 means universal smushing (any two non-space characters smush, right wins). + /// + public int SmushingRules { get; } + + /// + /// Gets the default Figlet font. /// public static FigletFont Default => _standard.Value; @@ -56,13 +69,29 @@ internal FigletFont(IEnumerable characters, FigletHeader header Height = header.Height; Baseline = header.Baseline; MaxWidth = header.MaxLength; + Hardblank = header.Hardblank; + + // Bits 0–5 (values 1, 2, 4, 8, 16, 32) encode the six horizontal smushing rules. + // A result of 0 means no specific rules are defined, i.e. use universal smushing. + if (header.FullLayout.HasValue) + { + SmushingRules = header.FullLayout.Value & 63; + } + else if (header.OldLayout > 0) + { + SmushingRules = header.OldLayout & 63; + } + else + { + SmushingRules = 0; + } } /// - /// Loads a FIGlet font from the specified stream. + /// Loads a Figlet font from the specified stream. /// - /// The stream to load the FIGlet font from. - /// The loaded FIGlet font. + /// The stream to load the Figlet font from. + /// The loaded Figlet font. public static FigletFont Load(Stream stream) { using (var reader = new StreamReader(stream)) @@ -72,20 +101,20 @@ public static FigletFont Load(Stream stream) } /// - /// Loads a FIGlet font from disk. + /// Loads a Figlet font from disk. /// - /// The path of the FIGlet font to load. - /// The loaded FIGlet font. + /// The path of the Figlet font to load. + /// The loaded Figlet font. public static FigletFont Load(string path) { return Parse(File.ReadAllText(path)); } /// - /// Parses a FIGlet font from the specified . + /// Parses a Figlet font from the specified . /// - /// The FIGlet font source. - /// The parsed FIGlet font. + /// The Figlet font source. + /// The parsed Figlet font. public static FigletFont Parse(string source) { return FigletFontParser.Parse(source); @@ -93,19 +122,7 @@ public static FigletFont Parse(string source) internal int GetWidth(string text) { - var width = 0; - foreach (var character in text) - { - width += GetCharacter(character)?.Width ?? 0; - } - - return width; - } - - internal FigletCharacter? GetCharacter(char character) - { - _characters.TryGetValue(character, out var result); - return result; + return text.Sum(character => GetCharacter(character)?.Width ?? 0); } internal IEnumerable GetCharacters(string text) @@ -123,4 +140,10 @@ internal IEnumerable GetCharacters(string text) return result; } + + private FigletCharacter? GetCharacter(char character) + { + _characters.TryGetValue(character, out var result); + return result; + } } \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/Figlet/FigletFontParser.cs b/src/Spectre.Console/Widgets/Figlet/FigletFontParser.cs index fc03993e1..5d66c5a15 100644 --- a/src/Spectre.Console/Widgets/Figlet/FigletFontParser.cs +++ b/src/Spectre.Console/Widgets/Figlet/FigletFontParser.cs @@ -38,10 +38,10 @@ public static FigletFont Parse(string source) if (hasOverriddenIndex && !indexOverridden) { - throw new InvalidOperationException("Unknown index for FIGlet character"); + continue; } - buffer.Add(line.Replace(header.Hardblank, ' ').TrimEnd('@')); + buffer.Add(line.TrimEnd('@')); if (line.EndsWith("@@", StringComparison.Ordinal)) { @@ -67,7 +67,6 @@ private static bool TryParseIndex(string index, out int result) var style = NumberStyles.Integer; if (index.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) { - // TODO: ReplaceExact should not be used index = index.ReplaceExact("0x", string.Empty).ReplaceExact("0x", string.Empty); style = NumberStyles.HexNumber; } @@ -93,6 +92,12 @@ private static FigletHeader ParseHeader(string text) throw new InvalidOperationException("Invalid Figlet font header signature"); } + int? fullLayout = null; + if (parts.Length > 7 && int.TryParse(parts[7], NumberStyles.Integer, CultureInfo.InvariantCulture, out var fl)) + { + fullLayout = fl; + } + return new FigletHeader { Hardblank = parts[0][5], @@ -101,6 +106,7 @@ private static FigletHeader ParseHeader(string text) MaxLength = int.Parse(parts[3], CultureInfo.InvariantCulture), OldLayout = int.Parse(parts[4], CultureInfo.InvariantCulture), CommentLines = int.Parse(parts[5], CultureInfo.InvariantCulture), + FullLayout = fullLayout, }; } diff --git a/src/Spectre.Console/Widgets/Figlet/FigletHeader.cs b/src/Spectre.Console/Widgets/Figlet/FigletHeader.cs index d04279896..bebfd5601 100644 --- a/src/Spectre.Console/Widgets/Figlet/FigletHeader.cs +++ b/src/Spectre.Console/Widgets/Figlet/FigletHeader.cs @@ -8,4 +8,5 @@ internal sealed class FigletHeader public int MaxLength { get; set; } public int OldLayout { get; set; } public int CommentLines { get; set; } + public int? FullLayout { get; set; } } \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/Figlet/FigletLayoutMode.cs b/src/Spectre.Console/Widgets/Figlet/FigletLayoutMode.cs new file mode 100644 index 000000000..cfad4546b --- /dev/null +++ b/src/Spectre.Console/Widgets/Figlet/FigletLayoutMode.cs @@ -0,0 +1,25 @@ +namespace Spectre.Console; + +/// +/// Represents the different Figlet layout modes. +/// +public enum FigletLayoutMode +{ + /// + /// Represents each Figlet character occupying the full width or + /// height of its arrangement of sub-characters as designed. + /// + FullSize = 0, + + /// + /// Characters are rendered touching each other by removing + /// trailing and leading spaces at character boundaries (kerning). + /// + Fitted = 1, + + /// + /// Boundary characters are merged according to the standard Figlet + /// smushing rules. Smushing implies fitting. + /// + Smushed = 2, +} \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/Figlet/FigletText.cs b/src/Spectre.Console/Widgets/Figlet/FigletText.cs index a4724ab0e..de83023eb 100644 --- a/src/Spectre.Console/Widgets/Figlet/FigletText.cs +++ b/src/Spectre.Console/Widgets/Figlet/FigletText.cs @@ -1,13 +1,15 @@ namespace Spectre.Console; /// -/// Represents text rendered with a FIGlet font. +/// Represents text rendered with a Figlet font. /// public sealed class FigletText : Renderable, IHasJustification { private readonly FigletFont _font; private readonly string _text; + private const string OverscoreChars = @"|/\[]{}()<>"; + /// /// Gets or sets the color of the text. /// @@ -23,6 +25,12 @@ public sealed class FigletText : Renderable, IHasJustification /// Defaults to false. public bool Pad { get; set; } + /// + /// Gets or sets the Figlet layout mode. + /// + /// Defaults to FullSize. + public FigletLayoutMode LayoutMode { get; set; } + /// /// Initializes a new instance of the class. /// @@ -35,7 +43,7 @@ public FigletText(string text) /// /// Initializes a new instance of the class. /// - /// The FIGlet font to use. + /// The Figlet font to use. /// The text. public FigletText(FigletFont font, string text) { @@ -47,16 +55,38 @@ public FigletText(FigletFont font, string text) protected override IEnumerable Render(RenderOptions options, int maxWidth) { var style = new Style(Color ?? Console.Color.Default); - var alignment = Justification ?? Console.Justify.Left; + var alignment = Justification ?? Justify.Left; foreach (var row in GetRows(maxWidth)) { + (int Amount, char[]? MergeChars)[]? junctions = null; + if (LayoutMode is FigletLayoutMode.Fitted or FigletLayoutMode.Smushed + && row.Count > 1) + { + junctions = new (int, char[]?)[row.Count - 1]; + var smushRules = LayoutMode == FigletLayoutMode.Smushed ? _font.SmushingRules : 0; + for (var i = 0; i < row.Count - 1; i++) + { + junctions[i] = ComputeJunction(row[i], row[i + 1], LayoutMode, smushRules, _font.Hardblank); + } + } + for (var index = 0; index < _font.Height; index++) { - var line = new Segment(string.Concat(row.Select(x => x.Lines[index])), style); + var lineText = junctions != null + ? BuildLine(row, index, junctions, _font.Hardblank) + : string.Concat(row.Select(x => x.Lines[index])); + + // Replace hardblanks with regular spaces for display + if (_font.Hardblank != ' ') + { + lineText = lineText.Replace(_font.Hardblank, ' '); + } + + var line = new Segment(lineText, style); var lineWidth = line.CellCount(); - if (alignment == Console.Justify.Left) + if (alignment == Justify.Left) { yield return line; @@ -65,7 +95,7 @@ protected override IEnumerable Render(RenderOptions options, int maxWid yield return Segment.Padding(maxWidth - lineWidth); } } - else if (alignment == Console.Justify.Center) + else if (alignment == Justify.Center) { var left = Math.Max(0, maxWidth - lineWidth) / 2; var right = left + (Math.Max(0, maxWidth - lineWidth) % 2); @@ -78,7 +108,7 @@ protected override IEnumerable Render(RenderOptions options, int maxWid yield return Segment.Padding(right); } } - else if (alignment == Console.Justify.Right) + else if (alignment == Justify.Right) { if (lineWidth < maxWidth) { @@ -93,10 +123,271 @@ protected override IEnumerable Render(RenderOptions options, int maxWid } } + private static (int Amount, char[]? MergeChars) ComputeJunction( + FigletCharacter left, FigletCharacter right, + FigletLayoutMode mode, int smushRules, char hardblank) + { + // Compute the fit amount: minimum across all rows of + // (trailing spaces of left + leading spaces of right). + // Per the Figlet spec, hardblanks count as blanks for overlap distance, + // so replace them with spaces before trimming. + var fit = int.MaxValue; + for (var y = 0; y < left.Height; y++) + { + var leftNorm = left.Lines[y].Replace(hardblank, ' '); + var rightNorm = right.Lines[y].Replace(hardblank, ' '); + var trailing = left.Lines[y].Length - leftNorm.TrimEnd(' ').Length; + var leading = right.Lines[y].Length - rightNorm.TrimStart(' ').Length; + fit = Math.Min(fit, trailing + leading); + } + + if (fit == int.MaxValue) + { + fit = 0; + } + + // Never fit or smush the input space character; preserve natural spacing + if (left.Code == 32 || right.Code == 32) + { + return (0, null); + } + + if (mode != FigletLayoutMode.Smushed) + { + return (fit, null); + } + + // Try smushing: go one column further than + // fitting by merging the boundary characters + var mergeChars = new char[left.Height]; + for (var i = 0; i < left.Height; i++) + { + var leftLine = left.Lines[i]; + var rightLine = right.Lines[i]; + var trailing = leftLine.Length - leftLine.Replace(hardblank, ' ').TrimEnd(' ').Length; + + var trimFromLeft = Math.Min(trailing, fit); + var trimFromRight = fit - trimFromLeft; + + var leftBound = leftLine.Length - trimFromLeft - 1; + var rightBound = trimFromRight; + + var leftChar = (leftBound >= 0) ? leftLine[leftBound] : ' '; + var rightChar = (rightBound < rightLine.Length) ? rightLine[rightBound] : ' '; + + // Both sides are space? + if (leftChar == ' ' && rightChar == ' ') + { + mergeChars[i] = ' '; + continue; + } + + var merged = SmushChars(leftChar, rightChar, smushRules, hardblank); + if (merged == null) + { + return (fit, null); + } + + mergeChars[i] = merged.Value; + } + + return (fit + 1, mergeChars); + } + + private static char? SmushChars(char left, char right, int rules, char hardblank) + { + switch (left) + { + // Both spaces: nothing to smush + case ' ' when right == ' ': + return null; + // Space is transparent: the non-space side wins + case ' ': + return right; + } + + if (right == ' ') + { + return left; + } + + // Rule 6 (Hardblank smushing) + if (left == hardblank && right == hardblank) + { + return rules == 0 || (rules & 32) != 0 ? hardblank : (char?)null; + } + + // A hardblank at the smushing boundary was already within the fitting/overlap + // zone (counted as space for distance purposes), so treat it as transparent here + if (left == hardblank) + { + left = ' '; + } + + if (right == hardblank) + { + right = ' '; + } + + if (left == ' ' && right == ' ') + { + return null; + } + + if (left == ' ') + { + return right; + } + + if (right == ' ') + { + return left; + } + + // Universal smushing + if (rules == 0) + { + return right; + } + + // Rule 1 (Equal character) + if ((rules & 1) != 0 && left == right) + { + return left; + } + + // Rule 2 (Underscore) + if ((rules & 2) != 0) + { + if (left == '_' && OverscoreChars.Contains(right)) + { + return right; + } + + if (right == '_' && OverscoreChars.Contains(left)) + { + return left; + } + } + + // Rule 3 (Hierarchy) + if ((rules & 4) != 0) + { + static int GetHierarchyClass(char c) => c switch + { + '|' => 1, + '/' or '\\' => 2, + '[' or ']' => 3, + '{' or '}' => 4, + '(' or ')' => 5, + '<' or '>' => 6, + _ => 0, + }; + + var leftClass = GetHierarchyClass(left); + var rightClass = GetHierarchyClass(right); + if (leftClass != 0 && rightClass != 0 && leftClass != rightClass) + { + return leftClass > rightClass ? left : right; + } + } + + // Rule 4 (Opposite pairs) + if ((rules & 8) != 0) + { + switch (left) + { + case '[' when right == ']': + case ']' when right == '[': + case '{' when right == '}': + case '}' when right == '{': + case '(' when right == ')': + case ')' when right == '(': + return '|'; + } + } + + // Rule 5 (Big X) + if ((rules & 16) != 0) + { + switch (left) + { + case '/' when right == '\\': + return '|'; + case '\\' when right == '/': + return 'Y'; + case '>' when right == '<': + return 'X'; + } + } + + // No smushing rule applies + return null; + } + + private static string BuildLine( + IReadOnlyList row, + int lineIndex, + IReadOnlyList<(int Amount, char[]? MergeChars)> junctions, + char hardblank) + { + if (row.Count == 0) + { + return string.Empty; + } + + var sb = new StringBuilder(row[0].Lines[lineIndex]); + + for (var rowIndex = 1; rowIndex < row.Count; rowIndex++) + { + var (amount, mergeChars) = junctions[rowIndex - 1]; + var rightLine = row[rowIndex].Lines[lineIndex]; + + var trailingSpaces = sb.CountTrailing([' ', hardblank]); + + if (mergeChars != null) + { + // Smushing: amount = fit + 1. Remove fit chars via normal trim, + // then swap boundary chars for the merged char + var fit = amount - 1; + var trimFromLeft = Math.Min(trailingSpaces, fit); + var trimFromRight = fit - trimFromLeft; + + // Remove trailing spaces and boundary char from accumulated left + var removeFromLeft = Math.Min(trimFromLeft + 1, sb.Length); + sb.Remove(sb.Length - removeFromLeft, removeFromLeft); + + sb.Append(mergeChars[lineIndex]); + + // Skip leading spaces and boundary char from right + var skipFromRight = trimFromRight + 1; + if (skipFromRight < rightLine.Length) + { + sb.Append(rightLine[skipFromRight..]); + } + } + else + { + // Fitting: remove trailing/leading spaces to bring characters together + var trimFromLeft = Math.Min(trailingSpaces, amount); + var trimFromRight = amount - trimFromLeft; + + if (trimFromLeft > 0) + { + sb.Remove(sb.Length - trimFromLeft, trimFromLeft); + } + + sb.Append(trimFromRight > 0 ? rightLine[trimFromRight..] : rightLine); + } + } + + return sb.ToString(); + } + private List> GetRows(int maxWidth) { var result = new List>(); - var words = _text.SplitWords(StringSplitOptions.None); + var words = _text.SplitWords(); var totalWidth = 0; var line = new List(); @@ -126,7 +417,7 @@ private List> GetRows(int maxWidth) } else { - // We need to split it up. + // We need to split it up var queue = new Queue(_font.GetCharacters(word)); while (queue.Count > 0) { @@ -161,7 +452,7 @@ private List> GetRows(int maxWidth) public static class FigletTextExtensions { /// - /// Sets the color of the FIGlet text. + /// Sets the color of the Figlet text. /// /// The text. /// The color. @@ -173,4 +464,54 @@ public static FigletText Color(this FigletText text, Color? color) text.Color = color ?? Console.Color.Default; return text; } + + /// + /// Sets the Figlet layout mode. + /// + /// The text. + /// The layout mode. + /// The same instance so that multiple calls can be chained. + public static FigletText LayoutMode(this FigletText text, FigletLayoutMode mode) + { + ArgumentNullException.ThrowIfNull(text); + + text.LayoutMode = mode; + return text; + } + + /// + /// Sets the Figlet layout mode to FullSize. + /// + /// The text. + /// The same instance so that multiple calls can be chained. + public static FigletText FullSize(this FigletText text) + { + ArgumentNullException.ThrowIfNull(text); + + return LayoutMode(text, FigletLayoutMode.FullSize); + } + + /// + /// Sets the Figlet layout mode to Fitted. + /// + /// The text. + /// The same instance so that multiple calls can be chained. + public static FigletText Fitted(this FigletText text) + { + ArgumentNullException.ThrowIfNull(text); + + return LayoutMode(text, FigletLayoutMode.Fitted); + } + + /// + /// Sets the Figlet layout mode to Smushed. + /// + /// The text. + /// The same instance so that multiple calls can be chained. + public static FigletText Smushed(this FigletText text) + { + ArgumentNullException.ThrowIfNull(text); + + return LayoutMode(text, FigletLayoutMode.Smushed); + } } \ No newline at end of file From 2bbdf1ca7ca198a719606382898575ccde849beb Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Mon, 20 Apr 2026 11:14:14 +0200 Subject: [PATCH 11/25] Fix various bugs, add Figlet font report --- .../Data/{banner.flf => figlet/big.flf} | 0 .../Data/figlet/fonts.zip | Bin 0 -> 676674 bytes .../Data/{ => figlet}/poison.flf | 0 .../{starwars.flf => figlet/star_wars.flf} | 0 .../Figlet/Figlet_Report.Output.verified.txt | 10612 ++++++++++++++++ .../Load_Stream.Output_name=big.verified.txt | 8 + ...ad_Stream.Output_name=poison.verified.txt} | 0 ..._Stream.Output_name=starwars.verified.txt} | 0 ...hed_Universal.Output_name=big.verified.txt | 8 + ...Universal.Output_name=poison.verified.txt} | 0 ...iversal.Output_name=starwars.verified.txt} | 0 .../Spectre.Console.Tests.csproj | 11 +- .../Unit/Widgets/FigletTests.cs | 45 +- .../Utilities/FigletReportGenerator.cs | 111 + .../Utilities/FigletTestFontLoader.cs | 50 + .../Widgets/Figlet/FigletCharacter.cs | 4 - .../Widgets/Figlet/FigletFont.cs | 5 +- .../Widgets/Figlet/FigletFontParser.cs | 32 +- 18 files changed, 10853 insertions(+), 33 deletions(-) rename src/Spectre.Console.Tests/Data/{banner.flf => figlet/big.flf} (100%) create mode 100644 src/Spectre.Console.Tests/Data/figlet/fonts.zip rename src/Spectre.Console.Tests/Data/{ => figlet}/poison.flf (100%) rename src/Spectre.Console.Tests/Data/{starwars.flf => figlet/star_wars.flf} (100%) create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Figlet_Report.Output.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Load_Stream.Output_name=big.verified.txt rename src/Spectre.Console.Tests/Expectations/Widgets/Figlet/{Load_Stream.Output_fontfile=poison.flf.verified.txt => Load_Stream.Output_name=poison.verified.txt} (100%) rename src/Spectre.Console.Tests/Expectations/Widgets/Figlet/{Load_Stream.Output_fontfile=starwars.flf.verified.txt => Load_Stream.Output_name=starwars.verified.txt} (100%) create mode 100644 src/Spectre.Console.Tests/Expectations/Widgets/Figlet/Render_Smushed_Universal.Output_name=big.verified.txt rename src/Spectre.Console.Tests/Expectations/Widgets/Figlet/{Render_Smushed_Universal.Output_fontfile=poison.flf.verified.txt => Render_Smushed_Universal.Output_name=poison.verified.txt} (100%) rename src/Spectre.Console.Tests/Expectations/Widgets/Figlet/{Render_Smushed_Universal.Output_fontfile=starwars.flf.verified.txt => Render_Smushed_Universal.Output_name=starwars.verified.txt} (100%) create mode 100644 src/Spectre.Console.Tests/Utilities/FigletReportGenerator.cs create mode 100644 src/Spectre.Console.Tests/Utilities/FigletTestFontLoader.cs diff --git a/src/Spectre.Console.Tests/Data/banner.flf b/src/Spectre.Console.Tests/Data/figlet/big.flf similarity index 100% rename from src/Spectre.Console.Tests/Data/banner.flf rename to src/Spectre.Console.Tests/Data/figlet/big.flf diff --git a/src/Spectre.Console.Tests/Data/figlet/fonts.zip b/src/Spectre.Console.Tests/Data/figlet/fonts.zip new file mode 100644 index 0000000000000000000000000000000000000000..85a11ea3cfad353e6a42e8a4a257350d1b53de92 GIT binary patch literal 676674 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%0Dt+Kn* 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