Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions internal/printer/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -71,7 +72,6 @@ func NewStdPrinter(os string, stdOut io.Writer, stdErr io.Writer) *StdPrinter {
MaxWidth: pterm.DefaultParagraph.MaxWidth,
}
pterm.EnableColor()

return x
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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)
}
70 changes: 70 additions & 0 deletions internal/printer/printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
Loading