Use file scoped namespace declarations

This commit is contained in:
Patrik Svensson
2021-12-21 11:06:46 +01:00
committed by Phil Scott
parent 1dbaf50935
commit ec1188b837
607 changed files with 28739 additions and 29245 deletions

View File

@ -2,96 +2,95 @@ using System;
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
internal static class Aligner
{
internal static class Aligner
public static string Align(string text, Justify? alignment, int maxWidth)
{
public static string Align(string text, Justify? alignment, int maxWidth)
if (alignment == null || alignment == Justify.Left)
{
if (alignment == null || alignment == Justify.Left)
{
return text;
}
var width = Cell.GetCellLength(text);
if (width >= maxWidth)
{
return text;
}
switch (alignment)
{
case Justify.Right:
{
var diff = maxWidth - width;
return new string(' ', diff) + text;
}
case Justify.Center:
{
var diff = (maxWidth - width) / 2;
var left = new string(' ', diff);
var right = new string(' ', diff);
// Right side
var remainder = (maxWidth - width) % 2;
if (remainder != 0)
{
right += new string(' ', remainder);
}
return left + text + right;
}
default:
throw new NotSupportedException("Unknown alignment");
}
return text;
}
public static void Align<T>(RenderContext context, T segments, Justify? alignment, int maxWidth)
where T : List<Segment>
var width = Cell.GetCellLength(text);
if (width >= maxWidth)
{
if (alignment == null || alignment == Justify.Left)
{
return;
}
return text;
}
var width = Segment.CellCount(segments);
if (width >= maxWidth)
{
return;
}
switch (alignment)
{
case Justify.Right:
{
var diff = maxWidth - width;
return new string(' ', diff) + text;
}
switch (alignment)
{
case Justify.Right:
case Justify.Center:
{
var diff = (maxWidth - width) / 2;
var left = new string(' ', diff);
var right = new string(' ', diff);
// Right side
var remainder = (maxWidth - width) % 2;
if (remainder != 0)
{
var diff = maxWidth - width;
segments.Insert(0, Segment.Padding(diff));
break;
right += new string(' ', remainder);
}
case Justify.Center:
{
// Left side.
var diff = (maxWidth - width) / 2;
segments.Insert(0, Segment.Padding(diff));
return left + text + right;
}
// Right side
segments.Add(Segment.Padding(diff));
var remainder = (maxWidth - width) % 2;
if (remainder != 0)
{
segments.Add(Segment.Padding(remainder));
}
break;
}
default:
throw new NotSupportedException("Unknown alignment");
}
default:
throw new NotSupportedException("Unknown alignment");
}
}
}
public static void Align<T>(RenderContext context, T segments, Justify? alignment, int maxWidth)
where T : List<Segment>
{
if (alignment == null || alignment == Justify.Left)
{
return;
}
var width = Segment.CellCount(segments);
if (width >= maxWidth)
{
return;
}
switch (alignment)
{
case Justify.Right:
{
var diff = maxWidth - width;
segments.Insert(0, Segment.Padding(diff));
break;
}
case Justify.Center:
{
// Left side.
var diff = (maxWidth - width) / 2;
segments.Insert(0, Segment.Padding(diff));
// Right side
segments.Add(Segment.Padding(diff));
var remainder = (maxWidth - width) % 2;
if (remainder != 0)
{
segments.Add(Segment.Padding(remainder));
}
break;
}
default:
throw new NotSupportedException("Unknown alignment");
}
}
}

View File

@ -4,108 +4,107 @@ using System.Text;
using Spectre.Console.Rendering;
using static Spectre.Console.AnsiSequences;
namespace Spectre.Console
namespace Spectre.Console;
internal static class AnsiBuilder
{
internal static class AnsiBuilder
private static readonly AnsiLinkHasher _linkHasher;
static AnsiBuilder()
{
private static readonly AnsiLinkHasher _linkHasher;
static AnsiBuilder()
{
_linkHasher = new AnsiLinkHasher();
}
public static string Build(IAnsiConsole console, IRenderable renderable)
{
var builder = new StringBuilder();
foreach (var segment in renderable.GetSegments(console))
{
if (segment.IsControlCode)
{
builder.Append(segment.Text);
continue;
}
var parts = segment.Text.NormalizeNewLines().Split(new[] { '\n' });
foreach (var (_, _, last, part) in parts.Enumerate())
{
if (!string.IsNullOrEmpty(part))
{
builder.Append(Build(console.Profile, part, segment.Style));
}
if (!last)
{
builder.Append(Environment.NewLine);
}
}
}
return builder.ToString();
}
private static string Build(Profile profile, string text, Style style)
{
if (profile is null)
{
throw new ArgumentNullException(nameof(profile));
}
else if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
else if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
var codes = AnsiDecorationBuilder.GetAnsiCodes(style.Decoration);
// Got foreground?
if (style.Foreground != Color.Default)
{
codes = codes.Concat(
AnsiColorBuilder.GetAnsiCodes(
profile.Capabilities.ColorSystem,
style.Foreground,
true));
}
// Got background?
if (style.Background != Color.Default)
{
codes = codes.Concat(
AnsiColorBuilder.GetAnsiCodes(
profile.Capabilities.ColorSystem,
style.Background,
false));
}
var result = codes.ToArray();
if (result.Length == 0 && style.Link == null)
{
return text;
}
var ansi = result.Length > 0
? $"{SGR(result)}{text}{SGR(0)}"
: text;
if (style.Link != null && !profile.Capabilities.Legacy)
{
var link = style.Link;
// Empty links means we should take the URL from the text.
if (link.Equals(Constants.EmptyLink, StringComparison.Ordinal))
{
link = text;
}
var linkId = _linkHasher.GenerateId(link, text);
ansi = $"{ESC}]8;id={linkId};{link}{ESC}\\{ansi}{ESC}]8;;{ESC}\\";
}
return ansi;
}
_linkHasher = new AnsiLinkHasher();
}
}
public static string Build(IAnsiConsole console, IRenderable renderable)
{
var builder = new StringBuilder();
foreach (var segment in renderable.GetSegments(console))
{
if (segment.IsControlCode)
{
builder.Append(segment.Text);
continue;
}
var parts = segment.Text.NormalizeNewLines().Split(new[] { '\n' });
foreach (var (_, _, last, part) in parts.Enumerate())
{
if (!string.IsNullOrEmpty(part))
{
builder.Append(Build(console.Profile, part, segment.Style));
}
if (!last)
{
builder.Append(Environment.NewLine);
}
}
}
return builder.ToString();
}
private static string Build(Profile profile, string text, Style style)
{
if (profile is null)
{
throw new ArgumentNullException(nameof(profile));
}
else if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
else if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
var codes = AnsiDecorationBuilder.GetAnsiCodes(style.Decoration);
// Got foreground?
if (style.Foreground != Color.Default)
{
codes = codes.Concat(
AnsiColorBuilder.GetAnsiCodes(
profile.Capabilities.ColorSystem,
style.Foreground,
true));
}
// Got background?
if (style.Background != Color.Default)
{
codes = codes.Concat(
AnsiColorBuilder.GetAnsiCodes(
profile.Capabilities.ColorSystem,
style.Background,
false));
}
var result = codes.ToArray();
if (result.Length == 0 && style.Link == null)
{
return text;
}
var ansi = result.Length > 0
? $"{SGR(result)}{text}{SGR(0)}"
: text;
if (style.Link != null && !profile.Capabilities.Legacy)
{
var link = style.Link;
// Empty links means we should take the URL from the text.
if (link.Equals(Constants.EmptyLink, StringComparison.Ordinal))
{
link = text;
}
var linkId = _linkHasher.GenerateId(link, text);
ansi = $"{ESC}]8;id={linkId};{link}{ESC}\\{ansi}{ESC}]8;;{ESC}\\";
}
return ansi;
}
}

View File

@ -2,69 +2,68 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Spectre.Console
namespace Spectre.Console;
internal static class AnsiColorBuilder
{
internal static class AnsiColorBuilder
public static IEnumerable<byte> GetAnsiCodes(ColorSystem system, Color color, bool foreground)
{
public static IEnumerable<byte> GetAnsiCodes(ColorSystem system, Color color, bool foreground)
return system switch
{
return system switch
{
ColorSystem.NoColors => Array.Empty<byte>(), // No colors
ColorSystem.TrueColor => GetTrueColor(color, foreground), // 24-bit
ColorSystem.EightBit => GetEightBit(color, foreground), // 8-bit
ColorSystem.Standard => GetFourBit(color, foreground), // 4-bit
ColorSystem.Legacy => GetThreeBit(color, foreground), // 3-bit
_ => throw new InvalidOperationException("Could not determine ANSI color."),
};
}
private static IEnumerable<byte> GetThreeBit(Color color, bool foreground)
{
var number = color.Number;
if (number == null || color.Number >= 8)
{
number = ColorPalette.ExactOrClosest(ColorSystem.Legacy, color).Number;
}
Debug.Assert(number >= 0 && number < 8, "Invalid range for 4-bit color");
var mod = foreground ? 30 : 40;
return new byte[] { (byte)(number.Value + mod) };
}
private static IEnumerable<byte> GetFourBit(Color color, bool foreground)
{
var number = color.Number;
if (number == null || color.Number >= 16)
{
number = ColorPalette.ExactOrClosest(ColorSystem.Standard, color).Number;
}
Debug.Assert(number >= 0 && number < 16, "Invalid range for 4-bit color");
var mod = number < 8 ? (foreground ? 30 : 40) : (foreground ? 82 : 92);
return new byte[] { (byte)(number.Value + mod) };
}
private static IEnumerable<byte> GetEightBit(Color color, bool foreground)
{
var number = color.Number ?? ColorPalette.ExactOrClosest(ColorSystem.EightBit, color).Number;
Debug.Assert(number >= 0 && number <= 255, "Invalid range for 8-bit color");
var mod = foreground ? (byte)38 : (byte)48;
return new byte[] { mod, 5, (byte)number };
}
private static IEnumerable<byte> GetTrueColor(Color color, bool foreground)
{
if (color.Number != null)
{
return GetEightBit(color, foreground);
}
var mod = foreground ? (byte)38 : (byte)48;
return new byte[] { mod, 2, color.R, color.G, color.B };
}
ColorSystem.NoColors => Array.Empty<byte>(), // No colors
ColorSystem.TrueColor => GetTrueColor(color, foreground), // 24-bit
ColorSystem.EightBit => GetEightBit(color, foreground), // 8-bit
ColorSystem.Standard => GetFourBit(color, foreground), // 4-bit
ColorSystem.Legacy => GetThreeBit(color, foreground), // 3-bit
_ => throw new InvalidOperationException("Could not determine ANSI color."),
};
}
}
private static IEnumerable<byte> GetThreeBit(Color color, bool foreground)
{
var number = color.Number;
if (number == null || color.Number >= 8)
{
number = ColorPalette.ExactOrClosest(ColorSystem.Legacy, color).Number;
}
Debug.Assert(number >= 0 && number < 8, "Invalid range for 4-bit color");
var mod = foreground ? 30 : 40;
return new byte[] { (byte)(number.Value + mod) };
}
private static IEnumerable<byte> GetFourBit(Color color, bool foreground)
{
var number = color.Number;
if (number == null || color.Number >= 16)
{
number = ColorPalette.ExactOrClosest(ColorSystem.Standard, color).Number;
}
Debug.Assert(number >= 0 && number < 16, "Invalid range for 4-bit color");
var mod = number < 8 ? (foreground ? 30 : 40) : (foreground ? 82 : 92);
return new byte[] { (byte)(number.Value + mod) };
}
private static IEnumerable<byte> GetEightBit(Color color, bool foreground)
{
var number = color.Number ?? ColorPalette.ExactOrClosest(ColorSystem.EightBit, color).Number;
Debug.Assert(number >= 0 && number <= 255, "Invalid range for 8-bit color");
var mod = foreground ? (byte)38 : (byte)48;
return new byte[] { mod, 5, (byte)number };
}
private static IEnumerable<byte> GetTrueColor(Color color, bool foreground)
{
if (color.Number != null)
{
return GetEightBit(color, foreground);
}
var mod = foreground ? (byte)38 : (byte)48;
return new byte[] { mod, 2, color.R, color.G, color.B };
}
}

