Merge pull request #82 from github/table-output

Extract generic row printer that adjusts itself for receiving terminal
This commit is contained in:
Mislav Marohnić 2019-11-21 14:15:36 +01:00 committed by GitHub
commit adfc014298
2 changed files with 195 additions and 45 deletions

View file

@ -10,7 +10,6 @@ import (
"github.com/github/gh-cli/git"
"github.com/github/gh-cli/utils"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
)
func init() {
@ -167,57 +166,38 @@ 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)
prNum := strconv.Itoa(pr.Number)
if table.IsTTY() {
prNum = "#" + prNum
}
table.AddField(prNum, nil, colorFuncForState(pr.State))
table.AddField(pr.Title, nil, nil)
table.AddField(pr.HeadLabel(), nil, utils.Cyan)
table.EndRow()
}
err = table.Render()
if err != nil {
return err
}
branchWidth := 40
titleWidth := ttyWidth - branchWidth - 2 - numWidth - 2
if maxTitleWidth < titleWidth {
branchWidth += titleWidth - maxTitleWidth
titleWidth = maxTitleWidth
}
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())
}
}
return nil
}
func colorFuncForState(state string) func(string) string {
switch state {
case "OPEN":
return utils.Green
case "CLOSED":
return utils.Red
case "MERGED":
return utils.Magenta
default:
return nil
}
}
func prView(cmd *cobra.Command, args []string) error {
ctx := contextForCommand(cmd)
baseRepo, err := ctx.BaseRepo()

170
utils/table_printer.go Normal file
View file

@ -0,0 +1,170 @@
package utils
import (
"fmt"
"io"
"os"
"golang.org/x/crypto/ssh/terminal"
)
type TablePrinter interface {
IsTTY() bool
AddField(string, func(int, string) string, func(string) string)
EndRow()
Render() error
}
func NewTablePrinter(w io.Writer) TablePrinter {
if outFile, isFile := w.(*os.File); isFile {
fd := int(outFile.Fd())
if terminal.IsTerminal(fd) {
ttyWidth := 80
if w, _, err := terminal.GetSize(fd); err == nil {
ttyWidth = w
}
return &ttyTablePrinter{
out: w,
maxWidth: ttyWidth,
}
}
}
return &tsvTablePrinter{
out: w,
}
}
type tableField struct {
Text string
TruncateFunc func(int, string) string
ColorFunc func(string) string
}
type ttyTablePrinter struct {
out io.Writer
maxWidth int
rows [][]tableField
}
func (t ttyTablePrinter) IsTTY() bool {
return true
}
func (t *ttyTablePrinter) AddField(text string, truncateFunc func(int, string) string, colorFunc func(string) string) {
if truncateFunc == nil {
truncateFunc = truncate
}
if t.rows == nil {
t.rows = [][]tableField{[]tableField{}}
}
rowI := len(t.rows) - 1
field := tableField{
Text: text,
TruncateFunc: truncateFunc,
ColorFunc: colorFunc,
}
t.rows[rowI] = append(t.rows[rowI], field)
}
func (t *ttyTablePrinter) EndRow() {
t.rows = append(t.rows, []tableField{})
}
func (t *ttyTablePrinter) Render() error {
if len(t.rows) == 0 {
return nil
}
numCols := len(t.rows[0])
colWidths := make([]int, numCols)
// measure maximum content width per column
for _, row := range t.rows {
for col, field := range row {
textLen := len(field.Text)
if textLen > colWidths[col] {
colWidths[col] = textLen
}
}
}
delim := " "
availWidth := t.maxWidth - colWidths[0] - ((numCols - 1) * len(delim))
// add extra space from columns that are already narrower than threshold
for col := 1; col < numCols; col++ {
availColWidth := availWidth / (numCols - 1)
if extra := availColWidth - colWidths[col]; extra > 0 {
availWidth += extra
}
}
// cap all but first column to fit available terminal width
// TODO: support weighted instead of even redistribution
for col := 1; col < numCols; col++ {
availColWidth := availWidth / (numCols - 1)
if colWidths[col] > availColWidth {
colWidths[col] = availColWidth
}
}
for _, row := range t.rows {
for col, field := range row {
if col > 0 {
_, err := fmt.Fprint(t.out, delim)
if err != nil {
return err
}
}
truncVal := field.TruncateFunc(colWidths[col], field.Text)
if col < numCols-1 {
// pad value with spaces on the right
truncVal = fmt.Sprintf("%-*s", colWidths[col], truncVal)
}
if field.ColorFunc != nil {
truncVal = field.ColorFunc(truncVal)
}
_, err := fmt.Fprint(t.out, truncVal)
if err != nil {
return err
}
}
if len(row) > 0 {
_, err := fmt.Fprint(t.out, "\n")
if err != nil {
return err
}
}
}
return nil
}
type tsvTablePrinter struct {
out io.Writer
currentCol int
}
func (t tsvTablePrinter) IsTTY() bool {
return false
}
func (t *tsvTablePrinter) AddField(text string, _ func(int, string) string, _ func(string) string) {
if t.currentCol > 0 {
fmt.Fprint(t.out, "\t")
}
fmt.Fprint(t.out, text)
t.currentCol++
}
func (t *tsvTablePrinter) EndRow() {
fmt.Fprint(t.out, "\n")
t.currentCol = 0
}
func (t *tsvTablePrinter) Render() error {
return nil
}
func truncate(maxLength int, title string) string {
if len(title) > maxLength {
return title[0:maxLength-3] + "..."
}
return title
}