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("