View File

@ -2,39 +2,38 @@ using System;
using Spectre.Console.Rendering;
using static Spectre.Console.AnsiSequences;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class AnsiConsoleBackend : IAnsiConsoleBackend
{
internal sealed class AnsiConsoleBackend : IAnsiConsoleBackend
private readonly IAnsiConsole _console;
public IAnsiConsoleCursor Cursor { get; }
public AnsiConsoleBackend(IAnsiConsole console)
{
private readonly IAnsiConsole _console;
_console = console ?? throw new ArgumentNullException(nameof(console));
Cursor = new AnsiConsoleCursor(this);
}
public IAnsiConsoleCursor Cursor { get; }
public void Clear(bool home)
{
Write(new ControlCode(ED(2)));
Write(new ControlCode(ED(3)));
public AnsiConsoleBackend(IAnsiConsole console)
if (home)
{
_console = console ?? throw new ArgumentNullException(nameof(console));
Cursor = new AnsiConsoleCursor(this);
}
public void Clear(bool home)
{
Write(new ControlCode(ED(2)));
Write(new ControlCode(ED(3)));
if (home)
{
Write(new ControlCode(CUP(1, 1)));
}
}
public void Write(IRenderable renderable)
{
var result = AnsiBuilder.Build(_console, renderable);
if (result?.Length > 0)
{
_console.Profile.Out.Writer.Write(result);
_console.Profile.Out.Writer.Flush();
}
Write(new ControlCode(CUP(1, 1)));
}
}
}
public void Write(IRenderable renderable)
{
var result = AnsiBuilder.Build(_console, renderable);
if (result?.Length > 0)
{
_console.Profile.Out.Writer.Write(result);
_console.Profile.Out.Writer.Flush();
}
}
}

View File

@ -1,56 +1,55 @@
using System;
using static Spectre.Console.AnsiSequences;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class AnsiConsoleCursor : IAnsiConsoleCursor
{
internal sealed class AnsiConsoleCursor : IAnsiConsoleCursor
private readonly AnsiConsoleBackend _backend;
public AnsiConsoleCursor(AnsiConsoleBackend backend)
{
private readonly AnsiConsoleBackend _backend;
_backend = backend ?? throw new ArgumentNullException(nameof(backend));
}
public AnsiConsoleCursor(AnsiConsoleBackend backend)
public void Show(bool show)
{
if (show)
{
_backend = backend ?? throw new ArgumentNullException(nameof(backend));
_backend.Write(new ControlCode(SM(DECTCEM)));
}
public void Show(bool show)
else
{
if (show)
{
_backend.Write(new ControlCode(SM(DECTCEM)));
}
else
{
_backend.Write(new ControlCode(RM(DECTCEM)));
}
}
public void Move(CursorDirection direction, int steps)
{
if (steps == 0)
{
return;
}
switch (direction)
{
case CursorDirection.Up:
_backend.Write(new ControlCode(CUU(steps)));
break;
case CursorDirection.Down:
_backend.Write(new ControlCode(CUD(steps)));
break;
case CursorDirection.Right:
_backend.Write(new ControlCode(CUF(steps)));
break;
case CursorDirection.Left:
_backend.Write(new ControlCode(CUB(steps)));
break;
}
}
public void SetPosition(int column, int line)
{
_backend.Write(new ControlCode(CUP(line, column)));
_backend.Write(new ControlCode(RM(DECTCEM)));
}
}
}
public void Move(CursorDirection direction, int steps)
{
if (steps == 0)
{
return;
}
switch (direction)
{
case CursorDirection.Up:
_backend.Write(new ControlCode(CUU(steps)));
break;
case CursorDirection.Down:
_backend.Write(new ControlCode(CUD(steps)));
break;
case CursorDirection.Right:
_backend.Write(new ControlCode(CUF(steps)));
break;
case CursorDirection.Left:
_backend.Write(new ControlCode(CUB(steps)));
break;
}
}
public void SetPosition(int column, int line)
{
_backend.Write(new ControlCode(CUP(line, column)));
}
}

View File

@ -1,56 +1,55 @@
using System.Collections.Generic;
namespace Spectre.Console
namespace Spectre.Console;
internal static class AnsiDecorationBuilder
{
internal static class AnsiDecorationBuilder
// TODO: Rewrite this to not yield
public static IEnumerable<byte> GetAnsiCodes(Decoration decoration)
{
// TODO: Rewrite this to not yield
public static IEnumerable<byte> GetAnsiCodes(Decoration decoration)
if ((decoration & Decoration.Bold) != 0)
{
if ((decoration & Decoration.Bold) != 0)
{
yield return 1;
}
yield return 1;
}
if ((decoration & Decoration.Dim) != 0)
{
yield return 2;
}
if ((decoration & Decoration.Dim) != 0)
{
yield return 2;
}
if ((decoration & Decoration.Italic) != 0)
{
yield return 3;
}
if ((decoration & Decoration.Italic) != 0)
{
yield return 3;
}
if ((decoration & Decoration.Underline) != 0)
{
yield return 4;
}
if ((decoration & Decoration.Underline) != 0)
{
yield return 4;
}
if ((decoration & Decoration.SlowBlink) != 0)
{
yield return 5;
}
if ((decoration & Decoration.SlowBlink) != 0)
{
yield return 5;
}
if ((decoration & Decoration.RapidBlink) != 0)
{
yield return 6;
}
if ((decoration & Decoration.RapidBlink) != 0)
{
yield return 6;
}
if ((decoration & Decoration.Invert) != 0)
{
yield return 7;
}
if ((decoration & Decoration.Invert) != 0)
{
yield return 7;
}
if ((decoration & Decoration.Conceal) != 0)
{
yield return 8;
}
if ((decoration & Decoration.Conceal) != 0)
{
yield return 8;
}
if ((decoration & Decoration.Strikethrough) != 0)
{
yield return 9;
}
if ((decoration & Decoration.Strikethrough) != 0)
{
yield return 9;
}
}
}
}

View File

@ -9,133 +9,132 @@ using System.Text.RegularExpressions;
// https://github.com/keqingrong/supports-ansi/blob/master/index.js
/////////////////////////////////////////////////////////////////////////////////////////////////////
namespace Spectre.Console
namespace Spectre.Console;
internal static class AnsiDetector
{
internal static class AnsiDetector
private static readonly Regex[] _regexes = new[]
{
private static readonly Regex[] _regexes = new[]
{
new Regex("^xterm"), // xterm, PuTTY, Mintty
new Regex("^rxvt"), // RXVT
new Regex("^eterm"), // Eterm
new Regex("^screen"), // GNU screen, tmux
new Regex("tmux"), // tmux
new Regex("^vt100"), // DEC VT series
new Regex("^vt102"), // DEC VT series
new Regex("^vt220"), // DEC VT series
new Regex("^vt320"), // DEC VT series
new Regex("ansi"), // ANSI
new Regex("scoansi"), // SCO ANSI
new Regex("cygwin"), // Cygwin, MinGW
new Regex("linux"), // Linux console
new Regex("konsole"), // Konsole
new Regex("bvterm"), // Bitvise SSH Client
};
new Regex("^xterm"), // xterm, PuTTY, Mintty
new Regex("^rxvt"), // RXVT
new Regex("^eterm"), // Eterm
new Regex("^screen"), // GNU screen, tmux
new Regex("tmux"), // tmux
new Regex("^vt100"), // DEC VT series
new Regex("^vt102"), // DEC VT series
new Regex("^vt220"), // DEC VT series
new Regex("^vt320"), // DEC VT series
new Regex("ansi"), // ANSI
new Regex("scoansi"), // SCO ANSI
new Regex("cygwin"), // Cygwin, MinGW
new Regex("linux"), // Linux console
new Regex("konsole"), // Konsole
new Regex("bvterm"), // Bitvise SSH Client
};
public static (bool SupportsAnsi, bool LegacyConsole) Detect(bool stdError, bool upgrade)
public static (bool SupportsAnsi, bool LegacyConsole) Detect(bool stdError, bool upgrade)
{
// Running on Windows?
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Running on Windows?
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
// Running under ConEmu?
var conEmu = Environment.GetEnvironmentVariable("ConEmuANSI");
if (!string.IsNullOrEmpty(conEmu) && conEmu.Equals("On", StringComparison.OrdinalIgnoreCase))
{
// Running under ConEmu?
var conEmu = Environment.GetEnvironmentVariable("ConEmuANSI");
if (!string.IsNullOrEmpty(conEmu) && conEmu.Equals("On", StringComparison.OrdinalIgnoreCase))
{
return (true, false);
}
var supportsAnsi = Windows.SupportsAnsi(upgrade, stdError, out var legacyConsole);
return (supportsAnsi, legacyConsole);
return (true, false);
}
return DetectFromTerm();
var supportsAnsi = Windows.SupportsAnsi(upgrade, stdError, out var legacyConsole);
return (supportsAnsi, legacyConsole);
}
private static (bool SupportsAnsi, bool LegacyConsole) DetectFromTerm()
return DetectFromTerm();
}
private static (bool SupportsAnsi, bool LegacyConsole) DetectFromTerm()
{
// Check if the terminal is of type ANSI/VT100/xterm compatible.
var term = Environment.GetEnvironmentVariable("TERM");
if (!string.IsNullOrWhiteSpace(term))
{
// Check if the terminal is of type ANSI/VT100/xterm compatible.
var term = Environment.GetEnvironmentVariable("TERM");
if (!string.IsNullOrWhiteSpace(term))
if (_regexes.Any(regex => regex.IsMatch(term)))
{
if (_regexes.Any(regex => regex.IsMatch(term)))
{
return (true, false);
}
return (true, false);
}
return (false, true);
}
internal static class Windows
return (false, true);
}
internal static class Windows
{
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")]
private const int STD_OUTPUT_HANDLE = -11;
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")]
private const int STD_ERROR_HANDLE = -12;
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")]
private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")]
private const uint DISABLE_NEWLINE_AUTO_RETURN = 0x0008;
[DllImport("kernel32.dll")]
private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
[DllImport("kernel32.dll")]
private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("kernel32.dll")]
public static extern uint GetLastError();
public static bool SupportsAnsi(bool upgrade, bool stdError, out bool isLegacy)
{
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")]
private const int STD_OUTPUT_HANDLE = -11;
isLegacy = false;
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")]
private const int STD_ERROR_HANDLE = -12;
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")]
private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")]
private const uint DISABLE_NEWLINE_AUTO_RETURN = 0x0008;
[DllImport("kernel32.dll")]
private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
[DllImport("kernel32.dll")]
private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("kernel32.dll")]
public static extern uint GetLastError();
public static bool SupportsAnsi(bool upgrade, bool stdError, out bool isLegacy)
try
{
isLegacy = false;
try
var @out = GetStdHandle(stdError ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE);
if (!GetConsoleMode(@out, out var mode))
{
var @out = GetStdHandle(stdError ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE);
if (!GetConsoleMode(@out, out var mode))
{
// Could not get console mode, try TERM (set in cygwin, WSL-Shell).
var (ansiFromTerm, legacyFromTerm) = DetectFromTerm();
// Could not get console mode, try TERM (set in cygwin, WSL-Shell).
var (ansiFromTerm, legacyFromTerm) = DetectFromTerm();
isLegacy = ansiFromTerm ? legacyFromTerm : isLegacy;
return ansiFromTerm;
isLegacy = ansiFromTerm ? legacyFromTerm : isLegacy;
return ansiFromTerm;
}
if ((mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0)
{
isLegacy = true;
if (!upgrade)
{
return false;
}
if ((mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0)
// Try enable ANSI support.
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
if (!SetConsoleMode(@out, mode))
{
isLegacy = true;
if (!upgrade)
{
return false;
}
// Try enable ANSI support.
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
if (!SetConsoleMode(@out, mode))
{
// Enabling failed.
return false;
}
isLegacy = false;
// Enabling failed.
return false;
}
return true;
}
catch
{
// All we know here is that we don't support ANSI.
return false;
isLegacy = false;
}
return true;
}
catch
{
// All we know here is that we don't support ANSI.
return false;
}
}
}
}
}

