Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1849,6 +1849,136 @@ public static void CSI_AppendBackgroundColorRGB (StringBuilder builder, int r, i

#endregion

#region Text Styles

/// <summary>
/// Appends an ANSI SGR (Select Graphic Rendition) escape sequence to switch printed text from one <see cref="TextStyle"/> to another.
/// </summary>
/// <param name="output"><see cref="StringBuilder"/> to add escape sequence to.</param>
/// <param name="prev">Previous <see cref="TextStyle"/> to change away from.</param>
/// <param name="next">Next <see cref="TextStyle"/> to change to.</param>
/// <remarks>
/// <para>
/// Unlike colors, most text styling options are not mutually exclusive with each other, and can be applied independently. This creates a problem when
/// switching from one style to another: For instance, if your previous style is just bold, and your next style is just italic, then simply adding the
/// sequence to enable italic text would cause the text to remain bold. This method automatically handles this problem, enabling and disabling styles as
/// necessary to apply exactly the next style.
/// </para>
/// </remarks>
internal static void CSI_AppendTextStyleChange (StringBuilder output, TextStyle prev, TextStyle next)
{
// Do nothing if styles are the same, as no changes are necessary.
if (prev == next)
{
return;
}

// Bitwise operations to determine flag changes. A ^ B are the flags different between two flag sets. These different flags that exist in the next flag
// set (diff & next) are the ones that were enabled in the switch, those that exist in the previous flag set (diff & prev) are the ones that were
// disabled.
var diff = prev ^ next;
var enabled = diff & next;
var disabled = diff & prev;

// List of escape codes to apply.
var sgr = new List<int> ();

if (disabled != TextStyle.None)
{
// Special case: Both bold and faint have the same disabling code. While unusual, it can be valid to have both enabled at the same time, so when
// one and only one of them is being disabled, we need to re-enable the other afterward. We can check what flags remain enabled by taking
// prev & next, as this is the set of flags both have.
if (disabled.HasFlag (TextStyle.Bold))
{
sgr.Add (22);

if ((prev & next).HasFlag (TextStyle.Faint))
{
sgr.Add (2);
}
}

if (disabled.HasFlag (TextStyle.Faint))
{
sgr.Add (22);

if ((prev & next).HasFlag (TextStyle.Bold))
{
sgr.Add (1);
}
}

if (disabled.HasFlag (TextStyle.Italic))
{
sgr.Add (23);
}

if (disabled.HasFlag (TextStyle.Underline))
{
sgr.Add (24);
}

if (disabled.HasFlag (TextStyle.Blink))
{
sgr.Add (25);
}

if (disabled.HasFlag (TextStyle.Reverse))
{
sgr.Add (27);
}

if (disabled.HasFlag (TextStyle.Strikethrough))
{
sgr.Add (29);
}
}

if (enabled != TextStyle.None)
{
if (enabled.HasFlag (TextStyle.Bold))
{
sgr.Add (1);
}

if (enabled.HasFlag (TextStyle.Faint))
{
sgr.Add (2);
}

if (enabled.HasFlag (TextStyle.Italic))
{
sgr.Add (3);
}

if (enabled.HasFlag (TextStyle.Underline))
{
sgr.Add (4);
}

if (enabled.HasFlag (TextStyle.Blink))
{
sgr.Add (5);
}

if (enabled.HasFlag (TextStyle.Reverse))
{
sgr.Add (7);
}

if (enabled.HasFlag (TextStyle.Strikethrough))
{
sgr.Add (9);
}
}

output.Append ("\x1b[");
output.Append (string.Join (';', sgr));
output.Append ('m');
}

#endregion Text Styles

#region Requests

/// <summary>
Expand Down
7 changes: 7 additions & 0 deletions Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ public class NetOutput : IConsoleOutput

private CursorVisibility? _cachedCursorVisibility;

// Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange().
private TextStyle _redrawTextStyle = TextStyle.None;

/// <summary>
/// Creates a new instance of the <see cref="NetOutput"/> class.
/// </summary>
Expand Down Expand Up @@ -134,6 +137,10 @@ public void Write (IOutputBuffer buffer)
attr.Background.G,
attr.Background.B
);

EscSeqUtils.CSI_AppendTextStyleChange (output, _redrawTextStyle, attr.TextStyle);

_redrawTextStyle = attr.TextStyle;
}

outputWidth++;
Expand Down
3 changes: 2 additions & 1 deletion Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ public Attribute CurrentAttribute
// TODO: This makes IConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed.
if (Application.Driver is { })
{
_currentAttribute = new (value.Foreground, value.Background);
// TODO: Update this when attributes can include TextStyle in the constructor
_currentAttribute = new (value.Foreground, value.Background) { TextStyle = value.TextStyle };

return;
}
Expand Down
5 changes: 5 additions & 0 deletions Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ private enum DesiredAccess : uint

