From 20e987ba512401f3c13b1eb1e025f748532036ad Mon Sep 17 00:00:00 2001 From: Eljees <3.14hell@gmail.com> Date: Tue, 28 Jul 2026 10:27:37 +0300 Subject: [PATCH] fix: wrap wide license columns to the terminal width A package carrying several licenses makes the row far wider than the terminal, so the table output wraps at arbitrary points and becomes hard to read. Limit the license columns to half the terminal width, letting go-pretty wrap inside the column instead of pushing the row off screen. The width is only applied when stdout is a terminal, so piped and redirected output keeps exactly the shape it has today. Signed-off-by: Eljees <3.14hell@gmail.com> --- cmd/grant/cli/command/check.go | 1 + cmd/grant/cli/command/list.go | 3 ++ cmd/grant/cli/internal/output.go | 2 ++ cmd/grant/cli/internal/table.go | 42 ++++++++++++++++++++++++++++ cmd/grant/cli/internal/table_test.go | 25 +++++++++++++++++ cmd/grant/cli/internal/terminal.go | 16 +++++++++++ 6 files changed, 89 insertions(+) create mode 100644 cmd/grant/cli/internal/table.go create mode 100644 cmd/grant/cli/internal/table_test.go diff --git a/cmd/grant/cli/command/check.go b/cmd/grant/cli/command/check.go index aaf87a8..5d0a4fc 100644 --- a/cmd/grant/cli/command/check.go +++ b/cmd/grant/cli/command/check.go @@ -471,6 +471,7 @@ func printPackageTableUnlicensed(packages []grant.PackageFinding) error { // Use uppercase headers to match grype style t.AppendHeader(table.Row{"NAME", "VERSION", "LICENSE STATUS"}) + internal.WrapWideColumns(t, "LICENSE STATUS") // Add rows for packages without licenses for _, pkg := range packages { diff --git a/cmd/grant/cli/command/list.go b/cmd/grant/cli/command/list.go index 3706504..58541f2 100644 --- a/cmd/grant/cli/command/list.go +++ b/cmd/grant/cli/command/list.go @@ -542,6 +542,7 @@ func printFilteredPackageTable(packages []grant.PackageFinding) error { // Set headers with uppercase to match grype style t.AppendHeader(table.Row{"NAME", "VERSION", "LICENSE", "RISK"}) + internal.WrapWideColumns(t, "LICENSE") // Add rows for matching packages for _, pkg := range packages { @@ -673,6 +674,7 @@ func printAggregatedLicenseTable(packages []grant.PackageFinding) error { // Set headers t.AppendHeader(table.Row{"LICENSE", "PACKAGES", "RISK"}) + internal.WrapWideColumns(t, "LICENSE", "PACKAGES") // Add rows for _, lc := range licenseCounts { @@ -910,6 +912,7 @@ func outputRiskGroupedTable(target grant.TargetResult) error { // Set headers t.AppendHeader(table.Row{"RISK CATEGORY", "LICENSES", "PACKAGES"}) + internal.WrapWideColumns(t, "LICENSES", "PACKAGES") // Add rows in order of risk severity categoryOrder := []string{riskCategoryStrongCopyleft, riskCategoryWeakCopyleft, riskCategoryPermissive} diff --git a/cmd/grant/cli/internal/output.go b/cmd/grant/cli/internal/output.go index 6f14561..d823536 100644 --- a/cmd/grant/cli/internal/output.go +++ b/cmd/grant/cli/internal/output.go @@ -244,6 +244,7 @@ func (o *Output) printPackageTable(packages []grant.PackageFinding) error { // Set headers with uppercase to match grype style t.AppendHeader(table.Row{"NAME", "VERSION", "LICENSE", "RISK"}) + WrapWideColumns(t, "LICENSE") // Add rows for denied packages only for _, pkg := range deniedPackages { @@ -432,6 +433,7 @@ func (o *Output) printAggregatedLicenseTable(packages []grant.PackageFinding) er // Set headers t.AppendHeader(table.Row{"LICENSE", "PACKAGES", "RISK"}) + WrapWideColumns(t, "LICENSE", "PACKAGES") // Add rows for _, lc := range licenseCounts { diff --git a/cmd/grant/cli/internal/table.go b/cmd/grant/cli/internal/table.go new file mode 100644 index 0000000..635808a --- /dev/null +++ b/cmd/grant/cli/internal/table.go @@ -0,0 +1,42 @@ +package internal + +import ( + "github.com/jedib0t/go-pretty/v6/table" +) + +// minWrappedColumnWidth keeps a wrapped column readable on a narrow terminal. +const minWrappedColumnWidth = 20 + +// wrappedColumnWidth returns the width to allow a wide text column, given the +// width of the terminal. A terminal width of 0 means the width is unknown (the +// output is not a terminal), in which case nothing should be wrapped. +func wrappedColumnWidth(terminalWidth int) int { + if terminalWidth <= 0 { + return 0 + } + + width := terminalWidth / 2 + if width < minWrappedColumnWidth { + width = minWrappedColumnWidth + } + + return width +} + +// WrapWideColumns limits the named columns to a share of the terminal width, so +// that a package carrying many licenses wraps instead of pushing the row far +// past the edge of the screen. It is a no-op when stdout is not a terminal, so +// piped and redirected output keeps its current shape. +func WrapWideColumns(t table.Writer, columns ...string) { + width := wrappedColumnWidth(TerminalWidth()) + if width == 0 { + return + } + + configs := make([]table.ColumnConfig, 0, len(columns)) + for _, name := range columns { + configs = append(configs, table.ColumnConfig{Name: name, WidthMax: width}) + } + + t.SetColumnConfigs(configs) +} diff --git a/cmd/grant/cli/internal/table_test.go b/cmd/grant/cli/internal/table_test.go new file mode 100644 index 0000000..d356e71 --- /dev/null +++ b/cmd/grant/cli/internal/table_test.go @@ -0,0 +1,25 @@ +package internal + +import "testing" + +func TestWrappedColumnWidth(t *testing.T) { + tests := []struct { + name string + terminalWidth int + want int + }{ + {name: "unknown width does not wrap", terminalWidth: 0, want: 0}, + {name: "negative width does not wrap", terminalWidth: -1, want: 0}, + {name: "wide terminal gets half its width", terminalWidth: 178, want: 89}, + {name: "narrow terminal keeps a readable minimum", terminalWidth: 30, want: minWrappedColumnWidth}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + if got := wrappedColumnWidth(tt.terminalWidth); got != tt.want { + t.Errorf("wrappedColumnWidth(%d) = %d, want %d", tt.terminalWidth, got, tt.want) + } + }) + } +} diff --git a/cmd/grant/cli/internal/terminal.go b/cmd/grant/cli/internal/terminal.go index 1317115..f3a9862 100644 --- a/cmd/grant/cli/internal/terminal.go +++ b/cmd/grant/cli/internal/terminal.go @@ -20,3 +20,19 @@ func IsTerminalError() bool { func IsTerminalInput() bool { return term.IsTerminal(int(os.Stdin.Fd())) // #nosec G115 -- file descriptors are safe to convert to int } + +// TerminalWidth returns the width of the terminal attached to stdout, or 0 when +// stdout is not a terminal or the size cannot be determined. Callers use 0 to +// mean "leave the output alone", so redirected and piped output is unchanged. +func TerminalWidth() int { + if !IsTerminalOutput() { + return 0 + } + + width, _, err := term.GetSize(int(os.Stdout.Fd())) // #nosec G115 -- file descriptors are safe to convert to int + if err != nil || width <= 0 { + return 0 + } + + return width +}