mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-06-19 13:28:16 +08:00

committed by
Patrik Svensson

parent
913a7b1e37
commit
a23bec4082
@ -1,112 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console.Internal
|
||||
{
|
||||
internal sealed class AnsiBackend : IAnsiConsole
|
||||
{
|
||||
private readonly TextWriter _out;
|
||||
private readonly AnsiBuilder _ansiBuilder;
|
||||
private readonly AnsiCursor _cursor;
|
||||
private readonly ConsoleInput _input;
|
||||
private readonly object _lock;
|
||||
|
||||
public Capabilities Capabilities { get; }
|
||||
public Encoding Encoding { get; }
|
||||
public RenderPipeline Pipeline { get; }
|
||||
public IAnsiConsoleCursor Cursor => _cursor;
|
||||
public IAnsiConsoleInput Input => _input;
|
||||
|
||||
public int Width
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_out.IsStandardOut())
|
||||
{
|
||||
return ConsoleHelper.GetSafeWidth(Constants.DefaultTerminalWidth);
|
||||
}
|
||||
|
||||
return Constants.DefaultTerminalWidth;
|
||||
}
|
||||
}
|
||||
|
||||
public int Height
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_out.IsStandardOut())
|
||||
{
|
||||
return ConsoleHelper.GetSafeHeight(Constants.DefaultTerminalHeight);
|
||||
}
|
||||
|
||||
return Constants.DefaultTerminalHeight;
|
||||
}
|
||||
}
|
||||
|
||||
public AnsiBackend(TextWriter @out, Capabilities capabilities, ILinkIdentityGenerator? linkHasher)
|
||||
{
|
||||
_out = @out ?? throw new ArgumentNullException(nameof(@out));
|
||||
|
||||
Capabilities = capabilities ?? throw new ArgumentNullException(nameof(capabilities));
|
||||
Encoding = _out.IsStandardOut() ? System.Console.OutputEncoding : Encoding.UTF8;
|
||||
Pipeline = new RenderPipeline();
|
||||
|
||||
_ansiBuilder = new AnsiBuilder(Capabilities, linkHasher);
|
||||
_cursor = new AnsiCursor(this);
|
||||
_input = new ConsoleInput();
|
||||
_lock = new object();
|
||||
}
|
||||
|
||||
public void Clear(bool home)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
Write(new[] { Segment.Control("\u001b[2J") });
|
||||
|
||||
if (home)
|
||||
{
|
||||
Cursor.SetPosition(0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(IEnumerable<Segment> segments)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
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(_ansiBuilder.GetAnsi(part, segment.Style));
|
||||
}
|
||||
|
||||
if (!last)
|
||||
{
|
||||
builder.Append(Environment.NewLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (builder.Length > 0)
|
||||
{
|
||||
_out.Write(builder.ToString());
|
||||
_out.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
74
src/Spectre.Console/Internal/Backends/Ansi/AnsiBuilder.cs
Normal file
74
src/Spectre.Console/Internal/Backends/Ansi/AnsiBuilder.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
internal sealed class AnsiBuilder
|
||||
{
|
||||
private readonly Profile _profile;
|
||||
private readonly AnsiLinkHasher _linkHasher;
|
||||
|
||||
public AnsiBuilder(Profile profile)
|
||||
{
|
||||
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
|
||||
_linkHasher = new AnsiLinkHasher();
|
||||
}
|
||||
|
||||
public string GetAnsi(string text, Style style)
|
||||
{
|
||||
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.ColorSystem,
|
||||
style.Foreground,
|
||||
true));
|
||||
}
|
||||
|
||||
// Got background?
|
||||
if (style.Background != Color.Default)
|
||||
{
|
||||
codes = codes.Concat(
|
||||
AnsiColorBuilder.GetAnsiCodes(
|
||||
_profile.ColorSystem,
|
||||
style.Background,
|
||||
false));
|
||||
}
|
||||
|
||||
var result = codes.ToArray();
|
||||
if (result.Length == 0 && style.Link == null)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
var ansiCodes = string.Join(";", result);
|
||||
var ansi = result.Length > 0
|
||||
? $"\u001b[{ansiCodes}m{text}\u001b[0m"
|
||||
: 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 = $"\u001b]8;id={linkId};{link}\u001b\\{ansi}\u001b]8;;\u001b\\";
|
||||
}
|
||||
|
||||
return ansi;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
internal static class AnsiColorBuilder
|
||||
{
|
||||
public static IEnumerable<byte> GetAnsiCodes(ColorSystem system, Color color, bool foreground)
|
||||
{
|
||||
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 };
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
internal sealed class AnsiConsoleBackend : IAnsiConsoleBackend
|
||||
{
|
||||
private readonly AnsiBuilder _builder;
|
||||
private readonly Profile _profile;
|
||||
|
||||
public IAnsiConsoleCursor Cursor { get; }
|
||||
|
||||
public AnsiConsoleBackend(Profile profile)
|
||||
{
|
||||
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
|
||||
_builder = new AnsiBuilder(profile);
|
||||
|
||||
Cursor = new AnsiConsoleCursor(this);
|
||||
}
|
||||
|
||||
public void Clear(bool home)
|
||||
{
|
||||
Render(new[] { Segment.Control("\u001b[2J") });
|
||||
|
||||
if (home)
|
||||
{
|
||||
Cursor.SetPosition(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void Render(IEnumerable<Segment> segments)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
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(_builder.GetAnsi(part, segment.Style));
|
||||
}
|
||||
|
||||
if (!last)
|
||||
{
|
||||
builder.Append(Environment.NewLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (builder.Length > 0)
|
||||
{
|
||||
_profile.Out.Write(builder.ToString());
|
||||
_profile.Out.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
internal sealed class AnsiConsoleCursor : IAnsiConsoleCursor
|
||||
{
|
||||
private readonly AnsiConsoleBackend _backend;
|
||||
|
||||
public AnsiConsoleCursor(AnsiConsoleBackend backend)
|
||||
{
|
||||
_backend = backend ?? throw new ArgumentNullException(nameof(backend));
|
||||
}
|
||||
|
||||
public void Show(bool show)
|
||||
{
|
||||
if (show)
|
||||
{
|
||||
_backend.Render(new[] { Segment.Control("\u001b[?25h") });
|
||||
}
|
||||
else
|
||||
{
|
||||
_backend.Render(new[] { Segment.Control("\u001b[?25l") });
|
||||
}
|
||||
}
|
||||
|
||||
public void Move(CursorDirection direction, int steps)
|
||||
{
|
||||
if (steps == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case CursorDirection.Up:
|
||||
_backend.Render(new[] { Segment.Control($"\u001b[{steps}A") });
|
||||
break;
|
||||
case CursorDirection.Down:
|
||||
_backend.Render(new[] { Segment.Control($"\u001b[{steps}B") });
|
||||
break;
|
||||
case CursorDirection.Right:
|
||||
_backend.Render(new[] { Segment.Control($"\u001b[{steps}C") });
|
||||
break;
|
||||
case CursorDirection.Left:
|
||||
_backend.Render(new[] { Segment.Control($"\u001b[{steps}D") });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetPosition(int column, int line)
|
||||
{
|
||||
_backend.Render(new[] { Segment.Control($"\u001b[{line};{column}H") });
|
||||
}
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
using System;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console.Internal
|
||||
{
|
||||
internal sealed class AnsiCursor : IAnsiConsoleCursor
|
||||
{
|
||||
private readonly AnsiBackend _renderer;
|
||||
|
||||
public AnsiCursor(AnsiBackend renderer)
|
||||
{
|
||||
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
|
||||
}
|
||||
|
||||
public void Show(bool show)
|
||||
{
|
||||
if (show)
|
||||
{
|
||||
_renderer.Write(Segment.Control("\u001b[?25h"));
|
||||
}
|
||||
else
|
||||
{
|
||||
_renderer.Write(Segment.Control("\u001b[?25l"));
|
||||
}
|
||||
}
|
||||
|
||||
public void Move(CursorDirection direction, int steps)
|
||||
{
|
||||
if (steps == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case CursorDirection.Up:
|
||||
_renderer.Write(Segment.Control($"\u001b[{steps}A"));
|
||||
break;
|
||||
case CursorDirection.Down:
|
||||
_renderer.Write(Segment.Control($"\u001b[{steps}B"));
|
||||
break;
|
||||
case CursorDirection.Right:
|
||||
_renderer.Write(Segment.Control($"\u001b[{steps}C"));
|
||||
break;
|
||||
case CursorDirection.Left:
|
||||
_renderer.Write(Segment.Control($"\u001b[{steps}D"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetPosition(int column, int line)
|
||||
{
|
||||
_renderer.Write(Segment.Control($"\u001b[{line};{column}H"));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
internal static class AnsiDecorationBuilder
|
||||
{
|
||||
// TODO: Rewrite this to not yield
|
||||
public static IEnumerable<byte> GetAnsiCodes(Decoration decoration)
|
||||
{
|
||||
if ((decoration & Decoration.Bold) != 0)
|
||||
{
|
||||
yield return 1;
|
||||
}
|
||||
|
||||
if ((decoration & Decoration.Dim) != 0)
|
||||
{
|
||||
yield return 2;
|
||||
}
|
||||
|
||||
if ((decoration & Decoration.Italic) != 0)
|
||||
{
|
||||
yield return 3;
|
||||
}
|
||||
|
||||
if ((decoration & Decoration.Underline) != 0)
|
||||
{
|
||||
yield return 4;
|
||||
}
|
||||
|
||||
if ((decoration & Decoration.SlowBlink) != 0)
|
||||
{
|
||||
yield return 5;
|
||||
}
|
||||
|
||||
if ((decoration & Decoration.RapidBlink) != 0)
|
||||
{
|
||||
yield return 6;
|
||||
}
|
||||
|
||||
if ((decoration & Decoration.Invert) != 0)
|
||||
{
|
||||
yield return 7;
|
||||
}
|
||||
|
||||
if ((decoration & Decoration.Conceal) != 0)
|
||||
{
|
||||
yield return 8;
|
||||
}
|
||||
|
||||
if ((decoration & Decoration.Strikethrough) != 0)
|
||||
{
|
||||
yield return 9;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
138
src/Spectre.Console/Internal/Backends/Ansi/AnsiDetector.cs
Normal file
138
src/Spectre.Console/Internal/Backends/Ansi/AnsiDetector.cs
Normal file
@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Portions of this code was ported from the supports-ansi project by Qingrong Ke
|
||||
// https://github.com/keqingrong/supports-ansi/blob/master/index.js
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
internal static class AnsiDetector
|
||||
{
|
||||
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
|
||||
};
|
||||
|
||||
public static (bool SupportsAnsi, bool LegacyConsole) Detect(bool upgrade)
|
||||
{
|
||||
// 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))
|
||||
{
|
||||
return (true, false);
|
||||
}
|
||||
|
||||
var supportsAnsi = Windows.SupportsAnsi(upgrade, out var legacyConsole);
|
||||
return (supportsAnsi, legacyConsole);
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
if (_regexes.Any(regex => regex.IsMatch(term)))
|
||||
{
|
||||
return (true, false);
|
||||
}
|
||||
}
|
||||
|
||||
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 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, out bool isLegacy)
|
||||
{
|
||||
isLegacy = false;
|
||||
|
||||
try
|
||||
{
|
||||
var @out = GetStdHandle(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();
|
||||
|
||||
isLegacy = ansiFromTerm ? legacyFromTerm : isLegacy;
|
||||
return ansiFromTerm;
|
||||
}
|
||||
|
||||
if ((mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// All we know here is that we don't support ANSI.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
42
src/Spectre.Console/Internal/Backends/Ansi/AnsiLinkHasher.cs
Normal file
42
src/Spectre.Console/Internal/Backends/Ansi/AnsiLinkHasher.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
internal sealed class AnsiLinkHasher
|
||||
{
|
||||
private readonly Random _random;
|
||||
|
||||
public AnsiLinkHasher()
|
||||
{
|
||||
_random = new Random(DateTime.Now.Millisecond);
|
||||
}
|
||||
|
||||
public int GenerateId(string link, string text)
|
||||
{
|
||||
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 NET5_0
|
||||
return link.GetHashCode(StringComparison.Ordinal);
|
||||
#else
|
||||
return link.GetHashCode();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
55
src/Spectre.Console/Internal/Backends/AnsiConsoleFacade.cs
Normal file
55
src/Spectre.Console/Internal/Backends/AnsiConsoleFacade.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
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 RenderPipeline Pipeline { get; }
|
||||
|
||||
public AnsiConsoleFacade(Profile profile)
|
||||
{
|
||||
_renderLock = new object();
|
||||
_ansiBackend = new AnsiConsoleBackend(profile);
|
||||
_legacyBackend = new LegacyConsoleBackend(profile);
|
||||
|
||||
Profile = profile ?? throw new ArgumentNullException(nameof(profile));
|
||||
Input = new DefaultInput(Profile);
|
||||
Pipeline = new RenderPipeline();
|
||||
}
|
||||
|
||||
public void Clear(bool home)
|
||||
{
|
||||
lock (_renderLock)
|
||||
{
|
||||
GetBackend().Clear(home);
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(IEnumerable<Segment> segments)
|
||||
{
|
||||
lock (_renderLock)
|
||||
{
|
||||
GetBackend().Render(segments);
|
||||
}
|
||||
}
|
||||
|
||||
private IAnsiConsoleBackend GetBackend()
|
||||
{
|
||||
if (Profile.Capabilities.Ansi)
|
||||
{
|
||||
return _ansiBackend;
|
||||
}
|
||||
|
||||
return _legacyBackend;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Spectre.Console.Internal
|
||||
{
|
||||
internal static class BackendBuilder
|
||||
{
|
||||
public static IAnsiConsole Build(AnsiConsoleSettings settings)
|
||||
{
|
||||
if (settings is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(settings));
|
||||
}
|
||||
|
||||
var buffer = settings.Out ?? System.Console.Out;
|
||||
|
||||
var supportsAnsi = settings.Ansi == AnsiSupport.Yes;
|
||||
var legacyConsole = false;
|
||||
|
||||
if (settings.Ansi == AnsiSupport.Detect)
|
||||
{
|
||||
(supportsAnsi, legacyConsole) = AnsiDetector.Detect(true);
|
||||
|
||||
// Check whether or not this is a legacy console from the existing instance (if any).
|
||||
// We need to do this because once we upgrade the console to support ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||
// on Windows, there is no way of detecting whether or not we're running on a legacy console or not.
|
||||
if (AnsiConsole.Created && !legacyConsole && buffer.IsStandardOut() && AnsiConsole.Capabilities.LegacyConsole)
|
||||
{
|
||||
legacyConsole = AnsiConsole.Capabilities.LegacyConsole;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (buffer.IsStandardOut())
|
||||
{
|
||||
// Are we running on Windows?
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// Not the first console we're creating?
|
||||
if (AnsiConsole.Created)
|
||||
{
|
||||
legacyConsole = AnsiConsole.Capabilities.LegacyConsole;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try detecting whether or not this
|
||||
(_, legacyConsole) = AnsiDetector.Detect(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var supportsInteraction = settings.Interactive == InteractionSupport.Yes;
|
||||
if (settings.Interactive == InteractionSupport.Detect)
|
||||
{
|
||||
supportsInteraction = InteractivityDetector.IsInteractive();
|
||||
}
|
||||
|
||||
var colorSystem = settings.ColorSystem == ColorSystemSupport.Detect
|
||||
? ColorSystemDetector.Detect(supportsAnsi)
|
||||
: (ColorSystem)settings.ColorSystem;
|
||||
|
||||
// Get the capabilities
|
||||
var capabilities = new Capabilities(supportsAnsi, colorSystem, legacyConsole, supportsInteraction);
|
||||
|
||||
// Create the renderer
|
||||
if (supportsAnsi)
|
||||
{
|
||||
return new AnsiBackend(buffer, capabilities, settings.LinkIdentityGenerator);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new FallbackBackend(buffer, capabilities);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console.Internal
|
||||
{
|
||||
internal sealed class FallbackBackend : IAnsiConsole
|
||||
{
|
||||
private readonly ColorSystem _system;
|
||||
private readonly FallbackCursor _cursor;
|
||||
private readonly ConsoleInput _input;
|
||||
private Style? _lastStyle;
|
||||
|
||||
public Capabilities Capabilities { get; }
|
||||
public Encoding Encoding { get; }
|
||||
public RenderPipeline Pipeline { get; }
|
||||
public IAnsiConsoleCursor Cursor => _cursor;
|
||||
public IAnsiConsoleInput Input => _input;
|
||||
|
||||
public int Width
|
||||
{
|
||||
get { return ConsoleHelper.GetSafeWidth(Constants.DefaultTerminalWidth); }
|
||||
}
|
||||
|
||||
public int Height
|
||||
{
|
||||
get { return ConsoleHelper.GetSafeHeight(Constants.DefaultTerminalHeight); }
|
||||
}
|
||||
|
||||
public FallbackBackend(TextWriter @out, Capabilities capabilities)
|
||||
{
|
||||
if (capabilities == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(capabilities));
|
||||
}
|
||||
|
||||
_system = capabilities.ColorSystem;
|
||||
_cursor = new FallbackCursor();
|
||||
_input = new ConsoleInput();
|
||||
|
||||
if (@out != System.Console.Out)
|
||||
{
|
||||
System.Console.SetOut(@out ?? throw new ArgumentNullException(nameof(@out)));
|
||||
}
|
||||
|
||||
Capabilities = capabilities;
|
||||
Encoding = System.Console.OutputEncoding;
|
||||
Pipeline = new RenderPipeline();
|
||||
}
|
||||
|
||||
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(IEnumerable<Segment> segments)
|
||||
{
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
if (segment.IsControlCode)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_lastStyle?.Equals(segment.Style) != true)
|
||||
{
|
||||
SetStyle(segment.Style);
|
||||
}
|
||||
|
||||
System.Console.Write(segment.Text.NormalizeNewLines(native: true));
|
||||
}
|
||||
}
|
||||
|
||||
private void SetStyle(Style style)
|
||||
{
|
||||
_lastStyle = style;
|
||||
|
||||
System.Console.ResetColor();
|
||||
|
||||
var background = Color.ToConsoleColor(style.Background);
|
||||
if (_system != ColorSystem.NoColors && (int)background != -1)
|
||||
{
|
||||
System.Console.BackgroundColor = background;
|
||||
}
|
||||
|
||||
var foreground = Color.ToConsoleColor(style.Foreground);
|
||||
if (_system != ColorSystem.NoColors && (int)foreground != -1)
|
||||
{
|
||||
System.Console.ForegroundColor = foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
src/Spectre.Console/Internal/Backends/IAnsiConsoleBackend.cs
Normal file
28
src/Spectre.Console/Internal/Backends/IAnsiConsoleBackend.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a console backend.
|
||||
/// </summary>
|
||||
internal interface IAnsiConsoleBackend
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the console cursor for the backend.
|
||||
/// </summary>
|
||||
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>
|
||||
/// Renders segments to the console.
|
||||
/// </summary>
|
||||
/// <param name="segments">The segments to render.</param>
|
||||
void Render(IEnumerable<Segment> segments);
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
internal static class InteractivityDetector
|
||||
{
|
||||
private static readonly Dictionary<string, Func<string, bool>> _environmentVariables;
|
||||
|
||||
static InteractivityDetector()
|
||||
{
|
||||
_environmentVariables = new Dictionary<string, Func<string, bool>>
|
||||
{
|
||||
{ "APPVEYOR", v => !string.IsNullOrWhiteSpace(v) },
|
||||
{ "bamboo_buildNumber", v => !string.IsNullOrWhiteSpace(v) },
|
||||
{ "BITBUCKET_REPO_OWNER", v => !string.IsNullOrWhiteSpace(v) },
|
||||
{ "BITBUCKET_REPO_SLUG", v => !string.IsNullOrWhiteSpace(v) },
|
||||
{ "BITBUCKET_COMMIT", v => !string.IsNullOrWhiteSpace(v) },
|
||||
{ "BITRISE_BUILD_URL", v => !string.IsNullOrWhiteSpace(v) },
|
||||
{ "ContinuaCI.Version", v => !string.IsNullOrWhiteSpace(v) },
|
||||
{ "CI_SERVER", v => v.Equals("yes", StringComparison.OrdinalIgnoreCase) }, // GitLab
|
||||
{ "GITHUB_ACTIONS", v => v.Equals("true", StringComparison.OrdinalIgnoreCase) },
|
||||
{ "GO_SERVER_URL", v => !string.IsNullOrWhiteSpace(v) },
|
||||
{ "JENKINS_URL", v => !string.IsNullOrWhiteSpace(v) },
|
||||
{ "BuildRunner", v => v.Equals("MyGet", StringComparison.OrdinalIgnoreCase) },
|
||||
{ "TEAMCITY_VERSION", v => !string.IsNullOrWhiteSpace(v) },
|
||||
{ "TF_BUILD", v => !string.IsNullOrWhiteSpace(v) }, // TFS and Azure
|
||||
{ "TRAVIS", v => !string.IsNullOrWhiteSpace(v) },
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsInteractive()
|
||||
{
|
||||
if (!Environment.UserInteractive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var variable in _environmentVariables)
|
||||
{
|
||||
var func = variable.Value;
|
||||
var value = Environment.GetEnvironmentVariable(variable.Key);
|
||||
if (!string.IsNullOrWhiteSpace(value) && variable.Value(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
using System.Collections.Generic;
|
||||
using Spectre.Console.Rendering;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
internal sealed class LegacyConsoleBackend : IAnsiConsoleBackend
|
||||
{
|
||||
private readonly Profile _profile;
|
||||
private Style _lastStyle;
|
||||
|
||||
public IAnsiConsoleCursor Cursor { get; }
|
||||
|
||||
public LegacyConsoleBackend(Profile profile)
|
||||
{
|
||||
_profile = profile ?? throw new System.ArgumentNullException(nameof(profile));
|
||||
_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 Render(IEnumerable<Segment> segments)
|
||||
{
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
if (segment.IsControlCode)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_lastStyle?.Equals(segment.Style) != true)
|
||||
{
|
||||
SetStyle(segment.Style);
|
||||
}
|
||||
|
||||
_profile.Out.Write(segment.Text.NormalizeNewLines(native: true));
|
||||
}
|
||||
}
|
||||
|
||||
private void SetStyle(Style style)
|
||||
{
|
||||
_lastStyle = style;
|
||||
|
||||
System.Console.ResetColor();
|
||||
|
||||
var background = Color.ToConsoleColor(style.Background);
|
||||
if (_profile.ColorSystem != ColorSystem.NoColors && (int)background != -1)
|
||||
{
|
||||
System.Console.BackgroundColor = background;
|
||||
}
|
||||
|
||||
var foreground = Color.ToConsoleColor(style.Foreground);
|
||||
if (_profile.ColorSystem != ColorSystem.NoColors && (int)foreground != -1)
|
||||
{
|
||||
System.Console.ForegroundColor = foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
namespace Spectre.Console.Internal
|
||||
namespace Spectre.Console
|
||||
{
|
||||
internal sealed class FallbackCursor : IAnsiConsoleCursor
|
||||
internal sealed class LegacyConsoleCursor : IAnsiConsoleCursor
|
||||
{
|
||||
public void Show(bool show)
|
||||
{
|
Reference in New Issue
Block a user