Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

There is an issue with calculating the length of line breaks in wrap func #344

Closed
L0nm4r opened this issue Dec 5, 2024 · 11 comments
Closed
Assignees
Labels
bug Something isn't working

Comments

@L0nm4r
Copy link

L0nm4r commented Dec 5, 2024

Describe the bug
code:

func TestRenderEndpointTable(tt *testing.T) {

	t := table.NewWriter()
	t.SetOutputMirror(os.Stdout)
	t.SetStyle(table.StyleLight)
	t.AppendHeader(table.Row{"idx", "A", "B", "C"})

	t.AppendRows([]table.Row{
		{1, text.WrapSoft("abcd甲乙丙丁abcd", 10), text.WrapHard("abcd甲乙丙丁abcd", 10), text.WrapText("abcd甲乙丙丁abcd", 10)},
		{2, text.WrapSoft("abcdabcdabcdabcdabcdabcd", 10), text.WrapHard("abcdabcdabcdabcdabcdabcd", 10), text.WrapText("abcdabcdabcdabcdabcdabcd", 10)},
		{3, text.WrapSoft("甲乙丙丁甲乙丙丁甲乙丙丁", 10), text.WrapHard("甲乙丙丁甲乙丙丁甲乙丙丁", 10), text.WrapText("甲乙丙丁", 10)},
	})

	t.Render()
}

render:

image

There is clearly still space in columns A and B of the first and second rows, but it has wrapped

Expected behavior

in columns A and B of the first and second rows should not be wrapped. like:

┌─────┬──────────────────────┬──────────────────────┬────────────────┐
│ IDX │ A                    │ B                    │ C              │
├─────┼──────────────────────┼──────────────────────┼────────────────┤
│   1 │ abcd甲乙丙丁abcd      │ abcd甲乙丙丁abcd       │ abcd甲乙丙丁ab │
│     │                      │                      │ cd             │
│   2 │ abcdabcdabcdabcdabcd │ abcdabcdabcdabcdabcd │ abcdabcdab     │
│     │                      │                      │ cdabcdabcd     │
│     │ abcd                 │ abcd                 │ abcd           │
│   3 │ 甲乙丙丁甲乙丙丁甲乙    │ 甲乙丙丁甲乙丙丁甲乙.    │ 甲乙丙丁       │
│     │ 丙丁                 │ 丙丁                 │                │
└─────┴──────────────────────┴──────────────────────┴────────────────┘

Software (please complete the following information):

  • OS: MacOS Sequoia 15.1.1
  • GoLang Version: go1.22.5 darwin/arm64
@jedib0t
Copy link
Owner

jedib0t commented Dec 6, 2024

This is probably related to the fact that the non-english (wide) characters are messing with the alignment. I'll see if I can fix this, but this may be an impossible one.

@jedib0t
Copy link
Owner

jedib0t commented Dec 13, 2024

The only way to consistently align strings with Asian and English characters is to render English characters in its "wide" format.

I'm working on introducing a config in the Style Style().Format.Widen = true/false. With this, the renderer will widen all the characters for the table. However, that means that you cannot use StyleLight/StyleBold/StyleRounded/etc as they don't play well with wide characters. You will have to use StyleDefault, or one of the colored styles.

Here is a sample:

package main

import (
	"os"

	"github.com/jedib0t/go-pretty/v6/table"
	"github.com/jedib0t/go-pretty/v6/text"
)

