From f30e973b9db937b6888b3d51408935e2648cfccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 15 Nov 2019 19:19:41 +0100 Subject: [PATCH] Extract generic row printer that adjusts itself for receiving terminal This makes the approach from `pr list` reusable across other commands that may benefit from table-based output, e.g. `issue list` or `pr status` The idea is: instantiate a printer, connect it to stdout, feed it some data, and it does the rest: colored, truncated column output that fits into a terminal, or tab-delimited output (no color, no truncation) for scripts. --- command/pr.go | 61 +++++++---------------- utils/table_printer.go | 108 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 43 deletions(-) create mode 100644 utils/table_printer.go diff --git a/command/pr.go b/command/pr.go index 0445b81d6..b7cca14a1 100644 --- a/command/pr.go +++ b/command/pr.go @@ -2,13 +2,11 @@ package command import ( "fmt" - "os" "strconv" "github.com/github/gh-cli/api" "github.com/github/gh-cli/utils" "github.com/spf13/cobra" - "golang.org/x/crypto/ssh/terminal" ) func init() { @@ -158,53 +156,30 @@ func prList(cmd *cobra.Command, args []string) error { return err } - tty := false - ttyWidth := 80 - out := cmd.OutOrStdout() - if outFile, isFile := out.(*os.File); isFile { - fd := int(outFile.Fd()) - tty = terminal.IsTerminal(fd) - if w, _, err := terminal.GetSize(fd); err == nil { - ttyWidth = w - } - } - - numWidth := 0 - maxTitleWidth := 0 + table := utils.NewTablePrinter(cmd.OutOrStdout()) for _, pr := range prs { - numLen := len(strconv.Itoa(pr.Number)) + 1 - if numLen > numWidth { - numWidth = numLen - } - if len(pr.Title) > maxTitleWidth { - maxTitleWidth = len(pr.Title) - } + table.SetContentWidth(0, len(strconv.Itoa(pr.Number))+1) + table.SetContentWidth(1, len(pr.Title)) + table.SetContentWidth(2, len(pr.HeadLabel())) } - branchWidth := 40 - titleWidth := ttyWidth - branchWidth - 2 - numWidth - 2 - - if maxTitleWidth < titleWidth { - branchWidth += titleWidth - maxTitleWidth - titleWidth = maxTitleWidth - } + table.FitColumns() + table.SetColorFunc(2, utils.Cyan) for _, pr := range prs { - if tty { - prNum := fmt.Sprintf("% *s", numWidth, fmt.Sprintf("#%d", pr.Number)) - switch pr.State { - case "OPEN": - prNum = utils.Green(prNum) - case "CLOSED": - prNum = utils.Red(prNum) - case "MERGED": - prNum = utils.Magenta(prNum) - } - prBranch := utils.Cyan(truncate(branchWidth, pr.HeadLabel())) - fmt.Fprintf(out, "%s %-*s %s\n", prNum, titleWidth, truncate(titleWidth, pr.Title), prBranch) - } else { - fmt.Fprintf(out, "%d\t%s\t%s\n", pr.Number, pr.Title, pr.HeadLabel()) + prNum := strconv.Itoa(pr.Number) + if table.IsTTY { + prNum = "#" + prNum } + switch pr.State { + case "OPEN": + table.SetColorFunc(0, utils.Green) + case "CLOSED": + table.SetColorFunc(0, utils.Red) + case "MERGED": + table.SetColorFunc(0, utils.Magenta) + } + table.WriteRow(prNum, pr.Title, pr.HeadLabel()) } return nil } diff --git a/utils/table_printer.go b/utils/table_printer.go new file mode 100644 index 000000000..fd38a928e --- /dev/null +++ b/utils/table_printer.go @@ -0,0 +1,108 @@ +package utils + +import ( + "fmt" + "io" + "os" + + "golang.org/x/crypto/ssh/terminal" +) + +func NewTablePrinter(w io.Writer) *TTYTablePrinter { + tty := false + ttyWidth := 80 + if outFile, isFile := w.(*os.File); isFile { + fd := int(outFile.Fd()) + tty = terminal.IsTerminal(fd) + if w, _, err := terminal.GetSize(fd); err == nil { + ttyWidth = w + } + } + return &TTYTablePrinter{ + out: w, + IsTTY: tty, + maxWidth: ttyWidth, + colWidths: []int{}, + colFuncs: make(map[int]func(string) string), + } +} + +type TTYTablePrinter struct { + out io.Writer + IsTTY bool + maxWidth int + colWidths []int + colFuncs map[int]func(string) string +} + +func (t *TTYTablePrinter) SetContentWidth(col, width int) { + if col == len(t.colWidths) { + t.colWidths = append(t.colWidths, 0) + } + if width > t.colWidths[col] { + t.colWidths[col] = width + } +} + +func (t *TTYTablePrinter) SetColorFunc(col int, colorize func(string) string) { + t.colFuncs[col] = colorize +} + +// FitColumns caps all but first column to fit available terminal width. +func (t *TTYTablePrinter) FitColumns() { + numCols := len(t.colWidths) + delimWidth := 2 + availWidth := t.maxWidth - t.colWidths[0] - ((numCols - 1) * delimWidth) + // TODO: avoid widening columns that already fit + // TODO: support weighted instead of even redistribution + for col := 1; col < len(t.colWidths); col++ { + t.colWidths[col] = availWidth / (numCols - 1) + } +} + +func (t *TTYTablePrinter) WriteRow(fields ...string) error { + lastCol := len(fields) - 1 + delim := "\t" + if t.IsTTY { + delim = " " + } + + for col, val := range fields { + if col > 0 { + _, err := fmt.Fprint(t.out, delim) + if err != nil { + return err + } + } + if t.IsTTY { + truncVal := truncate(t.colWidths[col], val) + if col != lastCol { + truncVal = fmt.Sprintf("%-*s", t.colWidths[col], truncVal) + } + if t.colFuncs[col] != nil { + truncVal = t.colFuncs[col](truncVal) + } + _, err := fmt.Fprint(t.out, truncVal) + if err != nil { + return err + } + } else { + _, err := fmt.Fprint(t.out, val) + if err != nil { + return err + } + } + } + _, err := fmt.Fprint(t.out, "\n") + if err != nil { + return err + } + return nil +} + +func truncate(maxLength int, title string) string { + if len(title) > maxLength { + return title[0:maxLength-3] + "..." + } + return title +}