Skip to content

Commit 41a5314

Browse files
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 <tig@users.noreply.github.com>
1 parent 3f8c665 commit 41a5314

File tree

6 files changed

+228
-1
lines changed

6 files changed

+228
-1
lines changed

Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1849,6 +1849,136 @@ public static void CSI_AppendBackgroundColorRGB (StringBuilder builder, int r, i
18491849

18501850
#endregion
18511851

1852+
#region Text Styles
1853+
1854+
/// <summary>
1855+
/// Appends an ANSI SGR (Select Graphic Rendition) escape sequence to switch printed text from one <see cref="TextStyle"/> to another.
1856+
/// </summary>
1857+
/// <param name="output"><see cref="StringBuilder"/> to add escape sequence to.</param>
1858+
/// <param name="prev">Previous <see cref="TextStyle"/> to change away from.</param>
1859+
/// <param name="next">Next <see cref="TextStyle"/> to change to.</param>
1860+
/// <remarks>
1861+
/// <para>
1862+
/// Unlike colors, most text styling options are not mutually exclusive with each other, and can be applied independently. This creates a problem when
1863+
/// 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
1864+
/// sequence to enable italic text would cause the text to remain bold. This method automatically handles this problem, enabling and disabling styles as
1865+
/// necessary to apply exactly the next style.
1866+
/// </para>
1867+
/// </remarks>
1868+
internal static void CSI_AppendTextStyleChange (StringBuilder output, TextStyle prev, TextStyle next)
1869+
{
1870+
// Do nothing if styles are the same, as no changes are necessary.
1871+
if (prev == next)
1872+
{
1873+
return;
1874+
}
1875+
1876+
// 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
1877+
// 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
1878+
// disabled.
1879+
var diff = prev ^ next;
1880+
var enabled = diff & next;
1881+
var disabled = diff & prev;
1882+
1883+
// List of escape codes to apply.
1884+
var sgr = new List<int> ();
1885+
1886+
if (disabled != TextStyle.None)
1887+
{
1888+
// 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
1889+
// 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
1890+
// prev & next, as this is the set of flags both have.
1891+
if (disabled.HasFlag (TextStyle.Bold))
1892+
{
1893+
sgr.Add (22);
1894+
1895+
if ((prev & next).HasFlag (TextStyle.Faint))
1896+
{
1897+
sgr.Add (2);
1898+
}
1899+
}
1900+
1901+
if (disabled.HasFlag (TextStyle.Faint))
1902+
{
1903+
sgr.Add (22);
1904+
1905+
if ((prev & next).HasFlag (TextStyle.Bold))
1906+
{
1907+
sgr.Add (1);
1908+
}
1909+
}
1910+
1911+
if (disabled.HasFlag (TextStyle.Italic))
1912+
{
1913+
sgr.Add (23);
1914+
}
1915+
1916+
if (disabled.HasFlag (TextStyle.Underline))
1917+
{
1918+
sgr.Add (24);
1919+
}
1920+
1921+
if (disabled.HasFlag (TextStyle.Blink))
1922+
{
1923+
sgr.Add (25);
1924+
}
1925+
1926+
if (disabled.HasFlag (TextStyle.Reverse))
1927+
{
1928+
sgr.Add (27);
1929+
}
1930+
1931+
if (disabled.HasFlag (TextStyle.Strikethrough))
1932+
{
1933+
sgr.Add (29);
1934+
}
1935+
}
1936+
1937+
if (enabled != TextStyle.None)
1938+
{
1939+
if (enabled.HasFlag (TextStyle.Bold))
1940+
{
1941+
sgr.Add (1);
1942+
}
1943+
1944+
if (enabled.HasFlag (TextStyle.Faint))
1945+
{
1946+
sgr.Add (2);
1947+
}
1948+
1949+
if (enabled.HasFlag (TextStyle.Italic))
1950+
{
1951+
sgr.Add (3);
1952+
}
1953+
1954+
if (enabled.HasFlag (TextStyle.Underline))
1955+
{
1956+
sgr.Add (4);
1957+
}
1958+
1959+
if (enabled.HasFlag (TextStyle.Blink))
1960+
{
1961+
sgr.Add (5);
1962+
}
1963+
1964+
if (enabled.HasFlag (TextStyle.Reverse))
1965+
{
1966+
sgr.Add (7);
1967+
}
1968+
1969+
if (enabled.HasFlag (TextStyle.Strikethrough))
1970+
{
1971+
sgr.Add (9);
1972+
}
1973+
}
1974+
1975+
output.Append ("\x1b[");
1976+
output.Append (string.Join (';', sgr));
1977+
output.Append ('m');
1978+
}
1979+
1980+
#endregion Text Styles
1981+
18521982
#region Requests
18531983

18541984
/// <summary>

Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ public class NetOutput : IConsoleOutput
1212

1313
private CursorVisibility? _cachedCursorVisibility;
1414

15+
// Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange().
16+
private TextStyle _redrawTextStyle = TextStyle.None;
17+
1518
/// <summary>
1619
/// Creates a new instance of the <see cref="NetOutput"/> class.
1720
/// </summary>
@@ -134,6 +137,10 @@ public void Write (IOutputBuffer buffer)
134137
attr.Background.G,
135138
attr.Background.B
136139
);
140+
141+
EscSeqUtils.CSI_AppendTextStyleChange (output, _redrawTextStyle, attr.TextStyle);
142+
143+
_redrawTextStyle = attr.TextStyle;
137144
}
138145

