Change IAnsiConsole to render IRenderable

This makes it possible for encoders to output better representation
of the actual objects instead of working with chopped up segments.

* IAnsiConsole.Write now takes an IRenderable instead of segments
* Calculating cell width does no longer require a render context
* Removed RenderContext.LegacyConsole
* Removed RenderContext.Encoding
* Added Capabilities.Unicode
This commit is contained in:
Patrik Svensson
2021-03-24 23:09:24 +01:00
committed by Phil Scott
parent 2ba6da3514
commit 20650f1e7e
75 changed files with 492 additions and 553 deletions

View File

@ -6,14 +6,14 @@ namespace Spectre.Console
{
internal static class Aligner
{
public static string Align(RenderContext context, string text, Justify? alignment, int maxWidth)
public static string Align(string text, Justify? alignment, int maxWidth)
{
if (alignment == null || alignment == Justify.Left)
{
return text;
}
var width = Cell.GetCellLength(context, text);
var width = Cell.GetCellLength(text);
if (width >= maxWidth)
{
return text;
@ -57,7 +57,7 @@ namespace Spectre.Console
return;
}
var width = Segment.CellCount(context, segments);
var width = Segment.CellCount(segments);
if (width >= maxWidth)
{
return;

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using Spectre.Console.Rendering;
@ -8,21 +7,21 @@ namespace Spectre.Console
internal sealed class AnsiConsoleBackend : IAnsiConsoleBackend
{
private readonly AnsiBuilder _builder;
private readonly Profile _profile;
private readonly IAnsiConsole _console;
public IAnsiConsoleCursor Cursor { get; }
public AnsiConsoleBackend(Profile profile)
public AnsiConsoleBackend(IAnsiConsole console)
{
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
_builder = new AnsiBuilder(profile);
_console = console ?? throw new ArgumentNullException(nameof(console));
_builder = new AnsiBuilder(_console.Profile);
Cursor = new AnsiConsoleCursor(this);
}
public void Clear(bool home)
{
Render(new[] { Segment.Control("\u001b[2J") });
Write(new ControlSequence("\u001b[2J"));
if (home)
{
@ -30,10 +29,10 @@ namespace Spectre.Console
}
}
public void Render(IEnumerable<Segment> segments)
public void Write(IRenderable renderable)
{
var builder = new StringBuilder();
foreach (var segment in segments)
foreach (var segment in renderable.GetSegments(_console))
{
if (segment.IsControlCode)
{
@ -58,8 +57,8 @@ namespace Spectre.Console
if (builder.Length > 0)
{
_profile.Out.Write(builder.ToString());
_profile.Out.Flush();
_console.Profile.Out.Write(builder.ToString());
_console.Profile.Out.Flush();
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
@ -16,11 +15,11 @@ namespace Spectre.Console
{
if (show)
{
_backend.Render(new[] { Segment.Control("\u001b[?25h") });
_backend.Write(new ControlSequence("\u001b[?25h"));
}
else
{
_backend.Render(new[] { Segment.Control("\u001b[?25l") });
_backend.Write(new ControlSequence("\u001b[?25l"));
}
}
@ -34,23 +33,23 @@ namespace Spectre.Console
switch (direction)
{
case CursorDirection.Up:
_backend.Render(new[] { Segment.Control($"\u001b[{steps}A") });
_backend.Write(new ControlSequence($"\u001b[{steps}A"));
break;
case CursorDirection.Down:
_backend.Render(new[] { Segment.Control($"\u001b[{steps}B") });
_backend.Write(new ControlSequence($"\u001b[{steps}B"));
break;
case CursorDirection.Right:
_backend.Render(new[] { Segment.Control($"\u001b[{steps}C") });
_backend.Write(new ControlSequence($"\u001b[{steps}C"));
break;
case CursorDirection.Left:
_backend.Render(new[] { Segment.Control($"\u001b[{steps}D") });
_backend.Write(new ControlSequence($"\u001b[{steps}D"));
break;
}
}
public void SetPosition(int column, int line)
{
_backend.Render(new[] { Segment.Control($"\u001b[{line};{column}H") });
_backend.Write(new ControlSequence($"\u001b[{line};{column}H"));
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
@ -19,13 +18,14 @@ namespace Spectre.Console
public AnsiConsoleFacade(Profile profile, IExclusivityMode exclusivityMode)
{
_renderLock = new object();
_ansiBackend = new AnsiConsoleBackend(profile);
_legacyBackend = new LegacyConsoleBackend(profile);
Profile = profile ?? throw new ArgumentNullException(nameof(profile));
Input = new DefaultInput(Profile);
ExclusivityMode = exclusivityMode ?? throw new ArgumentNullException(nameof(exclusivityMode));
Pipeline = new RenderPipeline();
_ansiBackend = new AnsiConsoleBackend(this);
_legacyBackend = new LegacyConsoleBackend(this);
}
public void Clear(bool home)
@ -36,11 +36,11 @@ namespace Spectre.Console
}
}
public void Write(IEnumerable<Segment> segments)
public void Write(IRenderable renderable)
{
lock (_renderLock)
{
GetBackend().Render(segments);
GetBackend().Write(renderable);
}
}

View File

@ -1,4 +1,3 @@
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
@ -20,9 +19,9 @@ namespace Spectre.Console
void Clear(bool home);
/// <summary>
/// Renders segments to the console.
/// Writes a <see cref="IRenderable"/> to the console backend.
/// </summary>
/// <param name="segments">The segments to render.</param>
void Render(IEnumerable<Segment> segments);
/// <param name="renderable">The <see cref="IRenderable"/> to write.</param>
void Write(IRenderable renderable);
}
}

View File

@ -1,18 +1,17 @@
using System.Collections.Generic;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
internal sealed class LegacyConsoleBackend : IAnsiConsoleBackend
{
private readonly Profile _profile;
private readonly IAnsiConsole _console;
private Style _lastStyle;
public IAnsiConsoleCursor Cursor { get; }
public LegacyConsoleBackend(Profile profile)
public LegacyConsoleBackend(IAnsiConsole console)
{
_profile = profile ?? throw new System.ArgumentNullException(nameof(profile));
_console = console ?? throw new System.ArgumentNullException(nameof(console));
_lastStyle = Style.Plain;
Cursor = new LegacyConsoleCursor();
@ -31,9 +30,9 @@ namespace Spectre.Console
}
}
public void Render(IEnumerable<Segment> segments)
public void Write(IRenderable renderable)
{
foreach (var segment in segments)
foreach (var segment in renderable.GetSegments(_console))
{
if (segment.IsControlCode)
{
@ -45,7 +44,7 @@ namespace Spectre.Console
SetStyle(segment.Style);
}
_profile.Out.Write(segment.Text.NormalizeNewLines(native: true));
_console.Profile.Out.Write(segment.Text.NormalizeNewLines(native: true));
}
}
@ -56,13 +55,13 @@ namespace Spectre.Console
System.Console.ResetColor();
var background = Color.ToConsoleColor(style.Background);
if (_profile.ColorSystem != ColorSystem.NoColors && (int)background != -1)
if (_console.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)
if (_console.Profile.ColorSystem != ColorSystem.NoColors && (int)foreground != -1)
{
System.Console.ForegroundColor = foreground;
}

View File

@ -1,34 +1,22 @@
using Spectre.Console.Rendering;
using Wcwidth;
namespace Spectre.Console
{
internal static class Cell
{
public static int GetCellLength(RenderContext context, string text)
public static int GetCellLength(string text)
{
var sum = 0;
foreach (var rune in text)
{
sum += GetCellLength(context, rune);
sum += GetCellLength(rune);
}
return sum;
}
public static int GetCellLength(RenderContext context, char rune)
public static int GetCellLength(char rune)
{
if (context.LegacyConsole)
{
// Is it represented by a single byte?
// In that case we don't have to calculate the
// actual cell width.
if (context.Encoding.GetByteCount(new[] { rune }) == 1)
{
return 1;
}
}
// TODO: We need to figure out why Segment.SplitLines fails
// if we let wcwidth (which returns -1 instead of 1)
// calculate the size for new line characters.

View File

@ -3,52 +3,57 @@ using System.Collections.Generic;
using System.Text;
using Spectre.Console.Rendering;
namespace Spectre.Console
namespace Spectre.Console.Internal
{
internal sealed class HtmlEncoder : IAnsiConsoleEncoder
{
public string Encode(IEnumerable<Segment> segments)
public string Encode(IAnsiConsole console, IEnumerable<IRenderable> renderables)
{
var context = new RenderContext(EncoderCapabilities.Default);
var builder = new StringBuilder();
builder.Append("<pre style=\"font-size:90%;font-family:consolas,'Courier New',monospace\">\n");
foreach (var (_, first, _, segment) in segments.Enumerate())
foreach (var renderable in renderables)
{
if (segment.IsControlCode)
var segments = renderable.Render(context, console.Profile.Width);
foreach (var (_, first, _, segment) in segments.Enumerate())
{
continue;
}
if (segment.Text == "\n" && !first)
{
builder.Append('\n');
continue;
}
var parts = segment.Text.Split(new[] { '\n' }, StringSplitOptions.None);
foreach (var (_, _, last, line) in parts.Enumerate())
{
if (string.IsNullOrEmpty(line))
if (segment.IsControlCode)
{
continue;
}
builder.Append("<span");
if (!segment.Style.Equals(Style.Plain))
{
builder.Append(" style=\"");
builder.Append(BuildCss(segment.Style));
builder.Append('"');
}
builder.Append('>');
builder.Append(line);
builder.Append("</span>");
if (parts.Length > 1 && !last)
if (segment.Text == "\n" && !first)
{
builder.Append('\n');
continue;
}
var parts = segment.Text.Split(new[] { '\n' }, StringSplitOptions.None);
foreach (var (_, _, last, line) in parts.Enumerate())
{
if (string.IsNullOrEmpty(line))
{
continue;
}
builder.Append("<span");
if (!segment.Style.Equals(Style.Plain))
{
builder.Append(" style=\"");
builder.Append(BuildCss(segment.Style));
builder.Append('"');
}
builder.Append('>');
builder.Append(line);
builder.Append("</span>");
if (parts.Length > 1 && !last)
{
builder.Append('\n');
}
}
}
}

View File

@ -2,22 +2,39 @@ using System.Collections.Generic;
using System.Text;
using Spectre.Console.Rendering;
namespace Spectre.Console
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(IEnumerable<Segment> segments)
public string Encode(IAnsiConsole console, IEnumerable<IRenderable> renderables)
{
var context = new RenderContext(EncoderCapabilities.Default);
var builder = new StringBuilder();
foreach (var segment in Segment.Merge(segments))
foreach (var renderable in renderables)
{
if (segment.IsControlCode)
var segments = renderable.Render(context, console.Profile.Width);
foreach (var segment in Segment.Merge(segments))
{
continue;
}
if (segment.IsControlCode)
{
continue;
}
builder.Append(segment.Text);
builder.Append(segment.Text);
}
}
return builder.ToString().TrimEnd('\n');