View File

@ -1,42 +1,41 @@
using System;
using System.Runtime.CompilerServices;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class AnsiLinkHasher
{
internal sealed class AnsiLinkHasher
private readonly Random _random;
public AnsiLinkHasher()
{
private readonly Random _random;
_random = new Random(Environment.TickCount);
}
public AnsiLinkHasher()
public int GenerateId(string link, string text)
{
if (link is null)
{
_random = new Random(Environment.TickCount);
throw new ArgumentNullException(nameof(link));
}
public int GenerateId(string link, string text)
link += text ?? string.Empty;
unchecked
{
if (link is null)
{
throw new ArgumentNullException(nameof(link));
}
link += text ?? string.Empty;
unchecked
{
return Math.Abs(
GetLinkHashCode(link) +
_random.Next(0, int.MaxValue));
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetLinkHashCode(string link)
{
#if NETSTANDARD2_0
return link.GetHashCode();
#else
return link.GetHashCode(StringComparison.Ordinal);
#endif
return Math.Abs(
GetLinkHashCode(link) +
_random.Next(0, int.MaxValue));
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetLinkHashCode(string link)
{
#if NETSTANDARD2_0
return link.GetHashCode();
#else
return link.GetHashCode(StringComparison.Ordinal);
#endif
}
}

View File

@ -1,163 +1,162 @@
using System.Linq;
namespace Spectre.Console
namespace Spectre.Console;
internal static class AnsiSequences
{
internal static class AnsiSequences
/// <summary>
/// The ASCII escape character (decimal 27).
/// </summary>
public const string ESC = "\u001b";
/// <summary>
/// Introduces a control sequence that uses 8-bit characters.
/// </summary>
public const string CSI = ESC + "[";
/// <summary>
/// Text cursor enable.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/DECRQM.html#T5-8"/>.
/// </remarks>
public const int DECTCEM = 25;
/// <summary>
/// This control function selects one or more character attributes at the same time.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/SGR.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string SGR(params byte[] codes)
{
/// <summary>
/// The ASCII escape character (decimal 27).
/// </summary>
public const string ESC = "\u001b";
/// <summary>
/// Introduces a control sequence that uses 8-bit characters.
/// </summary>
public const string CSI = ESC + "[";
/// <summary>
/// Text cursor enable.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/DECRQM.html#T5-8"/>.
/// </remarks>
public const int DECTCEM = 25;
/// <summary>
/// This control function selects one or more character attributes at the same time.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/SGR.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string SGR(params byte[] codes)
{
var joinedCodes = string.Join(";", codes.Select(c => c.ToString()));
return $"{CSI}{joinedCodes}m";
}
/// <summary>
/// This control function erases characters from part or all of the display.
/// When you erase complete lines, they become single-height, single-width lines,
/// with all visual character attributes cleared.
/// ED works inside or outside the scrolling margins.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/ED.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string ED(int code)
{
return $"{CSI}{code}J";
}
/// <summary>
/// Moves the cursor up a specified number of lines in the same column.
/// The cursor stops at the top margin.
/// If the cursor is already above the top margin, then the cursor stops at the top line.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/CUU.html"/>.
/// </remarks>
/// <param name="steps">The number of steps to move up.</param>
/// <returns>The ANSI escape code.</returns>
public static string CUU(int steps)
{
return $"{CSI}{steps}A";
}
/// <summary>
/// This control function moves the cursor down a specified number of lines in the same column.
/// The cursor stops at the bottom margin.
/// If the cursor is already below the bottom margin, then the cursor stops at the bottom line.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/CUD.html"/>.
/// </remarks>
/// <param name="steps">The number of steps to move down.</param>
/// <returns>The ANSI escape code.</returns>
public static string CUD(int steps)
{
return $"{CSI}{steps}B";
}
/// <summary>
/// This control function moves the cursor to the right by a specified number of columns.
/// The cursor stops at the right border of the page.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/CUF.html"/>.
/// </remarks>
/// <param name="steps">The number of steps to move forward.</param>
/// <returns>The ANSI escape code.</returns>
public static string CUF(int steps)
{
return $"{CSI}{steps}C";
}
/// <summary>
/// This control function moves the cursor to the left by a specified number of columns.
/// The cursor stops at the left border of the page.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/CUB.html"/>.
/// </remarks>
/// <param name="steps">The number of steps to move backward.</param>
/// <returns>The ANSI escape code.</returns>
public static string CUB(int steps)
{
return $"{CSI}{steps}D";
}
/// <summary>
/// Moves the cursor to the specified position.
/// </summary>
/// <param name="line">The line to move to.</param>
/// <param name="column">The column to move to.</param>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/CUP.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string CUP(int line, int column)
{
return $"{CSI}{line};{column}H";
}
/// <summary>
/// Hides the cursor.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/RM.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string RM(int code)
{
return $"{CSI}?{code}l";
}
/// <summary>
/// Shows the cursor.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/SM.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string SM(int code)
{
return $"{CSI}?{code}h";
}
/// <summary>
/// This control function erases characters on the line that has the cursor.
/// EL clears all character attributes from erased character positions.
/// EL works inside or outside the scrolling margins.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/EL.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string EL(int code)
{
return $"{CSI}{code}K";
}
var joinedCodes = string.Join(";", codes.Select(c => c.ToString()));
return $"{CSI}{joinedCodes}m";
}
}
/// <summary>
/// This control function erases characters from part or all of the display.
/// When you erase complete lines, they become single-height, single-width lines,
/// with all visual character attributes cleared.
/// ED works inside or outside the scrolling margins.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/ED.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string ED(int code)
{
return $"{CSI}{code}J";
}
/// <summary>
/// Moves the cursor up a specified number of lines in the same column.
/// The cursor stops at the top margin.
/// If the cursor is already above the top margin, then the cursor stops at the top line.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/CUU.html"/>.
/// </remarks>
/// <param name="steps">The number of steps to move up.</param>
/// <returns>The ANSI escape code.</returns>
public static string CUU(int steps)
{
return $"{CSI}{steps}A";
}
/// <summary>
/// This control function moves the cursor down a specified number of lines in the same column.
/// The cursor stops at the bottom margin.
/// If the cursor is already below the bottom margin, then the cursor stops at the bottom line.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/CUD.html"/>.
/// </remarks>
/// <param name="steps">The number of steps to move down.</param>
/// <returns>The ANSI escape code.</returns>
public static string CUD(int steps)
{
return $"{CSI}{steps}B";
}
/// <summary>
/// This control function moves the cursor to the right by a specified number of columns.
/// The cursor stops at the right border of the page.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/CUF.html"/>.
/// </remarks>
/// <param name="steps">The number of steps to move forward.</param>
/// <returns>The ANSI escape code.</returns>
public static string CUF(int steps)
{
return $"{CSI}{steps}C";
}
/// <summary>
/// This control function moves the cursor to the left by a specified number of columns.
/// The cursor stops at the left border of the page.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/CUB.html"/>.
/// </remarks>
/// <param name="steps">The number of steps to move backward.</param>
/// <returns>The ANSI escape code.</returns>
public static string CUB(int steps)
{
return $"{CSI}{steps}D";
}
/// <summary>
/// Moves the cursor to the specified position.
/// </summary>
/// <param name="line">The line to move to.</param>
/// <param name="column">The column to move to.</param>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/CUP.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string CUP(int line, int column)
{
return $"{CSI}{line};{column}H";
}
/// <summary>
/// Hides the cursor.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/RM.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string RM(int code)
{
return $"{CSI}?{code}l";
}
/// <summary>
/// Shows the cursor.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/SM.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string SM(int code)
{
return $"{CSI}?{code}h";
}
/// <summary>
/// This control function erases characters on the line that has the cursor.
/// EL clears all character attributes from erased character positions.
/// EL works inside or outside the scrolling margins.
/// </summary>
/// <remarks>
/// See <see href="https://vt100.net/docs/vt510-rm/EL.html"/>.
/// </remarks>
/// <returns>The ANSI escape code.</returns>
public static string EL(int code)
{
return $"{CSI}{code}K";
}
}

View File

@ -1,57 +1,56 @@
using System;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class AnsiConsoleFacade : IAnsiConsole
{
internal sealed class AnsiConsoleFacade : IAnsiConsole
private readonly object _renderLock;
private readonly AnsiConsoleBackend _ansiBackend;
private readonly LegacyConsoleBackend _legacyBackend;
public Profile Profile { get; }
public IAnsiConsoleCursor Cursor => GetBackend().Cursor;
public IAnsiConsoleInput Input { get; }
public IExclusivityMode ExclusivityMode { get; }
public RenderPipeline Pipeline { get; }
public AnsiConsoleFacade(Profile profile, IExclusivityMode exclusivityMode)
{
private readonly object _renderLock;
private readonly AnsiConsoleBackend _ansiBackend;
private readonly LegacyConsoleBackend _legacyBackend;
_renderLock = new object();
public Profile Profile { get; }
public IAnsiConsoleCursor Cursor => GetBackend().Cursor;
public IAnsiConsoleInput Input { get; }
public IExclusivityMode ExclusivityMode { get; }
public RenderPipeline Pipeline { get; }
Profile = profile ?? throw new ArgumentNullException(nameof(profile));
Input = new DefaultInput(Profile);
ExclusivityMode = exclusivityMode ?? throw new ArgumentNullException(nameof(exclusivityMode));
Pipeline = new RenderPipeline();
public AnsiConsoleFacade(Profile profile, IExclusivityMode exclusivityMode)
_ansiBackend = new AnsiConsoleBackend(this);
_legacyBackend = new LegacyConsoleBackend(this);
}
public void Clear(bool home)
{
lock (_renderLock)
{
_renderLock = new object();
Profile = profile ?? throw new ArgumentNullException(nameof(profile));
Input = new DefaultInput(Profile);
ExclusivityMode = exclusivityMode ?? throw new ArgumentNullException(nameof(exclusivityMode));
Pipeline = new RenderPipeline();
_ansiBackend = new AnsiConsoleBackend(this);
_legacyBackend = new LegacyConsoleBackend(this);
}
public void Clear(bool home)
{
lock (_renderLock)
{
GetBackend().Clear(home);
}
}
public void Write(IRenderable renderable)
{
lock (_renderLock)
{
GetBackend().Write(renderable);
}
}
private IAnsiConsoleBackend GetBackend()
{
if (Profile.Capabilities.Ansi)
{
return _ansiBackend;
}
return _legacyBackend;
GetBackend().Clear(home);
}
}
}
public void Write(IRenderable renderable)
{
lock (_renderLock)
{
GetBackend().Write(renderable);
}
}
private IAnsiConsoleBackend GetBackend()
{
if (Profile.Capabilities.Ansi)
{
return _ansiBackend;
}
return _legacyBackend;
}
}

View File

@ -1,27 +1,26 @@
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
/// <summary>
/// Represents a console backend.
/// </summary>
internal interface IAnsiConsoleBackend
{
/// <summary>
/// Represents a console backend.
/// Gets the console cursor for the backend.
/// </summary>
internal interface IAnsiConsoleBackend
{
/// <summary>
/// Gets the console cursor for the backend.
/// </summary>
IAnsiConsoleCursor Cursor { get; }
IAnsiConsoleCursor Cursor { get; }
/// <summary>
/// Clears the console.
/// </summary>
/// <param name="home">If the cursor should be moved to the home position.</param>
void Clear(bool home);
/// <summary>
/// Clears the console.
/// </summary>
/// <param name="home">If the cursor should be moved to the home position.</param>
void Clear(bool home);
/// <summary>
/// Writes a <see cref="IRenderable"/> to the console backend.
/// </summary>
/// <param name="renderable">The <see cref="IRenderable"/> to write.</param>
void Write(IRenderable renderable);
}
}
/// <summary>
/// Writes a <see cref="IRenderable"/> to the console backend.
/// </summary>
/// <param name="renderable">The <see cref="IRenderable"/> to write.</param>
void Write(IRenderable renderable);
}

View File

@ -1,70 +1,69 @@
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class LegacyConsoleBackend : IAnsiConsoleBackend
{
internal sealed class LegacyConsoleBackend : IAnsiConsoleBackend
private readonly IAnsiConsole _console;
private Style _lastStyle;
public IAnsiConsoleCursor Cursor { get; }
public LegacyConsoleBackend(IAnsiConsole console)
{
private readonly IAnsiConsole _console;
private Style _lastStyle;
_console = console ?? throw new System.ArgumentNullException(nameof(console));
_lastStyle = Style.Plain;
public IAnsiConsoleCursor Cursor { get; }
Cursor = new LegacyConsoleCursor();
}
public LegacyConsoleBackend(IAnsiConsole console)
public void Clear(bool home)
{
var (x, y) = (System.Console.CursorLeft, System.Console.CursorTop);
System.Console.Clear();
if (!home)
{
_console = console ?? throw new System.ArgumentNullException(nameof(console));
_lastStyle = Style.Plain;
Cursor = new LegacyConsoleCursor();
}
public void Clear(bool home)
{
var (x, y) = (System.Console.CursorLeft, System.Console.CursorTop);
System.Console.Clear();
if (!home)
{
// Set the cursor position
System.Console.SetCursorPosition(x, y);
}
}
public void Write(IRenderable renderable)
{
foreach (var segment in renderable.GetSegments(_console))
{
if (segment.IsControlCode)
{
continue;
}
if (_lastStyle?.Equals(segment.Style) != true)
{
SetStyle(segment.Style);
}
_console.Profile.Out.Writer.Write(segment.Text.NormalizeNewLines(native: true));
}
}
private void SetStyle(Style style)
{
_lastStyle = style;
System.Console.ResetColor();
var background = Color.ToConsoleColor(style.Background);
if (_console.Profile.Capabilities.ColorSystem != ColorSystem.NoColors && (int)background != -1)
{
System.Console.BackgroundColor = background;
}
var foreground = Color.ToConsoleColor(style.Foreground);
if (_console.Profile.Capabilities.ColorSystem != ColorSystem.NoColors && (int)foreground != -1)
{
System.Console.ForegroundColor = foreground;
}
// Set the cursor position
System.Console.SetCursorPosition(x, y);
}
}
}
public void Write(IRenderable renderable)
{
foreach (var segment in renderable.GetSegments(_console))
{
if (segment.IsControlCode)
{
continue;
}
if (_lastStyle?.Equals(segment.Style) != true)
{
SetStyle(segment.Style);
}
_console.Profile.Out.Writer.Write(segment.Text.NormalizeNewLines(native: true));
}
}
private void SetStyle(Style style)
{
_lastStyle = style;
System.Console.ResetColor();
var background = Color.ToConsoleColor(style.Background);
if (_console.Profile.Capabilities.ColorSystem != ColorSystem.NoColors && (int)background != -1)
{
System.Console.BackgroundColor = background;
}
var foreground = Color.ToConsoleColor(style.Foreground);
if (_console.Profile.Capabilities.ColorSystem != ColorSystem.NoColors && (int)foreground != -1)
{
System.Console.ForegroundColor = foreground;
}
}
}

