Repository: spectreconsole/spectre.console (2026-04-13T23:28:11Z)

Issue:
Some characters break table output
**Information**
 - OS: MacOS
 - Version: 15.5
 - Terminal: iTerm2 3.5.14

**Describe the bug**
Some characters like emoji have different width than usual and break the table in CLI programs.

**To Reproduce**
This is a demo project that reproduces the issue:

How to create it:

```
dotnet new console -n SpectreDemo
cd SpectreDemo
dotnet add package Spectre.Console
```

Replace all content inside `Program.cs` with this one:

```
using System;
using Spectre.Console;

class Program
{
    static void Main()
    {
        // Enable UTF-8 so emojis appear in Windows Console/PowerShell
        Console.OutputEncoding = System.Text.Encoding.UTF8;

        // ── Create the “Video Info” table ───────────────────────────────
        var info = new Table()
            .Border(TableBorder.Rounded)   // same look as TwitchDownloaderCLI
            .Expand()                      // use all available width
            .AddColumns("Key", "Value");

        info.AddRow("Streamer",  "BlueRangerPeter");
        info.AddRow("Title",
            "Frankie prob poopin' 💩 or sum sum idk 🤷‍♀️ I just love runnin' 🏃‍♂️ " +
            "n not dyin' 💀 from evil bunny robots 🐰🤖 n' shiiii'");
        info.AddRow("Length",     "2:17:00");
        info.AddRow("Category",   "Finding Frankie");
        info.AddRow("Views",      "19");
        info.AddRow("Created at", "2025-06-09 03:34:56 UTC");
        info.AddRow("Description","-");        // <-- shorter than the title

        // Render to the console
        AnsiConsole.Write(info);
    }
}
```

Then run it:

```
% dotnet run
╭──────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Key          │ Value                                                                                                                    │
├──────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Streamer     │ BlueRangerPeter                                                                                                          │
│ Title        │ Frankie prob poopin' 💩 or sum sum idk 🤷‍♀️ I just love runnin' 🏃‍♂️ n not dyin' 💀 from evil bunny robots 🐰🤖 n' shiiii' │
│ Length       │ 2:17:00                                                                                                                  │
│ Category     │ Finding Frankie                                                                                                          │
│ Views        │ 19                                                                                                                       │
│ Created at   │ 2025-06-09 03:34:56 UTC                                                                                                  │
│ Description  │ -                                                                                                                        │
╰──────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```

**Expected behavior**
Print the table correctly like this, notice there's still a small difference because I used spaces to mimic the missing distance:

```
% dotnet run
╭──────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Key          │ Value                                                                                                                    │
├──────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Streamer     │ BlueRangerPeter                                                                                                          │
│ Title        │ Frankie prob poopin' 💩 or sum sum idk 🤷‍♀️ I just love runnin' 🏃‍♂️ n not dyin' 💀 from evil bunny robots 🐰🤖 n' shiiii'     │
│ Length       │ 2:17:00                                                                                                                  │
│ Category     │ Finding Frankie                                                                                                          │
│ Views        │ 19                                                                                                                       │
│ Created at   │ 2025-06-09 03:34:56 UTC                                                                                                  │
│ Description  │ -                                                                                                                        │
╰──────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```

**Screenshots**
Not required

**Additional context**
Not required


Discussion:
I would suggest to change the method `Cell::GetCellLength` in `src/Spectre.Console/Internal/Cell.cs` to use something like `new StringInfo(text).LengthInTextElements;`. I think that would work better with complex emoji strings.
Here is a proof of concept:

### Screenshot

<img width="938" height="523" alt="Image" src="https://github.com/user-attachments/assets/3d126b59-ff92-4749-b467-d0bd99544fc1" />

### Code

