Add profile support

Closes #231
This commit is contained in:
Patrik Svensson
2021-01-16 17:23:58 +01:00
committed by Patrik Svensson
parent 913a7b1e37
commit a23bec4082
230 changed files with 1241 additions and 1628 deletions

View File

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static class Aligner
{

View File

@ -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();
}
}
}
}
}

View File

@ -1,17 +1,17 @@
using System;
using System.Linq;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal sealed class AnsiBuilder
{
private readonly Capabilities _capabilities;
private readonly ILinkIdentityGenerator _linkHasher;
private readonly Profile _profile;
private readonly AnsiLinkHasher _linkHasher;
public AnsiBuilder(Capabilities capabilities, ILinkIdentityGenerator? linkHasher)
public AnsiBuilder(Profile profile)
{
_capabilities = capabilities ?? throw new ArgumentNullException(nameof(capabilities));
_linkHasher = linkHasher ?? new LinkIdentityGenerator();
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
_linkHasher = new AnsiLinkHasher();
}
public string GetAnsi(string text, Style style)
@ -28,7 +28,7 @@ namespace Spectre.Console.Internal
{
codes = codes.Concat(
AnsiColorBuilder.GetAnsiCodes(
_capabilities.ColorSystem,
_profile.ColorSystem,
style.Foreground,
true));
}
@ -38,7 +38,7 @@ namespace Spectre.Console.Internal
{
codes = codes.Concat(
AnsiColorBuilder.GetAnsiCodes(
_capabilities.ColorSystem,
_profile.ColorSystem,
style.Background,
false));
}
@ -54,7 +54,7 @@ namespace Spectre.Console.Internal
? $"\u001b[{ansiCodes}m{text}\u001b[0m"
: text;
if (style.Link != null && !_capabilities.LegacyConsole)
if (style.Link != null && !_profile.Capabilities.Legacy)
{
var link = style.Link;

View File

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static class AnsiColorBuilder
{

View File

@ -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();
}
}
}
}

View File

@ -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") });
}
}
}

View File

@ -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"));
}
}
}

View File

@ -1,6 +1,6 @@
using System.Collections.Generic;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static class AnsiDecorationBuilder
{

View File

@ -9,7 +9,7 @@ using System.Text.RegularExpressions;
// https://github.com/keqingrong/supports-ansi/blob/master/index.js
/////////////////////////////////////////////////////////////////////////////////////////////////////
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static class AnsiDetector
{
@ -34,12 +34,6 @@ namespace Spectre.Console.Internal
public static (bool SupportsAnsi, bool LegacyConsole) Detect(bool upgrade)
{
// Github action doesn't setup a correct PTY but supports ANSI.
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("GITHUB_ACTION")))
{
return (true, false);
}
// Running on Windows?
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
@ -72,7 +66,6 @@ namespace Spectre.Console.Internal
return (false, true);
}
[SuppressMessage("Design", "CA1060:Move pinvokes to native methods class")]
internal static class Windows
{
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")]
@ -96,7 +89,6 @@ namespace Spectre.Console.Internal
[DllImport("kernel32.dll")]
public static extern uint GetLastError();
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
public static bool SupportsAnsi(bool upgrade, out bool isLegacy)
{
isLegacy = false;

View File

@ -1,13 +1,13 @@
using System;
using System.Runtime.CompilerServices;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal sealed class LinkIdentityGenerator : ILinkIdentityGenerator
internal sealed class AnsiLinkHasher
{
private readonly Random _random;
public LinkIdentityGenerator()
public AnsiLinkHasher()
{
_random = new Random(DateTime.Now.Millisecond);
}

View 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;
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}
}
}

View 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);
}
}

View File

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static class InteractivityDetector
{

View File

@ -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;
}
}
}
}

View File