View File

@ -1,40 +1,39 @@
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class LegacyConsoleCursor : IAnsiConsoleCursor
{
internal sealed class LegacyConsoleCursor : IAnsiConsoleCursor
public void Show(bool show)
{
public void Show(bool show)
System.Console.CursorVisible = show;
}
public void Move(CursorDirection direction, int steps)
{
if (steps == 0)
{
System.Console.CursorVisible = show;
return;
}
public void Move(CursorDirection direction, int steps)
switch (direction)
{
if (steps == 0)
{
return;
}
switch (direction)
{
case CursorDirection.Up:
System.Console.CursorTop -= steps;
break;
case CursorDirection.Down:
System.Console.CursorTop += steps;
break;
case CursorDirection.Left:
System.Console.CursorLeft -= steps;
break;
case CursorDirection.Right:
System.Console.CursorLeft += steps;
break;
}
}
public void SetPosition(int x, int y)
{
System.Console.CursorLeft = x;
System.Console.CursorTop = y;
case CursorDirection.Up:
System.Console.CursorTop -= steps;
break;
case CursorDirection.Down:
System.Console.CursorTop += steps;
break;
case CursorDirection.Left:
System.Console.CursorLeft -= steps;
break;
case CursorDirection.Right:
System.Console.CursorLeft += steps;
break;
}
}
}
public void SetPosition(int x, int y)
{
System.Console.CursorLeft = x;
System.Console.CursorTop = y;
}
}

View File

@ -1,37 +1,36 @@
using Wcwidth;
namespace Spectre.Console
namespace Spectre.Console;
internal static class Cell
{
internal static class Cell
private static readonly int?[] _runeWidthCache = new int?[char.MaxValue];
public static int GetCellLength(string text)
{
private static readonly int?[] _runeWidthCache = new int?[char.MaxValue];
public static int GetCellLength(string text)
var sum = 0;
for (var index = 0; index < text.Length; index++)
{
var sum = 0;
for (var index = 0; index < text.Length; index++)
{
var rune = text[index];
sum += GetCellLength(rune);
}
return sum;
var rune = text[index];
sum += GetCellLength(rune);
}
public static int GetCellLength(char rune)
{
// TODO: We need to figure out why Segment.SplitLines fails
// if we let wcwidth (which returns -1 instead of 1)
// calculate the size for new line characters.
// That is correct from a Unicode perspective, but the
// algorithm was written before wcwidth was added and used
// to work with string length and not cell length.
if (rune == '\n')
{
return 1;
}
return _runeWidthCache[rune] ??= UnicodeCalculator.GetWidth(rune);
}
return sum;
}
}
public static int GetCellLength(char rune)
{
// TODO: We need to figure out why Segment.SplitLines fails
// if we let wcwidth (which returns -1 instead of 1)
// calculate the size for new line characters.
// That is correct from a Unicode perspective, but the
// algorithm was written before wcwidth was added and used
// to work with string length and not cell length.
if (rune == '\n')
{
return 1;
}
return _runeWidthCache[rune] ??= UnicodeCalculator.GetWidth(rune);
}
}

View File

