Skip to content

Commit 2e0d321

Browse files
authored
STAC-23278: Fix emoji table alignment by removing Unicode variation selectors (#114)
1 parent beaa13d commit 2e0d321

File tree

2 files changed

+94
-3
lines changed

2 files changed

+94
-3
lines changed

internal/printer/printer.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/gookit/color"
1717
"github.com/jedib0t/go-pretty/v6/table"
1818
"github.com/jedib0t/go-pretty/v6/text"
19+
"github.com/mattn/go-runewidth"
1920
"github.com/pterm/pterm"
2021
"github.com/stackvista/stackstate-cli/internal/common"
2122
"github.com/stackvista/stackstate-cli/internal/util"
@@ -71,7 +72,6 @@ func NewStdPrinter(os string, stdOut io.Writer, stdErr io.Writer) *StdPrinter {
7172
MaxWidth: pterm.DefaultParagraph.MaxWidth,
7273
}
7374
pterm.EnableColor()
74-
7575
return x
7676
}
7777

@@ -273,6 +273,8 @@ func (p *StdPrinter) Table(t TableData) {
273273
columns := make(table.Row, 0)
274274
for _, v := range row {
275275
value := util.ToString(v)
276+
// Remove emoji variation selectors to fix width calculation issues
277+
value = removeEmojiVariationSelectors(value)
276278
columns = append(columns, value)
277279
}
278280
rows = append(rows, columns)
@@ -308,8 +310,9 @@ func calcColumnWidth(header []string, data [][]interface{}, maxWidth int, box ta
308310
for _, row := range data {
309311
for i, v := range row {
310312
value := util.ToString(v)
311-
if columnWidths[i] < len(value) {
312-
columnWidths[i] = len(value)
313+
value = removeEmojiVariationSelectors(value)
314+
if columnWidths[i] < runewidth.StringWidth(value) {
315+
columnWidths[i] = runewidth.StringWidth(value)
313316
}
314317
}
315318
}
@@ -339,6 +342,24 @@ func calcColumnWidth(header []string, data [][]interface{}, maxWidth int, box ta
339342
return adjustedColumnWidths
340343
}
341344

345+
// removeEmojiVariationSelectors removes Unicode variation selectors that cause
346+
// inconsistent width calculations across different terminals and libraries
347+
func removeEmojiVariationSelectors(s string) string {
348+
runes := []rune(s)
349+
result := make([]rune, 0, len(runes))
350+
351+
for _, r := range runes {
352+
// Skip variation selectors:
353+
// U+FE0E (VARIATION SELECTOR-15) - text presentation
354+
// U+FE0F (VARIATION SELECTOR-16) - emoji presentation
355+
if r == '\uFE0E' || r == '\uFE0F' {
356+
continue
357+
}
358+
result = append(result, r)
359+
}
360+
return string(result)
361+
}
362+
342363
func (p *StdPrinter) PrintLn(text string) {
343364
color.Fprintf(p.stdOut, "%s\n", text)
344365
}

internal/printer/printer_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,73 @@ func TestPrintTableNoDataCustomMessage(t *testing.T) {
290290
})
291291
assert.Equal(t, "No cats found.\n", stdOut.String())
292292
}
293+
294+
func TestPrintTableWithEmojiVariationSelectors(t *testing.T) {
295+
p, stdOut, _ := setupPrinter()
296+
p.SetUseColor(false)
297+
p.Table(TableData{
298+
Header: []string{"Name", "Status", "Description"},
299+
Data: [][]interface{}{
300+
{"Anton's demo dashboard 🌪️", "Active", "Weather dashboard"},
301+
{"Fire tracker 🔥️", "Inactive", "Fire monitoring"},
302+
{"Regular dashboard", "Active", "No emojis here"},
303+
{"Mixed ️🌪️ selectors️", "Testing", "Complex case"},
304+
},
305+
})
306+
307+
// Expected output should have variation selectors removed
308+
//nolint:lll
309+
expected := "NAME | STATUS | DESCRIPTION \nAnton's demo dashboard 🌪 | Active | Weather dashboard\nFire tracker 🔥 | Inactive | Fire monitoring \nRegular dashboard | Active | No emojis here \nMixed 🌪 selectors | Testing | Complex case \n"
310+
assert.Equal(t, expected, stdOut.String())
311+
}
312+
313+
func TestRemoveEmojiVariationSelectors(t *testing.T) {
314+
tests := []struct {
315+
name string
316+
input string
317+
expected string
318+
}{
319+
{
320+
name: "text with variation selector 16 (emoji presentation)",
321+
input: "Anton's demo dashboard 🌪️",
322+
expected: "Anton's demo dashboard 🌪",
323+
},
324+
{
325+
name: "text with variation selector 15 (text presentation)",
326+
input: "Number 1️⃣ with text selector",
327+
expected: "Number 1⃣ with text selector",
328+
},
329+
{
330+
name: "text without variation selectors",
331+
input: "Regular text with emoji 🚀",
332+
expected: "Regular text with emoji 🚀",
333+
},
334+
{
335+
name: "empty string",
336+
input: "",
337+
expected: "",
338+
},
339+
{
340+
name: "only variation selectors",
341+
input: "\uFE0E\uFE0F",
342+
expected: "",
343+
},
344+
{
345+
name: "multiple emojis with and without selectors",
346+
input: "Fire 🔥️ and water 💧 tornado 🌪️ rocket 🚀",
347+
expected: "Fire 🔥 and water 💧 tornado 🌪 rocket 🚀",
348+
},
349+
{
350+
name: "text with both variation selectors",
351+
input: "Mix️ed\uFE0E selectors️",
352+
expected: "Mixed selectors",
353+
},
354+
}
355+
356+
for _, tt := range tests {
357+
t.Run(tt.name, func(t *testing.T) {
358+
result := removeEmojiVariationSelectors(tt.input)
359+
assert.Equal(t, tt.expected, result)
360+
})
361+
}
362+
}

0 commit comments

Comments
 (0)