From a1d11e9d0c5828a9f7a2b9b5bbd18fea7480cb7e Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Wed, 28 Oct 2020 10:51:25 +0100 Subject: [PATCH] Add support for moving the cursor --- dotnet-tools.json | 2 +- examples/Cursor/Cursor.csproj | 15 ++ examples/Cursor/Program.cs | 21 +++ .../Tools/AnsiConsoleFixture.cs | 6 + .../Tools/MarkupConsoleFixture.cs | 6 + .../Tools/PlainConsole.cs | 5 + src/Spectre.Console.sln | 15 ++ src/Spectre.Console/AnsiConsole.cs | 7 +- src/Spectre.Console/CursorDirection.cs | 28 ++++ .../Extensions/CursorExtensions.cs | 152 ++++++++++++++++++ .../Extensions/ExceptionExtensions.cs | 1 + src/Spectre.Console/IAnsiConsole.cs | 11 ++ src/Spectre.Console/IAnsiConsoleCursor.cs | 28 ++++ .../Ansi/AnsiBackend.cs} | 17 +- .../Internal/Backends/Ansi/AnsiCursor.cs | 56 +++++++ .../BackendBuilder.cs} | 6 +- .../Backends/Fallback/FallbackBackend.cs | 89 ++++++++++ .../Backends/Fallback/FallbackCursor.cs | 40 +++++ .../Internal/Collections/ListWithCallback.cs | 2 +- .../Internal/{Utilities => }/ConsoleHelper.cs | 0 .../Internal/ExceptionFormatter.cs | 3 +- .../Internal/FallbackConsoleRenderer.cs | 97 ----------- .../Internal/{Utilities => }/Ratio.cs | 0 src/Spectre.Console/Recorder.cs | 21 ++- src/Spectre.Console/Rendering/Segment.cs | 25 ++- src/Spectre.Console/Widgets/Calendar.cs | 1 - src/Spectre.Console/Widgets/Grid.cs | 2 +- src/Spectre.Console/Widgets/Table.cs | 1 - src/Spectre.Console/Widgets/TableRow.cs | 2 +- 29 files changed, 543 insertions(+), 116 deletions(-) create mode 100644 examples/Cursor/Cursor.csproj create mode 100644 examples/Cursor/Program.cs create mode 100644 src/Spectre.Console/CursorDirection.cs create mode 100644 src/Spectre.Console/Extensions/CursorExtensions.cs create mode 100644 src/Spectre.Console/IAnsiConsoleCursor.cs rename src/Spectre.Console/Internal/{AnsiConsoleRenderer.cs => Backends/Ansi/AnsiBackend.cs} (78%) create mode 100644 src/Spectre.Console/Internal/Backends/Ansi/AnsiCursor.cs rename src/Spectre.Console/Internal/{AnsiConsoleBuilder.cs => Backends/BackendBuilder.cs} (91%) create mode 100644 src/Spectre.Console/Internal/Backends/Fallback/FallbackBackend.cs create mode 100644 src/Spectre.Console/Internal/Backends/Fallback/FallbackCursor.cs rename src/Spectre.Console/Internal/{Utilities => }/ConsoleHelper.cs (100%) delete mode 100644 src/Spectre.Console/Internal/FallbackConsoleRenderer.cs rename src/Spectre.Console/Internal/{Utilities => }/Ratio.cs (100%) diff --git a/dotnet-tools.json b/dotnet-tools.json index 4c4c563..72ca7f2 100644 --- a/dotnet-tools.json +++ b/dotnet-tools.json @@ -15,7 +15,7 @@ ] }, "dotnet-example": { - "version": "0.8.0", + "version": "0.10.0", "commands": [ "dotnet-example" ] diff --git a/examples/Cursor/Cursor.csproj b/examples/Cursor/Cursor.csproj new file mode 100644 index 0000000..1c06842 --- /dev/null +++ b/examples/Cursor/Cursor.csproj @@ -0,0 +1,15 @@ + + + + Exe + netcoreapp3.1 + false + Cursor + Demonstrates how to move the cursor. + + + + + + + diff --git a/examples/Cursor/Program.cs b/examples/Cursor/Program.cs new file mode 100644 index 0000000..ebfdfbf --- /dev/null +++ b/examples/Cursor/Program.cs @@ -0,0 +1,21 @@ +using System; +using Spectre.Console; + +namespace Cursor +{ + public static class Program + { + public static void Main(string[] args) + { + AnsiConsole.Write("Hello"); + + // Move the cursor 3 cells to the right + AnsiConsole.Cursor.Move(CursorDirection.Right, 3); + AnsiConsole.Write("World"); + + // Move the cursor 5 cells to the left. + AnsiConsole.Cursor.Move(CursorDirection.Left, 5); + AnsiConsole.WriteLine("Universe"); + } + } +} diff --git a/src/Spectre.Console.Tests/Tools/AnsiConsoleFixture.cs b/src/Spectre.Console.Tests/Tools/AnsiConsoleFixture.cs index 7101d6d..de71485 100644 --- a/src/Spectre.Console.Tests/Tools/AnsiConsoleFixture.cs +++ b/src/Spectre.Console.Tests/Tools/AnsiConsoleFixture.cs @@ -17,6 +17,7 @@ namespace Spectre.Console.Tests public Encoding Encoding => _console.Encoding; public int Width { get; } public int Height => _console.Height; + public IAnsiConsoleCursor Cursor => _console.Cursor; public TestableAnsiConsole(ColorSystem system, AnsiSupport ansi = AnsiSupport.Yes, int width = 80) { @@ -37,6 +38,11 @@ namespace Spectre.Console.Tests _writer?.Dispose(); } + public void Clear(bool home) + { + _console.Clear(home); + } + public void Write(Segment segment) { _console.Write(segment); diff --git a/src/Spectre.Console.Tests/Tools/MarkupConsoleFixture.cs b/src/Spectre.Console.Tests/Tools/MarkupConsoleFixture.cs index c9b9be5..d4ce6c3 100644 --- a/src/Spectre.Console.Tests/Tools/MarkupConsoleFixture.cs +++ b/src/Spectre.Console.Tests/Tools/MarkupConsoleFixture.cs @@ -14,6 +14,7 @@ namespace Spectre.Console.Tests.Tools public Capabilities Capabilities => _console.Capabilities; public Encoding Encoding => _console.Encoding; + public IAnsiConsoleCursor Cursor => _console.Cursor; public int Width { get; } public int Height => _console.Height; @@ -36,6 +37,11 @@ namespace Spectre.Console.Tests.Tools _writer?.Dispose(); } + public void Clear(bool home) + { + _console.Clear(home); + } + public void Write(Segment segment) { _console.Write(segment); diff --git a/src/Spectre.Console.Tests/Tools/PlainConsole.cs b/src/Spectre.Console.Tests/Tools/PlainConsole.cs index 5f00fe3..ca31b53 100644 --- a/src/Spectre.Console.Tests/Tools/PlainConsole.cs +++ b/src/Spectre.Console.Tests/Tools/PlainConsole.cs @@ -11,6 +11,7 @@ namespace Spectre.Console.Tests { public Capabilities Capabilities { get; } public Encoding Encoding { get; } + public IAnsiConsoleCursor Cursor => throw new NotSupportedException(); public int Width { get; } public int Height { get; } @@ -42,6 +43,10 @@ namespace Spectre.Console.Tests Writer.Dispose(); } + public void Clear(bool home) + { + } + public void Write(Segment segment) { if (segment is null) diff --git a/src/Spectre.Console.sln b/src/Spectre.Console.sln index 3109e9c..f40c75f 100644 --- a/src/Spectre.Console.sln +++ b/src/Spectre.Console.sln @@ -48,6 +48,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Calendars", "..\examples\Ca EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rules", "..\examples\Rules\Rules.csproj", "{8622A261-02C6-40CA-9797-E3F01ED87D6B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cursor", "..\examples\Cursor\Cursor.csproj", "{75C608C3-ABB4-4168-A229-7F8250B946D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -226,6 +228,18 @@ Global {8622A261-02C6-40CA-9797-E3F01ED87D6B}.Release|x64.Build.0 = Release|Any CPU {8622A261-02C6-40CA-9797-E3F01ED87D6B}.Release|x86.ActiveCfg = Release|Any CPU {8622A261-02C6-40CA-9797-E3F01ED87D6B}.Release|x86.Build.0 = Release|Any CPU + {75C608C3-ABB4-4168-A229-7F8250B946D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75C608C3-ABB4-4168-A229-7F8250B946D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75C608C3-ABB4-4168-A229-7F8250B946D1}.Debug|x64.ActiveCfg = Debug|Any CPU + {75C608C3-ABB4-4168-A229-7F8250B946D1}.Debug|x64.Build.0 = Debug|Any CPU + {75C608C3-ABB4-4168-A229-7F8250B946D1}.Debug|x86.ActiveCfg = Debug|Any CPU + {75C608C3-ABB4-4168-A229-7F8250B946D1}.Debug|x86.Build.0 = Debug|Any CPU + {75C608C3-ABB4-4168-A229-7F8250B946D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75C608C3-ABB4-4168-A229-7F8250B946D1}.Release|Any CPU.Build.0 = Release|Any CPU + {75C608C3-ABB4-4168-A229-7F8250B946D1}.Release|x64.ActiveCfg = Release|Any CPU + {75C608C3-ABB4-4168-A229-7F8250B946D1}.Release|x64.Build.0 = Release|Any CPU + {75C608C3-ABB4-4168-A229-7F8250B946D1}.Release|x86.ActiveCfg = Release|Any CPU + {75C608C3-ABB4-4168-A229-7F8250B946D1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -244,6 +258,7 @@ Global {C3E2CB5C-1517-4C75-B59A-93D4E22BEC8D} = {20595AD4-8D75-4AF8-B6BC-9C38C160423F} {57691C7D-683D-46E6-AA4F-57A8C5F65D25} = {F0575243-121F-4DEE-9F6B-246E26DC0844} {8622A261-02C6-40CA-9797-E3F01ED87D6B} = {F0575243-121F-4DEE-9F6B-246E26DC0844} + {75C608C3-ABB4-4168-A229-7F8250B946D1} = {F0575243-121F-4DEE-9F6B-246E26DC0844} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C} diff --git a/src/Spectre.Console/AnsiConsole.cs b/src/Spectre.Console/AnsiConsole.cs index f1d09e1..dcdc80f 100644 --- a/src/Spectre.Console/AnsiConsole.cs +++ b/src/Spectre.Console/AnsiConsole.cs @@ -27,6 +27,11 @@ namespace Spectre.Console /// public static IAnsiConsole Console => _recorder ?? _console.Value; + /// + /// Gets the . + /// + public static IAnsiConsoleCursor Cursor => _recorder?.Cursor ?? _console.Value.Cursor; + /// /// Gets the console's capabilities. /// @@ -56,7 +61,7 @@ namespace Spectre.Console /// An instance. public static IAnsiConsole Create(AnsiConsoleSettings settings) { - return AnsiConsoleBuilder.Build(settings); + return BackendBuilder.Build(settings); } } } diff --git a/src/Spectre.Console/CursorDirection.cs b/src/Spectre.Console/CursorDirection.cs new file mode 100644 index 0000000..bfea9ea --- /dev/null +++ b/src/Spectre.Console/CursorDirection.cs @@ -0,0 +1,28 @@ +namespace Spectre.Console +{ + /// + /// Represents cursor direction. + /// + public enum CursorDirection + { + /// + /// Up + /// + Up, + + /// + /// Down + /// + Down, + + /// + /// Left + /// + Left, + + /// + /// Right + /// + Right, + } +} diff --git a/src/Spectre.Console/Extensions/CursorExtensions.cs b/src/Spectre.Console/Extensions/CursorExtensions.cs new file mode 100644 index 0000000..57cd8b8 --- /dev/null +++ b/src/Spectre.Console/Extensions/CursorExtensions.cs @@ -0,0 +1,152 @@ +namespace Spectre.Console +{ + /// + /// Contains extension methods for . + /// + public static class CursorExtensions + { + /// + /// Shows the cursor. + /// + /// The cursor. + public static void Show(this IAnsiConsoleCursor cursor) + { + if (cursor is null) + { + throw new System.ArgumentNullException(nameof(cursor)); + } + + cursor.Show(true); + } + + /// + /// Hides the cursor. + /// + /// The cursor. + public static void Hide(this IAnsiConsoleCursor cursor) + { + if (cursor is null) + { + throw new System.ArgumentNullException(nameof(cursor)); + } + + cursor.Show(false); + } + + /// + /// Moves the cursor up. + /// + /// The cursor. + public static void MoveUp(this IAnsiConsoleCursor cursor) + { + if (cursor is null) + { + throw new System.ArgumentNullException(nameof(cursor)); + } + + cursor.Move(CursorDirection.Up, 1); + } + + /// + /// Moves the cursor up. + /// + /// The cursor. + /// The number of steps to move the cursor. + public static void MoveUp(this IAnsiConsoleCursor cursor, int steps) + { + if (cursor is null) + { + throw new System.ArgumentNullException(nameof(cursor)); + } + + cursor.Move(CursorDirection.Up, steps); + } + + /// + /// Moves the cursor down. + /// + /// The cursor. + public static void MoveDown(this IAnsiConsoleCursor cursor) + { + if (cursor is null) + { + throw new System.ArgumentNullException(nameof(cursor)); + } + + cursor.Move(CursorDirection.Down, 1); + } + + /// + /// Moves the cursor down. + /// + /// The cursor. + /// The number of steps to move the cursor. + public static void MoveDown(this IAnsiConsoleCursor cursor, int steps) + { + if (cursor is null) + { + throw new System.ArgumentNullException(nameof(cursor)); + } + + cursor.Move(CursorDirection.Down, steps); + } + + /// + /// Moves the cursor to the left. + /// + /// The cursor. + public static void MoveLeft(this IAnsiConsoleCursor cursor) + { + if (cursor is null) + { + throw new System.ArgumentNullException(nameof(cursor)); + } + + cursor.Move(CursorDirection.Left, 1); + } + + /// + /// Moves the cursor to the left. + /// + /// The cursor. + /// The number of steps to move the cursor. + public static void MoveLeft(this IAnsiConsoleCursor cursor, int steps) + { + if (cursor is null) + { + throw new System.ArgumentNullException(nameof(cursor)); + } + + cursor.Move(CursorDirection.Left, steps); + } + + /// + /// Moves the cursor to the right. + /// + /// The cursor. + public static void MoveRight(this IAnsiConsoleCursor cursor) + { + if (cursor is null) + { + throw new System.ArgumentNullException(nameof(cursor)); + } + + cursor.Move(CursorDirection.Right, 1); + } + + /// + /// Moves the cursor to the right. + /// + /// The cursor. + /// The number of steps to move the cursor. + public static void MoveRight(this IAnsiConsoleCursor cursor, int steps) + { + if (cursor is null) + { + throw new System.ArgumentNullException(nameof(cursor)); + } + + cursor.Move(CursorDirection.Right, steps); + } + } +} diff --git a/src/Spectre.Console/Extensions/ExceptionExtensions.cs b/src/Spectre.Console/Extensions/ExceptionExtensions.cs index aa0c0aa..e34c0e8 100644 --- a/src/Spectre.Console/Extensions/ExceptionExtensions.cs +++ b/src/Spectre.Console/Extensions/ExceptionExtensions.cs @@ -1,4 +1,5 @@ using System; +using Spectre.Console.Internal; using Spectre.Console.Rendering; namespace Spectre.Console diff --git a/src/Spectre.Console/IAnsiConsole.cs b/src/Spectre.Console/IAnsiConsole.cs index a4334b9..592dc4b 100644 --- a/src/Spectre.Console/IAnsiConsole.cs +++ b/src/Spectre.Console/IAnsiConsole.cs @@ -18,6 +18,11 @@ namespace Spectre.Console /// Encoding Encoding { get; } + /// + /// Gets the console cursor. + /// + IAnsiConsoleCursor Cursor { get; } + /// /// Gets the buffer width of the console. /// @@ -28,6 +33,12 @@ namespace Spectre.Console /// int Height { get; } + /// + /// Clears the console. + /// + /// If the cursor should be moved to the home position. + void Clear(bool home); + /// /// Writes a string followed by a line terminator to the console. /// diff --git a/src/Spectre.Console/IAnsiConsoleCursor.cs b/src/Spectre.Console/IAnsiConsoleCursor.cs new file mode 100644 index 0000000..c9019ee --- /dev/null +++ b/src/Spectre.Console/IAnsiConsoleCursor.cs @@ -0,0 +1,28 @@ +namespace Spectre.Console +{ + /// + /// Represents the console's cursor. + /// + public interface IAnsiConsoleCursor + { + /// + /// Shows or hides the cursor. + /// + /// true to show the cursor, false to hide it. + void Show(bool show); + + /// + /// Sets the cursor position. + /// + /// The column to move the cursor to. + /// The line to move the cursor to. + void SetPosition(int column, int line); + + /// + /// Moves the cursor relative to the current position. + /// + /// The direction to move the cursor. + /// The number of steps to move the cursor. + void Move(CursorDirection direction, int steps); + } +} diff --git a/src/Spectre.Console/Internal/AnsiConsoleRenderer.cs b/src/Spectre.Console/Internal/Backends/Ansi/AnsiBackend.cs similarity index 78% rename from src/Spectre.Console/Internal/AnsiConsoleRenderer.cs rename to src/Spectre.Console/Internal/Backends/Ansi/AnsiBackend.cs index 51810fe..082fac8 100644 --- a/src/Spectre.Console/Internal/AnsiConsoleRenderer.cs +++ b/src/Spectre.Console/Internal/Backends/Ansi/AnsiBackend.cs @@ -5,13 +5,15 @@ using Spectre.Console.Rendering; namespace Spectre.Console.Internal { - internal sealed class AnsiConsoleRenderer : IAnsiConsole + internal sealed class AnsiBackend : IAnsiConsole { private readonly TextWriter _out; private readonly AnsiBuilder _ansiBuilder; + private readonly AnsiCursor _cursor; public Capabilities Capabilities { get; } public Encoding Encoding { get; } + public IAnsiConsoleCursor Cursor => _cursor; public int Width { @@ -39,7 +41,7 @@ namespace Spectre.Console.Internal } } - public AnsiConsoleRenderer(TextWriter @out, Capabilities capabilities, ILinkIdentityGenerator? linkHasher) + public AnsiBackend(TextWriter @out, Capabilities capabilities, ILinkIdentityGenerator? linkHasher) { _out = @out ?? throw new ArgumentNullException(nameof(@out)); @@ -47,6 +49,17 @@ namespace Spectre.Console.Internal Encoding = _out.IsStandardOut() ? System.Console.OutputEncoding : Encoding.UTF8; _ansiBuilder = new AnsiBuilder(Capabilities, linkHasher); + _cursor = new AnsiCursor(this); + } + + public void Clear(bool home) + { + Write(Segment.Control("\u001b[2J")); + + if (home) + { + Cursor.SetPosition(0, 0); + } } public void Write(Segment segment) diff --git a/src/Spectre.Console/Internal/Backends/Ansi/AnsiCursor.cs b/src/Spectre.Console/Internal/Backends/Ansi/AnsiCursor.cs new file mode 100644 index 0000000..28b00d5 --- /dev/null +++ b/src/Spectre.Console/Internal/Backends/Ansi/AnsiCursor.cs @@ -0,0 +1,56 @@ +using System; +using Spectre.Console.Rendering; + +namespace Spectre.Console.Internal +{ + internal sealed class AnsiCursor : IAnsiConsoleCursor + { + private readonly AnsiBackend _renderer; + + public AnsiCursor(AnsiBackend renderer) + { + _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); + } + + public void Show(bool show) + { + if (show) + { + _renderer.Write(Segment.Control("\u001b[?25h")); + } + else + { + _renderer.Write(Segment.Control("\u001b[?25l")); + } + } + + public void Move(CursorDirection direction, int steps) + { + if (steps == 0) + { + return; + } + + switch (direction) + { + case CursorDirection.Up: + _renderer.Write(Segment.Control($"\u001b[{steps}A")); + break; + case CursorDirection.Down: + _renderer.Write(Segment.Control($"\u001b[{steps}B")); + break; + case CursorDirection.Right: + _renderer.Write(Segment.Control($"\u001b[{steps}C")); + break; + case CursorDirection.Left: + _renderer.Write(Segment.Control($"\u001b[{steps}D")); + break; + } + } + + public void SetPosition(int column, int line) + { + _renderer.Write(Segment.Control($"\u001b[{line};{column}H")); + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Internal/AnsiConsoleBuilder.cs b/src/Spectre.Console/Internal/Backends/BackendBuilder.cs similarity index 91% rename from src/Spectre.Console/Internal/AnsiConsoleBuilder.cs rename to src/Spectre.Console/Internal/Backends/BackendBuilder.cs index 0ec2d84..6069e2a 100644 --- a/src/Spectre.Console/Internal/AnsiConsoleBuilder.cs +++ b/src/Spectre.Console/Internal/Backends/BackendBuilder.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Spectre.Console.Internal { - internal static class AnsiConsoleBuilder + internal static class BackendBuilder { public static IAnsiConsole Build(AnsiConsoleSettings settings) { @@ -60,11 +60,11 @@ namespace Spectre.Console.Internal // Create the renderer if (supportsAnsi) { - return new AnsiConsoleRenderer(buffer, capabilities, settings.LinkIdentityGenerator); + return new AnsiBackend(buffer, capabilities, settings.LinkIdentityGenerator); } else { - return new FallbackConsoleRenderer(buffer, capabilities); + return new FallbackBackend(buffer, capabilities); } } } diff --git a/src/Spectre.Console/Internal/Backends/Fallback/FallbackBackend.cs b/src/Spectre.Console/Internal/Backends/Fallback/FallbackBackend.cs new file mode 100644 index 0000000..44c7c7b --- /dev/null +++ b/src/Spectre.Console/Internal/Backends/Fallback/FallbackBackend.cs @@ -0,0 +1,89 @@ +using System; +using System.IO; +using System.Text; +using Spectre.Console.Rendering; + +namespace Spectre.Console.Internal +{ + internal sealed class FallbackBackend : IAnsiConsole + { + private readonly ColorSystem _system; + private readonly FallbackCursor _cursor; + private Style? _lastStyle; + + public Capabilities Capabilities { get; } + public Encoding Encoding { get; } + public IAnsiConsoleCursor Cursor => _cursor; + + public int Width + { + get { return ConsoleHelper.GetSafeBufferWidth(Constants.DefaultBufferWidth); } + } + + public int Height + { + get { return ConsoleHelper.GetSafeBufferHeight(Constants.DefaultBufferHeight); } + } + + public FallbackBackend(TextWriter @out, Capabilities capabilities) + { + if (capabilities == null) + { + throw new ArgumentNullException(nameof(capabilities)); + } + + _system = capabilities.ColorSystem; + _cursor = new FallbackCursor(); + + if (@out != System.Console.Out) + { + System.Console.SetOut(@out ?? throw new ArgumentNullException(nameof(@out))); + } + + Encoding = System.Console.OutputEncoding; + Capabilities = capabilities; + } + + public void Clear(bool home) + { + var (x, y) = (System.Console.CursorLeft, System.Console.CursorTop); + + System.Console.Clear(); + + if (!home) + { + // Set the cursor position + System.Console.SetCursorPosition(x, y); + } + } + + public void Write(Segment segment) + { + if (_lastStyle?.Equals(segment.Style) != true) + { + SetStyle(segment.Style); + } + + System.Console.Write(segment.Text.NormalizeLineEndings(native: true)); + } + + private void SetStyle(Style style) + { + _lastStyle = style; + + System.Console.ResetColor(); + + var background = Color.ToConsoleColor(style.Background); + if (_system != ColorSystem.NoColors && (int)background != -1) + { + System.Console.BackgroundColor = background; + } + + var foreground = Color.ToConsoleColor(style.Foreground); + if (_system != ColorSystem.NoColors && (int)foreground != -1) + { + System.Console.ForegroundColor = foreground; + } + } + } +} diff --git a/src/Spectre.Console/Internal/Backends/Fallback/FallbackCursor.cs b/src/Spectre.Console/Internal/Backends/Fallback/FallbackCursor.cs new file mode 100644 index 0000000..475aa87 --- /dev/null +++ b/src/Spectre.Console/Internal/Backends/Fallback/FallbackCursor.cs @@ -0,0 +1,40 @@ +namespace Spectre.Console.Internal +{ + internal sealed class FallbackCursor : IAnsiConsoleCursor + { + public void Show(bool show) + { + System.Console.CursorVisible = show; + } + + public void Move(CursorDirection direction, int steps) + { + if (steps == 0) + { + return; + } + + switch (direction) + { + case CursorDirection.Up: + System.Console.CursorTop -= steps; + break; + case CursorDirection.Down: + System.Console.CursorTop += steps; + break; + case CursorDirection.Left: + System.Console.CursorLeft -= steps; + break; + case CursorDirection.Right: + System.Console.CursorLeft += steps; + break; + } + } + + public void SetPosition(int x, int y) + { + System.Console.CursorLeft = x; + System.Console.CursorTop = y; + } + } +} diff --git a/src/Spectre.Console/Internal/Collections/ListWithCallback.cs b/src/Spectre.Console/Internal/Collections/ListWithCallback.cs index 180899f..32e58b7 100644 --- a/src/Spectre.Console/Internal/Collections/ListWithCallback.cs +++ b/src/Spectre.Console/Internal/Collections/ListWithCallback.cs @@ -2,7 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; -namespace Spectre.Console.Internal.Collections +namespace Spectre.Console.Internal { internal sealed class ListWithCallback : IList, IReadOnlyList { diff --git a/src/Spectre.Console/Internal/Utilities/ConsoleHelper.cs b/src/Spectre.Console/Internal/ConsoleHelper.cs similarity index 100% rename from src/Spectre.Console/Internal/Utilities/ConsoleHelper.cs rename to src/Spectre.Console/Internal/ConsoleHelper.cs diff --git a/src/Spectre.Console/Internal/ExceptionFormatter.cs b/src/Spectre.Console/Internal/ExceptionFormatter.cs index 2f5c67a..d55168e 100644 --- a/src/Spectre.Console/Internal/ExceptionFormatter.cs +++ b/src/Spectre.Console/Internal/ExceptionFormatter.cs @@ -1,10 +1,9 @@ using System; using System.Linq; using System.Text; -using Spectre.Console.Internal; using Spectre.Console.Rendering; -namespace Spectre.Console +namespace Spectre.Console.Internal { internal static class ExceptionFormatter { diff --git a/src/Spectre.Console/Internal/FallbackConsoleRenderer.cs b/src/Spectre.Console/Internal/FallbackConsoleRenderer.cs deleted file mode 100644 index 150e961..0000000 --- a/src/Spectre.Console/Internal/FallbackConsoleRenderer.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.IO; -using System.Text; -using Spectre.Console.Rendering; - -namespace Spectre.Console.Internal -{ - internal sealed class FallbackConsoleRenderer : IAnsiConsole - { - private readonly TextWriter _out; - private readonly ColorSystem _system; - private Style? _lastStyle; - - public Capabilities Capabilities { get; } - public Encoding Encoding { get; } - - public int Width - { - get - { - if (_out.IsStandardOut()) - { - return ConsoleHelper.GetSafeBufferWidth(Constants.DefaultBufferWidth); - } - - return Constants.DefaultBufferWidth; - } - } - - public int Height - { - get - { - if (_out.IsStandardOut()) - { - return ConsoleHelper.GetSafeBufferHeight(Constants.DefaultBufferHeight); - } - - return Constants.DefaultBufferHeight; - } - } - - public FallbackConsoleRenderer(TextWriter @out, Capabilities capabilities) - { - if (capabilities == null) - { - throw new ArgumentNullException(nameof(capabilities)); - } - - _out = @out ?? throw new ArgumentNullException(nameof(@out)); - _system = capabilities.ColorSystem; - - if (_out.IsStandardOut()) - { - Encoding = System.Console.OutputEncoding; - } - else - { - Encoding = Encoding.UTF8; - } - - Capabilities = capabilities; - } - - public void Write(Segment segment) - { - if (_lastStyle?.Equals(segment.Style) != true) - { - SetStyle(segment.Style); - } - - _out.Write(segment.Text.NormalizeLineEndings(native: true)); - } - - private void SetStyle(Style style) - { - _lastStyle = style; - - if (_out.IsStandardOut()) - { - System.Console.ResetColor(); - - var background = Color.ToConsoleColor(style.Background); - if (_system != ColorSystem.NoColors && _out.IsStandardOut() && (int)background != -1) - { - System.Console.BackgroundColor = background; - } - - var foreground = Color.ToConsoleColor(style.Foreground); - if (_system != ColorSystem.NoColors && _out.IsStandardOut() && (int)foreground != -1) - { - System.Console.ForegroundColor = foreground; - } - } - } - } -} diff --git a/src/Spectre.Console/Internal/Utilities/Ratio.cs b/src/Spectre.Console/Internal/Ratio.cs similarity index 100% rename from src/Spectre.Console/Internal/Utilities/Ratio.cs rename to src/Spectre.Console/Internal/Ratio.cs diff --git a/src/Spectre.Console/Recorder.cs b/src/Spectre.Console/Recorder.cs index 140319b..1ba1afb 100644 --- a/src/Spectre.Console/Recorder.cs +++ b/src/Spectre.Console/Recorder.cs @@ -19,6 +19,9 @@ namespace Spectre.Console /// public Encoding Encoding => _console.Encoding; + /// + public IAnsiConsoleCursor Cursor => _console.Cursor; + /// public int Width => _console.Width; @@ -41,10 +44,26 @@ namespace Spectre.Console // Only used for scoping. } + /// + public void Clear(bool home) + { + _console.Clear(home); + } + /// public void Write(Segment segment) { - _recorded.Add(segment); + if (segment is null) + { + throw new ArgumentNullException(nameof(segment)); + } + + // Don't record control codes. + if (!segment.IsControlCode) + { + _recorded.Add(segment); + } + _console.Write(segment); } diff --git a/src/Spectre.Console/Rendering/Segment.cs b/src/Spectre.Console/Rendering/Segment.cs index 84b8e95..c7e045f 100644 --- a/src/Spectre.Console/Rendering/Segment.cs +++ b/src/Spectre.Console/Rendering/Segment.cs @@ -31,6 +31,12 @@ namespace Spectre.Console.Rendering /// public bool IsWhiteSpace { get; } + /// + /// Gets a value indicating whether or not his is a + /// control code such as cursor movement. + /// + public bool IsControlCode { get; } + /// /// Gets the segment style. /// @@ -39,12 +45,12 @@ namespace Spectre.Console.Rendering /// /// Gets a segment representing a line break. /// - public static Segment LineBreak { get; } = new Segment(Environment.NewLine, Style.Plain, true); + public static Segment LineBreak { get; } = new Segment(Environment.NewLine, Style.Plain, true, false); /// /// Gets an empty segment. /// - public static Segment Empty { get; } = new Segment(string.Empty, Style.Plain, false); + public static Segment Empty { get; } = new Segment(string.Empty, Style.Plain, false, false); /// /// Initializes a new instance of the class. @@ -61,16 +67,27 @@ namespace Spectre.Console.Rendering /// The segment text. /// The segment style. public Segment(string text, Style style) - : this(text, style, false) + : this(text, style, false, false) { } - private Segment(string text, Style style, bool lineBreak) + private Segment(string text, Style style, bool lineBreak, bool control) { Text = text?.NormalizeLineEndings() ?? throw new ArgumentNullException(nameof(text)); Style = style ?? throw new ArgumentNullException(nameof(style)); IsLineBreak = lineBreak; IsWhiteSpace = string.IsNullOrWhiteSpace(text); + IsControlCode = control; + } + + /// + /// Creates a control segment. + /// + /// The control code. + /// A segment representing a control code. + public static Segment Control(string control) + { + return new Segment(control, Style.Plain, false, true); } /// diff --git a/src/Spectre.Console/Widgets/Calendar.cs b/src/Spectre.Console/Widgets/Calendar.cs index 059b1a7..cd93b0d 100644 --- a/src/Spectre.Console/Widgets/Calendar.cs +++ b/src/Spectre.Console/Widgets/Calendar.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using Spectre.Console.Internal; -using Spectre.Console.Internal.Collections; using Spectre.Console.Rendering; namespace Spectre.Console diff --git a/src/Spectre.Console/Widgets/Grid.cs b/src/Spectre.Console/Widgets/Grid.cs index 0403e72..9fccaf9 100644 --- a/src/Spectre.Console/Widgets/Grid.cs +++ b/src/Spectre.Console/Widgets/Grid.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Spectre.Console.Internal.Collections; +using Spectre.Console.Internal; using Spectre.Console.Rendering; namespace Spectre.Console diff --git a/src/Spectre.Console/Widgets/Table.cs b/src/Spectre.Console/Widgets/Table.cs index 673cb78..f9c297b 100644 --- a/src/Spectre.Console/Widgets/Table.cs +++ b/src/Spectre.Console/Widgets/Table.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using Spectre.Console.Internal; using Spectre.Console.Rendering; -using Spectre.Console.Widgets; namespace Spectre.Console { diff --git a/src/Spectre.Console/Widgets/TableRow.cs b/src/Spectre.Console/Widgets/TableRow.cs index a491148..2ad6466 100644 --- a/src/Spectre.Console/Widgets/TableRow.cs +++ b/src/Spectre.Console/Widgets/TableRow.cs @@ -3,7 +3,7 @@ using System.Collections; using System.Collections.Generic; using Spectre.Console.Rendering; -namespace Spectre.Console.Widgets +namespace Spectre.Console { /// /// Represents a table row.