@ -2,87 +2,86 @@ using System;
using System.Collections;
using System.Collections.Generic;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class ListWithCallback<T> : IList<T>, IReadOnlyList<T>
{
internal sealed class ListWithCallback<T> : IList<T>, IReadOnlyList<T>
private readonly List<T> _list;
private readonly Action _callback;
public T this[int index]
{
private readonly List<T> _list;
private readonly Action _callback;
public T this[int index]
{
get => _list[index];
set => _list[index] = value;
}
public int Count => _list.Count;
public bool IsReadOnly => false;
public ListWithCallback(Action callback)
{
_list = new List<T>();
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
}
public void Add(T item)
{
_list.Add(item);
_callback();
}
public void Clear()
{
_list.Clear();
_callback();
}
public bool Contains(T item)
{
return _list.Contains(item);
}
public void CopyTo(T[] array, int arrayIndex)
{
_list.CopyTo(array, arrayIndex);
_callback();
}
public IEnumerator<T> GetEnumerator()
{
return _list.GetEnumerator();
}
public int IndexOf(T item)
{
return _list.IndexOf(item);
}
public void Insert(int index, T item)
{
_list.Insert(index, item);
_callback();
}
public bool Remove(T item)
{
var result = _list.Remove(item);
if (result)
{
_callback();
}
return result;
}
public void RemoveAt(int index)
{
_list.RemoveAt(index);
_callback();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
get => _list[index];
set => _list[index] = value;
}
}
public int Count => _list.Count;
public bool IsReadOnly => false;
public ListWithCallback(Action callback)
{
_list = new List<T>();
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
}
public void Add(T item)
{
_list.Add(item);
_callback();
}
public void Clear()
{
_list.Clear();
_callback();
}
public bool Contains(T item)
{
return _list.Contains(item);
}
public void CopyTo(T[] array, int arrayIndex)
{
_list.CopyTo(array, arrayIndex);
_callback();
}
public IEnumerator<T> GetEnumerator()
{
return _list.GetEnumerator();
}
public int IndexOf(T item)
{
return _list.IndexOf(item);
}
public void Insert(int index, T item)
{
_list.Insert(index, item);
_callback();
}
public bool Remove(T item)
{
var result = _list.Remove(item);
if (result)
{
_callback();
}
return result;
}
public void RemoveAt(int index)
{
_list.RemoveAt(index);
_callback();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

View File

@ -2,80 +2,79 @@ using System;
using System.Collections.Generic;
using System.Linq;
namespace Spectre.Console
namespace Spectre.Console;
internal static partial class ColorPalette
{
internal static partial class ColorPalette
public static IReadOnlyList<Color> Legacy { get; }
public static IReadOnlyList<Color> Standard { get; }
public static IReadOnlyList<Color> EightBit { get; }
static ColorPalette()
{
public static IReadOnlyList<Color> Legacy { get; }
public static IReadOnlyList<Color> Standard { get; }
public static IReadOnlyList<Color> EightBit { get; }
static ColorPalette()
{
Legacy = GenerateLegacyPalette();
Standard = GenerateStandardPalette(Legacy);
EightBit = GenerateEightBitPalette(Standard);
}
internal static Color ExactOrClosest(ColorSystem system, Color color)
{
var exact = Exact(system, color);
return exact ?? Closest(system, color);
}
private static Color? Exact(ColorSystem system, Color color)
{
if (system == ColorSystem.TrueColor)
{
return color;
}
var palette = system switch
{
ColorSystem.Legacy => Legacy,
ColorSystem.Standard => Standard,
ColorSystem.EightBit => EightBit,
_ => throw new NotSupportedException(),
};
return palette
.Where(c => c.Equals(color))
.Cast<Color?>()
.FirstOrDefault();
}
private static Color Closest(ColorSystem system, Color color)
{
if (system == ColorSystem.TrueColor)
{
return color;
}
var palette = system switch
{
ColorSystem.Legacy => Legacy,
ColorSystem.Standard => Standard,
ColorSystem.EightBit => EightBit,
_ => throw new NotSupportedException(),
};
// https://stackoverflow.com/a/9085524
static double Distance(Color first, Color second)
{
var rmean = ((float)first.R + second.R) / 2;
var r = first.R - second.R;
var g = first.G - second.G;
var b = first.B - second.B;
return Math.Sqrt(
((int)((512 + rmean) * r * r) >> 8)
+ (4 * g * g)
+ ((int)((767 - rmean) * b * b) >> 8));
}
return Enumerable.Range(0, int.MaxValue)
.Zip(palette, (id, other) => (Distance: Distance(other, color), Id: id, Color: other))
.OrderBy(x => x.Distance)
.FirstOrDefault().Color;
}
Legacy = GenerateLegacyPalette();
Standard = GenerateStandardPalette(Legacy);
EightBit = GenerateEightBitPalette(Standard);
}
}
internal static Color ExactOrClosest(ColorSystem system, Color color)
{
var exact = Exact(system, color);
return exact ?? Closest(system, color);
}
private static Color? Exact(ColorSystem system, Color color)
{
if (system == ColorSystem.TrueColor)
{
return color;
}
var palette = system switch
{
ColorSystem.Legacy => Legacy,
ColorSystem.Standard => Standard,
ColorSystem.EightBit => EightBit,
_ => throw new NotSupportedException(),
};
return palette
.Where(c => c.Equals(color))
.Cast<Color?>()
.FirstOrDefault();
}
private static Color Closest(ColorSystem system, Color color)
{
if (system == ColorSystem.TrueColor)
{
return color;
}
var palette = system switch
{
ColorSystem.Legacy => Legacy,
ColorSystem.Standard => Standard,
ColorSystem.EightBit => EightBit,
_ => throw new NotSupportedException(),
};
// https://stackoverflow.com/a/9085524
static double Distance(Color first, Color second)
{
var rmean = ((float)first.R + second.R) / 2;
var r = first.R - second.R;
var g = first.G - second.G;
var b = first.B - second.B;
return Math.Sqrt(
((int)((512 + rmean) * r * r) >> 8)
+ (4 * g * g)
+ ((int)((767 - rmean) * b * b) >> 8));
}
return Enumerable.Range(0, int.MaxValue)
.Zip(palette, (id, other) => (Distance: Distance(other, color), Id: id, Color: other))
.OrderBy(x => x.Distance)
.FirstOrDefault().Color;
}
}

View File

@ -1,92 +1,91 @@
using System;
using System.Runtime.InteropServices;
namespace Spectre.Console
namespace Spectre.Console;
internal static class ColorSystemDetector
{
internal static class ColorSystemDetector
// Adapted from https://github.com/willmcgugan/rich/blob/f0c29052c22d1e49579956a9207324d9072beed7/rich/console.py#L391
public static ColorSystem Detect(bool supportsAnsi)
{
// Adapted from https://github.com/willmcgugan/rich/blob/f0c29052c22d1e49579956a9207324d9072beed7/rich/console.py#L391
public static ColorSystem Detect(bool supportsAnsi)
// No colors?
if (Environment.GetEnvironmentVariables().Contains("NO_COLOR"))
{
// No colors?
if (Environment.GetEnvironmentVariables().Contains("NO_COLOR"))
{
return ColorSystem.NoColors;
}
// Windows?
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (!supportsAnsi)
{
// Figure out what we should do here.
// Does really all Windows terminals support
// eight-bit colors? Probably not...
return ColorSystem.EightBit;
}
// Windows 10.0.15063 and above support true color,
// and we can probably assume that the next major
// version of Windows will support true color as well.
if (GetWindowsVersionInformation(out var major, out var build))
{
if (major == 10 && build >= 15063)
{
return ColorSystem.TrueColor;
}
else if (major > 10)
{
return ColorSystem.TrueColor;
}
}
}
else
{
var colorTerm = Environment.GetEnvironmentVariable("COLORTERM");
if (!string.IsNullOrWhiteSpace(colorTerm))
{
if (colorTerm.Equals("truecolor", StringComparison.OrdinalIgnoreCase) ||
colorTerm.Equals("24bit", StringComparison.OrdinalIgnoreCase))
{
return ColorSystem.TrueColor;
}
}
}
// Should we default to eight-bit colors?
return ColorSystem.EightBit;
return ColorSystem.NoColors;
}
private static bool GetWindowsVersionInformation(out int major, out int build)
// Windows?
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
major = 0;
build = 0;
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (!supportsAnsi)
{
return false;
// Figure out what we should do here.
// Does really all Windows terminals support
// eight-bit colors? Probably not...
return ColorSystem.EightBit;
}
// Windows 10.0.15063 and above support true color,
// and we can probably assume that the next major
// version of Windows will support true color as well.
if (GetWindowsVersionInformation(out var major, out var build))
{
if (major == 10 && build >= 15063)
{
return ColorSystem.TrueColor;
}
else if (major > 10)
{
return ColorSystem.TrueColor;
}
}
}
else
{
var colorTerm = Environment.GetEnvironmentVariable("COLORTERM");
if (!string.IsNullOrWhiteSpace(colorTerm))
{
if (colorTerm.Equals("truecolor", StringComparison.OrdinalIgnoreCase) ||
colorTerm.Equals("24bit", StringComparison.OrdinalIgnoreCase))
{
return ColorSystem.TrueColor;
}
}
}
// Should we default to eight-bit colors?
return ColorSystem.EightBit;
}
private static bool GetWindowsVersionInformation(out int major, out int build)
{
major = 0;
build = 0;
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return false;
}
#if NET5_0_OR_GREATER
// The reason we're not always using this, is because it will return wrong values on other runtimes than .NET 5+
// See https://docs.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/5.0/environment-osversion-returns-correct-version
var version = Environment.OSVersion.Version;
major = version.Major;
build = version.Build;
return true;
// The reason we're not always using this, is because it will return wrong values on other runtimes than .NET 5+
// See https://docs.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/5.0/environment-osversion-returns-correct-version
var version = Environment.OSVersion.Version;
major = version.Major;
build = version.Build;
return true;
#else
var regex = new System.Text.RegularExpressions.Regex("Microsoft Windows (?'major'[0-9]*).(?'minor'[0-9]*).(?'build'[0-9]*)\\s*$");
var match = regex.Match(RuntimeInformation.OSDescription);
if (match.Success && int.TryParse(match.Groups["major"].Value, out major))
var regex = new System.Text.RegularExpressions.Regex("Microsoft Windows (?'major'[0-9]*).(?'minor'[0-9]*).(?'build'[0-9]*)\\s*$");
var match = regex.Match(RuntimeInformation.OSDescription);
if (match.Success && int.TryParse(match.Groups["major"].Value, out major))
{
if (int.TryParse(match.Groups["build"].Value, out build))
{
if (int.TryParse(match.Groups["build"].Value, out build))
{
return true;
}
return true;
}
return false;
#endif
}
return false;
#endif
}
}
}

View File

@ -1,57 +1,56 @@
using System;
using System.Collections.Generic;
namespace Spectre.Console
namespace Spectre.Console;
internal static partial class ColorTable
{
internal static partial class ColorTable
private static readonly Dictionary<int, string> _nameLookup;
private static readonly Dictionary<string, int> _numberLookup;
static ColorTable()
{
private static readonly Dictionary<int, string> _nameLookup;
private static readonly Dictionary<string, int> _numberLookup;
static ColorTable()
_numberLookup = GenerateTable();
_nameLookup = new Dictionary<int, string>();
foreach (var pair in _numberLookup)
{
_numberLookup = GenerateTable();
_nameLookup = new Dictionary<int, string>();
foreach (var pair in _numberLookup)
if (_nameLookup.ContainsKey(pair.Value))
{
if (_nameLookup.ContainsKey(pair.Value))
{
continue;
}
_nameLookup.Add(pair.Value, pair.Key);
}
}
public static Color GetColor(int number)
{
if (number < 0 || number > 255)
{
throw new InvalidOperationException("Color number must be between 0 and 255");
continue;
}
return ColorPalette.EightBit[number];
}
public static Color? GetColor(string name)
{
if (!_numberLookup.TryGetValue(name, out var number))
{
return null;
}
if (number > ColorPalette.EightBit.Count - 1)
{
return null;
}
return ColorPalette.EightBit[number];
}
public static string? GetName(int number)
{
_nameLookup.TryGetValue(number, out var name);
return name;
_nameLookup.Add(pair.Value, pair.Key);
}
}
}
public static Color GetColor(int number)
{
if (number < 0 || number > 255)
{
throw new InvalidOperationException("Color number must be between 0 and 255");
}
return ColorPalette.EightBit[number];
}
public static Color? GetColor(string name)
{
if (!_numberLookup.TryGetValue(name, out var number))
{
return null;
}
if (number > ColorPalette.EightBit.Count - 1)
{
return null;
}
return ColorPalette.EightBit[number];
}
public static string? GetName(int number)
{
_nameLookup.TryGetValue(number, out var name);
return name;
}
}

View File

@ -1,43 +1,42 @@
using System.IO;
namespace Spectre.Console
namespace Spectre.Console;
internal static class ConsoleHelper
{
internal static class ConsoleHelper
public static int GetSafeWidth(int defaultValue = Constants.DefaultTerminalWidth)
{
public static int GetSafeWidth(int defaultValue = Constants.DefaultTerminalWidth)
try
{
try
var width = System.Console.BufferWidth;
if (width == 0)
{
var width = System.Console.BufferWidth;
if (width == 0)
{
width = defaultValue;
}
width = defaultValue;
}
return width;
}
catch (IOException)
{
return defaultValue;
}
return width;
}
public static int GetSafeHeight(int defaultValue = Constants.DefaultTerminalHeight)
catch (IOException)
{
try
{
var height = System.Console.WindowHeight;
if (height == 0)
{
height = defaultValue;
}
return height;
}
catch (IOException)
{
return defaultValue;
}
return defaultValue;
}
}
}
public static int GetSafeHeight(int defaultValue = Constants.DefaultTerminalHeight)
{
try
{
var height = System.Console.WindowHeight;
if (height == 0)
{
height = defaultValue;
}
return height;
}
catch (IOException)
{
return defaultValue;
}
}
}

View File

@ -1,10 +1,9 @@
namespace Spectre.Console
{
internal static class Constants
{
public const int DefaultTerminalWidth = 80;
public const int DefaultTerminalHeight = 24;
namespace Spectre.Console;
public const string EmptyLink = "https://emptylink";
}
}
internal static class Constants
{
public const int DefaultTerminalWidth = 80;
public const int DefaultTerminalHeight = 24;
public const string EmptyLink = "https://emptylink";
}

View File