@ -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)
{

View File

@ -2,7 +2,7 @@ using System.Linq;
using Spectre.Console.Rendering;
using Wcwidth;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static class Cell
{

View File

@ -2,7 +2,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal sealed class ListWithCallback<T> : IList<T>, IReadOnlyList<T>
{

View File

@ -10,7 +10,7 @@
using System.Collections.Generic;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static partial class ColorPalette
{

View File

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static partial class ColorPalette
{

View File

@ -2,7 +2,7 @@ using System;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static class ColorSystemDetector
{

View File

@ -11,7 +11,7 @@
using System;
using System.Collections.Generic;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static partial class ColorTable
{

View File

@ -1,15 +1,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static partial class ColorTable
{
private static readonly Dictionary<int, string> _nameLookup;
private static readonly Dictionary<string, int> _numberLookup;
[SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline")]
static ColorTable()
{
_numberLookup = GenerateTable();

View File

@ -1,6 +1,6 @@
using System.IO;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static class ConsoleHelper
{

View File

@ -1,17 +0,0 @@
using System;
namespace Spectre.Console.Internal
{
internal sealed class ConsoleInput : IAnsiConsoleInput
{
public ConsoleKeyInfo ReadKey(bool intercept)
{
if (!Environment.UserInteractive)
{
throw new InvalidOperationException("Failed to read input in non-interactive mode.");
}
return System.Console.ReadKey(intercept);
}
}
}

View File

@ -1,4 +1,4 @@
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static class Constants
{

View File

@ -1,16 +1,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static class DecorationTable
{
private static readonly Dictionary<string, Decoration?> _lookup;
private static readonly Dictionary<Decoration, string> _reverseLookup;
[SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline")]
static DecorationTable()
{
_lookup = new Dictionary<string, Decoration?>(StringComparer.OrdinalIgnoreCase)

View File

@ -0,0 +1,29 @@
using System;
namespace Spectre.Console
{
internal sealed class DefaultInput : IAnsiConsoleInput
{
private readonly Profile _profile;
public DefaultInput(Profile profile)
{
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
}
public ConsoleKeyInfo ReadKey(bool intercept)
{
if (_profile.Capabilities.Tty)
{
throw new InvalidOperationException("Cannot read input from a TTY console.");
}
if (!_profile.Capabilities.Interactive)
{
throw new InvalidOperationException("Failed to read input in non-interactive mode.");
}
return System.Console.ReadKey(intercept);
}
}
}

View File

@ -1,171 +0,0 @@
using System;
using System.Linq;
using System.Text;
using Spectre.Console.Rendering;
namespace Spectre.Console.Internal
{
internal static class ExceptionFormatter
{
public static IRenderable Format(Exception exception, ExceptionSettings settings)
{
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
var info = ExceptionParser.Parse(exception.ToString());
if (info == null)
{
return new Text(exception.ToString());
}
return GetException(info, settings);
}
private static IRenderable GetException(ExceptionInfo info, ExceptionSettings settings)
{
if (info is null)
{
throw new ArgumentNullException(nameof(info));
}
return new Rows(new IRenderable[]
{
GetMessage(info, settings),
GetStackFrames(info, settings),
}).Expand();
}
private static Markup GetMessage(ExceptionInfo ex, ExceptionSettings settings)
{
var shortenTypes = (settings.Format & ExceptionFormats.ShortenTypes) != 0;
var type = Emphasize(ex.Type, new[] { '.' }, settings.Style.Exception, shortenTypes, settings);
var message = $"[{settings.Style.Message.ToMarkup()}]{ex.Message.EscapeMarkup()}[/]";
return new Markup(string.Concat(type, ": ", message));
}
private static Grid GetStackFrames(ExceptionInfo ex, ExceptionSettings settings)
{
var styles = settings.Style;
var grid = new Grid();
grid.AddColumn(new GridColumn().PadLeft(2).PadRight(0).NoWrap());
grid.AddColumn(new GridColumn().PadLeft(1).PadRight(0));
// Inner
if (ex.Inner != null)
{
grid.AddRow(
Text.Empty,
GetException(ex.Inner, settings));
}
// Stack frames
foreach (var frame in ex.Frames)
{
var builder = new StringBuilder();
// Method
var shortenMethods = (settings.Format & ExceptionFormats.ShortenMethods) != 0;
builder.Append(Emphasize(frame.Method, new[] { '.' }, styles.Method, shortenMethods, settings));
builder.AppendWithStyle(styles.Parenthesis, "(");
AppendParameters(builder, frame, settings);
builder.AppendWithStyle(styles.Parenthesis, ")");
if (frame.Path != null)
{
builder.Append(' ');
builder.AppendWithStyle(styles.Dimmed, "in");
builder.Append(' ');
// Path
AppendPath(builder, frame, settings);
// Line number
if (frame.LineNumber != null)
{
builder.AppendWithStyle(styles.Dimmed, ":");
builder.AppendWithStyle(styles.LineNumber, frame.LineNumber);
}
}
grid.AddRow(
$"[{styles.Dimmed.ToMarkup()}]at[/]",
builder.ToString());
}
return grid;
}
private static void AppendParameters(StringBuilder builder, StackFrameInfo frame, ExceptionSettings settings)
{
var typeColor = settings.Style.ParameterType.ToMarkup();
var nameColor = settings.Style.ParameterName.ToMarkup();
var parameters = frame.Parameters.Select(x => $"[{typeColor}]{x.Type.EscapeMarkup()}[/] [{nameColor}]{x.Name.EscapeMarkup()}[/]");
builder.Append(string.Join(", ", parameters));
}
private static void AppendPath(StringBuilder builder, StackFrameInfo frame, ExceptionSettings settings)
{
if (frame?.Path is null)
{
return;
}
void AppendPath()
{
var shortenPaths = (settings.Format & ExceptionFormats.ShortenPaths) != 0;
builder.Append(Emphasize(frame.Path, new[] { '/', '\\' }, settings.Style.Path, shortenPaths, settings));
}
if ((settings.Format & ExceptionFormats.ShowLinks) != 0)
{
var hasLink = frame.TryGetUri(out var uri);
if (hasLink && uri != null)
{
builder.Append("[link=").Append(uri.AbsoluteUri).Append(']');
}
AppendPath();
if (hasLink && uri != null)
{
builder.Append("[/]");
}
}
else
{
AppendPath();
}
}
private static string Emphasize(string input, char[] separators, Style color, bool compact, ExceptionSettings settings)
{
var builder = new StringBuilder();
var type = input;
var index = type.LastIndexOfAny(separators);
if (index != -1)
{
if (!compact)
{
builder.AppendWithStyle(
settings.Style.NonEmphasized,
type.Substring(0, index + 1).EscapeMarkup());
}
builder.AppendWithStyle(
color,
type.Substring(index + 1, type.Length - index - 1).EscapeMarkup());
}
else
{
builder.Append(type.EscapeMarkup());
}
return builder.ToString();
}
}
}

View File

@ -1,23 +0,0 @@
using System.Collections.Generic;
namespace Spectre.Console.Internal
{
internal sealed class ExceptionInfo
{
public string Type { get; }
public string Message { get; }
public List<StackFrameInfo> Frames { get; }
public ExceptionInfo? Inner { get; }
public ExceptionInfo(
string type, string message,
List<StackFrameInfo> frames,
ExceptionInfo? inner)
{
Type = type ?? string.Empty;
Message = message ?? string.Empty;
Frames = frames ?? new List<StackFrameInfo>();
Inner = inner;
}
}
}

View File

@ -1,142 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Spectre.Console.Internal
{
internal static class ExceptionParser
{
private static readonly Regex _messageRegex = new Regex(@"^(?'type'.*):\s(?'message'.*)$");
private static readonly Regex _stackFrameRegex = new Regex(@"^\s*\w*\s(?'method'.*)\((?'params'.*)\)");
private static readonly Regex _fullStackFrameRegex = new Regex(@"^\s*(?'at'\w*)\s(?'method'.*)\((?'params'.*)\)\s(?'in'\w*)\s(?'path'.*)\:(?'line'\w*)\s(?'linenumber'\d*)$");
public static ExceptionInfo? Parse(string exception)
{
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
var lines = exception.SplitLines();
return Parse(new Queue<string>(lines));
}
private static ExceptionInfo? Parse(Queue<string> lines)
{
if (lines.Count == 0)
{
// Error: No lines to parse
return null;
}
var line = lines.Dequeue();
line = line.ReplaceExact(" ---> ", string.Empty);
var match = _messageRegex.Match(line);
if (!match.Success)
{
return null;
}
var inner = (ExceptionInfo?)null;
// Stack frames
var frames = new List<StackFrameInfo>();
while (lines.Count > 0)
{
if (lines.Peek().TrimStart().StartsWith("---> ", StringComparison.OrdinalIgnoreCase))
{
inner = Parse(lines);
if (inner == null)
{
// Error: Could not parse inner exception
return null;
}
continue;
}
line = lines.Dequeue();
if (string.IsNullOrWhiteSpace(line))
{
// Empty line
continue;
}
if (line.TrimStart().StartsWith("--- ", StringComparison.OrdinalIgnoreCase))
{
// End of inner exception
break;
}
var stackFrame = ParseStackFrame(line);
if (stackFrame == null)
{
// Error: Could not parse stack frame
return null;
}
frames.Add(stackFrame);
}
return new ExceptionInfo(
match.Groups["type"].Value,
match.Groups["message"].Value,
frames, inner);
}
private static StackFrameInfo? ParseStackFrame(string frame)
{
var match = _fullStackFrameRegex.Match(frame);
if (match?.Success != true)
{
match = _stackFrameRegex.Match(frame);
if (match?.Success != true)
{
return null;
}
}
var parameters = ParseMethodParameters(match.Groups["params"].Value);
if (parameters == null)
{
// Error: Could not parse parameters
return null;
}
var method = match.Groups["method"].Value;
var path = match.Groups["path"].Success ? match.Groups["path"].Value : null;
var lineNumber = (int?)null;
if (!string.IsNullOrWhiteSpace(match.Groups["linenumber"].Value))
{
lineNumber = int.Parse(match.Groups["linenumber"].Value, CultureInfo.InvariantCulture);
}
return new StackFrameInfo(method, parameters, path, lineNumber);
}
private static List<(string Type, string Name)>? ParseMethodParameters(string parameters)
{
var result = new List<(string Type, string Name)>();
foreach (var parameterPart in parameters.Split(new[] { ", " }, StringSplitOptions.RemoveEmptyEntries))
{
var parameterNameIndex = parameterPart.LastIndexOf(' ');
if (parameterNameIndex == -1)
{
// Error: Could not parse parameter
return null;
}
var type = parameterPart.Substring(0, parameterNameIndex);
var name = parameterPart.Substring(parameterNameIndex + 1, parameterPart.Length - parameterNameIndex - 1);
result.Add((type, name));
}
return result;
}
}
}

View File

@ -1,7 +1,7 @@
using System;
using System.Globalization;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal struct FileSize
{
@ -31,7 +31,7 @@ namespace Spectre.Console.Internal
var bytes = Bytes / @base;
return Unit == FileSizeUnit.Bytes
return Unit == FileSizeUnit.Byte
? ((int)bytes).ToString(culture ?? CultureInfo.InvariantCulture)
: bytes.ToString("F1", culture ?? CultureInfo.InvariantCulture);
}
@ -78,7 +78,7 @@ namespace Spectre.Console.Internal
}
}
return FileSizeUnit.Bytes;
return FileSizeUnit.Byte;
}
private static double GetBase(FileSizeUnit unit)

View File

@ -1,10 +1,8 @@
using System;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal enum FileSizeUnit
{
Bytes = 0,
Byte = 0,
KiloByte = 1,
MegaByte = 2,
GigaByte = 3,

View File

@ -6,7 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static class Ratio
{

View File

@ -1,7 +1,7 @@
using System;
using System.IO;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static class ResourceReader
{

View File

@ -1,65 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
namespace Spectre.Console.Internal
{
internal sealed class StackFrameInfo
{
public string Method { get; }
public List<(string Type, string Name)> Parameters { get; }
public string? Path { get; }
public int? LineNumber { get; }
public StackFrameInfo(
string method, List<(string Type, string Name)> parameters,
string? path, int? lineNumber)
{
Method = method ?? throw new ArgumentNullException(nameof(method));
Parameters = parameters ?? throw new ArgumentNullException(nameof(parameters));
Path = path;
LineNumber = lineNumber;
}
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
public bool TryGetUri([NotNullWhen(true)] out Uri? result)
{
try
{
if (Path == null)
{
result = null;
return false;
}
if (!Uri.TryCreate(Path, UriKind.Absolute, out var uri))
{
result = null;
return false;
}
if (uri.Scheme == "file")
{
// For local files, we need to append
// the host name. Otherwise the terminal
// will most probably not allow it.
var builder = new UriBuilder(uri)
{
Host = Dns.GetHostName(),
};
uri = builder.Uri;
}
result = uri;
return true;
}
catch
{
result = null;
return false;
}
}
}
}

View File

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Text;
using Spectre.Console.Rendering;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal sealed class HtmlEncoder : IAnsiConsoleEncoder
{

View File

@ -2,7 +2,7 @@ using System.Collections.Generic;
using System.Text;
using Spectre.Console.Rendering;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal sealed class TextEncoder : IAnsiConsoleEncoder
{

View File

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static class MarkupParser
{

View File

@ -1,6 +1,6 @@
using System;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal sealed class MarkupToken
{

View File

@ -1,4 +1,4 @@
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal enum MarkupTokenKind
{

View File

@ -1,7 +1,7 @@
using System;
using System.Text;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal sealed class MarkupTokenizer : IDisposable
{

View File

@ -1,12 +1,10 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal sealed class StringBuffer : IDisposable
{
[SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "False positive")]
private readonly StringReader _reader;
private readonly int _length;

View File

@ -1,218 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace Spectre.Console.Internal
{
internal static class StyleParser
{
public static Style Parse(string text)
{
var style = Parse(text, out var error);
if (error != null)
{
throw new InvalidOperationException(error);
}
if (style == null)
{
// This should not happen, but we need to please the compiler
// which cannot know that style isn't null here.
throw new InvalidOperationException("Could not parse style.");
}
return style;
}
public static bool TryParse(string text, out Style? style)
{
style = Parse(text, out var error);
return error == null;
}
private static Style? Parse(string text, out string? error)
{
var effectiveDecoration = (Decoration?)null;
var effectiveForeground = (Color?)null;
var effectiveBackground = (Color?)null;
var effectiveLink = (string?)null;
var parts = text.Split(new[] { ' ' });
var foreground = true;
foreach (var part in parts)
{
if (part.Equals("default", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (part.Equals("on", StringComparison.OrdinalIgnoreCase))
{
foreground = false;
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)
{
effectiveDecoration ??= Decoration.None;
effectiveDecoration |= decoration.Value;
}
else
{
var color = ColorTable.GetColor(part);
if (color == null)
{
if (part.StartsWith("#", StringComparison.OrdinalIgnoreCase))
{
color = ParseHexColor(part, out error);
if (!string.IsNullOrWhiteSpace(error))
{
return null;
}
}
else if (part.StartsWith("rgb", StringComparison.OrdinalIgnoreCase))
{
color = ParseRgbColor(part, out error);
if (!string.IsNullOrWhiteSpace(error))
{
return null;
}
}
else
{
error = !foreground
? $"Could not find color '{part}'."
: $"Could not find color or style '{part}'.";
return null;
}
}
if (foreground)
{
if (effectiveForeground != null)
{
error = "A foreground color has already been set.";
return null;
}
effectiveForeground = color;
}
else
{
if (effectiveBackground != null)
{
error = "A background color has already been set.";
return null;
}
effectiveBackground = color;
}
}
}
error = null;
return new Style(
effectiveForeground,
effectiveBackground,
effectiveDecoration,
effectiveLink);
}
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
private static Color? ParseHexColor(string hex, out string? error)
{
error = null;
hex ??= string.Empty;
hex = hex.ReplaceExact("#", string.Empty).Trim();
try
{
if (!string.IsNullOrWhiteSpace(hex))
{
if (hex.Length == 6)
{
return new Color(
(byte)Convert.ToUInt32(hex.Substring(0, 2), 16),
(byte)Convert.ToUInt32(hex.Substring(2, 2), 16),
(byte)Convert.ToUInt32(hex.Substring(4, 2), 16));
}
else if (hex.Length == 3)
{
return new Color(
(byte)Convert.ToUInt32(new string(hex[0], 2), 16),
(byte)Convert.ToUInt32(new string(hex[1], 2), 16),
(byte)Convert.ToUInt32(new string(hex[2], 2), 16));
}
}
}
catch (Exception ex)
{
error = $"Invalid hex color '#{hex}'. {ex.Message}";
return null;
}
error = $"Invalid hex color '#{hex}'.";
return null;
}
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
private static Color? ParseRgbColor(string rgb, out string? error)
{
try
{
error = null;
var normalized = rgb ?? string.Empty;
if (normalized.Length >= 3)
{
// Trim parentheses
normalized = normalized.Substring(3).Trim();
if (normalized.StartsWith("(", StringComparison.OrdinalIgnoreCase) &&
normalized.EndsWith(")", StringComparison.OrdinalIgnoreCase))
{
normalized = normalized.Trim('(').Trim(')');
var parts = normalized.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 3)
{
return new Color(
(byte)Convert.ToInt32(parts[0], CultureInfo.InvariantCulture),
(byte)Convert.ToInt32(parts[1], CultureInfo.InvariantCulture),
(byte)Convert.ToInt32(parts[2], CultureInfo.InvariantCulture));
}
}
}
}
catch (Exception ex)
{
error = $"Invalid RGB color '{rgb}'. {ex.Message}";
return null;
}
error = $"Invalid RGB color '{rgb}'.";
return null;
}
}
}

View File

@ -3,7 +3,7 @@ using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
namespace Spectre.Console.Internal
namespace Spectre.Console
{
internal static class TypeConverterHelper
{
@ -12,7 +12,6 @@ namespace Spectre.Console.Internal
return GetTypeConverter<T>().ConvertToInvariantString(input);
}
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
public static bool TryConvertFromString<T>(string input, [MaybeNull] out T result)
{
try