Add link support for supported terminals

Also refactors the code quite a bit, to make it a bit more
easier to add features like this in the future.

Closes #75
This commit is contained in:
Patrik Svensson
2020-09-11 00:34:45 +02:00
committed by Patrik Svensson
parent 1601ef24b3
commit 504746c5dc
33 changed files with 574 additions and 1539 deletions

View File

@ -1,3 +1,4 @@
using System;
using System.Linq;
namespace Spectre.Console.Internal
@ -5,40 +6,59 @@ namespace Spectre.Console.Internal
internal static class AnsiBuilder
{
public static string GetAnsi(
ColorSystem system,
Capabilities capabilities,
string text,
Decoration decoration,
Color foreground,
Color background)
Color background,
string? link)
{
var codes = AnsiDecorationBuilder.GetAnsiCodes(decoration);
// Got foreground?
if (foreground != Color.Default)
{
codes = codes.Concat(AnsiColorBuilder.GetAnsiCodes(system, foreground, foreground: true));
codes = codes.Concat(
AnsiColorBuilder.GetAnsiCodes(
capabilities.ColorSystem,
foreground,
true));
}
// Got background?
if (background != Color.Default)
{
codes = codes.Concat(AnsiColorBuilder.GetAnsiCodes(system, background, foreground: false));
codes = codes.Concat(
AnsiColorBuilder.GetAnsiCodes(
capabilities.ColorSystem,
background,
false));
}
var result = codes.ToArray();
if (result.Length == 0)
if (result.Length == 0 && link == null)
{
return text;
}
var lol = string.Concat(
"\u001b[",
string.Join(";", result),
"m",
text,
"\u001b[0m");
var ansiCodes = string.Join(";", result);
var ansi = result.Length > 0
? $"\u001b[{ansiCodes}m{text}\u001b[0m"
: text;
return lol;
if (link != null && !capabilities.LegacyConsole)
{
// Empty links means we should take the URL from the text.
if (link.Equals(Constants.EmptyLink, StringComparison.Ordinal))
{
link = text;
}
var linkId = Math.Abs(link.GetDeterministicHashCode());
ansi = $"\u001b]8;id={linkId};{link}\u001b\\{ansi}\u001b]8;;\u001b\\";
}
return ansi;
}
}
}

View File

