Skip to content

Commit 318c91d

Browse files
add vertical table layout
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1ea5889 commit 318c91d

3 files changed

Lines changed: 173 additions & 5 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Modules from this library will obey GitHub CLI conventions by default:
1111
- [Terminal capabilities](https://pkg.go.dev/github.com/cli/go-gh/v2/pkg/term) are determined by taking environment variables `GH_FORCE_TTY`, `NO_COLOR`, `CLICOLOR`, etc. into account.
1212

1313
- Generating [table](https://pkg.go.dev/github.com/cli/go-gh/v2/pkg/tableprinter) or [Go template](https://pkg.go.dev/github.com/cli/go-gh/pkg/template) output uses the same engine as gh.
14+
Set `GH_TABLE_LAYOUT=vertical` to render header-based table rows as vertical `Header: value` entries.
1415

1516
- The [`browser`](https://pkg.go.dev/github.com/cli/go-gh/v2/pkg/browser) module activates the user's preferred web browser.
1617

pkg/tableprinter/table.go

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
// Package tableprinter facilitates rendering column-formatted data to a terminal and TSV-formatted data to
2-
// a script or a file. It is suitable for presenting tabular data in a human-readable format that is
3-
// guaranteed to fit within the given viewport, while at the same time offering the same data in a
4-
// machine-readable format for scripts.
1+
// Package tableprinter facilitates rendering table data to a terminal and to redirected output.
2+
// It renders as columns by default, but when GH_TABLE_LAYOUT=vertical is set, header-based rows
3+
// are rendered in vertical key/value format.
54
package tableprinter
65

76
import (
87
"fmt"
98
"io"
9+
"os"
10+
"strings"
1011

1112
"github.com/cli/go-gh/v2/pkg/text"
1213
)
@@ -20,6 +21,11 @@ type TablePrinter interface {
2021
Render() error
2122
}
2223

24+
const (
25+
tableLayoutEnv = "GH_TABLE_LAYOUT"
26+
tableLayoutVertical = "vertical"
27+
)
28+
2329
// WithTruncate overrides the truncation function for the field. The function should transform a string
2430
// argument into a string that fits within the given display width. The default behavior is to truncate the
2531
// value by adding "..." in the end. The truncation function will be called before padding and coloring.
@@ -51,8 +57,22 @@ func WithColor(fn func(string) string) fieldOption {
5157

5258
// New initializes a table printer with terminal mode and terminal width. When terminal mode is enabled, the
5359
// output will be human-readable, column-formatted to fit available width, and rendered with color support.
54-
// In non-terminal mode, the output is tab-separated and all truncation of values is disabled.
60+
// In non-terminal mode, the output is tab-separated and all truncation of values is disabled, unless
61+
// GH_TABLE_LAYOUT=vertical is set and headers are provided.
5562
func New(w io.Writer, isTTY bool, maxWidth int) TablePrinter {
63+
defaultPrinter := newDefaultTablePrinter(w, isTTY, maxWidth)
64+
if !isVerticalLayoutEnabled() {
65+
return defaultPrinter
66+
}
67+
68+
return &verticalTablePrinter{
69+
out: w,
70+
isTTY: isTTY,
71+
fallback: defaultPrinter,
72+
}
73+
}
74+
75+
func newDefaultTablePrinter(w io.Writer, isTTY bool, maxWidth int) TablePrinter {
5676
if isTTY {
5777
return &ttyTablePrinter{
5878
out: w,
@@ -65,6 +85,11 @@ func New(w io.Writer, isTTY bool, maxWidth int) TablePrinter {
6585
}
6686
}
6787

88+
func isVerticalLayoutEnabled() bool {
89+
layout, isSet := os.LookupEnv(tableLayoutEnv)
90+
return isSet && strings.EqualFold(strings.TrimSpace(layout), tableLayoutVertical)
91+
}
92+
6893
type tableField struct {
6994
text string
7095
truncateFunc func(int, string) string
@@ -79,6 +104,74 @@ type ttyTablePrinter struct {
79104
rows [][]tableField
80105
}
81106

107+
type verticalTablePrinter struct {
108+
out io.Writer
109+
isTTY bool
110+
fallback TablePrinter
111+
hasHeaders bool
112+
headers []string
113+
rows [][]tableField
114+
}
115+
116+
func (t *verticalTablePrinter) AddHeader(columns []string, opts ...fieldOption) {
117+
if t.hasHeaders {
118+
return
119+
}
120+
t.hasHeaders = true
121+
t.headers = append([]string(nil), columns...)
122+
}
123+
124+
func (t *verticalTablePrinter) AddField(s string, opts ...fieldOption) {
125+
if t.rows == nil {
126+
t.rows = make([][]tableField, 1)
127+
}
128+
rowI := len(t.rows) - 1
129+
field := tableField{
130+
text: s,
131+
truncateFunc: text.Truncate,
132+
}
133+
for _, opt := range opts {
134+
opt(&field)
135+
}
136+
t.rows[rowI] = append(t.rows[rowI], field)
137+
if !t.hasHeaders {
138+
t.fallback.AddField(s, opts...)
139+
}
140+
}
141+
142+
func (t *verticalTablePrinter) EndRow() {
143+
t.rows = append(t.rows, []tableField{})
144+
if !t.hasHeaders {
145+
t.fallback.EndRow()
146+
}
147+
}
148+
149+
func (t *verticalTablePrinter) Render() error {
150+
if !t.hasHeaders {
151+
return t.fallback.Render()
152+
}
153+
154+
for _, row := range t.rows {
155+
for col, field := range row {
156+
value := field.text
157+
if t.isTTY && field.colorFunc != nil {
158+
value = field.colorFunc(value)
159+
}
160+
if _, err := fmt.Fprintf(t.out, "%s: %s\n", t.headerForColumn(col), value); err != nil {
161+
return err
162+
}
163+
}
164+
}
165+
return nil
166+
}
167+
168+
func (t *verticalTablePrinter) headerForColumn(col int) string {
169+
if col < len(t.headers) {
170+
return t.headers[col]
171+
}
172+
return fmt.Sprintf("Column%d", col+1)
173+
}
174+
82175
func (t *ttyTablePrinter) AddHeader(columns []string, opts ...fieldOption) {
83176
if t.hasHeaders {
84177
return

pkg/tableprinter/table_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,77 @@ func Test_tsvTablePrinter_AddHeader(t *testing.T) {
184184
t.Errorf("expected: %q, got: %q", expected, buf.String())
185185
}
186186
}
187+
188+
func Test_verticalLayout_ttyWithHeaders(t *testing.T) {
189+
t.Setenv("GH_TABLE_LAYOUT", "vertical")
190+
191+
buf := bytes.Buffer{}
192+
tp := New(&buf, true, 80)
193+
194+
tp.AddHeader([]string{"ID", "TITLE"})
195+
tp.AddField("1")
196+
tp.AddField("foo")
197+
tp.EndRow()
198+
tp.AddField("2")
199+
tp.AddField("bar")
200+
tp.EndRow()
201+
202+
err := tp.Render()
203+
if err != nil {
204+
t.Fatalf("unexpected error: %v", err)
205+
}
206+
207+
expected := "ID: 1\nTITLE: foo\nID: 2\nTITLE: bar\n"
208+
if buf.String() != expected {
209+
t.Errorf("expected: %q, got: %q", expected, buf.String())
210+
}
211+
}
212+
213+
func Test_verticalLayout_nonTTYWithHeaders(t *testing.T) {
214+
t.Setenv("GH_TABLE_LAYOUT", "vertical")
215+
216+
buf := bytes.Buffer{}
217+
tp := New(&buf, false, 0)
218+
219+
tp.AddHeader([]string{"ID", "TITLE"})
220+
tp.AddField("1")
221+
tp.AddField("foo")
222+
tp.EndRow()
223+
tp.AddField("2")
224+
tp.AddField("bar")
225+
tp.EndRow()
226+
227+
err := tp.Render()
228+
if err != nil {
229+
t.Fatalf("unexpected error: %v", err)
230+
}
231+
232+
expected := "ID: 1\nTITLE: foo\nID: 2\nTITLE: bar\n"
233+
if buf.String() != expected {
234+
t.Errorf("expected: %q, got: %q", expected, buf.String())
235+
}
236+
}
237+
238+
func Test_verticalLayout_noHeadersKeepsDefaultBehavior(t *testing.T) {
239+
t.Setenv("GH_TABLE_LAYOUT", "vertical")
240+
241+
buf := bytes.Buffer{}
242+
tp := New(&buf, false, 0)
243+
244+
tp.AddField("1")
245+
tp.AddField("hello")
246+
tp.EndRow()
247+
tp.AddField("2")
248+
tp.AddField("world")
249+
tp.EndRow()
250+
251+
err := tp.Render()
252+
if err != nil {
253+
t.Fatalf("unexpected error: %v", err)
254+
}
255+
256+
expected := "1\thello\n2\tworld\n"
257+
if buf.String() != expected {
258+
t.Errorf("expected: %q, got: %q", expected, buf.String())
259+
}
260+
}

0 commit comments

Comments
 (0)