Add output abstraction and reorganize profile

* Moves ColorSystem from Profile to Capabilities
* Renames Tty to IsTerminal
* Adds IAnsiConsoleOutput to make output more flexible

Closes #343
Closes #359
Closes #369
This commit is contained in:
Patrik Svensson 2021-04-12 18:15:21 +02:00 committed by Phil Scott
parent bc9f610258
commit 3e2eea730b
27 changed files with 194 additions and 139 deletions

View File

@ -7,7 +7,7 @@ namespace Spectre.Console.Examples
/////////////////////////////////////////////////////////////////
// No colors
/////////////////////////////////////////////////////////////////
if (AnsiConsole.Profile.ColorSystem == ColorSystem.NoColors)
if (AnsiConsole.Profile.Capabilities.ColorSystem == ColorSystem.NoColors)
{
AnsiConsole.WriteLine("No colors are supported.");
return;

View File

@ -8,13 +8,13 @@ namespace Spectre.Console.Examples
.AddColumn(new GridColumn().NoWrap().PadRight(4))
.AddColumn()
.AddRow("[b]Enrichers[/]", string.Join(", ", AnsiConsole.Profile.Enrichers))
.AddRow("[b]Color system[/]", $"{AnsiConsole.Profile.ColorSystem}")
.AddRow("[b]Color system[/]", $"{AnsiConsole.Profile.Capabilities.ColorSystem}")
.AddRow("[b]Unicode?[/]", $"{YesNo(AnsiConsole.Profile.Capabilities.Unicode)}")
.AddRow("[b]Supports ansi?[/]", $"{YesNo(AnsiConsole.Profile.Capabilities.Ansi)}")
.AddRow("[b]Supports links?[/]", $"{YesNo(AnsiConsole.Profile.Capabilities.Links)}")
.AddRow("[b]Legacy console?[/]", $"{YesNo(AnsiConsole.Profile.Capabilities.Legacy)}")
.AddRow("[b]Interactive?[/]", $"{YesNo(AnsiConsole.Profile.Capabilities.Interactive)}")
.AddRow("[b]TTY?[/]", $"{YesNo(AnsiConsole.Profile.Capabilities.Tty)}")
.AddRow("[b]Terminal?[/]", $"{YesNo(AnsiConsole.Profile.Out.IsTerminal)}")
.AddRow("[b]Buffer width[/]", $"{AnsiConsole.Console.Profile.Width}")
.AddRow("[b]Buffer height[/]", $"{AnsiConsole.Console.Profile.Height}")
.AddRow("[b]Encoding[/]", $"{AnsiConsole.Console.Profile.Encoding.EncodingName}");

View File

@ -33,7 +33,7 @@ namespace Spectre.Console.Testing
{
Ansi = ansi,
ColorSystem = (ColorSystemSupport)colors,
Out = _writer,
Out = new AnsiConsoleOutput(_writer),
Enrichment = new ProfileEnrichment
{
UseDefaultEnrichers = false,

View File

@ -2,13 +2,15 @@ namespace Spectre.Console.Testing
{
public sealed class FakeCapabilities : IReadOnlyCapabilities
{
public ColorSystem ColorSystem { get; set; } = ColorSystem.TrueColor;
public bool Ansi { get; set; }
public bool Links { get; set; }
public bool Legacy { get; set; }
public bool Tty { get; set; }
public bool IsTerminal { get; set; }
public bool Interactive { get; set; }

View File

@ -16,7 +16,7 @@ namespace Spectre.Console.Testing
public RenderPipeline Pipeline { get; }
public FakeConsoleInput Input { get; }
public string Output => Profile.Out.ToString();
public string Output => Profile.Out.Writer.ToString();
public IReadOnlyList<string> Lines => Output.TrimEnd('\n').Split(new char[] { '\n' });
public FakeConsole(
@ -28,10 +28,10 @@ namespace Spectre.Console.Testing
ExclusivityMode = new FakeExclusivityMode();
Pipeline = new RenderPipeline();
Profile = new Profile(new StringWriter(), encoding ?? Encoding.UTF8);
Profile = new Profile(new AnsiConsoleOutput(new StringWriter()), encoding ?? Encoding.UTF8);
Profile.Width = width;
Profile.Height = height;
Profile.ColorSystem = colorSystem;
Profile.Capabilities.ColorSystem = colorSystem;
Profile.Capabilities.Ansi = supportsAnsi;
Profile.Capabilities.Legacy = legacyConsole;
Profile.Capabilities.Interactive = interactive;
@ -41,7 +41,7 @@ namespace Spectre.Console.Testing
public void Dispose()
{
Profile.Out.Dispose();
Profile.Out.Writer.Dispose();
}
public void Clear(bool home)
@ -52,7 +52,7 @@ namespace Spectre.Console.Testing
{
foreach (var segment in renderable.GetSegments(this))
{
Profile.Out.Write(segment.Text);
Profile.Out.Writer.Write(segment.Text);
}
}

View File

@ -19,11 +19,11 @@ namespace Spectre.Console.Tests.Unit
var console = AnsiConsole.Create(new AnsiConsoleSettings
{
ColorSystem = requested,
Out = new StringWriter(),
Out = new AnsiConsoleOutput(new StringWriter()),
});
// Then
console.Profile.ColorSystem.ShouldBe(expected);
console.Profile.Capabilities.ColorSystem.ShouldBe(expected);
}
public sealed class TrueColor

View File

@ -20,7 +20,7 @@ namespace Spectre.Console.Tests.Unit
public string Render()
{
var console = new FakeConsole();
var context = new RenderContext(console.Profile.ColorSystem, console.Profile.Capabilities);
var context = new RenderContext(console.Profile.Capabilities);
console.Write(Column.Render(context, Task, TimeSpan.Zero));
return console.Output;
}

View File

@ -130,7 +130,6 @@ namespace Spectre.Console.Tests.Unit
}
[Theory]
[InlineData(0, "Hello World Hello World Hello World Hello World Hello World", "")]
[InlineData(1, "Hello World Hello World Hello World Hello World Hello World", "─")]
[InlineData(2, "Hello World Hello World Hello World Hello World Hello World", "──")]
[InlineData(3, "Hello World Hello World Hello World Hello World Hello World", "───")]

View File

@ -15,7 +15,7 @@ namespace Spectre.Console.Tests.Unit
var text = new Text("Foo Bar Baz\nQux\nLol mobile");
// When
var result = ((IRenderable)text).Measure(new RenderContext(ColorSystem.TrueColor, caps), 80);
var result = ((IRenderable)text).Measure(new RenderContext(caps), 80);
// Then
result.Min.ShouldBe(6);
@ -29,7 +29,7 @@ namespace Spectre.Console.Tests.Unit
var text = new Text("Foo Bar Baz\nQux\nLol mobile");
// When
var result = ((IRenderable)text).Measure(new RenderContext(ColorSystem.TrueColor, caps), 80);
var result = ((IRenderable)text).Measure(new RenderContext(caps), 80);
// Then
result.Max.ShouldBe(11);

View File

@ -15,7 +15,7 @@ namespace Spectre.Console
{
Ansi = AnsiSupport.Detect,
ColorSystem = ColorSystemSupport.Detect,
Out = System.Console.Out,
Out = new AnsiConsoleOutput(System.Console.Out),
});
Created = true;

View File

@ -22,13 +22,18 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(settings));
}
var buffer = settings.Out ?? System.Console.Out;
var output = settings.Out ?? new AnsiConsoleOutput(System.Console.Out);
if (output.Writer == null)
{
throw new InvalidOperationException("Output writer was null");
}
// Detect if the terminal support ANSI or not
var (supportsAnsi, legacyConsole) = DetectAnsi(settings, buffer);
var (supportsAnsi, legacyConsole) = DetectAnsi(settings, output.Writer);
// Use console encoding or fall back to provided encoding
var encoding = buffer.IsStandardOut() || buffer.IsStandardError() ? System.Console.OutputEncoding : buffer.Encoding;
var encoding = output.Writer.IsStandardOut() || output.Writer.IsStandardError()
? System.Console.OutputEncoding : output.Writer.Encoding;
// Get the color system
var colorSystem = settings.ColorSystem == ColorSystemSupport.Detect
@ -42,11 +47,9 @@ namespace Spectre.Console
interactive = Environment.UserInteractive;
}
var profile = new Profile(buffer, encoding)
{
ColorSystem = colorSystem,
};
var profile = new Profile(output, encoding);
profile.Capabilities.ColorSystem = colorSystem;
profile.Capabilities.Ansi = supportsAnsi;
profile.Capabilities.Links = supportsAnsi && !legacyConsole;
profile.Capabilities.Legacy = legacyConsole;