```csharp
using System.Globalization;
using System.Text;
using Wcwidth;

Console.OutputEncoding = Encoding.UTF8;

List<string> names = [
    "This is fun 🏆🏃‍♀️🏁",
    "Another example 🐣🐥🦅",
    "Hello world 👁️👗👁️",
    "Short 👷🖌️🔧",
    "One Two Three Four Five ♾️🗿🔥",
    "nothing interesting here",
    "slightly more interesting 🚲",
    "OMG lol 🍣♻️"
    ];


Console.WriteLine("GetCellLength using Wcwidth UnicodeCalculator:");
Console.WriteLine();
int maxlength = names.Max(x => GetCellLength(x));
foreach (var name in names)
{
    Console.WriteLine(name + new string(' ', maxlength - GetCellLength(name)) + " |");
}
Console.WriteLine();
Console.WriteLine();

Console.WriteLine("GetCellLength using StringInfo LengthInTextElements and Emoji counting twice:");
Console.WriteLine();
maxlength = names.Max(x => GetCellLengthNew(x));
foreach (var name in names)
{
    Console.WriteLine(name + new string(' ', maxlength - GetCellLengthNew(name)) + " |");
}

static int CountEmoji(StringInfo inputString)
{
    int count = 0;

    for (int i = 0; i < inputString.LengthInTextElements; i++)
    {
        if (IsEmoji(inputString, i))
        {
            count++;
        }
    }

    return count;
}

static bool IsEmoji(StringInfo inputString, int index)
{
    var firstUnicodeChar = inputString.SubstringByTextElements(index, 1); // gets the char at the given index
    var charCode = Char.ConvertToUtf32(firstUnicodeChar, 0); // gets a numeric value for this char; note: we first get the char by index rather than just passing the index as an additional argument here since if there are additional utf32 chars earlier in the string our index would be offset
    return IsEmoticon(charCode)
    || IsMiscPictograph(charCode)
    || IsTransport(charCode)
    || IsMiscSymbol(charCode)
    || IsDingbat(charCode)
    || IsVariationSelector(charCode)
    || IsSupplemental(charCode)
    || IsFlag(charCode);
}

static bool IsEmoticon(int charCode) => 0x1F600 <= charCode && charCode <= 0x1F64F;
static bool IsMiscPictograph(int charCode) => 0x1F680 <= charCode && charCode <= 0x1F5FF;
static bool IsTransport(int charCode) => 0x2600 <= charCode && charCode <= 0x1F6FF;
static bool IsMiscSymbol(int charCode) => 0x2700 <= charCode && charCode <= 0x26FF;
static bool IsDingbat(int charCode) => 0x2700 <= charCode && charCode <= 0x27BF;
static bool IsVariationSelector(int charCode) => 0xFE00 <= charCode && charCode <= 0xFE0F;
static bool IsSupplemental(int charCode) => 0x1F900 <= charCode && charCode <= 0x1F9FF;
static bool IsFlag(int charCode) => 0x1F1E6 <= charCode && charCode <= 0x1F1FF;

static int GetCellLengthNew(string text)
{
    var inputString = new StringInfo(text);
    int len = inputString.LengthInTextElements;
    int emojiCount = CountEmoji(inputString);

    return len + emojiCount;    // Emojis take 2 characters wide, so we count them twice
}


// Legacy way of counting lengh
static int GetCellLength(string text)
{
    var sum = 0;
    for (var index = 0; index < text.Length; index++)
    {
        var rune = text[index];
        sum += GetCellLengthOfChar(rune);
    }

    return sum;
}

static int GetCellLengthOfChar(char rune)
{
    if (rune == '\n')
    {
        return 1;
    }

    return (sbyte)UnicodeCalculator.GetWidth(rune);
}

```



Patch:
diff --git a/src/Spectre.Console/Internal/Cell.cs b/src/Spectre.Console/Internal/Cell.cs
index d2f46fc75..6d49f5fe4 100644
--- a/src/Spectre.Console/Internal/Cell.cs
+++ b/src/Spectre.Console/Internal/Cell.cs
@@ -23,10 +23,11 @@ static Cell()
 #endif
     }
 
-    public static int GetCellLength(string text) => GetCellLength(text.AsSpan());
-
-    public static int GetCellLength(ReadOnlySpan<char> text)
+    public static int GetCellLength(string text)
     {
+#if !NETSTANDARD2_0
+        return UnicodeCalculator.GetWidth(text);
+#else
         var sum = 0;
         foreach (var rune in text)
         {
@@ -34,8 +35,12 @@ public static int GetCellLength(ReadOnlySpan<char> text)
         }
 
         return sum;
+#endif
     }
 
+    public static int GetCellLength(ReadOnlySpan<char> text)
+        => GetCellLength(text.ToString());
+
     public static int GetCellLength(char rune)
     {
         // TODO: We need to figure out why Segment.SplitLines fails
diff --git a/src/Spectre.Console/Rendering/Segment.cs b/src/Spectre.Console/Rendering/Segment.cs
index 6f30e9970..63b87754b 100644
--- a/src/Spectre.Console/Rendering/Segment.cs
+++ b/src/Spectre.Console/Rendering/Segment.cs
@@ -109,6 +109,11 @@ public int CellCount()
             return 0;
         }
 
+        if (Text == "\n") 
+        {
+            return 1;
+        }
+
         return Cell.GetCellLength(Text);
     }
 