@ -2,16 +2,16 @@ using System;
using System.Collections.Generic;
using System.Linq;
namespace Spectre.Console
{
internal static class DecorationTable
{
private static readonly Dictionary<string, Decoration?> _lookup;
private static readonly Dictionary<Decoration, string> _reverseLookup;
namespace Spectre.Console;
static DecorationTable()
{
_lookup = new Dictionary<string, Decoration?>(StringComparer.OrdinalIgnoreCase)
internal static class DecorationTable
{
private static readonly Dictionary<string, Decoration?> _lookup;
private static readonly Dictionary<Decoration, string> _reverseLookup;
static DecorationTable()
{
_lookup = new Dictionary<string, Decoration?>(StringComparer.OrdinalIgnoreCase)
{
{ "none", Decoration.None },
{ "bold", Decoration.Bold },
@ -32,44 +32,43 @@ namespace Spectre.Console
{ "s", Decoration.Strikethrough },
};
_reverseLookup = new Dictionary<Decoration, string>();
foreach (var (name, decoration) in _lookup)
_reverseLookup = new Dictionary<Decoration, string>();
foreach (var (name, decoration) in _lookup)
{
// Cannot happen, but the compiler thinks so...
if (decoration == null)
{
// Cannot happen, but the compiler thinks so...
if (decoration == null)
{
continue;
}
continue;
}
if (!_reverseLookup.ContainsKey(decoration.Value))
{
_reverseLookup[decoration.Value] = name;
}
if (!_reverseLookup.ContainsKey(decoration.Value))
{
_reverseLookup[decoration.Value] = name;
}
}
public static Decoration? GetDecoration(string name)
{
_lookup.TryGetValue(name, out var result);
return result;
}
public static List<string> GetMarkupNames(Decoration decoration)
{
var result = new List<string>();
Enum.GetValues(typeof(Decoration))
.Cast<Decoration>()
.Where(flag => (decoration & flag) != 0)
.ForEach(flag =>
{
if (flag != Decoration.None && _reverseLookup.TryGetValue(flag, out var name))
{
result.Add(name);
}
});
return result;
}
}
}
public static Decoration? GetDecoration(string name)
{
_lookup.TryGetValue(name, out var result);
return result;
}
public static List<string> GetMarkupNames(Decoration decoration)
{
var result = new List<string>();
Enum.GetValues(typeof(Decoration))
.Cast<Decoration>()
.Where(flag => (decoration & flag) != 0)
.ForEach(flag =>
{
if (flag != Decoration.None && _reverseLookup.TryGetValue(flag, out var name))
{
result.Add(name);
}
});
return result;
}
}

View File

@ -2,56 +2,55 @@ using System;
using System.Threading;
using System.Threading.Tasks;
namespace Spectre.Console.Internal
namespace Spectre.Console.Internal;
internal sealed class DefaultExclusivityMode : IExclusivityMode
{
internal sealed class DefaultExclusivityMode : IExclusivityMode
private readonly SemaphoreSlim _semaphore;
public DefaultExclusivityMode()
{
private readonly SemaphoreSlim _semaphore;
public DefaultExclusivityMode()
{
_semaphore = new SemaphoreSlim(1, 1);
}
public T Run<T>(Func<T> func)
{
// Try acquiring the exclusivity semaphore
if (!_semaphore.Wait(0))
{
throw CreateExclusivityException();
}
try
{
return func();
}
finally
{
_semaphore.Release(1);
}
}
public async Task<T> RunAsync<T>(Func<Task<T>> func)
{
// Try acquiring the exclusivity semaphore
if (!await _semaphore.WaitAsync(0).ConfigureAwait(false))
{
throw CreateExclusivityException();
}
try
{
return await func().ConfigureAwait(false);
}
finally
{
_semaphore.Release(1);
}
}
private static Exception CreateExclusivityException() => new InvalidOperationException(
"Trying to run one or more interactive functions concurrently. " +
"Operations with dynamic displays (e.g. a prompt and a progress display) " +
"cannot be running at the same time.");
_semaphore = new SemaphoreSlim(1, 1);
}
}
public T Run<T>(Func<T> func)
{
// Try acquiring the exclusivity semaphore
if (!_semaphore.Wait(0))
{
throw CreateExclusivityException();
}
try
{
return func();
}
finally
{
_semaphore.Release(1);
}
}
public async Task<T> RunAsync<T>(Func<Task<T>> func)
{
// Try acquiring the exclusivity semaphore
if (!await _semaphore.WaitAsync(0).ConfigureAwait(false))
{
throw CreateExclusivityException();
}
try
{
return await func().ConfigureAwait(false);
}
finally
{
_semaphore.Release(1);
}
}
private static Exception CreateExclusivityException() => new InvalidOperationException(
"Trying to run one or more interactive functions concurrently. " +
"Operations with dynamic displays (e.g. a prompt and a progress display) " +
"cannot be running at the same time.");
}

View File

@ -2,60 +2,59 @@ using System;
using System.Threading;
using System.Threading.Tasks;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class DefaultInput : IAnsiConsoleInput
{
internal sealed class DefaultInput : IAnsiConsoleInput
private readonly Profile _profile;
public DefaultInput(Profile profile)
{
private readonly Profile _profile;
public DefaultInput(Profile profile)
{
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
}
public bool IsKeyAvailable()
{
if (!_profile.Capabilities.Interactive)
{
throw new InvalidOperationException("Failed to read input in non-interactive mode.");
}
return System.Console.KeyAvailable;
}
public ConsoleKeyInfo? ReadKey(bool intercept)
{
if (!_profile.Capabilities.Interactive)
{
throw new InvalidOperationException("Failed to read input in non-interactive mode.");
}
return System.Console.ReadKey(intercept);
}
public async Task<ConsoleKeyInfo?> ReadKeyAsync(bool intercept, CancellationToken cancellationToken)
{
if (!_profile.Capabilities.Interactive)
{
throw new InvalidOperationException("Failed to read input in non-interactive mode.");
}
while (true)
{
if (cancellationToken.IsCancellationRequested)
{
return null;
}
if (System.Console.KeyAvailable)
{
break;
}
await Task.Delay(5, cancellationToken).ConfigureAwait(false);
}
return ReadKey(intercept);
}
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
}
}
public bool IsKeyAvailable()
{
if (!_profile.Capabilities.Interactive)
{
throw new InvalidOperationException("Failed to read input in non-interactive mode.");
}
return System.Console.KeyAvailable;
}
public ConsoleKeyInfo? ReadKey(bool intercept)
{
if (!_profile.Capabilities.Interactive)
{
throw new InvalidOperationException("Failed to read input in non-interactive mode.");
}
return System.Console.ReadKey(intercept);
}
public async Task<ConsoleKeyInfo?> ReadKeyAsync(bool intercept, CancellationToken cancellationToken)
{
if (!_profile.Capabilities.Interactive)
{
throw new InvalidOperationException("Failed to read input in non-interactive mode.");
}
while (true)
{
if (cancellationToken.IsCancellationRequested)
{
return null;
}
if (System.Console.KeyAvailable)
{
break;
}
await Task.Delay(5, cancellationToken).ConfigureAwait(false);
}
return ReadKey(intercept);
}
}

View File

@ -1,89 +1,88 @@
using System;
using System.Globalization;
namespace Spectre.Console
namespace Spectre.Console;
internal struct FileSize
{
internal struct FileSize
public double Bytes { get; }
public FileSizeUnit Unit { get; }
public string Suffix => GetSuffix();
public FileSize(double bytes)
{
public double Bytes { get; }
public FileSizeUnit Unit { get; }
public string Suffix => GetSuffix();
public FileSize(double bytes)
{
Bytes = bytes;
Unit = Detect(bytes);
}
public FileSize(double bytes, FileSizeUnit unit)
{
Bytes = bytes;
Unit = unit;
}
public string Format(CultureInfo? culture = null)
{
var @base = GetBase(Unit);
if (@base == 0)
{
@base = 1;
}
var bytes = Bytes / @base;
return Unit == FileSizeUnit.Byte
? ((int)bytes).ToString(culture ?? CultureInfo.InvariantCulture)
: bytes.ToString("F1", culture ?? CultureInfo.InvariantCulture);
}
public override string ToString()
{
return ToString(suffix: true, CultureInfo.InvariantCulture);
}
public string ToString(bool suffix = true, CultureInfo? culture = null)
{
if (suffix)
{
return $"{Format(culture)} {Suffix}";
}
return Format(culture);
}
private string GetSuffix()
{
return (Bytes, Unit) switch
{
(_, FileSizeUnit.KiloByte) => "KB",
(_, FileSizeUnit.MegaByte) => "MB",
(_, FileSizeUnit.GigaByte) => "GB",
(_, FileSizeUnit.TeraByte) => "TB",
(_, FileSizeUnit.PetaByte) => "PB",
(_, FileSizeUnit.ExaByte) => "EB",
(_, FileSizeUnit.ZettaByte) => "ZB",
(_, FileSizeUnit.YottaByte) => "YB",
(1, _) => "byte",
(_, _) => "bytes",
};
}
private static FileSizeUnit Detect(double bytes)
{
foreach (var unit in (FileSizeUnit[])Enum.GetValues(typeof(FileSizeUnit)))
{
if (bytes < (GetBase(unit) * 1024))
{
return unit;
}
}
return FileSizeUnit.Byte;
}
private static double GetBase(FileSizeUnit unit)
{
return Math.Pow(1024, (int)unit);
}
Bytes = bytes;
Unit = Detect(bytes);
}
}
public FileSize(double bytes, FileSizeUnit unit)
{
Bytes = bytes;
Unit = unit;
}
public string Format(CultureInfo? culture = null)
{
var @base = GetBase(Unit);
if (@base == 0)
{
@base = 1;
}
var bytes = Bytes / @base;
return Unit == FileSizeUnit.Byte
? ((int)bytes).ToString(culture ?? CultureInfo.InvariantCulture)
: bytes.ToString("F1", culture ?? CultureInfo.InvariantCulture);
}
public override string ToString()
{
return ToString(suffix: true, CultureInfo.InvariantCulture);
}
public string ToString(bool suffix = true, CultureInfo? culture = null)
{
if (suffix)
{
return $"{Format(culture)} {Suffix}";
}
return Format(culture);
}
private string GetSuffix()
{
return (Bytes, Unit) switch
{
(_, FileSizeUnit.KiloByte) => "KB",
(_, FileSizeUnit.MegaByte) => "MB",
(_, FileSizeUnit.GigaByte) => "GB",
(_, FileSizeUnit.TeraByte) => "TB",
(_, FileSizeUnit.PetaByte) => "PB",
(_, FileSizeUnit.ExaByte) => "EB",
(_, FileSizeUnit.ZettaByte) => "ZB",
(_, FileSizeUnit.YottaByte) => "YB",
(1, _) => "byte",
(_, _) => "bytes",
};
}
private static FileSizeUnit Detect(double bytes)
{
foreach (var unit in (FileSizeUnit[])Enum.GetValues(typeof(FileSizeUnit)))
{
if (bytes < (GetBase(unit) * 1024))
{
return unit;
}
}
return FileSizeUnit.Byte;
}
private static double GetBase(FileSizeUnit unit)
{
return Math.Pow(1024, (int)unit);
}
}

View File

@ -1,15 +1,14 @@
namespace Spectre.Console
namespace Spectre.Console;
internal enum FileSizeUnit
{
internal enum FileSizeUnit
{
Byte = 0,
KiloByte = 1,
MegaByte = 2,
GigaByte = 3,
TeraByte = 4,
PetaByte = 5,
ExaByte = 6,
ZettaByte = 7,
YottaByte = 8,
}
}
Byte = 0,
KiloByte = 1,
MegaByte = 2,
GigaByte = 3,
TeraByte = 4,
PetaByte = 5,
ExaByte = 6,
ZettaByte = 7,
YottaByte = 8,
}

View File