View File

@ -0,0 +1,58 @@
using System;
using System.IO;
using System.Text;
namespace Spectre.Console
{
/// <summary>
/// Represents console output.
/// </summary>
public sealed class AnsiConsoleOutput : IAnsiConsoleOutput
{
/// <inheritdoc/>
public TextWriter Writer { get; }
/// <inheritdoc/>
public bool IsTerminal
{
get
{
if (Writer.IsStandardOut())
{
return !System.Console.IsOutputRedirected;
}
if (Writer.IsStandardError())
{
return !System.Console.IsErrorRedirected;
}
return false;
}
}
/// <inheritdoc/>
public int Width => ConsoleHelper.GetSafeWidth(Constants.DefaultTerminalWidth);
/// <inheritdoc/>
public int Height => ConsoleHelper.GetSafeHeight(Constants.DefaultTerminalWidth);
/// <summary>
/// Initializes a new instance of the <see cref="AnsiConsoleOutput"/> class.
/// </summary>
/// <param name="writer">The output writer.</param>
public AnsiConsoleOutput(TextWriter writer)
{
Writer = writer ?? throw new ArgumentNullException(nameof(writer));
}
/// <inheritdoc/>
public void SetEncoding(Encoding encoding)
{
if (Writer.IsStandardOut() || Writer.IsStandardError())
{
System.Console.OutputEncoding = encoding;
}
}
}
}

