mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-06-19 13:28:16 +08:00
Use file scoped namespace declarations
This commit is contained in:

committed by
Phil Scott

parent
1dbaf50935
commit
ec1188b837
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 };
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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)));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
namespace Spectre.Console
|
||||
namespace Spectre.Console;
|
||||
|
||||
internal enum MarkupTokenKind
|
||||
{
|
||||
internal enum MarkupTokenKind
|
||||
{
|
||||
Text = 0,
|
||||
Open,
|
||||
Close,
|
||||
}
|
||||
}
|
||||
Text = 0,
|
||||
Open,
|
||||
Close,
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user