From fd39193da1bd33f765dcca6f54a48eba6f090fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Sun, 28 Nov 2021 14:23:15 +0100 Subject: [PATCH] Decouple drivers from terminal sequence construction. Also, refactor some core types to make unit testing more practical, e.g. you can now create a custom VirtualTerminal. Fixes #22. Fixes #19. Fixes #32. --- src/core/Drivers/TerminalDriver.Sequences.cs | 307 ------------- src/core/Drivers/TerminalDriver.cs | 2 - src/core/SystemVirtualTerminal.cs | 57 +++ src/core/Terminal.cs | 267 ++--------- src/core/TerminalClearMode.cs | 8 - src/core/TerminalConstants.cs | 12 - src/core/TerminalCursorStyle.cs | 12 - src/core/TerminalKeyMode.cs | 7 - src/core/TerminalScreen.cs | 114 ----- src/core/TerminalSignalContext.cs | 4 +- src/core/TerminalSize.cs | 6 +- src/core/Text/Control/ClearMode.cs | 9 + src/core/Text/Control/ControlBuilder.cs | 424 ++++++++++++++++++ src/core/Text/Control/ControlConstants.cs | 20 + src/core/Text/Control/ControlSequences.cs | 210 +++++++++ src/core/Text/Control/CursorKeyMode.cs | 8 + src/core/Text/Control/CursorStyle.cs | 12 + src/core/Text/Control/KeypadMode.cs | 8 + .../Control/MouseEvents.cs} | 4 +- src/core/Text/Control/ProgressState.cs | 10 + src/core/Text/Control/ScreenBuffer.cs | 8 + src/core/VirtualTerminal.cs | 124 +++++ .../Logging/Terminal/TerminalLoggerOptions.cs | 96 ++-- src/extensions/extensions.csproj | 4 + src/input/TerminalEditor.cs | 5 - src/sample/Program.cs | 34 +- src/sample/Scenarios/AttributeScenario.cs | 105 ++--- src/sample/Scenarios/CursorScenario.cs | 14 +- src/sample/Scenarios/FullScreenScenario.cs | 10 +- src/sample/Scenarios/RawScenario.cs | 4 +- src/sample/Scenarios/ScrollRegionScenario.cs | 36 +- src/sample/Scenarios/SignalScenario.cs | 4 +- src/sample/sample.csproj | 1 + 33 files changed, 1095 insertions(+), 851 deletions(-) delete mode 100644 src/core/Drivers/TerminalDriver.Sequences.cs create mode 100644 src/core/SystemVirtualTerminal.cs delete mode 100644 src/core/TerminalClearMode.cs delete mode 100644 src/core/TerminalConstants.cs delete mode 100644 src/core/TerminalCursorStyle.cs delete mode 100644 src/core/TerminalKeyMode.cs delete mode 100644 src/core/TerminalScreen.cs create mode 100644 src/core/Text/Control/ClearMode.cs create mode 100644 src/core/Text/Control/ControlBuilder.cs create mode 100644 src/core/Text/Control/ControlConstants.cs create mode 100644 src/core/Text/Control/ControlSequences.cs create mode 100644 src/core/Text/Control/CursorKeyMode.cs create mode 100644 src/core/Text/Control/CursorStyle.cs create mode 100644 src/core/Text/Control/KeypadMode.cs rename src/core/{TerminalMouseEvents.cs => Text/Control/MouseEvents.cs} (64%) create mode 100644 src/core/Text/Control/ProgressState.cs create mode 100644 src/core/Text/Control/ScreenBuffer.cs create mode 100644 src/core/VirtualTerminal.cs diff --git a/src/core/Drivers/TerminalDriver.Sequences.cs b/src/core/Drivers/TerminalDriver.Sequences.cs deleted file mode 100644 index 451661f..0000000 --- a/src/core/Drivers/TerminalDriver.Sequences.cs +++ /dev/null @@ -1,307 +0,0 @@ -using static System.TerminalConstants; - -namespace System.Drivers; - -abstract partial class TerminalDriver -{ - // TODO: Decouple all of this from the driver. - - public TerminalMouseEvents MouseEvents { get; private set; } - - public string Title - { - get => _title; - set - { - ArgumentNullException.ThrowIfNull(value); - - lock (_sequenceLock) - { - Sequence($"{OSC}0;{value}{BEL}"); - - _title = value; - } - } - } - - public TerminalKeyMode CursorKeyMode - { - get => _cursor; - set - { - var type = value switch - { - TerminalKeyMode.Normal => 'l', - TerminalKeyMode.Application => 'h', - _ => throw new ArgumentOutOfRangeException(nameof(value)), - }; - - lock (_sequenceLock) - { - Sequence($"{CSI}?1{type}"); - - _cursor = value; - } - } - } - - public TerminalKeyMode NumericKeyMode - { - get => _numeric; - set - { - var type = value switch - { - TerminalKeyMode.Normal => '>', - TerminalKeyMode.Application => '=', - _ => throw new ArgumentOutOfRangeException(nameof(value)), - }; - - lock (_sequenceLock) - { - Sequence($"{ESC}{type}"); - - _numeric = value; - } - } - } - - string _title = string.Empty; - - TerminalKeyMode _cursor; - - TerminalKeyMode _numeric; - - public void SetMouseEvents(TerminalMouseEvents events) - { - lock (_sequenceLock) - { - Sequence($"{CSI}?1003{(events.HasFlag(TerminalMouseEvents.Movement) ? 'h' : 'l')}"); - Sequence($"{CSI}?1006{(events.HasFlag(TerminalMouseEvents.Buttons) ? 'h' : 'l')}"); - - MouseEvents = events; - } - } - - public void Sequence(string value) - { - if (!StdOut.IsRedirected) - StdOut.Write(value); - else if (!StdError.IsRedirected) - StdError.Write(value); - } - - public void Beep() - { - Sequence(BEL); - } - - public void Insert(int count) - { - Sequence($"{CSI}{count}@"); - } - - public void Delete(int count) - { - Sequence($"{CSI}{count}P"); - } - - public void Erase(int count) - { - Sequence($"{CSI}{count}X"); - } - - public void InsertLine(int count) - { - Sequence($"{CSI}{count}L"); - } - - public void DeleteLine(int count) - { - Sequence($"{CSI}{count}M"); - } - - void Clear(char type, TerminalClearMode mode) - { - var m = mode switch - { - TerminalClearMode.Before => 1, - TerminalClearMode.After => 0, - TerminalClearMode.Full => 2, - _ => throw new ArgumentOutOfRangeException(nameof(mode)), - }; - - Sequence($"{CSI}{m}{type}"); - } - - public void ClearScreen(TerminalClearMode mode) - { - Clear('J', mode); - } - - public void ClearLine(TerminalClearMode mode) - { - Clear('K', mode); - } - - public void SetScrollRegion(int top, int bottom) - { - _ = top >= 0 ? true : throw new ArgumentOutOfRangeException(nameof(top)); - _ = bottom >= 0 ? true : throw new ArgumentOutOfRangeException(nameof(bottom)); - - var size = Size; - - // Most terminals will clamp the values so that there are at least 2 lines of scrollable text: One for the last - // line written and one for the current line. Enforce this. We throw TerminalException instead of - // ArgumentException since the Size property could change between the caller observing it and calling this - // method, so it would be somewhat inaccurate to call this condition a programmer error. - if (size.Height - top - bottom < 2) - throw new TerminalException("Scroll region is too small."); - - Sequence($"{CSI}{top + 1};{size.Height - bottom}r"); - } - - void MoveBuffer(char type, int count) - { - _ = count >= 0 ? true : throw new ArgumentOutOfRangeException(nameof(count)); - - if (count != 0) - Sequence($"{CSI}{count}{type}"); - } - - public void MoveBufferUp(int count) - { - MoveBuffer('S', count); - } - - public void MoveBufferDown(int count) - { - MoveBuffer('T', count); - } - - public void MoveCursorTo(int row, int column) - { - _ = row >= 0 ? true : throw new ArgumentOutOfRangeException(nameof(row)); - _ = column >= 0 ? true : throw new ArgumentOutOfRangeException(nameof(column)); - - Sequence($"{CSI}{column + 1};{row + 1}H"); - } - - void MoveCursor(char type, int count) - { - _ = count >= 0 ? true : throw new ArgumentOutOfRangeException(nameof(count)); - - if (count != 0) - Sequence($"{CSI}{count}{type}"); - } - - public void MoveCursorUp(int count) - { - MoveCursor('A', count); - } - - public void MoveCursorDown(int count) - { - MoveCursor('B', count); - } - - public void MoveCursorLeft(int count) - { - MoveCursor('D', count); - } - - public void MoveCursorRight(int count) - { - MoveCursor('C', count); - } - - public void SaveCursorPosition() - { - Sequence($"{ESC}7"); - } - - public void RestoreCursorPosition() - { - Sequence($"{ESC}8"); - } - - public void ForegroundColor(byte r, byte g, byte b) - { - Sequence($"{CSI}38;2;{r};{g};{b}m"); - } - - public void BackgroundColor(byte r, byte g, byte b) - { - Sequence($"{CSI}48;2;{r};{g};{b}m"); - } - - public void Decorations( - bool bold = false, - bool faint = false, - bool italic = false, - bool underline = false, - bool blink = false, - bool invert = false, - bool invisible = false, - bool strike = false, - bool doubleUnderline = false, - bool overline = false) - { - Span codes = stackalloc char[64]; - - codes.Clear(); - - var i = 0; - - void Handle(Span result, ReadOnlySpan code, bool value) - { - if (!value) - return; - - if (i != 0) - result[i++] = ';'; - - code.CopyTo(result[i..code.Length]); - - i += code.Length; - } - - Handle(codes, "1", bold); - Handle(codes, "2", faint); - Handle(codes, "3", italic); - Handle(codes, "4", underline); - Handle(codes, "5", blink); - Handle(codes, "7", invert); - Handle(codes, "8", invisible); - Handle(codes, "9", strike); - Handle(codes, "21", doubleUnderline); - Handle(codes, "53", overline); - - Sequence($"{CSI}{codes.TrimEnd(char.MinValue).ToString()}m"); - } - - public void ResetAttributes() - { - Sequence($"{CSI}0m"); - } - - public void OpenHyperlink(Uri uri, string? id = null) - { - ArgumentNullException.ThrowIfNull(uri); - - if (id != null) - id = $"id={id}"; - - Sequence($"{OSC}8;{id};{uri}{BEL}"); - } - - public void CloseHyperlink() - { - Sequence($"{OSC}8;;{BEL}"); - } - - public void ResetAll() - { - Sequence($"{CSI}!p"); - } -} diff --git a/src/core/Drivers/TerminalDriver.cs b/src/core/Drivers/TerminalDriver.cs index afebd06..788b75d 100644 --- a/src/core/Drivers/TerminalDriver.cs +++ b/src/core/Drivers/TerminalDriver.cs @@ -51,8 +51,6 @@ public TerminalSize Size readonly object _rawLock = new(); - readonly object _sequenceLock = new(); - readonly ManualResetEventSlim _event = new(); [SuppressMessage("Style", "IDE0052")] diff --git a/src/core/SystemVirtualTerminal.cs b/src/core/SystemVirtualTerminal.cs new file mode 100644 index 0000000..59d4ac8 --- /dev/null +++ b/src/core/SystemVirtualTerminal.cs @@ -0,0 +1,57 @@ +using System.Drivers; +using System.Drivers.Unix; +using System.Drivers.Windows; + +namespace System; + +public sealed class SystemVirtualTerminal : VirtualTerminal +{ + public override event Action? Resize + { + add => _driver.Resize += value; + remove => _driver.Resize -= value; + } + + public override event Action? Signal + { + add => _driver.Signal += value; + remove => _driver.Signal -= value; + } + + public static SystemVirtualTerminal Instance { get; } = new(); + + public override TerminalReader StdIn => _driver.StdIn; + + public override TerminalWriter StdOut => _driver.StdOut; + + public override TerminalWriter StdError => _driver.StdError; + + public override bool IsRawMode => _driver.IsRawMode; + + public override TerminalSize Size => _driver.Size; + + readonly TerminalDriver _driver = + OperatingSystem.IsLinux() ? LinuxTerminalDriver.Instance : + OperatingSystem.IsMacOS() ? MacOSTerminalDriver.Instance : + OperatingSystem.IsWindows() ? WindowsTerminalDriver.Instance : + throw new TerminalException("This platform is not supported."); + + SystemVirtualTerminal() + { + } + + public override void GenerateSignal(TerminalSignal signal) + { + _driver.GenerateSignal(signal); + } + + public override void EnableRawMode() + { + _driver.EnableRawMode(); + } + + public override void DisableRawMode() + { + _driver.DisableRawMode(); + } +} diff --git a/src/core/Terminal.cs b/src/core/Terminal.cs index 227867a..eed845b 100644 --- a/src/core/Terminal.cs +++ b/src/core/Terminal.cs @@ -1,346 +1,145 @@ -using System.Drivers; -using System.Drivers.Unix; -using System.Drivers.Windows; - namespace System; public static class Terminal { public static event Action? Resize { - add => _driver.Resize += value; - remove => _driver.Resize -= value; + add => System.Resize += value; + remove => System.Resize -= value; } public static event Action? Signal { - add => _driver.Signal += value; - remove => _driver.Signal -= value; + add => System.Signal += value; + remove => System.Signal -= value; } public static Encoding Encoding { get; } = new UTF8Encoding(false); - public static TerminalReader StdIn => _driver.StdIn; - - public static TerminalWriter StdOut => _driver.StdOut; + public static SystemVirtualTerminal System => SystemVirtualTerminal.Instance; - public static TerminalWriter StdError => _driver.StdError; + public static TerminalReader StdIn => System.StdIn; - public static bool IsRawMode => _driver.IsRawMode; + public static TerminalWriter StdOut => System.StdOut; - public static TerminalMouseEvents MouseEvents => _driver.MouseEvents; + public static TerminalWriter StdError => System.StdError; - public static string Title - { - get => _driver.Title; - set => _driver.Title = value; - } + public static bool IsRawMode => System.IsRawMode; - public static TerminalSize Size => _driver.Size; - - public static TerminalKeyMode CursorKeyMode - { - get => _driver.CursorKeyMode; - set => _driver.CursorKeyMode = value; - } - - public static TerminalKeyMode NumericKeyMode - { - get => _driver.NumericKeyMode; - set => _driver.NumericKeyMode = value; - } - - public static bool IsCursorVisible - { - get => Screen.IsCursorVisible; - set => Screen.IsCursorVisible = value; - } - - public static TerminalCursorStyle CursorStyle - { - get => Screen.CursorStyle; - set => Screen.CursorStyle = value; - } - - public static TerminalScreen MainScreen { get; } - - public static TerminalScreen AlternateScreen { get; } - - public static TerminalScreen Screen { get; internal set; } - - static readonly TerminalDriver _driver = - OperatingSystem.IsWindows() ? WindowsTerminalDriver.Instance : - OperatingSystem.IsLinux() ? LinuxTerminalDriver.Instance : - OperatingSystem.IsMacOS() ? MacOSTerminalDriver.Instance : - throw new TerminalException("This platforms is not supported."); - - static Terminal() - { - MainScreen = new(_driver); - AlternateScreen = new(_driver); - Screen = MainScreen; - - // Reset all terminal state to sane values. - _driver.ResetAll(); - } + public static TerminalSize Size => System.Size; public static void GenerateSignal(TerminalSignal signal) { - _driver.GenerateSignal(signal); + System.GenerateSignal(signal); } public static void EnableRawMode() { - _driver.EnableRawMode(); + System.EnableRawMode(); } public static void DisableRawMode() { - _driver.DisableRawMode(); - } - - public static void SetMouseEvents(TerminalMouseEvents events) - { - _driver.SetMouseEvents(events); + System.DisableRawMode(); } public static byte? ReadRaw() { - return StdIn.ReadRaw(); + return System.ReadRaw(); } public static string? ReadLine() { - return StdIn.ReadLine(); + return System.ReadLine(); } public static void Out(ReadOnlySpan value) { - StdOut.Write(value); + System.Out(value); } public static void Out(ReadOnlySpan value) { - StdOut.Write(value); + System.Out(value); } public static void Out(string? value) { - StdOut.Write(value); + System.Out(value); } public static void Out(T value) { - StdOut.Write(value); + System.Out(value); } public static void Out(string format, params object?[] args) { - StdOut.Write(format, args); + System.Out(format, args); } public static void OutLine() { - StdOut.WriteLine(); + System.OutLine(); } public static void OutLine(string? value) { - StdOut.WriteLine(value); + System.OutLine(value); } public static void OutLine(T value) { - StdOut.WriteLine(value); + System.OutLine(value); } public static void OutLine(string format, params object?[] args) { - StdOut.WriteLine(format, args); + System.OutLine(format, args); } public static void Error(ReadOnlySpan value) { - StdError.Write(value); + System.Error(value); } public static void Error(ReadOnlySpan value) { - StdError.Write(value); + System.Error(value); } public static void Error(string? value) { - StdError.Write(value); + System.Error(value); } public static void Error(T value) { - StdError.Write(value); + System.Error(value); } public static void Error(string format, params object?[] args) { - StdError.Write(format, args); + System.Error(format, args); } public static void ErrorLine() { - StdError.WriteLine(); + System.ErrorLine(); } public static void ErrorLine(string? value) { - StdError.WriteLine(value); + System.ErrorLine(value); } public static void ErrorLine(T value) { - StdError.WriteLine(value); + System.ErrorLine(value); } public static void ErrorLine(string format, params object?[] args) { - StdError.WriteLine(format, args); - } - - public static void Beep() - { - _driver.Beep(); - } - - public static void Insert(int count = 1) - { - _driver.Insert(count); - } - - public static void Delete(int count = 1) - { - _driver.Delete(count); - } - - public static void Erase(int count = 1) - { - _driver.Erase(count); - } - - public static void InsertLine(int count = 1) - { - _driver.InsertLine(count); - } - - public static void DeleteLine(int count = 1) - { - _driver.DeleteLine(count); - } - - public static void ClearScreen(TerminalClearMode mode = TerminalClearMode.Full) - { - _driver.ClearScreen(mode); - } - - public static void ClearLine(TerminalClearMode mode = TerminalClearMode.Full) - { - _driver.ClearLine(mode); - } - - public static void SetScrollRegion(int top, int bottom) - { - _driver.SetScrollRegion(top, bottom); - } - - public static void MoveBufferUp(int count = 1) - { - _driver.MoveBufferUp(count); - } - - public static void MoveBufferDown(int count = 1) - { - _driver.MoveBufferDown(count); - } - - public static void MoveCursorTo(int row, int column) - { - _driver.MoveCursorTo(row, column); - } - - public static void MoveCursorUp(int count = 1) - { - _driver.MoveCursorUp(count); - } - - public static void MoveCursorDown(int count = 1) - { - _driver.MoveCursorDown(count); - } - - public static void MoveCursorLeft(int count = 1) - { - _driver.MoveCursorLeft(count); - } - - public static void MoveCursorRight(int count = 1) - { - _driver.MoveCursorRight(count); - } - - public static void SaveCursorPosition() - { - _driver.SaveCursorPosition(); - } - - public static void RestoreCursorPosition() - { - _driver.RestoreCursorPosition(); - } - - public static void ForegroundColor(byte r, byte g, byte b) - { - _driver.ForegroundColor(r, g, b); - } - - public static void BackgroundColor(byte r, byte g, byte b) - { - _driver.BackgroundColor(r, g, b); - } - - public static void Decorations( - bool bold = false, - bool faint = false, - bool italic = false, - bool underline = false, - bool blink = false, - bool invert = false, - bool invisible = false, - bool strike = false, - bool doubleUnderline = false, - bool overline = false) - { - _driver.Decorations( - bold, - faint, - italic, - underline, - blink, - invert, - invisible, - strike, - doubleUnderline, - overline); - } - - public static void ResetAttributes() - { - _driver.ResetAttributes(); - } - - public static void OpenHyperlink(Uri uri, string? id = null) - { - _driver.OpenHyperlink(uri, id); - } - - public static void CloseHyperlink() - { - _driver.CloseHyperlink(); + System.ErrorLine(format, args); } } diff --git a/src/core/TerminalClearMode.cs b/src/core/TerminalClearMode.cs deleted file mode 100644 index 7022549..0000000 --- a/src/core/TerminalClearMode.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace System; - -public enum TerminalClearMode -{ - Before, - After, - Full, -} diff --git a/src/core/TerminalConstants.cs b/src/core/TerminalConstants.cs deleted file mode 100644 index 51da08c..0000000 --- a/src/core/TerminalConstants.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace System; - -static class TerminalConstants -{ - public const string ESC = "\x1b"; - - public const string CSI = ESC + "["; - - public const string OSC = ESC + "]"; - - public const string BEL = "\a"; -} diff --git a/src/core/TerminalCursorStyle.cs b/src/core/TerminalCursorStyle.cs deleted file mode 100644 index 6cefbed..0000000 --- a/src/core/TerminalCursorStyle.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace System; - -public enum TerminalCursorStyle -{ - Default, - BlockStatic, - BlockBlinking, - UnderlineStatic, - UnderlineBlinking, - BarStatic, - BarBlinking, -} diff --git a/src/core/TerminalKeyMode.cs b/src/core/TerminalKeyMode.cs deleted file mode 100644 index 0d38822..0000000 --- a/src/core/TerminalKeyMode.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace System; - -public enum TerminalKeyMode -{ - Normal, - Application, -} diff --git a/src/core/TerminalScreen.cs b/src/core/TerminalScreen.cs deleted file mode 100644 index 7dd62e7..0000000 --- a/src/core/TerminalScreen.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Drivers; -using static System.TerminalConstants; - -namespace System; - -public sealed class TerminalScreen -{ - [SuppressMessage("Performance", "CA1815")] - public readonly struct ScreenActivator : IDisposable - { - public TerminalScreen NewScreen { get; } - - public TerminalScreen OldScreen { get; } - - internal ScreenActivator(TerminalScreen screen) - { - NewScreen = screen; - OldScreen = Terminal.Screen; - - Switch(screen); - } - - public void Dispose() - { - // We might be default-initialized. - if (OldScreen != null) - Switch(OldScreen); - } - - static void Switch(TerminalScreen screen) - { - lock (_lock) - { - screen._driver.Sequence($"{CSI}?1049{(screen.IsMain ? 'l' : 'h')}"); - - Terminal.Screen = screen; - } - } - } - - public bool IsMain => this == Terminal.MainScreen; - - public bool IsAlternate => this == Terminal.AlternateScreen; - - public bool IsActive => this == Terminal.Screen; - - public bool IsCursorVisible - { - get => _visible; - set - { - lock (_lock) - { - CheckActive(); - - _driver.Sequence($"{CSI}?25{(value ? 'h' : 'l')}"); - - _visible = value; - } - } - } - - public TerminalCursorStyle CursorStyle - { - get => _style; - set - { - var type = value switch - { - TerminalCursorStyle.Default => '0', - TerminalCursorStyle.BlockStatic => '2', - TerminalCursorStyle.BlockBlinking => '1', - TerminalCursorStyle.UnderlineStatic => '4', - TerminalCursorStyle.UnderlineBlinking => '3', - TerminalCursorStyle.BarStatic => '6', - TerminalCursorStyle.BarBlinking => '5', - _ => throw new ArgumentOutOfRangeException(nameof(value)), - }; - - lock (_lock) - { - CheckActive(); - - _driver.Sequence($"{CSI}{type} q"); - - _style = value; - } - } - } - - static readonly object _lock = new(); - - readonly TerminalDriver _driver; - - bool _visible = true; - - TerminalCursorStyle _style; - - internal TerminalScreen(TerminalDriver driver) - { - _driver = driver; - } - - public ScreenActivator Activate() - { - return new(this); - } - - void CheckActive() - { - if (!IsActive) - throw new InvalidOperationException("This screen is inactive."); - } -} diff --git a/src/core/TerminalSignalContext.cs b/src/core/TerminalSignalContext.cs index 9cc581a..f853543 100644 --- a/src/core/TerminalSignalContext.cs +++ b/src/core/TerminalSignalContext.cs @@ -6,8 +6,10 @@ public sealed class TerminalSignalContext public bool Cancel { get; set; } - internal TerminalSignalContext(TerminalSignal signal) + public TerminalSignalContext(TerminalSignal signal) { + _ = Enum.IsDefined(signal) ? true : throw new ArgumentOutOfRangeException(nameof(signal)); + Signal = signal; } } diff --git a/src/core/TerminalSize.cs b/src/core/TerminalSize.cs index db9af4d..a0a8907 100644 --- a/src/core/TerminalSize.cs +++ b/src/core/TerminalSize.cs @@ -6,8 +6,12 @@ namespace System; public int Height { get; } - internal TerminalSize(int width, int height) + public TerminalSize(int width, int height) { + // TODO: Can these ever actually be zero? + _ = width >= 0 ? true : throw new ArgumentOutOfRangeException(nameof(width)); + _ = height >= 0 ? true : throw new ArgumentOutOfRangeException(nameof(height)); + Width = width; Height = height; } diff --git a/src/core/Text/Control/ClearMode.cs b/src/core/Text/Control/ClearMode.cs new file mode 100644 index 0000000..0a7409a --- /dev/null +++ b/src/core/Text/Control/ClearMode.cs @@ -0,0 +1,9 @@ +namespace System.Text.Control; + +[SuppressMessage("Design", "CA1008")] +public enum ClearMode +{ + After = 0, + Before = 1, + Full = 2, +} diff --git a/src/core/Text/Control/ControlBuilder.cs b/src/core/Text/Control/ControlBuilder.cs new file mode 100644 index 0000000..70d65a0 --- /dev/null +++ b/src/core/Text/Control/ControlBuilder.cs @@ -0,0 +1,424 @@ +using static System.Text.Control.ControlConstants; + +namespace System.Text.Control; + +public sealed class ControlBuilder +{ + // TODO: We are missing many escape sequences. + + public ReadOnlySpan Span => _writer.WrittenSpan; + + readonly ArrayBufferWriter _writer; + + public ControlBuilder(int capacity = 1024) + { + _ = capacity > 0 ? true : throw new ArgumentOutOfRangeException(nameof(capacity)); + + _writer = new(capacity); + } + + public void Clear() + { + _writer.Clear(); + } + + public override string ToString() + { + return Span.ToString(); + } + + public ControlBuilder Print(ReadOnlySpan value) + { + _writer.Write(value); + + return this; + } + + public ControlBuilder Print(string? value) + { + return Print(value.AsSpan()); + } + + public ControlBuilder Print(T value) + { + return Print(value?.ToString()); + } + + public ControlBuilder Print(string format, params object?[] args) + { + return Print(string.Format(CultureInfo.CurrentCulture, format, args)); + } + + public ControlBuilder PrintLine() + { + return PrintLine(null); + } + + public ControlBuilder PrintLine(string? value) + { + return Print(value + Environment.NewLine); + } + + public ControlBuilder PrintLine(T value) + { + return PrintLine(value?.ToString()); + } + + public ControlBuilder PrintLine(string format, params object?[] args) + { + return PrintLine(string.Format(CultureInfo.CurrentCulture, format, args)); + } + + // Keep methods in sync with the ControlSequences class. + + public ControlBuilder Beep() + { + return Print(BEL); + } + + public ControlBuilder LineFeed() + { + return Print(LF); + } + + public ControlBuilder CarriageReturn() + { + return Print(CR); + } + + public ControlBuilder Space() + { + return Print(SP); + } + + public ControlBuilder SetTitle(string title) + { + ArgumentNullException.ThrowIfNull(title); + + return Print(OSC).Print("0;").Print(title).Print(ST); + } + + public ControlBuilder SetProgress(ProgressState state, int value) + { + _ = Math.Clamp(value, 0, 100) == value ? true : throw new ArgumentOutOfRangeException(nameof(value)); + + Span stateSpan = stackalloc char[32]; + Span valueSpan = stackalloc char[32]; + + _ = ((int)state).TryFormat(stateSpan, out var stateLen); + _ = value.TryFormat(valueSpan, out var valueLen); + + return Print(OSC).Print("9;4;").Print(stateSpan[..stateLen]).Print(";").Print(valueSpan[..valueLen]).Print(ST); + } + + public ControlBuilder SetCursorKeyMode(CursorKeyMode mode) + { + _ = Enum.IsDefined(mode) ? true : throw new ArgumentOutOfRangeException(nameof(mode)); + + var ch = (char)mode; + + return Print(CSI).Print("?1").Print(MemoryMarshal.CreateSpan(ref ch, 1)); + } + + public ControlBuilder SetKeypadMode(KeypadMode mode) + { + _ = Enum.IsDefined(mode) ? true : throw new ArgumentOutOfRangeException(nameof(mode)); + + var ch = (char)mode; + + return Print(ESC).Print(MemoryMarshal.CreateSpan(ref ch, 1)); + } + + public ControlBuilder SetMouseEvents(MouseEvents events) + { + return Print(CSI).Print("?1003").Print(events.HasFlag(MouseEvents.Movement) ? "h" : "l") + .Print(CSI).Print("?1006").Print(events.HasFlag(MouseEvents.Buttons) ? "h" : "l"); + } + + public ControlBuilder SetFocusEvents(bool enable) + { + return Print(CSI).Print("?1004").Print(enable ? "h" : "l"); + } + + public ControlBuilder SetScreenBuffer(ScreenBuffer buffer) + { + _ = Enum.IsDefined(buffer) ? true : throw new ArgumentOutOfRangeException(nameof(buffer)); + + var ch = (char)buffer; + + return Print(CSI).Print("?1049").Print(MemoryMarshal.CreateSpan(ref ch, 1)); + } + + public ControlBuilder SetCursorVisibility(bool visible) + { + return Print(CSI).Print("?25").Print(visible ? "h" : "l"); + } + + public ControlBuilder SetCursorStyle(CursorStyle style) + { + _ = Enum.IsDefined(style) ? true : throw new ArgumentOutOfRangeException(nameof(style)); + + Span styleSpan = stackalloc char[32]; + + _ = ((int)style).TryFormat(styleSpan, out var styleLen); + + return Print(CSI).Print(styleSpan[..styleLen]).Space().Print("q"); + } + + public ControlBuilder SetScrollMargin(int top, int bottom) + { + _ = top >= 0 ? true : throw new ArgumentOutOfRangeException(nameof(top)); + _ = bottom >= 0 ? true : throw new ArgumentOutOfRangeException(nameof(bottom)); + _ = bottom >= top ? true : throw new ArgumentException(null, nameof(bottom)); + + Span topSpan = stackalloc char[32]; + Span bottomSpan = stackalloc char[32]; + + _ = (top + 1).TryFormat(topSpan, out var topLen); + _ = (bottom + 1).TryFormat(bottomSpan, out var bottomLen); + + return Print(CSI).Print(topSpan[..topLen]).Print(";").Print(bottomSpan[..bottomLen]).Print("r"); + } + + ControlBuilder ModifyText(string type, int count) + { + _ = count >= 0 ? true : throw new ArgumentOutOfRangeException(nameof(count)); + + if (count == 0) + return this; + + Span countSpan = stackalloc char[32]; + + _ = count.TryFormat(countSpan, out var countLen); + + return Print(CSI).Print(countSpan[..countLen]).Print(type); + } + + public ControlBuilder InsertCharacters(int count) + { + return ModifyText("@", count); + } + + public ControlBuilder DeleteCharacters(int count) + { + return ModifyText("P", count); + } + + public ControlBuilder EraseCharacters(int count) + { + return ModifyText("X", count); + } + + public ControlBuilder InsertLines(int count) + { + return ModifyText("L", count); + } + + public ControlBuilder DeleteLines(int count) + { + return ModifyText("M", count); + } + + ControlBuilder Clear(string type, ClearMode mode) + { + _ = Enum.IsDefined(mode) ? true : throw new ArgumentOutOfRangeException(nameof(mode)); + + Span modeSpan = stackalloc char[32]; + + _ = ((int)mode).TryFormat(modeSpan, out var modeLen); + + return Print(CSI).Print(modeSpan[..modeLen]).Print(type); + } + + public ControlBuilder ClearScreen(ClearMode mode = ClearMode.Full) + { + return Clear("J", mode); + } + + public ControlBuilder ClearLine(ClearMode mode = ClearMode.Full) + { + return Clear("K", mode); + } + + ControlBuilder MoveBuffer(string type, int count) + { + _ = count >= 0 ? true : throw new ArgumentOutOfRangeException(nameof(count)); + + if (count == 0) + return this; + + Span countSpan = stackalloc char[32]; + + _ = count.TryFormat(countSpan, out var countLen); + + return Print(CSI).Print(countSpan[..countLen]).Print(type); + } + + public ControlBuilder MoveBufferUp(int count) + { + return MoveBuffer("S", count); + } + + public ControlBuilder MoveBufferDown(int count) + { + return MoveBuffer("T", count); + } + + public ControlBuilder MoveCursorTo(int line, int column) + { + _ = line >= 0 ? true : throw new ArgumentOutOfRangeException(nameof(line)); + _ = column >= 0 ? true : throw new ArgumentOutOfRangeException(nameof(column)); + + Span lineSpan = stackalloc char[32]; + Span columnSpan = stackalloc char[32]; + + _ = (line + 1).TryFormat(lineSpan, out var lineLen); + _ = (column + 1).TryFormat(columnSpan, out var columnLen); + + return Print(CSI).Print(columnSpan[..columnLen]).Print(";").Print(lineSpan[..lineLen]).Print("H"); + } + + ControlBuilder MoveCursor(string type, int count) + { + _ = count >= 0 ? true : throw new ArgumentOutOfRangeException(nameof(count)); + + if (count == 0) + return this; + + Span countSpan = stackalloc char[32]; + + _ = count.TryFormat(countSpan, out var countLen); + + return Print(CSI).Print(countSpan[..countLen]).Print(type); + } + + public ControlBuilder MoveCursorUp(int count) + { + return MoveCursor("A", count); + } + + public ControlBuilder MoveCursorDown(int count) + { + return MoveCursor("B", count); + } + + public ControlBuilder MoveCursorLeft(int count) + { + return MoveCursor("D", count); + } + + public ControlBuilder MoveCursorRight(int count) + { + return MoveCursor("C", count); + } + + public ControlBuilder SaveCursorState() + { + return Print(ESC).Print("7"); + } + + public ControlBuilder RestoreCursorState() + { + return Print(ESC).Print("8"); + } + + public ControlBuilder SetForegroundColor(byte r, byte g, byte b) + { + Span rSpan = stackalloc char[32]; + Span gSpan = stackalloc char[32]; + Span bSpan = stackalloc char[32]; + + _ = r.TryFormat(rSpan, out var rLen); + _ = g.TryFormat(gSpan, out var gLen); + _ = b.TryFormat(bSpan, out var bLen); + + return Print(CSI).Print("38;2;").Print(rSpan[..rLen]).Print(";") + .Print(gSpan[..gLen]).Print(";").Print(bSpan[..bLen]).Print("m"); + } + + public ControlBuilder SetBackgroundColor(byte r, byte g, byte b) + { + Span rSpan = stackalloc char[32]; + Span gSpan = stackalloc char[32]; + Span bSpan = stackalloc char[32]; + + _ = r.TryFormat(rSpan, out var rLen); + _ = g.TryFormat(gSpan, out var gLen); + _ = b.TryFormat(bSpan, out var bLen); + + return Print(CSI).Print("48;2;").Print(rSpan[..rLen]).Print(";") + .Print(gSpan[..gLen]).Print(";").Print(bSpan[..bLen]).Print("m"); + } + + public ControlBuilder SetDecorations( + bool bold = false, + bool faint = false, + bool italic = false, + bool underline = false, + bool blink = false, + bool invert = false, + bool invisible = false, + bool strike = false, + bool doubleUnderline = false, + bool overline = false) + { + _ = Print(CSI); + + var i = 0; + + void HandleMode(bool value, ReadOnlySpan code) + { + if (!value) + return; + + if (i != 0) + _ = Print(";"); + + i++; + + _ = Print(code); + } + + // TODO: Support more exotic decorations? + HandleMode(bold, "1"); + HandleMode(faint, "2"); + HandleMode(italic, "3"); + HandleMode(underline, "4"); + HandleMode(blink, "5"); + HandleMode(invert, "7"); + HandleMode(invisible, "8"); + HandleMode(strike, "9"); + HandleMode(doubleUnderline, "21"); + HandleMode(overline, "53"); + + return Print("m"); + } + + public ControlBuilder ResetAttributes() + { + return Print(CSI).Print("0m"); + } + + public ControlBuilder OpenHyperlink(Uri uri, ReadOnlySpan id = default) + { + ArgumentNullException.ThrowIfNull(uri); + + _ = Print(OSC).Print("8;"); + + if (!id.IsEmpty) + _ = Print("id=").Print(id); + + // TODO: Avoid string allocation for the URI value. + return Print(";").Print(uri).Print(ST); + } + + public ControlBuilder CloseHyperlink() + { + return Print(OSC).Print("8;;").Print(ST); + } + + public ControlBuilder SoftReset() + { + return Print(CSI).Print("!p"); + } +} diff --git a/src/core/Text/Control/ControlConstants.cs b/src/core/Text/Control/ControlConstants.cs new file mode 100644 index 0000000..b700b9e --- /dev/null +++ b/src/core/Text/Control/ControlConstants.cs @@ -0,0 +1,20 @@ +namespace System.Text.Control; + +public static class ControlConstants +{ + public const string ESC = "\x1b"; + + public const string CSI = $"{ESC}["; + + public const string OSC = $"{ESC}]"; + + public const string ST = $"{ESC}\\"; + + public const string BEL = "\a"; + + public const string LF = "\n"; + + public const string CR = "\r"; + + public const string SP = " "; +} diff --git a/src/core/Text/Control/ControlSequences.cs b/src/core/Text/Control/ControlSequences.cs new file mode 100644 index 0000000..894b1e7 --- /dev/null +++ b/src/core/Text/Control/ControlSequences.cs @@ -0,0 +1,210 @@ +namespace System.Text.Control; + +public static class ControlSequences +{ + static readonly ThreadLocal _builder = new(() => new()); + + static string Create(ReadOnlySpanAction action, ReadOnlySpan span) + { + var cb = _builder.Value!; + + cb.Clear(); + + action(span, cb); + + return cb.ToString(); + } + + static string Create(Action action) + { + return Create((_, cb) => action(cb), ReadOnlySpan.Empty); + } + + // Keep methods in sync with the ControlStringBuilder class. + + public static string SetTitle(string title) + { + return Create(cb => cb.SetTitle(title)); + } + + public static string SetProgress(ProgressState state, int value) + { + return Create(cb => cb.SetProgress(state, value)); + } + + public static string SetCursorKeyMode(CursorKeyMode mode) + { + return Create(cb => cb.SetCursorKeyMode(mode)); + } + + public static string SetKeypadMode(KeypadMode mode) + { + return Create(cb => cb.SetKeypadMode(mode)); + } + + public static string SetMouseEvents(MouseEvents events) + { + return Create(cb => cb.SetMouseEvents(events)); + } + + public static string SetFocusEvents(bool enable) + { + return Create(cb => cb.SetFocusEvents(enable)); + } + + public static string SetScreenBuffer(ScreenBuffer buffer) + { + return Create(cb => cb.SetScreenBuffer(buffer)); + } + + public static string SetCursorVisibility(bool visible) + { + return Create(cb => cb.SetCursorVisibility(visible)); + } + + public static string SetCursorStyle(CursorStyle style) + { + return Create(cb => cb.SetCursorStyle(style)); + } + + public static string SetScrollMargin(int top, int bottom) + { + return Create(cb => cb.SetScrollMargin(top, bottom)); + } + + public static string InsertCharacters(int count) + { + return Create(cb => cb.InsertCharacters(count)); + } + + public static string DeleteCharacters(int count) + { + return Create(cb => cb.DeleteCharacters(count)); + } + + public static string EraseCharacters(int count) + { + return Create(cb => cb.EraseCharacters(count)); + } + + public static string InsertLines(int count) + { + return Create(cb => cb.InsertLines(count)); + } + + public static string DeleteLines(int count) + { + return Create(cb => cb.DeleteLines(count)); + } + + public static string ClearScreen(ClearMode mode = ClearMode.Full) + { + return Create(cb => cb.ClearScreen(mode)); + } + + public static string ClearLine(ClearMode mode = ClearMode.Full) + { + return Create(cb => cb.ClearLine(mode)); + } + + public static string MoveBufferUp(int count) + { + return Create(cb => cb.MoveBufferUp(count)); + } + + public static string MoveBufferDown(int count) + { + return Create(cb => cb.MoveBufferDown(count)); + } + + public static string MoveCursorTo(int line, int column) + { + return Create(cb => cb.MoveCursorTo(line, column)); + } + + public static string MoveCursorUp(int count) + { + return Create(cb => cb.MoveCursorUp(count)); + } + + public static string MoveCursorDown(int count) + { + return Create(cb => cb.MoveCursorDown(count)); + } + + public static string MoveCursorLeft(int count) + { + return Create(cb => cb.MoveCursorLeft(count)); + } + + public static string MoveCursorRight(int count) + { + return Create(cb => cb.MoveCursorRight(count)); + } + + public static string SaveCursorState() + { + return Create(cb => cb.SaveCursorState()); + } + + public static string RestoreCursorState() + { + return Create(cb => cb.RestoreCursorState()); + } + + public static string SetForegroundColor(byte r, byte g, byte b) + { + return Create(cb => cb.SetForegroundColor(r, g, b)); + } + + public static string SetBackgroundColor(byte r, byte g, byte b) + { + return Create(cb => cb.SetForegroundColor(r, g, b)); + } + + public static string SetDecorations( + bool bold = false, + bool faint = false, + bool italic = false, + bool underline = false, + bool blink = false, + bool invert = false, + bool invisible = false, + bool strike = false, + bool doubleUnderline = false, + bool overline = false) + { + return Create( + cb => cb.SetDecorations( + bold, + faint, + italic, + underline, + blink, + invert, + invisible, + strike, + doubleUnderline, + overline)); + } + + public static string ResetAttributes() + { + return Create(cb => cb.ResetAttributes()); + } + + public static string OpenHyperlink(Uri uri, ReadOnlySpan id = default) + { + return Create((id, csb) => csb.OpenHyperlink(uri, id), id); + } + + public static string CloseHyperlink() + { + return Create(cb => cb.CloseHyperlink()); + } + + public static string SoftReset() + { + return Create(cb => cb.SoftReset()); + } +} diff --git a/src/core/Text/Control/CursorKeyMode.cs b/src/core/Text/Control/CursorKeyMode.cs new file mode 100644 index 0000000..373498a --- /dev/null +++ b/src/core/Text/Control/CursorKeyMode.cs @@ -0,0 +1,8 @@ +namespace System.Text.Control; + +[SuppressMessage("Design", "CA1008")] +public enum CursorKeyMode +{ + Normal = 'l', + Application = 'h', +} diff --git a/src/core/Text/Control/CursorStyle.cs b/src/core/Text/Control/CursorStyle.cs new file mode 100644 index 0000000..afc6009 --- /dev/null +++ b/src/core/Text/Control/CursorStyle.cs @@ -0,0 +1,12 @@ +namespace System.Text.Control; + +public enum CursorStyle +{ + Default = 0, + BlinkingBlock = 1, + StaticBlock = 2, + BlinkingUnderline = 3, + StaticUnderline = 4, + BlinkingBar = 5, + StaticBar = 6, +} diff --git a/src/core/Text/Control/KeypadMode.cs b/src/core/Text/Control/KeypadMode.cs new file mode 100644 index 0000000..79c12ab --- /dev/null +++ b/src/core/Text/Control/KeypadMode.cs @@ -0,0 +1,8 @@ +namespace System.Text.Control; + +[SuppressMessage("Design", "CA1008")] +public enum KeypadMode +{ + Numeric = '>', + Application = '=', +} diff --git a/src/core/TerminalMouseEvents.cs b/src/core/Text/Control/MouseEvents.cs similarity index 64% rename from src/core/TerminalMouseEvents.cs rename to src/core/Text/Control/MouseEvents.cs index f46c5f6..d7c10d6 100644 --- a/src/core/TerminalMouseEvents.cs +++ b/src/core/Text/Control/MouseEvents.cs @@ -1,7 +1,7 @@ -namespace System; +namespace System.Text.Control; [Flags] -public enum TerminalMouseEvents +public enum MouseEvents { None = 0b00, Movement = 0b01, diff --git a/src/core/Text/Control/ProgressState.cs b/src/core/Text/Control/ProgressState.cs new file mode 100644 index 0000000..f777716 --- /dev/null +++ b/src/core/Text/Control/ProgressState.cs @@ -0,0 +1,10 @@ +namespace System.Text.Control; + +public enum ProgressState +{ + None = 0, + Running = 1, + Error = 2, + Indeterminate = 3, + Warning = 4, +} diff --git a/src/core/Text/Control/ScreenBuffer.cs b/src/core/Text/Control/ScreenBuffer.cs new file mode 100644 index 0000000..2f3d35a --- /dev/null +++ b/src/core/Text/Control/ScreenBuffer.cs @@ -0,0 +1,8 @@ +namespace System.Text.Control; + +[SuppressMessage("Design", "CA1008")] +public enum ScreenBuffer +{ + Main = 'l', + Alternate = 'h', +} diff --git a/src/core/VirtualTerminal.cs b/src/core/VirtualTerminal.cs new file mode 100644 index 0000000..c213919 --- /dev/null +++ b/src/core/VirtualTerminal.cs @@ -0,0 +1,124 @@ +namespace System; + +public abstract class VirtualTerminal +{ + public abstract event Action? Resize; + + public abstract event Action? Signal; + + public abstract TerminalReader StdIn { get; } + + public abstract TerminalWriter StdOut { get; } + + public abstract TerminalWriter StdError { get; } + + public abstract bool IsRawMode { get; } + + public abstract TerminalSize Size { get; } + + public abstract void GenerateSignal(TerminalSignal signal); + + public abstract void EnableRawMode(); + + public abstract void DisableRawMode(); + + public byte? ReadRaw() + { + return StdIn.ReadRaw(); + } + + public string? ReadLine() + { + return StdIn.ReadLine(); + } + + public void Out(ReadOnlySpan value) + { + StdOut.Write(value); + } + + public void Out(ReadOnlySpan value) + { + StdOut.Write(value); + } + + public void Out(string? value) + { + StdOut.Write(value); + } + + public void Out(T value) + { + StdOut.Write(value); + } + + public void Out(string format, params object?[] args) + { + StdOut.Write(format, args); + } + + public void OutLine() + { + StdOut.WriteLine(); + } + + public void OutLine(string? value) + { + StdOut.WriteLine(value); + } + + public void OutLine(T value) + { + StdOut.WriteLine(value); + } + + public void OutLine(string format, params object?[] args) + { + StdOut.WriteLine(format, args); + } + + public void Error(ReadOnlySpan value) + { + StdError.Write(value); + } + + public void Error(ReadOnlySpan value) + { + StdError.Write(value); + } + + public void Error(string? value) + { + StdError.Write(value); + } + + public void Error(T value) + { + StdError.Write(value); + } + + public void Error(string format, params object?[] args) + { + StdError.Write(format, args); + } + + public void ErrorLine() + { + StdError.WriteLine(); + } + + public void ErrorLine(string? value) + { + StdError.WriteLine(value); + } + + public void ErrorLine(T value) + { + StdError.WriteLine(value); + } + + public void ErrorLine(string format, params object?[] args) + { + StdError.WriteLine(format, args); + } +} diff --git a/src/extensions/Logging/Terminal/TerminalLoggerOptions.cs b/src/extensions/Logging/Terminal/TerminalLoggerOptions.cs index cb32087..7976aac 100644 --- a/src/extensions/Logging/Terminal/TerminalLoggerOptions.cs +++ b/src/extensions/Logging/Terminal/TerminalLoggerOptions.cs @@ -4,24 +4,22 @@ public sealed class TerminalLoggerOptions { readonly ref struct Decorator { + readonly ControlBuilder _builder; + readonly bool _set; - public Decorator((byte R, byte G, byte B)? colors) + public Decorator(ControlBuilder builder, byte r, byte g, byte b) { - if (colors is (byte r, byte g, byte b)) - { - _set = true; - - System.Terminal.ForegroundColor(r, g, b); - } - else - _set = false; + _builder = builder; + _set = true; + + _ = builder.SetForegroundColor(r, g, b); } public void Dispose() { if (_set) - System.Terminal.ResetAttributes(); + _ = _builder.ResetAttributes(); } } @@ -30,8 +28,7 @@ public LogLevel LogToStandardErrorThreshold get => _logToStandardErrorThreshold; set { - if (!Enum.IsDefined(typeof(LogLevel), value)) - throw new ArgumentOutOfRangeException(nameof(value)); + _ = Enum.IsDefined(value) ? true : throw new ArgumentOutOfRangeException(nameof(value)); _logToStandardErrorThreshold = value; } @@ -55,6 +52,8 @@ public int LogQueueSize public TerminalLoggerWriter Writer { get; set; } = DefaultWriter; + static readonly ThreadLocal _builder = new(() => new()); + LogLevel _logToStandardErrorThreshold; int _logQueueSize = 4096; @@ -71,59 +70,54 @@ public static void DefaultWriter( if (entry.CategoryName == null) throw new ArgumentException(null, nameof(entry)); - Decorator Decorate((byte R, byte G, byte B)? colors) + var (lvl, r, g, b) = entry.LogLevel switch { - return !options.DisableColors && colors is not null and var c ? new(c) : default; - } - - writer.Write("["); + LogLevel.Trace => ("TRC", 127, 0, 127), + LogLevel.Debug => ("DBG", 0, 127, 255), + LogLevel.Information => ("INF", 255, 255, 255), + LogLevel.Warning => ("WRN", 255, 255, 0), + LogLevel.Error => ("ERR", 255, 63, 0), + LogLevel.Critical => ("CRT", 255, 0, 0), + _ => throw new ArgumentException(null, nameof(entry)), + }; - using (_ = Decorate((127, 127, 127))) - writer.Write(entry.Timestamp.ToString("HH:mm:ss.fff", CultureInfo.CurrentCulture)); + var cb = _builder.Value!; - writer.Write("]["); + cb.Clear(); - (byte R, byte G, byte B)? colors = entry.LogLevel switch + Decorator Decorate(byte r, byte g, byte b) { - LogLevel.Trace => (127, 0, 127), - LogLevel.Debug => (0, 127, 255), - LogLevel.Information => (255, 255, 255), - LogLevel.Warning => (255, 255, 0), - LogLevel.Error => (255, 63, 0), - LogLevel.Critical => (255, 0, 0), - _ => null, - }; - - using (_ = Decorate(colors)) - { - writer.Write(entry.LogLevel switch - { - LogLevel.Trace => "TRC", - LogLevel.Debug => "DBG", - LogLevel.Information => "INF", - LogLevel.Warning => "WRN", - LogLevel.Error => "ERR", - LogLevel.Critical => "CRT", - _ => "UNK", - }); + return !options.DisableColors ? new(cb, r, g, b) : default; } - writer.Write("]["); + _ = cb.Print("["); + + using (_ = Decorate(127, 127, 127)) + _ = cb.Print("{0:HH:mm:ss.fff}", entry.Timestamp); + + _ = cb.Print("]["); - using (_ = Decorate((233, 233, 233))) - writer.Write(entry.CategoryName); + using (_ = Decorate((byte)r, (byte)g, (byte)b)) + _ = cb.Print(lvl); - writer.Write("]["); + _ = cb.Print("]["); - using (_ = Decorate((0, 155, 155))) - writer.Write(entry.EventId); + using (_ = Decorate(233, 233, 233)) + _ = cb.Print(entry.CategoryName); - writer.Write("] "); + _ = cb.Print("]["); + + using (_ = Decorate(0, 155, 155)) + _ = cb.Print(entry.EventId); + + _ = cb.Print("] "); if (entry.Message is string m) - writer.WriteLine("{0}", m); + _ = cb.PrintLine(m); if (entry.Exception is Exception e) - writer.WriteLine(e); + _ = cb.PrintLine(e); + + writer.Write(cb.Span); } } diff --git a/src/extensions/extensions.csproj b/src/extensions/extensions.csproj index f3f09d4..3b406ed 100644 --- a/src/extensions/extensions.csproj +++ b/src/extensions/extensions.csproj @@ -9,6 +9,10 @@ This package provides terminal hosting and logging for the .NET Generic Host.