func main() {
	// your code (reordered a little)
	t := table.NewWriter()
	t.AppendHeader(table.Row{"idx", "A", "B", "C"})
	t.AppendRows([]table.Row{
		{1, text.WrapSoft("abcd甲乙丙丁abcd", 10), text.WrapHard("abcd甲乙丙丁abcd", 10), text.WrapText("abcd甲乙丙丁abcd", 10)},
		{2, text.WrapSoft("abcdabcdabcdabcdabcdabcd", 10), text.WrapHard("abcdabcdabcdabcdabcdabcd", 10), text.WrapText("abcdabcdabcdabcdabcdabcd", 10)},
		{3, text.WrapSoft("甲乙丙丁甲乙丙丁甲乙丙丁", 10), text.WrapHard("甲乙丙丁甲乙丙丁甲乙丙丁", 10), text.WrapText("甲乙丙丁", 10)},
	})
	t.SetOutputMirror(os.Stdout)
	t.SetStyle(table.StyleLight)
	t.SetTitle("Your Code")
	t.Render()

	// your code (refactored with SetColumnConfigs)
	t = table.NewWriter()
	t.AppendHeader(table.Row{"idx", "A", "B", "C"})
	t.AppendRows([]table.Row{
		{1, "abcd甲乙丙丁abcd", "abcd甲乙丙丁abcd", "abcd甲乙丙丁abcd"},
		{2, "abcdabcdabcdabcdabcdabcd", "abcdabcdabcdabcdabcdabcd", "abcdabcdabcdabcdabcdabcd"},
		{3, "甲乙丙丁甲乙丙丁甲乙丙丁", "甲乙丙丁甲乙丙丁甲乙丙丁", "甲乙丙丁"},
	})
	t.SetColumnConfigs([]table.ColumnConfig{
		{Name: "A", WidthMax: 10, WidthMaxEnforcer: text.WrapSoft},
		{Name: "B", WidthMax: 10, WidthMaxEnforcer: text.WrapHard},
		{Name: "C", WidthMax: 10, WidthMaxEnforcer: text.WrapText},
	})
	t.SetOutputMirror(os.Stdout)
	t.SetStyle(table.StyleLight)
	t.SetTitle("Your Code Refactored")
	t.Render()

	// use the default style and widen
	t.SetStyle(table.StyleDefault)
	t.Style().Format.Widen = true
	t.SetTitle("StyleDefault + Widen")
	t.Render()

	// use one of the colored styles and widen
	t.SetStyle(table.StyleColoredDark)
	t.Style().Format.Widen = true
	t.SetTitle("StyleColoredDark + Widen")
	t.Render()
}

Output in my terminal not configured well for East Asian characters:
image

@L0nm4r
Copy link
Author

L0nm4r commented Dec 13, 2024

hi jedib0t, i found this module: https://pkg.go.dev/github.com/mattn/go-runewidth.

it seems that this module can roughly calculate the character display width, like:

package main

import (
	"fmt"
	"github.com/mattn/go-runewidth"
)

func main() {
	test_data := []string{
		"这是一个测试",
		"aaaaaaaaaa",
		"つのだ☆HIRO",
		"aaaaaaaaaa",
	}

	for _, text := range test_data {
		fmt.Printf("| text: %s | width: %d\n", text, runewidth.StringWidth(text))
	}
}

output:

| text: 这是一个测试 | width: 12
| text: aaaaaaaaaa | width: 10
| text: つのだ☆HIRO | width: 12
| text: aaaaaaaaaa | width: 10

although there are still not perfect, can we implement runewith restrictions within the cells, like:

package main

import (
	"fmt"
	"github.com/mattn/go-runewidth"
)

func main() {
	data := []string{
		"abcd甲乙丙丁abcd",
		"abcdabcdabcdabcdabcdabcd",
		"甲乙丙丁甲乙丙丁甲乙丙丁",
	}

	fmt.Println("==== ==== ")
	max_width := 12
	for _, text := range data {
		output_str := ""
		str_len := 0
		for _, ch := range text {
			if str_len+runewidth.RuneWidth(ch) > max_width {
				break
			}
			str_len = str_len + runewidth.RuneWidth(ch)
			output_str += string(ch)
		}
		fmt.Printf("| width: %d| text: %s | \n", runewidth.StringWidth(output_str), output_str)
	}
	fmt.Println("==== ==== ")
}

in windows terminal, it looks like:

image

windows cmd:
image