View File

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.IO;
namespace Spectre.Console
{
@ -22,7 +21,7 @@ namespace Spectre.Console
/// <summary>
/// Gets or sets the out buffer.
/// </summary>
public TextWriter? Out { get; set; }
public IAnsiConsoleOutput? Out { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not the

View File

@ -7,7 +7,12 @@ namespace Spectre.Console
/// </summary>
public sealed class Capabilities : IReadOnlyCapabilities
{
private readonly Profile _profile;
private readonly IAnsiConsoleOutput _out;
/// <summary>
/// Gets or sets the color system.
/// </summary>
public ColorSystem ColorSystem { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not
@ -33,26 +38,10 @@ namespace Spectre.Console
/// <summary>
/// Gets a value indicating whether or not
/// console output has been redirected.
/// the output is a terminal.
/// </summary>
public bool Tty
{
get
{
if (_profile.Out.IsStandardOut())
{
return System.Console.IsOutputRedirected;
}
if (_profile.Out.IsStandardError())
{
return System.Console.IsErrorRedirected;
}
// Not stdout, so must be a TTY.
return true;
}
}
[Obsolete("Use Profile.Out.IsTerminal instead")]
public bool IsTerminal => _out.IsTerminal;
/// <summary>
/// Gets or sets a value indicating whether
@ -67,11 +56,12 @@ namespace Spectre.Console
public bool Unicode { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Capabilities"/> class.
/// Initializes a new instance of the
/// <see cref="Capabilities"/> class.
/// </summary>
internal Capabilities(Profile profile)
internal Capabilities(IAnsiConsoleOutput @out)
{
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
_out = @out ?? throw new ArgumentNullException(nameof(@out));
}
}
}

View File

@ -27,7 +27,7 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(renderable));
}
var context = new RenderContext(console.Profile.ColorSystem, console.Profile.Capabilities);
var context = new RenderContext(console.Profile.Capabilities);
var renderables = console.Pipeline.Process(context, new[] { renderable });
return GetSegments(console, context, renderables);

View File

@ -0,0 +1,37 @@
using System.IO;
using System.Text;
namespace Spectre.Console
{
/// <summary>
/// Represents console output.
/// </summary>
public interface IAnsiConsoleOutput
{
/// <summary>
/// Gets the <see cref="TextWriter"/> used to write to the output.
/// </summary>
TextWriter Writer { get; }
/// <summary>
/// Gets a value indicating whether or not the output is a terminal.
/// </summary>
bool IsTerminal { get; }
/// <summary>
/// Gets the output width.
/// </summary>
int Width { get; }
/// <summary>
/// Gets the output height.
/// </summary>
int Height { get; }
/// <summary>
/// Sets the output encoding.
/// </summary>
/// <param name="encoding">The encoding.</param>
void SetEncoding(Encoding encoding);
}
}

View File

@ -1,3 +1,5 @@
using System;
namespace Spectre.Console
{
/// <summary>
@ -5,6 +7,11 @@ namespace Spectre.Console
/// </summary>
public interface IReadOnlyCapabilities
{
/// <summary>
/// Gets the color system.
/// </summary>
ColorSystem ColorSystem { get; }
/// <summary>
/// Gets a value indicating whether or not
/// the console supports Ansi.
@ -31,7 +38,8 @@ namespace Spectre.Console
/// Gets a value indicating whether or not
/// console output has been redirected.
/// </summary>
bool Tty { get; }
[Obsolete("Use Profile.Out.IsTerminal instead")]
bool IsTerminal { get; }
/// <summary>
/// Gets a value indicating whether

View File

@ -29,7 +29,7 @@ namespace Spectre.Console
{
codes = codes.Concat(
AnsiColorBuilder.GetAnsiCodes(
_profile.ColorSystem,
_profile.Capabilities.ColorSystem,
style.Foreground,
true));
}
@ -39,7 +39,7 @@ namespace Spectre.Console
{
codes = codes.Concat(
AnsiColorBuilder.GetAnsiCodes(
_profile.ColorSystem,
_profile.Capabilities.ColorSystem,
style.Background,
false));
}

View File

@ -59,8 +59,8 @@ namespace Spectre.Console
if (builder.Length > 0)
{
_console.Profile.Out.Write(builder.ToString());
_console.Profile.Out.Flush();
_console.Profile.Out.Writer.Write(builder.ToString());
_console.Profile.Out.Writer.Flush();
}
}
}

View File

@ -44,7 +44,7 @@ namespace Spectre.Console
SetStyle(segment.Style);
}
_console.Profile.Out.Write(segment.Text.NormalizeNewLines(native: true));
_console.Profile.Out.Writer.Write(segment.Text.NormalizeNewLines(native: true));
}
}
@ -55,13 +55,13 @@ namespace Spectre.Console
System.Console.ResetColor();
var background = Color.ToConsoleColor(style.Background);
if (_console.Profile.ColorSystem != ColorSystem.NoColors && (int)background != -1)
if (_console.Profile.Capabilities.ColorSystem != ColorSystem.NoColors && (int)background != -1)
{
System.Console.BackgroundColor = background;
}
var foreground = Color.ToConsoleColor(style.Foreground);
if (_console.Profile.ColorSystem != ColorSystem.NoColors && (int)foreground != -1)
if (_console.Profile.Capabilities.ColorSystem != ColorSystem.NoColors && (int)foreground != -1)
{
System.Console.ForegroundColor = foreground;
}