Microsoft.Extensions + + + + diff --git a/src/input/TerminalEditor.cs b/src/input/TerminalEditor.cs index 2251f78..2e9a0fc 100644 --- a/src/input/TerminalEditor.cs +++ b/src/input/TerminalEditor.cs @@ -17,10 +17,6 @@ public TerminalEditor(TerminalEditorOptions options) { lock (_lock) { - var raw = Terminal.IsRawMode; - var events = Terminal.MouseEvents; - - Terminal.SetMouseEvents(TerminalMouseEvents.None); Terminal.EnableRawMode(); try @@ -112,7 +108,6 @@ public TerminalEditor(TerminalEditorOptions options) finally { Terminal.DisableRawMode(); - Terminal.SetMouseEvents(events); } } } diff --git a/src/sample/Program.cs b/src/sample/Program.cs index 66d9c31..3a2f10a 100644 --- a/src/sample/Program.cs +++ b/src/sample/Program.cs @@ -1,4 +1,5 @@ using Sample.Scenarios; +using static System.Text.Control.ControlSequences; namespace Sample; @@ -20,14 +21,15 @@ static class Program static async Task Main(string[] args) { - Terminal.Title = nameof(Sample); + Terminal.Out(SetTitle(nameof(Sample))); if (args.Length == 0 || string.IsNullOrWhiteSpace(args[0])) { - Terminal.ForegroundColor(255, 0, 0); - Terminal.ErrorLine("Please supply a scenario name."); - Terminal.ResetAttributes(); - + Terminal.ErrorLine( + new ControlBuilder() + .SetForegroundColor(255, 0, 0) + .Print("Please supply a scenario name.") + .ResetAttributes()); Terminal.ErrorLine(); Terminal.ErrorLine("Available scenarios:"); Terminal.ErrorLine(); @@ -44,21 +46,27 @@ static async Task Main(string[] args) if (match == null) { - Terminal.ForegroundColor(255, 0, 0); - Terminal.ErrorLine("Could not find a scenario matching '{0}'.", args[0]); - Terminal.ResetAttributes(); + Terminal.ErrorLine( + new ControlBuilder() + .SetForegroundColor(255, 0, 0) + .Print("Could not find a scenario matching '{0}'.", args[0]) + .ResetAttributes()); return 1; } - Terminal.ForegroundColor(0, 255, 0); - Terminal.OutLine("Press Enter to run the {0} scenario.", match.Name); - Terminal.ResetAttributes(); + Terminal.ErrorLine( + new ControlBuilder() + .SetForegroundColor(0, 255, 0) + .Print("Press Enter to run the {0} scenario.", match.Name) + .ResetAttributes()); _ = Terminal.ReadLine(); - Terminal.ClearScreen(); - Terminal.MoveCursorTo(0, 0); + Terminal.Out( + new ControlBuilder() + .ClearScreen() + .MoveCursorTo(0, 0)); await match.RunAsync().ConfigureAwait(false); diff --git a/src/sample/Scenarios/AttributeScenario.cs b/src/sample/Scenarios/AttributeScenario.cs index 788d0c8..ba999e9 100644 --- a/src/sample/Scenarios/AttributeScenario.cs +++ b/src/sample/Scenarios/AttributeScenario.cs @@ -5,65 +5,52 @@ sealed class AttributeScenario : Scenario { public override Task RunAsync() { - Terminal.ForegroundColor(255, 0, 0); - Terminal.OutLine("This text is red."); - Terminal.ResetAttributes(); - - Terminal.ForegroundColor(0, 255, 0); - Terminal.OutLine("This text is green."); - Terminal.ResetAttributes(); - - Terminal.ForegroundColor(0, 0, 255); - Terminal.OutLine("This text is blue."); - Terminal.ResetAttributes(); - - Terminal.Decorations(bold: true); - Terminal.OutLine("This text is bold."); - Terminal.ResetAttributes(); - - Terminal.Decorations(faint: true); - Terminal.OutLine("This text is faint."); - Terminal.ResetAttributes(); - - Terminal.Decorations(italic: true); - Terminal.OutLine("This text is in italics."); - Terminal.ResetAttributes(); - - Terminal.Decorations(underline: true); - Terminal.OutLine("This text is underlined."); - Terminal.ResetAttributes(); - - Terminal.Decorations(blink: true); - Terminal.OutLine("This text is blinking."); - Terminal.ResetAttributes(); - - Terminal.Decorations(invert: true); - Terminal.OutLine("This text is inverted."); - Terminal.ResetAttributes(); - - Terminal.Decorations(invisible: true); - Terminal.OutLine("This text is invisible."); - Terminal.ResetAttributes(); - - Terminal.Decorations(strike: true); - Terminal.OutLine("This text is struck through."); - Terminal.ResetAttributes(); - - Terminal.Decorations(overline: true); - Terminal.OutLine("This text is overlined."); - Terminal.ResetAttributes(); - - Terminal.Decorations(doubleUnderline: true); - Terminal.OutLine("This text is doubly underlined."); - Terminal.ResetAttributes(); - - Terminal.OpenHyperlink(new("https://google.com")); - Terminal.OutLine("This is a hyperlink."); - Terminal.CloseHyperlink(); - - Terminal.OpenHyperlink(new("https://google.com"), "google"); - Terminal.OutLine("This is a hyperlink with an ID."); - Terminal.CloseHyperlink(); + Terminal.Out( + new ControlBuilder() + .SetForegroundColor(255, 0, 0) + .PrintLine("This text is red.") + .ResetAttributes() + .SetForegroundColor(0, 255, 0) + .PrintLine("This text is green.") + .ResetAttributes() + .SetForegroundColor(0, 0, 255) + .PrintLine("This text is blue.") + .ResetAttributes() + .SetDecorations(bold: true) + .PrintLine("This text is bold.") + .ResetAttributes() + .SetDecorations(faint: true) + .PrintLine("This text is faint.") + .ResetAttributes() + .SetDecorations(italic: true) + .PrintLine("This text is in italics.") + .ResetAttributes() + .SetDecorations(underline: true) + .PrintLine("This text is underlined.") + .ResetAttributes() + .SetDecorations(blink: true) + .PrintLine("This text is blinking.") + .ResetAttributes() + .SetDecorations(invert: true) + .PrintLine("This text is inverted.") + .ResetAttributes() + .SetDecorations(invisible: true) + .PrintLine("This text is invisible.") + .SetDecorations(strike: true) + .PrintLine("This text is struck through.") + .ResetAttributes() + .SetDecorations(overline: true) + .PrintLine("This text is overlined.") + .ResetAttributes() + .SetDecorations(doubleUnderline: true) + .PrintLine("This text is doubly underlined.") + .ResetAttributes() + .OpenHyperlink(new("https://google.com")) + .PrintLine("This is a Google hyperlink.") + .CloseHyperlink() + .OpenHyperlink(new("https://google.com"), "google") + .PrintLine("This is a Google hyperlink with an ID.") + .CloseHyperlink()); return Task.CompletedTask; } diff --git a/src/sample/Scenarios/CursorScenario.cs b/src/sample/Scenarios/CursorScenario.cs index 4558b5c..c1a93d9 100644 --- a/src/sample/Scenarios/CursorScenario.cs +++ b/src/sample/Scenarios/CursorScenario.cs @@ -1,3 +1,5 @@ +using static System.Text.Control.ControlSequences; + namespace Sample.Scenarios; [SuppressMessage("Performance", "CA1812")] @@ -11,6 +13,10 @@ public override Task RunAsync() Terminal.OutLine("