Ubuntu default Shell:

image

Mac iTerm:

img_v3_02hh_3f26b639-2107-4bed-af8f-bea062d71f8g

goland:

image

@jedib0t
Copy link
Owner

jedib0t commented Dec 13, 2024

go-pretty uses that library already to calculate the width.

@jedib0t
Copy link
Owner

jedib0t commented Dec 13, 2024

Can you try the second block of code I shared? The one that says "your code refactored", and share the output?

	// your code (refactored with SetColumnConfigs)
	t = table.NewWriter()
	t.AppendHeader(table.Row{"idx", "A", "B", "C"})
	t.AppendRows([]table.Row{
		{1, "abcd甲乙丙丁abcd", "abcd甲乙丙丁abcd", "abcd甲乙丙丁abcd"},
		{2, "abcdabcdabcdabcdabcdabcd", "abcdabcdabcdabcdabcdabcd", "abcdabcdabcdabcdabcdabcd"},
		{3, "甲乙丙丁甲乙丙丁甲乙丙丁", "甲乙丙丁甲乙丙丁甲乙丙丁", "甲乙丙丁"},
	})
	t.SetColumnConfigs([]table.ColumnConfig{
		{Name: "A", WidthMax: 10, WidthMaxEnforcer: text.WrapSoft},
		{Name: "B", WidthMax: 10, WidthMaxEnforcer: text.WrapHard},
		{Name: "C", WidthMax: 10, WidthMaxEnforcer: text.WrapText},
	})
	t.SetOutputMirror(os.Stdout)
	t.SetStyle(table.StyleLight)
	t.SetTitle("Your Code Refactored")
	t.Render()

@L0nm4r
Copy link
Author

L0nm4r commented Dec 13, 2024

Can you try the second block of code I shared? The one that says "your code refactored", and share the output?您能试试我分享的第二个代码块吗?那个说“您的代码已重构”并共享输出的那个?

	// your code (refactored with SetColumnConfigs)
	t = table.NewWriter()
	t.AppendHeader(table.Row{"idx", "A", "B", "C"})
	t.AppendRows([]table.Row{
		{1, "abcd甲乙丙丁abcd", "abcd甲乙丙丁abcd", "abcd甲乙丙丁abcd"},
		{2, "abcdabcdabcdabcdabcdabcd", "abcdabcdabcdabcdabcdabcd", "abcdabcdabcdabcdabcdabcd"},
		{3, "甲乙丙丁甲乙丙丁甲乙丙丁", "甲乙丙丁甲乙丙丁甲乙丙丁", "甲乙丙丁"},
	})
	t.SetColumnConfigs([]table.ColumnConfig{
		{Name: "A", WidthMax: 10, WidthMaxEnforcer: text.WrapSoft},
		{Name: "B", WidthMax: 10, WidthMaxEnforcer: text.WrapHard},
		{Name: "C", WidthMax: 10, WidthMaxEnforcer: text.WrapText},
	})
	t.SetOutputMirror(os.Stdout)
	t.SetStyle(table.StyleLight)
	t.SetTitle("Your Code Refactored")
	t.Render()

sure:

image

@L0nm4r
Copy link
Author

L0nm4r commented Dec 13, 2024

go-pretty uses that library already to calculate the width.

do you mean the WidthMax?

	t.SetColumnConfigs([]table.ColumnConfig{
		{Name: "A", WidthMax: 10, WidthMaxEnforcer: text.WrapSoft},
		{Name: "B", WidthMax: 10, WidthMaxEnforcer: text.WrapHard},
		{Name: "C", WidthMax: 10, WidthMaxEnforcer: text.WrapText},
	})

but it looks like widthMax limits the word length not the width, i believe something must be wrong.

@jedib0t
Copy link
Owner

jedib0t commented Dec 13, 2024

No all alignment and width calculations done under the hood uses that library. Share your terminal settings esp your LANG environment variable.

@L0nm4r
Copy link
Author