@ -7,13 +7,9 @@ namespace Spectre.Console.Internal
internal sealed class AnsiConsoleRenderer : IAnsiConsole
{
private readonly TextWriter _out;
private readonly ColorSystem _system;
public Capabilities Capabilities { get; }
public Encoding Encoding { get; }
public Decoration Decoration { get; set; }
public Color Foreground { get; set; }
public Color Background { get; set; }
public int Width
{
@ -44,28 +40,26 @@ namespace Spectre.Console.Internal
public AnsiConsoleRenderer(TextWriter @out, ColorSystem system, bool legacyConsole)
{
_out = @out ?? throw new ArgumentNullException(nameof(@out));
_system = system;
Capabilities = new Capabilities(true, system, legacyConsole);
Encoding = @out.IsStandardOut() ? System.Console.OutputEncoding : Encoding.UTF8;
Foreground = Color.Default;
Background = Color.Default;
Decoration = Decoration.None;
}
public void Write(string text)
public void Write(string text, Style style)
{
if (string.IsNullOrEmpty(text))
{
return;
}
style ??= Style.Plain;
var parts = text.NormalizeLineEndings().Split(new[] { '\n' });
foreach (var (_, _, last, part) in parts.Enumerate())
{
if (!string.IsNullOrEmpty(part))
{
_out.Write(AnsiBuilder.GetAnsi(_system, part, Decoration, Foreground, Background));
_out.Write(AnsiBuilder.GetAnsi(Capabilities, part, style.Decoration, style.Foreground, style.Background, style.Link));
}
if (!last)

View File

@ -56,10 +56,7 @@ namespace Spectre.Console.Internal
if (supportsAnsi)
{
return new AnsiConsoleRenderer(buffer, colorSystem, legacyConsole)
{
Decoration = Decoration.None,
};
return new AnsiConsoleRenderer(buffer, colorSystem, legacyConsole);
}
return new FallbackConsoleRenderer(buffer, colorSystem, legacyConsole);

View File

@ -4,5 +4,7 @@ namespace Spectre.Console.Internal
{
public const int DefaultBufferWidth = 80;
public const int DefaultBufferHeight = 9001;
public const string EmptyLink = "https://emptylink";
}
}

View File

@ -1,147 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace Spectre.Console.Internal
{
internal static class AnsiConsoleExtensions
{
public static IDisposable PushStyle(this IAnsiConsole console, Style style)
{
if (console is null)
{
throw new ArgumentNullException(nameof(console));
}
if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
var current = new Style(console.Foreground, console.Background, console.Decoration);
console.SetColor(style.Foreground, true);
console.SetColor(style.Background, false);
console.Decoration = style.Decoration;
return new StyleScope(console, current);
}
public static IDisposable PushColor(this IAnsiConsole console, Color color, bool foreground)
{
if (console is null)
{
throw new ArgumentNullException(nameof(console));
}
var current = foreground ? console.Foreground : console.Background;
console.SetColor(color, foreground);
return new ColorScope(console, current, foreground);
}
public static IDisposable PushDecoration(this IAnsiConsole console, Decoration decoration)
{
if (console is null)
{
throw new ArgumentNullException(nameof(console));
}
var current = console.Decoration;
console.Decoration = decoration;
return new DecorationScope(console, current);
}
public static void SetColor(this IAnsiConsole console, Color color, bool foreground)
{
if (console is null)
{
throw new ArgumentNullException(nameof(console));
}
if (foreground)
{
console.Foreground = color;
}
else
{
console.Background = color;
}
}
}
internal sealed class StyleScope : IDisposable
{
private readonly IAnsiConsole _console;
private readonly Style _style;
public StyleScope(IAnsiConsole console, Style style)
{
_console = console ?? throw new ArgumentNullException(nameof(console));
_style = style ?? throw new ArgumentNullException(nameof(style));
}
[SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations")]
[SuppressMessage("Performance", "CA1821:Remove empty Finalizers")]
~StyleScope()
{
throw new InvalidOperationException("Style scope was not disposed.");
}
public void Dispose()
{
GC.SuppressFinalize(this);
_console.SetColor(_style.Foreground, true);
_console.SetColor(_style.Background, false);
_console.Decoration = _style.Decoration;
}
}
internal sealed class ColorScope : IDisposable
{
private readonly IAnsiConsole _console;
private readonly Color _color;
private readonly bool _foreground;
public ColorScope(IAnsiConsole console, Color color, bool foreground)
{
_console = console ?? throw new ArgumentNullException(nameof(console));
_color = color;
_foreground = foreground;
}
[SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations")]
[SuppressMessage("Performance", "CA1821:Remove empty Finalizers")]
~ColorScope()
{
throw new InvalidOperationException("Color scope was not disposed.");
}
public void Dispose()
{
GC.SuppressFinalize(this);
_console.SetColor(_color, _foreground);
}
}
internal sealed class DecorationScope : IDisposable
{
private readonly IAnsiConsole _console;
private readonly Decoration _decoration;
public DecorationScope(IAnsiConsole console, Decoration decoration)
{
_console = console ?? throw new ArgumentNullException(nameof(console));
_decoration = decoration;
}
[SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations")]
[SuppressMessage("Performance", "CA1821:Remove empty Finalizers")]
~DecorationScope()
{
throw new InvalidOperationException("Decoration scope was not disposed.");
}
public void Dispose()
{
GC.SuppressFinalize(this);
_console.Decoration = _decoration;
}
}
}

View File

@ -78,5 +78,28 @@ namespace Spectre.Console.Internal
return result.ToArray();
}
// https://andrewlock.net/why-is-string-gethashcode-different-each-time-i-run-my-program-in-net-core/
public static int GetDeterministicHashCode(this string str)
{
unchecked
{
var hash1 = (5381 << 16) + 5381;
var hash2 = hash1;
for (var i = 0; i < str.Length; i += 2)
{
hash1 = ((hash1 << 5) + hash1) ^ str[i];
if (i == str.Length - 1)
{
break;
}
hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
}
return hash1 + (hash2 * 1566083941);
}
}
}
}

View File

@ -1,4 +1,3 @@
using System;
using System.IO;
using System.Text;
@ -6,16 +5,11 @@ namespace Spectre.Console.Internal
{
internal sealed class FallbackConsoleRenderer : IAnsiConsole
{
private readonly ConsoleColor _defaultForeground;
private readonly ConsoleColor _defaultBackground;
private readonly TextWriter _out;
private readonly ColorSystem _system;
private ConsoleColor _foreground;
private ConsoleColor _background;
private Style? _lastStyle;
public Capabilities Capabilities { get; }
public Encoding Encoding { get; }
public int Width
@ -44,47 +38,6 @@ namespace Spectre.Console.Internal
}
}
public Decoration Decoration { get; set; }
public Color Foreground
{
get => _foreground;
set
{
_foreground = Color.ToConsoleColor(value);
if (_system != ColorSystem.NoColors && _out.IsStandardOut())
{
if ((int)_foreground == -1)
{
_foreground = _defaultForeground;
}
System.Console.ForegroundColor = _foreground;
}
}
}
public Color Background
{
get => _background;
set
{
_background = Color.ToConsoleColor(value);
if (_system != ColorSystem.NoColors && _out.IsStandardOut())
{
if ((int)_background == -1)
{
_background = _defaultBackground;
}
if (_system != ColorSystem.NoColors)
{
System.Console.BackgroundColor = _background;
}
}
}
}
public FallbackConsoleRenderer(TextWriter @out, ColorSystem system, bool legacyConsole)
{
_out = @out;
@ -92,25 +45,46 @@ namespace Spectre.Console.Internal
if (_out.IsStandardOut())
{
_defaultForeground = System.Console.ForegroundColor;
_defaultBackground = System.Console.BackgroundColor;
Encoding = System.Console.OutputEncoding;
}
else
{
_defaultForeground = ConsoleColor.Gray;
_defaultBackground = ConsoleColor.Black;
Encoding = Encoding.UTF8;
}
Capabilities = new Capabilities(false, _system, legacyConsole);
}
public void Write(string text)
public void Write(string text, Style style)
{
if (_lastStyle?.Equals(style) != true)
{
SetStyle(style);
}
_out.Write(text.NormalizeLineEndings(native: true));
}
private void SetStyle(Style style)
{
_lastStyle = style;
if (_out.IsStandardOut())
{
System.Console.ResetColor();
var background = Color.ToConsoleColor(style.Background);
if (_system != ColorSystem.NoColors && _out.IsStandardOut() && (int)background != -1)
{
System.Console.BackgroundColor = background;
}
var foreground = Color.ToConsoleColor(style.Foreground);
if (_system != ColorSystem.NoColors && _out.IsStandardOut() && (int)foreground != -1)
{
System.Console.ForegroundColor = foreground;
}
}
}
}
}

View File

@ -35,6 +35,7 @@ namespace Spectre.Console.Internal
var effectiveDecoration = (Decoration?)null;
var effectiveForeground = (Color?)null;
var effectiveBackground = (Color?)null;
var effectiveLink = (string?)null;
var parts = text.Split(new[] { ' ' });
var foreground = true;
@ -51,6 +52,23 @@ namespace Spectre.Console.Internal
continue;
}
if (part.StartsWith("link=", StringComparison.OrdinalIgnoreCase))
{
if (effectiveLink != null)
{
error = "A link has already been set.";
return null;
}
effectiveLink = part.Substring(5);
continue;
}
else if (part.StartsWith("link", StringComparison.OrdinalIgnoreCase))
{
effectiveLink = Constants.EmptyLink;
continue;
}
var decoration = DecorationTable.GetDecoration(part);
if (decoration != null)
{
@ -116,7 +134,11 @@ namespace Spectre.Console.Internal
}
error = null;
return new Style(effectiveForeground, effectiveBackground, effectiveDecoration);
return new Style(
effectiveForeground,
effectiveBackground,
effectiveDecoration,
effectiveLink);
}
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]