private readonly nint _screenBuffer;

// Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange().
private TextStyle _redrawTextStyle = TextStyle.None;

public WindowsOutput ()
{
Logging.Logger.LogInformation ($"Creating {nameof (WindowsOutput)}");
Expand Down Expand Up @@ -233,6 +236,8 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord
prev = attr;
EscSeqUtils.CSI_AppendForegroundColorRGB (stringBuilder, attr.Foreground.R, attr.Foreground.G, attr.Foreground.B);
EscSeqUtils.CSI_AppendBackgroundColorRGB (stringBuilder, attr.Background.R, attr.Background.G, attr.Background.B);
EscSeqUtils.CSI_AppendTextStyleChange (stringBuilder, _redrawTextStyle, attr.TextStyle);
_redrawTextStyle = attr.TextStyle;
}

if (info.Char != '\x1b')
Expand Down
5 changes: 5 additions & 0 deletions Terminal.Gui/Drawing/Attribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ namespace Terminal.Gui;
[JsonConverter (typeof (ColorJsonConverter))]
public Color Background { get; }

// TODO: Add constructors which permit including a TextStyle.
/// <summary>The text style (bold, italic, underlined, etc.).</summary>
public TextStyle TextStyle { get; init; } = TextStyle.None;

/// <summary>Initializes a new instance with default values.</summary>
public Attribute ()
{
Expand Down Expand Up @@ -103,6 +107,7 @@ public Attribute (in Color color) : this (color, color) { }
/// <inheritdoc/>
public override int GetHashCode () { return HashCode.Combine (PlatformColor, Foreground, Background); }

// TODO: Add TextStyle to Attribute.ToString(), modify unit tests to account
/// <inheritdoc/>
public override string ToString ()
{
Expand Down
79 changes: 79 additions & 0 deletions Terminal.Gui/Drawing/TextStyle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
namespace Terminal.Gui;

/// <summary>
/// Defines non-color text style flags for an <see cref="Attribute"/>.
/// </summary>
/// <remarks>
/// <para>
/// Only a subset of ANSI SGR (Select Graphic Rendition) styles are represented.
/// Styles that are poorly supported, non-visual, or redundant with other APIs are omitted.
/// </para>
/// <para>
/// Multiple styles can be combined using bitwise operations. Use <see cref="Attribute.TextStyle"/>
/// to get or set these styles on an <see cref="Attribute"/>.
/// </para>
/// <para>
/// Note that <see cref="TextStyle.Bold"/> and <see cref="TextStyle.Faint"/> may be mutually exclusive depending on
/// the user's terminal and its settings. For instance, if a terminal displays faint text as a darker color, and
/// bold text as a lighter color, then both cannot
/// be shown at the same time, and it will be up to the terminal to decide which to display.
/// </para>
/// </remarks>
[Flags]
public enum TextStyle : byte
{
/// <summary>
/// No text style.
/// </summary>
/// <remarks>Corresponds to no active SGR styles.</remarks>
None = 0b_0000_0000,

/// <summary>
/// Bold text.
/// </summary>
/// <remarks>
/// SGR code: 1 (Bold). May be mutually exclusive with <see cref="TextStyle.Faint"/>, see <see cref="TextStyle"/>
/// remarks.
/// </remarks>
Bold = 0b_0000_0001,

/// <summary>
/// Faint (dim) text.
/// </summary>
/// <remarks>
/// SGR code: 2 (Faint). Not widely supported on all terminals. May be mutually exclusive with
/// <see cref="TextStyle.Bold"/>, see
/// <see cref="TextStyle"/> remarks.
/// </remarks>
Faint = 0b_0000_0010,

/// <summary>
/// Italic text.
/// </summary>
/// <remarks>SGR code: 3 (Italic). Some terminals may not support italic rendering.</remarks>
Italic = 0b_0000_0100,

/// <summary>
/// Underlined text.
/// </summary>
/// <remarks>SGR code: 4 (Underline).</remarks>
Underline = 0b_0000_1000,

/// <summary>
/// Slow blinking text.
/// </summary>
/// <remarks>SGR code: 5 (Slow Blink). Support varies; blinking is often disabled in modern terminals.</remarks>
Blink = 0b_0001_0000,

/// <summary>
/// Reverse video (swaps foreground and background colors).
/// </summary>
/// <remarks>SGR code: 7 (Reverse Video).</remarks>
Reverse = 0b_0010_0000,

/// <summary>
/// Strikethrough (crossed-out) text.
/// </summary>
/// <remarks>SGR code: 9 (Crossed-out / Strikethrough).</remarks>
Strikethrough = 0b_0100_0000
}
Loading