diff --git a/internal/printer/printer.go b/internal/printer/printer.go index 4fbb7ec3..b677a68e 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -16,6 +16,7 @@ import ( "github.com/gookit/color" "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" + "github.com/mattn/go-runewidth" "github.com/pterm/pterm" "github.com/stackvista/stackstate-cli/internal/common" "github.com/stackvista/stackstate-cli/internal/util" @@ -71,7 +72,6 @@ func NewStdPrinter(os string, stdOut io.Writer, stdErr io.Writer) *StdPrinter { MaxWidth: pterm.DefaultParagraph.MaxWidth, } pterm.EnableColor() - return x } @@ -273,6 +273,8 @@ func (p *StdPrinter) Table(t TableData) { columns := make(table.Row, 0) for _, v := range row { value := util.ToString(v) + // Remove emoji variation selectors to fix width calculation issues + value = removeEmojiVariationSelectors(value) columns = append(columns, value) } rows = append(rows, columns) @@ -308,8 +310,9 @@ func calcColumnWidth(header []string, data [][]interface{}, maxWidth int, box ta for _, row := range data { for i, v := range row { value := util.ToString(v) - if columnWidths[i] < len(value) { - columnWidths[i] = len(value) + value = removeEmojiVariationSelectors(value) + if columnWidths[i] < runewidth.StringWidth(value) { + columnWidths[i] = runewidth.StringWidth(value) } } } @@ -339,6 +342,24 @@ func calcColumnWidth(header []string, data [][]interface{}, maxWidth int, box ta return adjustedColumnWidths } +// removeEmojiVariationSelectors removes Unicode variation selectors that cause +// inconsistent width calculations across different terminals and libraries +func removeEmojiVariationSelectors(s string) string { + runes := []rune(s) + result := make([]rune, 0, len(runes)) + + for _, r := range runes { + // Skip variation selectors: + // U+FE0E (VARIATION SELECTOR-15) - text presentation + // U+FE0F (VARIATION SELECTOR-16) - emoji presentation + if r == '\uFE0E' || r == '\uFE0F' { + continue + } + result = append(result, r) + } + return string(result) +} + func (p *StdPrinter) PrintLn(text string) { color.Fprintf(p.stdOut, "%s\n", text) } diff --git a/internal/printer/printer_test.go b/internal/printer/printer_test.go index 2c17c41d..f55e19c5 100644 --- a/internal/printer/printer_test.go +++ b/internal/printer/printer_test.go @@ -290,3 +290,73 @@ func TestPrintTableNoDataCustomMessage(t *testing.T) { }) assert.Equal(t, "No cats found.\n", stdOut.String()) } + +func TestPrintTableWithEmojiVariationSelectors(t *testing.T) { + p, stdOut, _ := setupPrinter() + p.SetUseColor(false) + p.Table(TableData{ + Header: []string{"Name", "Status", "Description"}, + Data: [][]interface{}{ + {"Anton's demo dashboard 🌪️", "Active", "Weather dashboard"}, + {"Fire tracker 🔥️", "Inactive", "Fire monitoring"}, + {"Regular dashboard", "Active", "No emojis here"}, + {"Mixed ️🌪️ selectors️", "Testing", "Complex case"}, + }, + }) + + // Expected output should have variation selectors removed + //nolint:lll + 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" + assert.Equal(t, expected, stdOut.String()) +} + +func TestRemoveEmojiVariationSelectors(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "text with variation selector 16 (emoji presentation)", + input: "Anton's demo dashboard 🌪️", + expected: "Anton's demo dashboard 🌪", + }, + { + name: "text with variation selector 15 (text presentation)", + input: "Number 1️⃣ with text selector", + expected: "Number 1⃣ with text selector", + }, + { + name: "text without variation selectors", + input: "Regular text with emoji 🚀", + expected: "Regular text with emoji 🚀", + }, + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "only variation selectors", + input: "\uFE0E\uFE0F", + expected: "", + }, + { + name: "multiple emojis with and without selectors", + input: "Fire 🔥️ and water 💧 tornado 🌪️ rocket 🚀", + expected: "Fire 🔥 and water 💧 tornado 🌪 rocket 🚀", + }, + { + name: "text with both variation selectors", + input: "Mix️ed\uFE0E selectors️", + expected: "Mixed selectors", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := removeEmojiVariationSelectors(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +}