Skip to content

Commit c39831e

Browse files
committed
fix: correct Unicode 16.0 width mismatch for ☰ (U+2630) and 85 other codepoints
Wcwidth library defaults to Unicode 16.0 tables where trigrams (U+2630-2637), hexagrams (U+4DC0-4DFF), and others were reclassified from width 1 to 2. No terminals have adopted these new widths yet, causing layout misalignment. Pin UnicodeWidth to Unicode 15.0 tables by default and add a terminal probe that detects Unicode 16.0 width support at startup, auto-switching when terminals catch up.
1 parent bb3cb0a commit c39831e

2 files changed

Lines changed: 70 additions & 4 deletions

File tree

SharpConsoleUI/Helpers/TerminalCapabilities.cs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace SharpConsoleUI.Helpers
1515
public static class TerminalCapabilities
1616
{
1717
private static bool? _supportsVS16Widening;
18+
private static bool? _supportsUnicode16Widths;
1819

1920
/// <summary>
2021
/// Whether the terminal renders emoji+VS16 (U+FE0F) as 2 columns.
@@ -27,8 +28,19 @@ public static bool SupportsVS16Widening
2728
}
2829

2930
/// <summary>
30-
/// Probes the terminal to determine if VS16 (U+FE0F) widens emoji.
31-
/// Writes a test character, queries cursor position via DSR, and compares.
31+
/// Whether the terminal renders Unicode 16.0 newly-widened characters
32+
/// (e.g. U+2630 ☰ trigrams) as 2 columns.
33+
/// When false, these characters are treated as width 1 (Unicode 15.0 behavior).
34+
/// Defaults to false (most terminals haven't adopted Unicode 16.0 widths yet).
35+
/// </summary>
36+
public static bool SupportsUnicode16Widths
37+
{
38+
get => _supportsUnicode16Widths ?? false;
39+
}
40+
41+
/// <summary>
42+
/// Probes the terminal to determine rendering capabilities.
43+
/// Tests VS16 emoji widening and Unicode 16.0 width changes.
3244
/// Must be called after raw mode is entered and before input loops start.
3345
/// </summary>
3446
/// <param name="write">Action to write escape sequences to the terminal.</param>
@@ -45,6 +57,16 @@ public static void Probe(Action<string> write, Func<int> readByte)
4557
// If probing fails, assume modern terminal
4658
_supportsVS16Widening = true;
4759
}
60+
61+
try
62+
{
63+
_supportsUnicode16Widths = ProbeUnicode16Width(write, readByte);
64+
}
65+
catch
66+
{
67+
// If probing fails, assume terminal hasn't adopted Unicode 16.0 widths
68+
_supportsUnicode16Widths = false;
69+
}
4870
}
4971

5072
/// <summary>
@@ -56,12 +78,22 @@ public static void SetVS16Widening(bool supported)
5678
_supportsVS16Widening = supported;
5779
}
5880

81+
/// <summary>
82+
/// Allows manual override of the Unicode 16.0 width capability.
83+
/// Useful for testing or when the terminal is known ahead of time.
84+
/// </summary>
85+
public static void SetUnicode16Widths(bool supported)
86+
{
87+
_supportsUnicode16Widths = supported;
88+
}
89+
5990
/// <summary>
6091
/// Resets all cached capabilities (for testing).
6192
/// </summary>
6293
internal static void Reset()
6394
{
6495
_supportsVS16Widening = null;
96+
_supportsUnicode16Widths = null;
6597
}
6698

6799
private static bool ProbeVS16(Action<string> write, Func<int> readByte)
@@ -90,6 +122,28 @@ private static bool ProbeVS16(Action<string> write, Func<int> readByte)
90122
return col >= 3; // col is 1-based; 3 means cursor at column 3 → char was 2 wide
91123
}
92124

125+
/// <summary>
126+
/// Probes whether the terminal renders Unicode 16.0 newly-widened characters as 2 columns.
127+
/// Tests U+2630 (☰ TRIGRAM FOR HEAVEN), which changed from width 1 to 2 in Unicode 16.0.
128+
/// </summary>
129+
private static bool ProbeUnicode16Width(Action<string> write, Func<int> readByte)
130+
{
131+
// Write ☰ (U+2630) and query cursor position.
132+
// Unicode 15.0: width 1 → cursor at column 2
133+
// Unicode 16.0: width 2 → cursor at column 3
134+
write("\r\u2630\x1b[6n");
135+
136+
int col = ReadDSRColumn(readByte);
137+
138+
// Clean up probe output
139+
write("\r\x1b[K");
140+
141+
if (col < 0)
142+
return false; // Timeout/error → assume pre-Unicode 16.0
143+
144+
return col >= 3; // col 3 means 2-wide rendering (Unicode 16.0)
145+
}
146+
93147
/// <summary>
94148
/// Reads a DSR (Device Status Report) response and extracts the column number.
95149
/// Expected format: ESC [ row ; col R

SharpConsoleUI/Helpers/UnicodeWidth.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,28 @@ namespace SharpConsoleUI.Helpers
1818
/// including zero-width characters (combining marks, variation selectors, ZWJ).
1919
/// Spacing Combining Marks (Unicode category Mc) are corrected to width 1,
2020
/// as they occupy visual space in terminals despite Wcwidth marking them zero-width.
21+
/// Adapts to terminal capabilities: uses Unicode 15.0 width tables unless the
22+
/// terminal is detected to support Unicode 16.0 widths (probed at startup).
2123
/// </summary>
2224
public static class UnicodeWidth
2325
{
26+
/// <summary>
27+
/// Returns the Wcwidth Unicode version to use based on terminal capabilities.
28+
/// Unicode 16.0 widened 86 codepoints (trigrams U+2630-2637, hexagrams U+4DC0-4DFF, etc.)
29+
/// from 1 to 2 columns. Most terminals haven't adopted this yet, so we default to 15.0.
30+
/// </summary>
31+
private static Unicode EffectiveUnicodeVersion
32+
=> TerminalCapabilities.SupportsUnicode16Widths
33+
? Unicode.Version_16_0_0
34+
: Unicode.Version_15_0_0;
35+
2436
/// <summary>
2537
/// Returns the display width of a character in terminal columns (0, 1, or 2).
2638
/// Spacing Combining Marks (Mc) are corrected to width 1.
2739
/// </summary>
2840
public static int GetCharWidth(char c)
2941
{
30-
int w = UnicodeCalculator.GetWidth(c);
42+
int w = UnicodeCalculator.GetWidth(c, EffectiveUnicodeVersion);
3143
if (w <= 0)
3244
{
3345
// Spacing Combining Marks (Mc) occupy visual space in terminals
@@ -44,7 +56,7 @@ public static int GetCharWidth(char c)
4456
/// </summary>
4557
public static int GetRuneWidth(Rune r)
4658
{
47-
int w = UnicodeCalculator.GetWidth(r.Value);
59+
int w = UnicodeCalculator.GetWidth(r.Value, EffectiveUnicodeVersion);
4860
if (w <= 0)
4961
{
5062
// Spacing Combining Marks (Mc) occupy visual space in terminals

0 commit comments

Comments
 (0)