View File

@ -13,11 +13,6 @@ namespace Spectre.Console
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.");

View File

@ -0,0 +1,19 @@
namespace Spectre.Console.Internal
{
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)
{
ColorSystem = colors;
}
}
}

View File

@ -9,7 +9,7 @@ namespace Spectre.Console.Internal
{
public string Encode(IAnsiConsole console, IEnumerable<IRenderable> renderables)
{
var context = new RenderContext(ColorSystem.TrueColor, EncoderCapabilities.Default);
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");

View File

@ -4,23 +4,11 @@ using Spectre.Console.Rendering;
namespace Spectre.Console.Internal
{
internal sealed class EncoderCapabilities : IReadOnlyCapabilities
{
public bool Ansi => false;
public bool Links => false;
public bool Legacy => false;
public bool Tty => false;
public bool Interactive => false;
public bool Unicode => true;
public static EncoderCapabilities Default { get; } = new EncoderCapabilities();
}
internal sealed class TextEncoder : IAnsiConsoleEncoder
{
public string Encode(IAnsiConsole console, IEnumerable<IRenderable> renderables)
{
var context = new RenderContext(ColorSystem.TrueColor, EncoderCapabilities.Default);
var context = new RenderContext(new EncoderCapabilities(ColorSystem.TrueColor));
var builder = new StringBuilder();
foreach (var renderable in renderables)

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Spectre.Console
@ -13,7 +12,7 @@ namespace Spectre.Console
private readonly HashSet<string> _enrichers;
private static readonly string[] _defaultEnricher = new[] { "Default" };
private TextWriter _out;
private IAnsiConsoleOutput _out;
private Encoding _encoding;
private Capabilities _capabilities;
private int? _width;
@ -38,15 +37,15 @@ namespace Spectre.Console
/// <summary>
/// Gets or sets the out buffer.
/// </summary>
public TextWriter Out
public IAnsiConsoleOutput Out
{
get => _out;
set
{
_out = value ?? throw new InvalidOperationException("Output buffer cannot be null");
// Reset the width and height if not a TTY.
if (!Capabilities.Tty)
// Reset the width and height if this is a terminal.
if (value.IsTerminal)
{
_width = null;
_height = null;
@ -67,12 +66,7 @@ namespace Spectre.Console
throw new InvalidOperationException("Encoding cannot be null");
}
// Need to update the output encoding for stdout?
if (_out.IsStandardOut() || _out.IsStandardError())
{
System.Console.OutputEncoding = value;
}
_out.SetEncoding(value);
_encoding = value;
}
}
@ -82,10 +76,10 @@ namespace Spectre.Console
/// </summary>
public int Width
{
get => GetWidth();
get => _width ?? _out.Width;
set
{
if (_width <= 0)
if (value <= 0)
{
throw new InvalidOperationException("Console width must be greater than zero");
}
@ -99,10 +93,10 @@ namespace Spectre.Console
/// </summary>
public int Height
{
get => GetHeight();
get => _height ?? _out.Height;
set
{
if (_height <= 0)
if (value <= 0)
{
throw new InvalidOperationException("Console height must be greater than zero");
}
@ -111,11 +105,6 @@ namespace Spectre.Console
}
}
/// <summary>
/// Gets or sets the color system.
/// </summary>
public ColorSystem ColorSystem { get; set; }
/// <summary>
/// Gets or sets the capabilities of the profile.
/// </summary>
@ -133,12 +122,12 @@ namespace Spectre.Console
/// </summary>
/// <param name="out">The output buffer.</param>
/// <param name="encoding">The output encoding.</param>
public Profile(TextWriter @out, Encoding encoding)
public Profile(IAnsiConsoleOutput @out, Encoding encoding)
{
_enrichers = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_out = @out ?? throw new ArgumentNullException(nameof(@out));
_encoding = encoding ?? throw new ArgumentNullException(nameof(encoding));
_capabilities = new Capabilities(this);
_capabilities = new Capabilities(_out);
}
/// <summary>
@ -149,7 +138,7 @@ namespace Spectre.Console
/// <returns><c>true</c> if the color system is supported, otherwise <c>false</c>.</returns>
public bool Supports(ColorSystem colorSystem)
{
return (int)colorSystem <= (int)ColorSystem;
return (int)colorSystem <= (int)Capabilities.ColorSystem;
}
internal void AddEnricher(string name)
@ -161,35 +150,5 @@ namespace Spectre.Console
_enrichers.Add(name);
}
private int GetWidth()
{
if (_width != null)
{
return _width.Value;
}
if (!Capabilities.Tty)
{
return ConsoleHelper.GetSafeWidth(Constants.DefaultTerminalWidth);
}
return Constants.DefaultTerminalWidth;
}
private int GetHeight()
{
if (_height != null)
{
return _height.Value;
}
if (!Capabilities.Tty)
{
return ConsoleHelper.GetSafeHeight(Constants.DefaultTerminalHeight);
}
return Constants.DefaultTerminalHeight;
}
}
}

View File

@ -12,7 +12,7 @@ namespace Spectre.Console.Rendering
/// <summary>
/// Gets the current color system.
/// </summary>
public ColorSystem ColorSystem { get; }
public ColorSystem ColorSystem => _capabilities.ColorSystem;
/// <summary>
/// Gets a value indicating whether or not unicode is supported.
@ -33,19 +33,17 @@ namespace Spectre.Console.Rendering
/// <summary>
/// Initializes a new instance of the <see cref="RenderContext"/> class.
/// </summary>
/// <param name="colorSystem">The color system.</param>
/// <param name="capabilities">The capabilities.</param>
/// <param name="justification">The justification.</param>
public RenderContext(ColorSystem colorSystem, IReadOnlyCapabilities capabilities, Justify? justification = null)
: this(colorSystem, capabilities, justification, false)
public RenderContext(IReadOnlyCapabilities capabilities, Justify? justification = null)
: this(capabilities, justification, false)
{
}
private RenderContext(ColorSystem colorSystem, IReadOnlyCapabilities capabilities, Justify? justification = null, bool singleLine = false)
private RenderContext(IReadOnlyCapabilities capabilities, Justify? justification = null, bool singleLine = false)
{
_capabilities = capabilities ?? throw new ArgumentNullException(nameof(capabilities));
ColorSystem = colorSystem;
Justification = justification;
SingleLine = singleLine;
}
@ -57,7 +55,7 @@ namespace Spectre.Console.Rendering
/// <returns>A new <see cref="RenderContext"/> instance.</returns>
public RenderContext WithJustification(Justify? justification)
{
return new RenderContext(ColorSystem, _capabilities, justification, SingleLine);
return new RenderContext(_capabilities, justification, SingleLine);
}
/// <summary>
@ -72,7 +70,7 @@ namespace Spectre.Console.Rendering
/// <returns>A new <see cref="RenderContext"/> instance.</returns>
internal RenderContext WithSingleLine()
{
return new RenderContext(ColorSystem, _capabilities, Justification, true);
return new RenderContext(_capabilities, Justification, true);
}
}
}

View File

@ -62,7 +62,7 @@ namespace Spectre.Console
_stopwatch.Start();
}
var renderContext = new RenderContext(_console.Profile.ColorSystem, _console.Profile.Capabilities);
var renderContext = new RenderContext(_console.Profile.Capabilities);
var delta = _stopwatch.Elapsed - _lastUpdate;
_lastUpdate = _stopwatch.Elapsed;