@@ -160,10 +165,12 @@ public Segment StripLineEndings()
         if (offset > 0)
         {
             var accumulated = 0;
-            foreach (var character in Text)
+            var enumerator = StringInfo.GetTextElementEnumerator(Text);
+            while (enumerator.MoveNext())
             {
-                index++;
-                accumulated += Cell.GetCellLength(character);
+                var cluster = enumerator.GetTextElement();
+                accumulated += Cell.GetCellLength(cluster);
+                index += cluster.Length;
                 if (accumulated >= offset)
                 {
                     break;
@@ -429,16 +436,18 @@ public static List<Segment> Truncate(IEnumerable<Segment> segments, int maxWidth
 
         var builder = new StringBuilder();
         var accumulatedCellWidth = 0;
-        foreach (var character in segment.Text)
+        var truncateEnumerator = StringInfo.GetTextElementEnumerator(segment.Text);
+        while (truncateEnumerator.MoveNext())
         {
-            var characterWidth = UnicodeCalculator.GetWidth(character);
-            if (accumulatedCellWidth + characterWidth > maxWidth)
+            var cluster = truncateEnumerator.GetTextElement();
+            var clusterWidth = Cell.GetCellLength(cluster);
+            if (accumulatedCellWidth + clusterWidth > maxWidth)
             {
                 break;
             }
 
-            builder.Append(character);
-            accumulatedCellWidth += characterWidth;
+            builder.Append(cluster);
+            accumulatedCellWidth += clusterWidth;
         }
 
         if (builder.Length == 0)
@@ -571,17 +580,20 @@ internal static List<string> SplitSegment(string text, int maxCellLength)
 
         var length = 0;
         var sb = new StringBuilder();
-        foreach (var ch in text)
+        var splitEnumerator = StringInfo.GetTextElementEnumerator(text);
+        while (splitEnumerator.MoveNext())
         {
-            if (length + UnicodeCalculator.GetWidth(ch) > maxCellLength)
+            var cluster = splitEnumerator.GetTextElement();
+            var clusterWidth = Cell.GetCellLength(cluster);
+            if (length + clusterWidth > maxCellLength)
             {
                 list.Add(sb.ToString());
                 sb.Clear();
                 length = 0;
             }
 
-            length += UnicodeCalculator.GetWidth(ch);
-            sb.Append(ch);
+            length += clusterWidth;
+            sb.Append(cluster);
         }
 
         list.Add(sb.ToString());


Test patch:
diff --git a/src/Spectre.Console.Tests/Expectations/Widgets/Table/Render_Wide_Emoji_Hearts.Output.verified.txt b/src/Spectre.Console.Tests/Expectations/Widgets/Table/Render_Wide_Emoji_Hearts.Output.verified.txt
new file mode 100644
index 000000000..dec848135
--- /dev/null
+++ b/src/Spectre.Console.Tests/Expectations/Widgets/Table/Render_Wide_Emoji_Hearts.Output.verified.txt
@@ -0,0 +1,6 @@
+┌──────────┬────────┬──────────────────────┐
+│ hearts   │ length │ LengthInTextElements │
+├──────────┼────────┼──────────────────────┤
+│ wide ❤️  │ 2      │ 1                    │
+│ normal ♥ │ 1      │ 1                    │
+└──────────┴────────┴──────────────────────┘
diff --git a/src/Spectre.Console.Tests/Unit/CellTests.cs b/src/Spectre.Console.Tests/Unit/CellTests.cs
new file mode 100644
index 000000000..19c43cf8a
--- /dev/null
+++ b/src/Spectre.Console.Tests/Unit/CellTests.cs
@@ -0,0 +1,20 @@
+namespace Spectre.Console.Tests.Unit;
+
+public sealed class CellTests
+{
+    [Theory]
+    [InlineData("A", 1)]               // ASCII
+    [InlineData("中", 2)]              // CJK wide
+    [InlineData("♥", 1)]              // U+2665 text heart, no variation selector
+    [InlineData("❤️", 2)]             // U+2764 + U+FE0F emoji variation selector
+    [InlineData("👨‍👩‍👧", 2)]       // ZWJ family sequence
+    [InlineData("❤️‍🔥", 2)]          // U+2764 + FE0F + ZWJ + U+1F525 (heart on fire)
+    [InlineData("🇩🇪", 2)]           // Regional Indicator pair (flag)
+    [InlineData("", 0)]               // empty string
+    [InlineData("Hello World", 11)]
+    public void GetCellLength_Returns_Correct_Display_Width(string text, int expectedWidth)
+    {
+        Cell.GetCellLength(text).ShouldBe(expectedWidth);
+    }
+
+}
diff --git a/src/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs b/src/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs
index be1b8094b..b7ed372c6 100644
--- a/src/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs
+++ b/src/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs
@@ -1034,4 +1034,23 @@ public void Should_Not_Throw_When_Rendering_Multiple_Long_CJK_Headers_In_Narrow_
         // Then
         result.ShouldBeNull();
     }
+
+    [Fact]
+    [Expectation("Render_Wide_Emoji_Hearts")]
+    [GitHubIssue("https://github.com/spectreconsole/spectre.console/issues/1847")]
+    public Task Should_Render_Table_With_Wide_Emoji_Correctly()
+    {
+        // Given
+        var console = new TestConsole();
+        var table = new Table();
+        table.AddColumns("hearts", "length", "LengthInTextElements");
+        table.AddRow("wide ❤️", "2", "1");
+        table.AddRow("normal ♥", "1", "1");
+
+        // When
+        console.Write(table);
+
+        // Then
+        return Verifier.Verify(console.Output);
+    }
 }
\ No newline at end of file