L0nm4r commented Dec 13, 2024

I use the go-runewidth package impl the WrapRuneWidth func, then it looks like:

package main

import (
	"os"
	"strings"

	"github.com/jedib0t/go-pretty/text"
	"github.com/jedib0t/go-pretty/v6/table"
	"github.com/mattn/go-runewidth"
)

func WrapRuneWidth(str string, wrapWidth int) string {
	if wrapWidth <= 0 {
		return ""
	}

	str = strings.Replace(str, "\t", "    ", -1)
	sLen := runewidth.StringWidth(str)
	if sLen <= wrapWidth {
		return str
	}

	output_str := ""
	current_line_width := 0
	for _, rune := range str {
		rune_width := runewidth.RuneWidth(rune)
		if current_line_width+rune_width <= wrapWidth {
			current_line_width = current_line_width + rune_width
			output_str += string(rune)
		} else {
			current_line_width = rune_width
			output_str += "\n" + string(rune)
		}
	}

	return output_str
}

func main() {
	// your code (refactored with SetColumnConfigs)
	t := table.NewWriter()
	t.AppendHeader(table.Row{"idx", "A", "B", "C"})
	t.AppendRows([]table.Row{
		{1, "abcd甲乙丙丁abcd", "abcd甲乙丙丁abcd", "abcd甲乙丙丁abcd"},
		{2, "abcdabcdabcdabcdabcdabcd", "abcdabcdabcdabcdabcdabcd", "abcdabcdabcdabcdabcdabcd"},
		{3, "甲乙丙丁甲乙丙丁甲乙丙丁", "甲乙丙丁甲乙丙丁甲乙丙丁", "甲乙丙丁"},
	})
	t.SetColumnConfigs([]table.ColumnConfig{
		{Name: "A", WidthMax: 10, WidthMaxEnforcer: text.WrapSoft},
		{Name: "B", WidthMax: 10, WidthMaxEnforcer: text.WrapHard},
		{Name: "C", WidthMax: 10, WidthMaxEnforcer: text.WrapText},
	})
	t.SetOutputMirror(os.Stdout)
	t.SetStyle(table.StyleLight)
	t.SetTitle("Your Code Refactored")
	t.Render()

	t = table.NewWriter()
	t.AppendHeader(table.Row{"idx", "A", "B", "C"})
	t.AppendRows([]table.Row{
		{1, "abcd甲乙丙丁abcd", "abcd甲乙丙丁abcd", "abcd甲乙丙丁abcd"},
		{2, "abcdabcdabcdabcdabcdabcd", "abcdabcdabcdabcdabcdabcd", "abcdabcdabcdabcdabcdabcd"},
		{3, "甲乙丙丁甲乙丙丁甲乙丙丁", "甲乙丙丁甲乙丙丁甲乙丙丁", "甲乙丙丁"},
	})
	t.SetColumnConfigs([]table.ColumnConfig{
		{Name: "A", WidthMax: 10, WidthMaxEnforcer: WrapRuneWidth},
		{Name: "B", WidthMax: 10, WidthMaxEnforcer: WrapRuneWidth},
		{Name: "C", WidthMax: 10, WidthMaxEnforcer: WrapRuneWidth},
	})
	t.SetOutputMirror(os.Stdout)
	t.SetStyle(table.StyleLight)
	t.SetTitle("Wrap with WrapRuneWidth")
	t.Render()

}

output:

image

@jedib0t
Copy link
Owner

jedib0t commented Dec 13, 2024

Good catch. Alignment function uses that library. None of the Wrap functions do (I assumed I'd already changed it). Will fix.

@jedib0t jedib0t added the bug Something isn't working label Dec 13, 2024
@jedib0t jedib0t self-assigned this Dec 13, 2024
@jedib0t
Copy link
Owner

jedib0t commented Dec 13, 2024

Tag with the fix: https://github.com/jedib0t/go-pretty/releases/tag/v6.6.5

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants