Skip to content
Open
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
1 change: 1 addition & 0 deletions cmd/grant/cli/command/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions cmd/grant/cli/command/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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}
Expand Down
2 changes: 2 additions & 0 deletions cmd/grant/cli/internal/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
42 changes: 42 additions & 0 deletions cmd/grant/cli/internal/table.go
Original file line number Diff line number Diff line change
@@ -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)
}
25 changes: 25 additions & 0 deletions cmd/grant/cli/internal/table_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
16 changes: 16 additions & 0 deletions cmd/grant/cli/internal/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}