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.
This commit is contained in:
Mislav Marohnić 2019-11-15 19:19:41 +01:00
parent 624c44efda
commit f30e973b9d
2 changed files with 126 additions and 43 deletions

View file

@ -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
}

108
utils/table_printer.go Normal file
View file

@ -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
}