139146
outputWidth++;

Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ public Attribute CurrentAttribute
3333
// TODO: This makes IConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed.
3434
if (Application.Driver is { })
3535
{
36-
_currentAttribute = new (value.Foreground, value.Background);
36+
// TODO: Update this when attributes can include TextStyle in the constructor
37+
_currentAttribute = new (value.Foreground, value.Background) { TextStyle = value.TextStyle };
3738

3839
return;
3940
}

Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ private enum DesiredAccess : uint
6161

6262
private readonly nint _screenBuffer;
6363

64+
// Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange().
65+
private TextStyle _redrawTextStyle = TextStyle.None;
66+
6467
public WindowsOutput ()
6568
{
6669
Logging.Logger.LogInformation ($"Creating {nameof (WindowsOutput)}");
@@ -233,6 +236,8 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord
233236
prev = attr;
234237
EscSeqUtils.CSI_AppendForegroundColorRGB (stringBuilder, attr.Foreground.R, attr.Foreground.G, attr.Foreground.B);
235238
EscSeqUtils.CSI_AppendBackgroundColorRGB (stringBuilder, attr.Background.R, attr.Background.G, attr.Background.B);
239+
EscSeqUtils.CSI_AppendTextStyleChange (stringBuilder, _redrawTextStyle, attr.TextStyle);
240+
_redrawTextStyle = attr.TextStyle;
236241
}
237242

238243
if (info.Char != '\x1b')

Terminal.Gui/Drawing/Attribute.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ namespace Terminal.Gui;
3434
[JsonConverter (typeof (ColorJsonConverter))]
3535
public Color Background { get; }
3636

37+
// TODO: Add constructors which permit including a TextStyle.
38+
/// <summary>The text style (bold, italic, underlined, etc.).</summary>
39+
public TextStyle TextStyle { get; init; } = TextStyle.None;
40+
3741
/// <summary>Initializes a new instance with default values.</summary>
3842
public Attribute ()
3943
{
@@ -103,6 +107,7 @@ public Attribute (in Color color) : this (color, color) { }
103107
/// <inheritdoc/>
104108
public override int GetHashCode () { return HashCode.Combine (PlatformColor, Foreground, Background); }
105109

110+
// TODO: Add TextStyle to Attribute.ToString(), modify unit tests to account
106111
/// <inheritdoc/>
107112
public override string ToString ()
108113
{

Terminal.Gui/Drawing/TextStyle.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
namespace Terminal.Gui;
2+
3+
/// <summary>
4+
/// Defines non-color text style flags for an <see cref="Attribute"/>.
5+
/// </summary>
6+
/// <remarks>
7+
/// <para>
8+
/// Only a subset of ANSI SGR (Select Graphic Rendition) styles are represented.
9+
/// Styles that are poorly supported, non-visual, or redundant with other APIs are omitted.
10+
/// </para>
11+
/// <para>
12+
/// Multiple styles can be combined using bitwise operations. Use <see cref="Attribute.TextStyle"/>
13+
/// to get or set these styles on an <see cref="Attribute"/>.
14+
/// </para>
15+
/// <para>
16+
/// Note that <see cref="TextStyle.Bold"/> and <see cref="TextStyle.Faint"/> may be mutually exclusive depending on
17+
/// the user's terminal and its settings. For instance, if a terminal displays faint text as a darker color, and
18+
/// bold text as a lighter color, then both cannot
19+
/// be shown at the same time, and it will be up to the terminal to decide which to display.
20+
/// </para>
21+
/// </remarks>
22+
[Flags]
23+
public enum TextStyle : byte
24+
{
25+
/// <summary>
26+
/// No text style.
27+
/// </summary>
28+
/// <remarks>Corresponds to no active SGR styles.</remarks>
29+
None = 0b_0000_0000,
30+
31+
/// <summary>
32+
/// Bold text.
33+
/// </summary>
34+
/// <remarks>
35+
/// SGR code: 1 (Bold). May be mutually exclusive with <see cref="TextStyle.Faint"/>, see <see cref="TextStyle"/>
36+
/// remarks.
37+
/// </remarks>
38+
Bold = 0b_0000_0001,
39+
40+
/// <summary>
41+
/// Faint (dim) text.
42+
/// </summary>
43+
/// <remarks>
44+
/// SGR code: 2 (Faint). Not widely supported on all terminals. May be mutually exclusive with
45+
/// <see cref="TextStyle.Bold"/>, see
46+
/// <see cref="TextStyle"/> remarks.
47+
/// </remarks>
48+
Faint = 0b_0000_0010,
49+
50+
/// <summary>
51+
/// Italic text.
52+
/// </summary>
53+
/// <remarks>SGR code: 3 (Italic). Some terminals may not support italic rendering.</remarks>
54+
Italic = 0b_0000_0100,
55+
56+
/// <summary>
57+
/// Underlined text.
58+
/// </summary>
59+
/// <remarks>SGR code: 4 (Underline).</remarks>
60+
Underline = 0b_0000_1000,
61+
62+
/// <summary>
63+
/// Slow blinking text.
64+
/// </summary>
65+
/// <remarks>SGR code: 5 (Slow Blink). Support varies; blinking is often disabled in modern terminals.</remarks>
66+
Blink = 0b_0001_0000,
67+
68+
/// <summary>
69+
/// Reverse video (swaps foreground and background colors).
70+
/// </summary>
71+
/// <remarks>SGR code: 7 (Reverse Video).</remarks>
72+
Reverse = 0b_0010_0000,
73+
74+
/// <summary>
75+
/// Strikethrough (crossed-out) text.
76+
/// </summary>
77+
/// <remarks>SGR code: 9 (Crossed-out / Strikethrough).</remarks>
78+
Strikethrough = 0b_0100_0000
79+
}

0 commit comments

Comments
 (0)