@ -6,67 +6,66 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Spectre.Console
namespace Spectre.Console;
internal static class Ratio
{
internal static class Ratio
public static List<int> Reduce(int total, List<int> ratios, List<int> maximums, List<int> values)
{
public static List<int> Reduce(int total, List<int> ratios, List<int> maximums, List<int> values)
ratios = ratios.Zip(maximums, (a, b) => (ratio: a, max: b)).Select(a => a.max > 0 ? a.ratio : 0).ToList();
var totalRatio = ratios.Sum();
if (totalRatio <= 0)
{
ratios = ratios.Zip(maximums, (a, b) => (ratio: a, max: b)).Select(a => a.max > 0 ? a.ratio : 0).ToList();
var totalRatio = ratios.Sum();
if (totalRatio <= 0)
{
return values;
}
var totalRemaining = total;
var result = new List<int>();
foreach (var (ratio, maximum, value) in ratios.Zip(maximums, values))
{
if (ratio != 0 && totalRatio > 0)
{
var distributed = (int)Math.Min(maximum, Math.Round((double)(ratio * totalRemaining / totalRatio)));
result.Add(value - distributed);
totalRemaining -= distributed;
totalRatio -= ratio;
}
else
{
result.Add(value);
}
}
return result;
return values;
}
public static List<int> Distribute(int total, IList<int> ratios, IList<int>? minimums = null)
var totalRemaining = total;
var result = new List<int>();
foreach (var (ratio, maximum, value) in ratios.Zip(maximums, values))
{
if (minimums != null)
if (ratio != 0 && totalRatio > 0)
{
ratios = ratios.Zip(minimums, (a, b) => (ratio: a, min: b)).Select(a => a.min > 0 ? a.ratio : 0).ToList();
}
var totalRatio = ratios.Sum();
Debug.Assert(totalRatio > 0, "Sum or ratios must be > 0");
var totalRemaining = total;
var distributedTotal = new List<int>();
minimums ??= ratios.Select(_ => 0).ToList();
foreach (var (ratio, minimum) in ratios.Zip(minimums, (a, b) => (a, b)))
{
var distributed = (totalRatio > 0)
? Math.Max(minimum, (int)Math.Ceiling(ratio * totalRemaining / (double)totalRatio))
: totalRemaining;
distributedTotal.Add(distributed);
totalRatio -= ratio;
var distributed = (int)Math.Min(maximum, Math.Round((double)(ratio * totalRemaining / totalRatio)));
result.Add(value - distributed);
totalRemaining -= distributed;
totalRatio -= ratio;
}
else
{
result.Add(value);
}
return distributedTotal;
}
return result;
}
}
public static List<int> Distribute(int total, IList<int> ratios, IList<int>? minimums = null)
{
if (minimums != null)
{
ratios = ratios.Zip(minimums, (a, b) => (ratio: a, min: b)).Select(a => a.min > 0 ? a.ratio : 0).ToList();
}
var totalRatio = ratios.Sum();
Debug.Assert(totalRatio > 0, "Sum or ratios must be > 0");
var totalRemaining = total;
var distributedTotal = new List<int>();
minimums ??= ratios.Select(_ => 0).ToList();
foreach (var (ratio, minimum) in ratios.Zip(minimums, (a, b) => (a, b)))
{
var distributed = (totalRatio > 0)
? Math.Max(minimum, (int)Math.Ceiling(ratio * totalRemaining / (double)totalRatio))
: totalRemaining;
distributedTotal.Add(distributed);
totalRatio -= ratio;
totalRemaining -= distributed;
}
return distributedTotal;
}
}

View File

@ -1,32 +1,31 @@
using System;
using System.IO;
namespace Spectre.Console
namespace Spectre.Console;
internal static class ResourceReader
{
internal static class ResourceReader
public static string ReadManifestData(string resourceName)
{
public static string ReadManifestData(string resourceName)
if (resourceName is null)
{
if (resourceName is null)
throw new ArgumentNullException(nameof(resourceName));
}
var assembly = typeof(ResourceReader).Assembly;
resourceName = resourceName.ReplaceExact("/", ".");
using (var stream = assembly.GetManifestResourceStream(resourceName))
{
if (stream == null)
{
throw new ArgumentNullException(nameof(resourceName));
throw new InvalidOperationException("Could not load manifest resource stream.");
}
var assembly = typeof(ResourceReader).Assembly;
resourceName = resourceName.ReplaceExact("/", ".");
using (var stream = assembly.GetManifestResourceStream(resourceName))
using (var reader = new StreamReader(stream))
{
if (stream == null)
{
throw new InvalidOperationException("Could not load manifest resource stream.");
}
using (var reader = new StreamReader(stream))
{
return reader.ReadToEnd().NormalizeNewLines();
}
return reader.ReadToEnd().NormalizeNewLines();
}
}
}
}
}

View File

@ -1,19 +1,18 @@
namespace Spectre.Console.Internal
namespace Spectre.Console.Internal;
internal sealed class EncoderCapabilities : IReadOnlyCapabilities
{
internal sealed class EncoderCapabilities : IReadOnlyCapabilities
public ColorSystem ColorSystem { get; }
public bool Ansi => false;
public bool Links => false;
public bool Legacy => false;
public bool IsTerminal => false;
public bool Interactive => false;
public bool Unicode => true;
public EncoderCapabilities(ColorSystem colors)
{
public ColorSystem ColorSystem { get; }
public bool Ansi => false;
public bool Links => false;
public bool Legacy => false;
public bool IsTerminal => false;
public bool Interactive => false;
public bool Unicode => true;
public EncoderCapabilities(ColorSystem colors)
{
ColorSystem = colors;
}
ColorSystem = colors;
}
}
}

View File

@ -3,122 +3,121 @@ using System.Collections.Generic;
using System.Text;
using Spectre.Console.Rendering;
namespace Spectre.Console.Internal
namespace Spectre.Console.Internal;
internal sealed class HtmlEncoder : IAnsiConsoleEncoder
{
internal sealed class HtmlEncoder : IAnsiConsoleEncoder
public string Encode(IAnsiConsole console, IEnumerable<IRenderable> renderables)
{
public string Encode(IAnsiConsole console, IEnumerable<IRenderable> renderables)
var context = new RenderContext(new EncoderCapabilities(ColorSystem.TrueColor));
var builder = new StringBuilder();
builder.Append("<pre style=\"font-size:90%;font-family:consolas,'Courier New',monospace\">\n");
foreach (var renderable in renderables)
{
var context = new RenderContext(new EncoderCapabilities(ColorSystem.TrueColor));
var builder = new StringBuilder();
builder.Append("<pre style=\"font-size:90%;font-family:consolas,'Courier New',monospace\">\n");
foreach (var renderable in renderables)
var segments = renderable.Render(context, console.Profile.Width);
foreach (var (_, first, _, segment) in segments.Enumerate())
{
var segments = renderable.Render(context, console.Profile.Width);
foreach (var (_, first, _, segment) in segments.Enumerate())
if (segment.IsControlCode)
{
if (segment.IsControlCode)
continue;
}
if (segment.Text == "\n" && !first)
{
builder.Append('\n');
continue;
}
var parts = segment.Text.Split(new[] { '\n' }, StringSplitOptions.None);
foreach (var (_, _, last, line) in parts.Enumerate())
{
if (string.IsNullOrEmpty(line))
{
continue;
}
if (segment.Text == "\n" && !first)
builder.Append("<span");
if (!segment.Style.Equals(Style.Plain))
{
builder.Append(" style=\"");
builder.Append(BuildCss(segment.Style));
builder.Append('"');
}
builder.Append('>');
builder.Append(line);
builder.Append("</span>");
if (parts.Length > 1 && !last)
{
builder.Append('\n');
continue;
}
var parts = segment.Text.Split(new[] { '\n' }, StringSplitOptions.None);
foreach (var (_, _, last, line) in parts.Enumerate())
{
if (string.IsNullOrEmpty(line))
{
continue;
}
builder.Append("<span");
if (!segment.Style.Equals(Style.Plain))
{
builder.Append(" style=\"");
builder.Append(BuildCss(segment.Style));
builder.Append('"');
}
builder.Append('>');
builder.Append(line);
builder.Append("</span>");
if (parts.Length > 1 && !last)
{
builder.Append('\n');
}
}
}
}
builder.Append("</pre>");
return builder.ToString().TrimEnd('\n');
}
private static string BuildCss(Style style)
{
var css = new List<string>();
builder.Append("</pre>");
var foreground = style.Foreground;
var background = style.Background;
if ((style.Decoration & Decoration.Invert) != 0)
{
var temp = foreground;
foreground = background;
background = temp;
}
if ((style.Decoration & Decoration.Dim) != 0)
{
var blender = background;
if (blender.Equals(Color.Default))
{
blender = Color.White;
}
foreground = foreground.Blend(blender, 0.5f);
}
if (!foreground.Equals(Color.Default))
{
css.Add($"color: #{foreground.ToHex()}");
}
if (!background.Equals(Color.Default))
{
css.Add($"background-color: #{background.ToHex()}");
}
if ((style.Decoration & Decoration.Bold) != 0)
{
css.Add("font-weight: bold");
}
if ((style.Decoration & Decoration.Bold) != 0)
{
css.Add("font-style: italic");
}
if ((style.Decoration & Decoration.Underline) != 0)
{
css.Add("text-decoration: underline");
}
if ((style.Decoration & Decoration.Strikethrough) != 0)
{
css.Add("text-decoration: line-through");
}
return string.Join(";", css);
}
return builder.ToString().TrimEnd('\n');
}
}
private static string BuildCss(Style style)
{
var css = new List<string>();
var foreground = style.Foreground;
var background = style.Background;
if ((style.Decoration & Decoration.Invert) != 0)
{
var temp = foreground;
foreground = background;
background = temp;
}
if ((style.Decoration & Decoration.Dim) != 0)
{
var blender = background;
if (blender.Equals(Color.Default))
{
blender = Color.White;
}
foreground = foreground.Blend(blender, 0.5f);
}
if (!foreground.Equals(Color.Default))
{
css.Add($"color: #{foreground.ToHex()}");
}
if (!background.Equals(Color.Default))
{
css.Add($"background-color: #{background.ToHex()}");
}
if ((style.Decoration & Decoration.Bold) != 0)
{
css.Add("font-weight: bold");
}
if ((style.Decoration & Decoration.Bold) != 0)
{
css.Add("font-style: italic");
}
if ((style.Decoration & Decoration.Underline) != 0)
{
css.Add("text-decoration: underline");
}
if ((style.Decoration & Decoration.Strikethrough) != 0)
{
css.Add("text-decoration: line-through");
}
return string.Join(";", css);
}
}

View File

@ -2,30 +2,29 @@ using System.Collections.Generic;
using System.Text;
using Spectre.Console.Rendering;
namespace Spectre.Console.Internal
namespace Spectre.Console.Internal;
internal sealed class TextEncoder : IAnsiConsoleEncoder
{
internal sealed class TextEncoder : IAnsiConsoleEncoder
public string Encode(IAnsiConsole console, IEnumerable<IRenderable> renderables)
{
public string Encode(IAnsiConsole console, IEnumerable<IRenderable> renderables)
var context = new RenderContext(new EncoderCapabilities(ColorSystem.TrueColor));
var builder = new StringBuilder();
foreach (var renderable in renderables)
{
var context = new RenderContext(new EncoderCapabilities(ColorSystem.TrueColor));
var builder = new StringBuilder();
foreach (var renderable in renderables)
var segments = renderable.Render(context, console.Profile.Width);
foreach (var segment in Segment.Merge(segments))
{
var segments = renderable.Render(context, console.Profile.Width);
foreach (var segment in Segment.Merge(segments))
if (segment.IsControlCode)
{
if (segment.IsControlCode)
{
continue;
}
builder.Append(segment.Text);
continue;
}
}
return builder.ToString().TrimEnd('\n');
builder.Append(segment.Text);
}
}
return builder.ToString().TrimEnd('\n');
}
}
}

