From 41a5314ad889eec4b1fc04f728ee30d52f41993a Mon Sep 17 00:00:00 2001 From: Error-String-Expected-Got-Nil <103805191+Error-String-Expected-Got-Nil@users.noreply.github.com> Date: Mon, 12 May 2025 17:54:22 -0500 Subject: [PATCH] Addresses #4058. Basic support for non-color text styles. (#4071) * TextStyle enum * CSI_AppendTextStyleChange * Add TextStyle to Attribute * Apply text style in NetOutput.Write() * Don't append escape code if nothing to change * Make TextStyle an init property * Apply TextStyle to OutputBuffer attributes * Fix flag checking Misunderstood how Enum.HasFlag worked, fixed now * Allow bold-faint text Also adds remarks to TextStyle noting that they may be incompatible depending on terminal settings. * Remove unnecessary check Realized it's actually impossible for no escape codes to be added, as this is only the case when prev and next are the same, which is already accounted for. * Remove redundant check Attributes are records, and thus already use equality-by-value, meaning attr != redrawAttr will already be false when the TextStyle changes. * WindowsOutput support for text style --------- Co-authored-by: Tig --- .../ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs | 130 ++++++++++++++++++ Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 7 + .../ConsoleDrivers/V2/OutputBuffer.cs | 3 +- .../ConsoleDrivers/V2/WindowsOutput.cs | 5 + Terminal.Gui/Drawing/Attribute.cs | 5 + Terminal.Gui/Drawing/TextStyle.cs | 79 +++++++++++ 6 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 Terminal.Gui/Drawing/TextStyle.cs diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs index adc463cc2..e0b2e2aed 100644 --- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs +++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs @@ -1849,6 +1849,136 @@ public static class EscSeqUtils #endregion + #region Text Styles + + /// + /// Appends an ANSI SGR (Select Graphic Rendition) escape sequence to switch printed text from one to another. + /// + /// to add escape sequence to. + /// Previous to change away from. + /// Next to change to. + /// + /// + /// 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. + /// + /// + 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 (); + + 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 /// diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index 69defb82e..fb93f6405 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -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; + /// /// Creates a new instance of the class. /// @@ -134,6 +137,10 @@ public class NetOutput : IConsoleOutput attr.Background.G, attr.Background.B ); + + EscSeqUtils.CSI_AppendTextStyleChange (output, _redrawTextStyle, attr.TextStyle); + + _redrawTextStyle = attr.TextStyle; } outputWidth++; diff --git a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs index 248c266fa..1b6295985 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs @@ -33,7 +33,8 @@ public class OutputBuffer : IOutputBuffer // 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; } diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index 81142fb83..4ec616616 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -61,6 +61,9 @@ internal partial class WindowsOutput : IConsoleOutput 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)}"); @@ -233,6 +236,8 @@ internal partial class WindowsOutput : IConsoleOutput 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') diff --git a/Terminal.Gui/Drawing/Attribute.cs b/Terminal.Gui/Drawing/Attribute.cs index 28097f377..cba89e682 100644 --- a/Terminal.Gui/Drawing/Attribute.cs +++ b/Terminal.Gui/Drawing/Attribute.cs @@ -34,6 +34,10 @@ public readonly record struct Attribute : IEqualityOperatorsThe text style (bold, italic, underlined, etc.). + public TextStyle TextStyle { get; init; } = TextStyle.None; + /// Initializes a new instance with default values. public Attribute () { @@ -103,6 +107,7 @@ public readonly record struct Attribute : IEqualityOperators public override int GetHashCode () { return HashCode.Combine (PlatformColor, Foreground, Background); } + // TODO: Add TextStyle to Attribute.ToString(), modify unit tests to account /// public override string ToString () { diff --git a/Terminal.Gui/Drawing/TextStyle.cs b/Terminal.Gui/Drawing/TextStyle.cs new file mode 100644 index 000000000..5a7fbb806 --- /dev/null +++ b/Terminal.Gui/Drawing/TextStyle.cs @@ -0,0 +1,79 @@ +namespace Terminal.Gui; + +/// +/// Defines non-color text style flags for an . +/// +/// +/// +/// 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. +/// +/// +/// Multiple styles can be combined using bitwise operations. Use +/// to get or set these styles on an . +/// +/// +/// Note that and 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. +/// +/// +[Flags] +public enum TextStyle : byte +{ + /// + /// No text style. + /// + /// Corresponds to no active SGR styles. + None = 0b_0000_0000, + + /// + /// Bold text. + /// + /// + /// SGR code: 1 (Bold). May be mutually exclusive with , see + /// remarks. + /// + Bold = 0b_0000_0001, + + /// + /// Faint (dim) text. + /// + /// + /// SGR code: 2 (Faint). Not widely supported on all terminals. May be mutually exclusive with + /// , see + /// remarks. + /// + Faint = 0b_0000_0010, + + /// + /// Italic text. + /// + /// SGR code: 3 (Italic). Some terminals may not support italic rendering. + Italic = 0b_0000_0100, + + /// + /// Underlined text. + /// + /// SGR code: 4 (Underline). + Underline = 0b_0000_1000, + + /// + /// Slow blinking text. + /// + /// SGR code: 5 (Slow Blink). Support varies; blinking is often disabled in modern terminals. + Blink = 0b_0001_0000, + + /// + /// Reverse video (swaps foreground and background colors). + /// + /// SGR code: 7 (Reverse Video). + Reverse = 0b_0010_0000, + + /// + /// Strikethrough (crossed-out) text. + /// + /// SGR code: 9 (Crossed-out / Strikethrough). + Strikethrough = 0b_0100_0000 +}