View File

@ -2,64 +2,63 @@ using System;
using System.Collections.Generic;
using System.Linq;
namespace Spectre.Console
namespace Spectre.Console;
internal static class MarkupParser
{
internal static class MarkupParser
public static Paragraph Parse(string text, Style? style = null)
{
public static Paragraph Parse(string text, Style? style = null)
if (text is null)
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
style ??= Style.Plain;
var result = new Paragraph();
using var tokenizer = new MarkupTokenizer(text);
var stack = new Stack<Style>();
while (tokenizer.MoveNext())
{
var token = tokenizer.Current;
if (token == null)
{
break;
}
if (token.Kind == MarkupTokenKind.Open)
{
var parsedStyle = StyleParser.Parse(token.Value);
stack.Push(parsedStyle);
}
else if (token.Kind == MarkupTokenKind.Close)
{
if (stack.Count == 0)
{
throw new InvalidOperationException($"Encountered closing tag when none was expected near position {token.Position}.");
}
stack.Pop();
}
else if (token.Kind == MarkupTokenKind.Text)
{
// Get the effective style.
var effectiveStyle = style.Combine(stack.Reverse());
result.Append(Emoji.Replace(token.Value), effectiveStyle);
}
else
{
throw new InvalidOperationException("Encountered unknown markup token.");
}
}
if (stack.Count > 0)
{
throw new InvalidOperationException("Unbalanced markup stack. Did you forget to close a tag?");
}
return result;
throw new ArgumentNullException(nameof(text));
}
style ??= Style.Plain;
var result = new Paragraph();
using var tokenizer = new MarkupTokenizer(text);
var stack = new Stack<Style>();
while (tokenizer.MoveNext())
{
var token = tokenizer.Current;
if (token == null)
{
break;
}
if (token.Kind == MarkupTokenKind.Open)
{
var parsedStyle = StyleParser.Parse(token.Value);
stack.Push(parsedStyle);
}
else if (token.Kind == MarkupTokenKind.Close)
{
if (stack.Count == 0)
{
throw new InvalidOperationException($"Encountered closing tag when none was expected near position {token.Position}.");
}
stack.Pop();
}
else if (token.Kind == MarkupTokenKind.Text)
{
// Get the effective style.
var effectiveStyle = style.Combine(stack.Reverse());
result.Append(Emoji.Replace(token.Value), effectiveStyle);
}
else
{
throw new InvalidOperationException("Encountered unknown markup token.");
}
}
if (stack.Count > 0)
{
throw new InvalidOperationException("Unbalanced markup stack. Did you forget to close a tag?");
}
return result;
}
}
}

View File

@ -1,18 +1,17 @@
using System;
namespace Spectre.Console
{
internal sealed class MarkupToken
{
public MarkupTokenKind Kind { get; }
public string Value { get; }
public int Position { get; set; }
namespace Spectre.Console;
public MarkupToken(MarkupTokenKind kind, string value, int position)
{
Kind = kind;
Value = value ?? throw new ArgumentNullException(nameof(value));
Position = position;
}
internal sealed class MarkupToken
{
public MarkupTokenKind Kind { get; }
public string Value { get; }
public int Position { get; set; }
public MarkupToken(MarkupTokenKind kind, string value, int position)
{
Kind = kind;
Value = value ?? throw new ArgumentNullException(nameof(value));
Position = position;
}
}
}

View File

@ -1,9 +1,8 @@
namespace Spectre.Console
namespace Spectre.Console;
internal enum MarkupTokenKind
{
internal enum MarkupTokenKind
{
Text = 0,
Open,
Close,
}
}
Text = 0,
Open,
Close,
}

View File

@ -1,36 +1,53 @@
using System;
using System.Text;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class MarkupTokenizer : IDisposable
{
internal sealed class MarkupTokenizer : IDisposable
private readonly StringBuffer _reader;
public MarkupToken? Current { get; private set; }
public MarkupTokenizer(string text)
{
private readonly StringBuffer _reader;
_reader = new StringBuffer(text ?? throw new ArgumentNullException(nameof(text)));
}
public MarkupToken? Current { get; private set; }
public void Dispose()
{
_reader.Dispose();
}
public MarkupTokenizer(string text)
public bool MoveNext()
{
if (_reader.Eof)
{
_reader = new StringBuffer(text ?? throw new ArgumentNullException(nameof(text)));
return false;
}
public void Dispose()
var current = _reader.Peek();
if (current == '[')
{
_reader.Dispose();
}
var position = _reader.Position;
_reader.Read();
public bool MoveNext()
{
if (_reader.Eof)
{
return false;
throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}.");
}
var current = _reader.Peek();
current = _reader.Peek();
if (current == '[')
{
var position = _reader.Position;
_reader.Read();
Current = new MarkupToken(MarkupTokenKind.Text, "[", position);
return true;
}
if (current == '/')
{
_reader.Read();
if (_reader.Eof)
@ -39,98 +56,80 @@ namespace Spectre.Console
}
current = _reader.Peek();
if (current == '[')
{
_reader.Read();
Current = new MarkupToken(MarkupTokenKind.Text, "[", position);
return true;
}
if (current == '/')
{
_reader.Read();
if (_reader.Eof)
{
throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}.");
}
current = _reader.Peek();
if (current != ']')
{
throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}.");
}
_reader.Read();
Current = new MarkupToken(MarkupTokenKind.Close, string.Empty, position);
return true;
}
var builder = new StringBuilder();
while (!_reader.Eof)
{
current = _reader.Peek();
if (current == ']')
{
break;
}
builder.Append(_reader.Read());
}
if (_reader.Eof)
if (current != ']')
{
throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}.");
}
_reader.Read();
Current = new MarkupToken(MarkupTokenKind.Open, builder.ToString(), position);
Current = new MarkupToken(MarkupTokenKind.Close, string.Empty, position);
return true;
}
else
var builder = new StringBuilder();
while (!_reader.Eof)
{
var position = _reader.Position;
var builder = new StringBuilder();
var encounteredClosing = false;
while (!_reader.Eof)
current = _reader.Peek();
if (current == ']')
{
current = _reader.Peek();
if (current == '[')
{
break;
}
else if (current == ']')
{
if (encounteredClosing)
{
_reader.Read();
encounteredClosing = false;
continue;
}
encounteredClosing = true;
}
else
{
if (encounteredClosing)
{
throw new InvalidOperationException(
$"Encountered unescaped ']' token at position {_reader.Position}");
}
}
builder.Append(_reader.Read());
break;
}
if (encounteredClosing)
{
throw new InvalidOperationException($"Encountered unescaped ']' token at position {_reader.Position}");
}
Current = new MarkupToken(MarkupTokenKind.Text, builder.ToString(), position);
return true;
builder.Append(_reader.Read());
}
if (_reader.Eof)
{
throw new InvalidOperationException($"Encountered malformed markup tag at position {_reader.Position}.");
}
_reader.Read();
Current = new MarkupToken(MarkupTokenKind.Open, builder.ToString(), position);
return true;
}
else
{
var position = _reader.Position;
var builder = new StringBuilder();
var encounteredClosing = false;
while (!_reader.Eof)
{
current = _reader.Peek();
if (current == '[')
{
break;
}
else if (current == ']')
{
if (encounteredClosing)
{
_reader.Read();
encounteredClosing = false;
continue;
}
encounteredClosing = true;
}
else
{
if (encounteredClosing)
{
throw new InvalidOperationException(
$"Encountered unescaped ']' token at position {_reader.Position}");
}
}
builder.Append(_reader.Read());
}
if (encounteredClosing)
{
throw new InvalidOperationException($"Encountered unescaped ']' token at position {_reader.Position}");
}
Current = new MarkupToken(MarkupTokenKind.Text, builder.ToString(), position);
return true;
}
}
}
}

View File

@ -1,50 +1,49 @@
using System;
using System.IO;
namespace Spectre.Console
namespace Spectre.Console;
internal sealed class StringBuffer : IDisposable
{
internal sealed class StringBuffer : IDisposable
private readonly StringReader _reader;
private readonly int _length;
public int Position { get; private set; }
public bool Eof => Position >= _length;
public StringBuffer(string text)
{
private readonly StringReader _reader;
private readonly int _length;
text ??= string.Empty;
public int Position { get; private set; }
public bool Eof => Position >= _length;
_reader = new StringReader(text);
_length = text.Length;
public StringBuffer(string text)
{
text ??= string.Empty;
_reader = new StringReader(text);
_length = text.Length;
Position = 0;
}
public void Dispose()
{
_reader.Dispose();
}
public char Peek()
{
if (Eof)
{
throw new InvalidOperationException("Tried to peek past the end of the text.");
}
return (char)_reader.Peek();
}
public char Read()
{
if (Eof)
{
throw new InvalidOperationException("Tried to read past the end of the text.");
}
Position++;
return (char)_reader.Read();
}
Position = 0;
}
}
public void Dispose()
{
_reader.Dispose();
}
public char Peek()
{
if (Eof)
{
throw new InvalidOperationException("Tried to peek past the end of the text.");
}
return (char)_reader.Peek();
}
public char Read()
{
if (Eof)
{
throw new InvalidOperationException("Tried to read past the end of the text.");
}
Position++;
return (char)_reader.Read();
}
}

View File

@ -3,52 +3,51 @@ using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
namespace Spectre.Console
namespace Spectre.Console;
internal static class TypeConverterHelper
{
internal static class TypeConverterHelper
public static string ConvertToString<T>(T input)
{
public static string ConvertToString<T>(T input)
return GetTypeConverter<T>().ConvertToInvariantString(input);
}
public static bool TryConvertFromString<T>(string input, [MaybeNull] out T result)
{
try
{
return GetTypeConverter<T>().ConvertToInvariantString(input);
result = (T)GetTypeConverter<T>().ConvertFromInvariantString(input);
return true;
}
public static bool TryConvertFromString<T>(string input, [MaybeNull] out T result)
catch
{
try
{
result = (T)GetTypeConverter<T>().ConvertFromInvariantString(input);
return true;
}
catch
{
result = default;
return false;
}
}
public static TypeConverter GetTypeConverter<T>()
{
var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null)
{
return converter;
}
var attribute = typeof(T).GetCustomAttribute<TypeConverterAttribute>();
if (attribute != null)
{
var type = Type.GetType(attribute.ConverterTypeName, false, false);
if (type != null)
{
converter = Activator.CreateInstance(type) as TypeConverter;
if (converter != null)
{
return converter;
}
}
}
throw new InvalidOperationException("Could not find type converter");
result = default;
return false;
}
}
}
public static TypeConverter GetTypeConverter<T>()
{
var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null)
{
return converter;
}
var attribute = typeof(T).GetCustomAttribute<TypeConverterAttribute>();
if (attribute != null)
{
var type = Type.GetType(attribute.ConverterTypeName, false, false);
if (type != null)
{
converter = Activator.CreateInstance(type) as TypeConverter;
if (converter != null)
{
return converter;
}
}
}
throw new InvalidOperationException("Could not find type converter");
}
}