mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
feat: fit list tables to the terminal and write TSV when piped
Lists had no header, wrapped every row once a title was longer than the terminal, and were space-padded when piped, so cut -f and awk -F'\t' could not split them and empty cells collapsed. terminal.Table now takes column specs and a Display. On a terminal it prints an uppercase header, aligns columns two spaces apart and shrinks flexible columns, widest first, to Streams.Width (the terminal, then $COLUMNS, then 80), cutting by display width with "…". States use the terminal's ANSI 16 colors, hex colors fall back to the 256-color palette unless COLORTERM advertises 24-bit, and OSC 8 links appear only on the first column, independently of color (TEA_HYPERLINKS=auto|always|never). Piped tables are tab-separated without header, color or links, and times are RFC3339 UTC instead of "about 2 hours ago". terminal.View lays out single resources (title, badges, metadata, fields, body, footer URL) and renders markdown bodies with glamour on a terminal only (TEA_MARKDOWN=raw keeps them raw). Shared formatters cover relative times, timestamps, sizes, durations and status icons. In the app, writeTable and writeView are the new list and view writers: a "Showing N of M …" context line, an empty-state message on stderr and the next-page hint. writeList, writeItem and batch keep working and now render through the new table: tab-separated when piped, truncated at the widest column on a terminal, with headers for issue list and pr list. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
17 files changed
+1253
-103
No files matched your search
@@ -6,10 +6,12 @@ package app
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/tea/internal/settings"
|
||||
"gitea.dev/tea/internal/terminal"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -28,6 +30,22 @@ func (a *application) colorEnabled() bool {
|
||||
return terminal.ColorEnabled(preferences.Color, a.deps.Streams.OutTTY, a.deps.Getenv)
|
||||
}
|
||||
|
||||
// display describes stdout for tables and views. The hyperlinks (auto, always, never) and markdown
|
||||
// (auto, raw) preferences are read from TEA_HYPERLINKS and TEA_MARKDOWN until config.yml stores them.
|
||||
func (a *application) display() terminal.Display {
|
||||
preferences, _ := a.preferences()
|
||||
streams := a.deps.Streams
|
||||
return terminal.Display{
|
||||
TTY: streams.OutTTY,
|
||||
Color: terminal.ColorEnabled(preferences.Color, streams.OutTTY, a.deps.Getenv),
|
||||
TrueColor: terminal.TrueColor(a.deps.Getenv),
|
||||
Links: terminal.HyperlinksEnabled(a.deps.Getenv("TEA_HYPERLINKS"), streams.OutTTY, a.deps.Getenv),
|
||||
Markdown: terminal.MarkdownEnabled(a.deps.Getenv("TEA_MARKDOWN"), streams.OutTTY),
|
||||
Width: streams.Width,
|
||||
Now: a.deps.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// writeHuman pages multi-line human list/view text. JSON callers must not use it.
|
||||
// --no-input does not disable paging.
|
||||
func (a *application) writeHuman(cmd *cobra.Command, text string) error {
|
||||
@@ -40,6 +58,81 @@ func (a *application) writeJSON(cmd *cobra.Command, v any) error {
|
||||
return terminal.WriteJSON(cmd.OutOrStdout(), v, a.options.json, a.options.jq)
|
||||
}
|
||||
|
||||
// listSpec describes how a list command shows its items to people.
|
||||
type listSpec[T any] struct {
|
||||
Columns []terminal.Column
|
||||
Row func(T) []terminal.Cell
|
||||
// Empty, such as "no open issues in alice/x", goes to stderr on a TTY when nothing is listed.
|
||||
Empty string
|
||||
// Context names the listed items, such as "open issues in alice/x", for the line that heads the
|
||||
// table on a TTY: "Showing 30 of 112 open issues in alice/x".
|
||||
Context string
|
||||
}
|
||||
|
||||
// listPage places a page of items in the whole listing.
|
||||
type listPage struct {
|
||||
// Number is the 1-based --page value.
|
||||
Number int
|
||||
// More reports later pages. Total is the size of the whole listing when Counted.
|
||||
More bool
|
||||
Total int
|
||||
Counted bool
|
||||
}
|
||||
|
||||
// writeTable writes items as a JSON array, or as a table through the pager. On a TTY the table has
|
||||
// a header row and, with a spec Context, a context line that also points at the next page; an empty
|
||||
// list prints the spec's Empty message to stderr instead. Piped tables are tab-separated, and the
|
||||
// next-page hint goes to stderr.
|
||||
func writeTable[T any](a *application, cmd *cobra.Command, items []T, spec listSpec[T], page listPage) error {
|
||||
if a.options.json != "" {
|
||||
return a.writeJSON(cmd, items)
|
||||
}
|
||||
d := a.display()
|
||||
if len(items) == 0 {
|
||||
if !d.TTY || spec.Empty == "" {
|
||||
return nil
|
||||
}
|
||||
_, err := fmt.Fprintln(cmd.ErrOrStderr(), spec.Empty)
|
||||
return err
|
||||
}
|
||||
rows := make([][]terminal.Cell, len(items))
|
||||
for i, item := range items {
|
||||
rows[i] = spec.Row(item)
|
||||
}
|
||||
text := terminal.Table(spec.Columns, rows, d)
|
||||
if d.TTY && spec.Context != "" {
|
||||
return a.writeHuman(cmd, showing(len(items), page, spec.Context)+"\n\n"+text)
|
||||
}
|
||||
if err := a.writeHuman(cmd, text); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.morePagesHint(cmd, page.More, page.Number)
|
||||
}
|
||||
|
||||
// showing is the context line above a table: "Showing 30 of 112 open issues in alice/x · next: --page 2".
|
||||
func showing(shown int, page listPage, what string) string {
|
||||
line := fmt.Sprintf("Showing %d", shown)
|
||||
switch {
|
||||
case page.Counted:
|
||||
line += fmt.Sprintf(" of %d", page.Total)
|
||||
case !page.More && page.Number <= 1:
|
||||
line += fmt.Sprintf(" of %d", shown)
|
||||
}
|
||||
line += " " + what
|
||||
if page.More {
|
||||
line += fmt.Sprintf(" · next: --page %d", page.Number+1)
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
// writeView writes item as a JSON object, or as a view through the pager.
|
||||
func writeView[T any](a *application, cmd *cobra.Command, item T, view func(T) terminal.View) error {
|
||||
if a.options.json != "" {
|
||||
return a.writeJSON(cmd, item)
|
||||
}
|
||||
return a.writeHuman(cmd, view(item).Render(a.display()))
|
||||
}
|
||||
|
||||
// abortBatch stops a batch instead of recording an item failure.
|
||||
type abortBatch struct{ error }
|
||||
|
||||
@@ -70,7 +163,7 @@ func batch[K, T any](a *application, cmd *cobra.Command, keys []K, label func(K)
|
||||
var err error
|
||||
switch {
|
||||
case a.options.json == "":
|
||||
err = a.writeHuman(cmd, table(results, row, a.colorEnabled()))
|
||||
err = a.writeHuman(cmd, legacyTable(a, cmd, results, row))
|
||||
case len(keys) != 1:
|
||||
err = a.writeJSON(cmd, results)
|
||||
case len(results) == 1:
|
||||
@@ -79,20 +172,64 @@ func batch[K, T any](a *application, cmd *cobra.Command, keys []K, label func(K)
|
||||
return errors.Join(append(failures, err)...)
|
||||
}
|
||||
|
||||
// writeList writes items as a JSON array, or as one aligned table through the pager.
|
||||
// writeList writes items as a JSON array, or as one table through the pager. List commands move
|
||||
// to writeTable.
|
||||
func writeList[T any](a *application, cmd *cobra.Command, items []T, row func(T) []terminal.Cell) error {
|
||||
if a.options.json != "" {
|
||||
return a.writeJSON(cmd, items)
|
||||
}
|
||||
return a.writeHuman(cmd, table(items, row, a.colorEnabled()))
|
||||
return a.writeHuman(cmd, legacyTable(a, cmd, items, row))
|
||||
}
|
||||
|
||||
// writeItem writes one item as a JSON object, or as its list row.
|
||||
// writeItem writes one item as a JSON object, or as its list row. Views move to writeView.
|
||||
func writeItem[T any](a *application, cmd *cobra.Command, item T, row func(T) []terminal.Cell) error {
|
||||
if a.options.json != "" {
|
||||
return a.writeJSON(cmd, item)
|
||||
}
|
||||
return a.writeHuman(cmd, table([]T{item}, row, a.colorEnabled()))
|
||||
return a.writeHuman(cmd, legacyTable(a, cmd, []T{item}, row))
|
||||
}
|
||||
|
||||
// legacyColumns gives lists that still render through writeList a header row and a flexible
|
||||
// column, keyed by command path without the root. Their owners replace an entry with a listSpec.
|
||||
var legacyColumns = map[string][]terminal.Column{
|
||||
"issue list": numberedColumns,
|
||||
"pr list": numberedColumns,
|
||||
}
|
||||
|
||||
// numberedColumns head the rows of issueRow and pullRow.
|
||||
var numberedColumns = []terminal.Column{{Header: "ID", Prefix: "#"}, {Header: "Title", Flex: true}, {Header: "State"}, {Header: "Author"}}
|
||||
|
||||
// legacyTable renders rows of commands that predate listSpec: tab-separated when piped, and on a
|
||||
// TTY fit to the terminal width by truncating their widest column, unless legacyColumns names
|
||||
// their columns.
|
||||
func legacyTable[T any](a *application, cmd *cobra.Command, items []T, row func(T) []terminal.Cell) string {
|
||||
rows := make([][]terminal.Cell, len(items))
|
||||
for i, item := range items {
|
||||
rows[i] = row(item)
|
||||
}
|
||||
columns, ok := legacyColumns[strings.TrimPrefix(cmd.CommandPath(), cmd.Root().Name()+" ")]
|
||||
if !ok {
|
||||
columns = widestFlex(rows)
|
||||
}
|
||||
return terminal.Table(columns, rows, a.display())
|
||||
}
|
||||
|
||||
// widestFlex makes the column with the widest cell flexible.
|
||||
func widestFlex(rows [][]terminal.Cell) []terminal.Column {
|
||||
var widths []int
|
||||
for _, row := range rows {
|
||||
for i, cell := range row {
|
||||
if i == len(widths) {
|
||||
widths = append(widths, 0)
|
||||
}
|
||||
widths[i] = max(widths[i], runewidth.StringWidth(terminal.SafeLine(cell.Text)))
|
||||
}
|
||||
}
|
||||
columns := make([]terminal.Column, len(widths))
|
||||
if len(widths) > 0 {
|
||||
columns[slices.Index(widths, slices.Max(widths))].Flex = true
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
// writeRecord writes one item as a JSON object, or as a human-readable record.
|
||||
@@ -121,15 +258,17 @@ func numberedRecord(number int64, title, url string) terminal.Record {
|
||||
return terminal.Record{Heading: []terminal.Cell{{Text: fmt.Sprintf("#%d", number), Bold: true}, {Text: title}}, Lines: []string{url}}
|
||||
}
|
||||
|
||||
// stateColors colors issue, pull request and milestone states on color terminals.
|
||||
var stateColors = map[string]string{"open": "2da44e", "closed": "cf222e", "merged": "8250df"}
|
||||
// stateColors colors issue, pull request and milestone states with the terminal's own palette.
|
||||
var stateColors = map[string]string{"open": "green", "closed": "red", "merged": "magenta", "draft": "muted"}
|
||||
|
||||
// table renders rows aligned like a TTY table without header or truncation, for commands that
|
||||
// predate listSpec and compose their own output.
|
||||
func table[T any](items []T, row func(T) []terminal.Cell, color bool) string {
|
||||
rows := make([][]terminal.Cell, len(items))
|
||||
for i, item := range items {
|
||||
rows[i] = row(item)
|
||||
}
|
||||
return terminal.Table(rows, color)
|
||||
return terminal.Table(nil, rows, terminal.Display{TTY: true, Color: color, TrueColor: true, Links: color})
|
||||
}
|
||||
|
||||
// morePagesHint tells human readers on stderr that the listing continues past this page.
|
||||
|
||||
@@ -12,8 +12,11 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/tea/internal/terminal"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -56,7 +59,7 @@ func TestLabelListAppliesColorAndPagesWithNoInput(t *testing.T) {
|
||||
_, _ = w.Write([]byte(`[{"id":1,"name":"bug","color":"ff0000","description":"Defect"},{"id":2,"name":"docs","color":"00ff00","description":"Docs"}]`))
|
||||
}))
|
||||
defer server.Close()
|
||||
deps, out, diagnostics, pager := pagedDeps(t, server.URL, map[string]string{"PAGER": "fake-pager"}, "color", "always")
|
||||
deps, out, diagnostics, pager := pagedDeps(t, server.URL, map[string]string{"PAGER": "fake-pager", "COLORTERM": "truecolor"}, "color", "always")
|
||||
require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "-R", "alice/project", "--no-input"}))
|
||||
require.Equal(t, 1, pager.calls)
|
||||
require.Equal(t, "fake-pager", pager.command)
|
||||
@@ -114,7 +117,7 @@ func TestTeaPagerAndColorEnvironmentOverrides(t *testing.T) {
|
||||
deps, out, _, pager := pagedDeps(t, server.URL, env, "pager", "configured-pager", "color", "never")
|
||||
require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "-R", "alice/project", "--no-input"}))
|
||||
require.Equal(t, "tea-pager", pager.command)
|
||||
require.Contains(t, pager.input, "\x1b[38;2;255;0;0mbug\x1b[0m")
|
||||
require.Contains(t, pager.input, "\x1b[38;5;196mbug\x1b[0m")
|
||||
out.Reset()
|
||||
require.NoError(t, Execute(context.Background(), deps, []string{"config", "get", "pager", "--show-source", "--json"}))
|
||||
require.JSONEq(t, `{"pager":{"value":"tea-pager","source":"TEA_PAGER"}}`, out.String())
|
||||
@@ -131,13 +134,12 @@ func TestColorAutoUsesStdoutTTY(t *testing.T) {
|
||||
deps, out, _ := serverDeps(t, server.URL)
|
||||
deps.Streams.OutTTY = false
|
||||
require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "-R", "alice/project", "--no-input"}))
|
||||
require.Equal(t, "1 bug ff0000\n", out.String())
|
||||
require.NotContains(t, out.String(), "\x1b[")
|
||||
require.Equal(t, "1\tbug\tff0000\t\n", out.String())
|
||||
|
||||
out.Reset()
|
||||
deps.Streams.OutTTY = true
|
||||
require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "-R", "alice/project", "--no-input"}))
|
||||
require.Contains(t, out.String(), "\x1b[38;2;255;0;0mbug\x1b[0m")
|
||||
require.Contains(t, out.String(), "\x1b[38;5;196mbug\x1b[0m")
|
||||
}
|
||||
|
||||
func TestMultiItemHumanOutputPagesOnce(t *testing.T) {
|
||||
@@ -171,7 +173,7 @@ func TestMultiItemHumanOutputPagesOnce(t *testing.T) {
|
||||
{[]string{"issue", "comment", "list", "5"}, "1 alice\none\n\n2 bob\ntwo\n\n"},
|
||||
} {
|
||||
t.Run(tc.args[0]+" "+tc.args[1], func(t *testing.T) {
|
||||
deps, out, _, pager := pagedDeps(t, server.URL, nil, "pager", "fake-pager", "color", "never")
|
||||
deps, out, _, pager := pagedDeps(t, server.URL, map[string]string{"TEA_HYPERLINKS": "never"}, "pager", "fake-pager", "color", "never")
|
||||
require.NoError(t, Execute(context.Background(), deps, append(tc.args, "-R", "alice/project", "--yes")))
|
||||
require.Equal(t, 1, pager.calls)
|
||||
require.Equal(t, tc.want, pager.input)
|
||||
@@ -210,3 +212,154 @@ func TestListsHintWhenMorePagesExist(t *testing.T) {
|
||||
require.Empty(t, diagnostics.String(), args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListsFitTheTerminalAndSplitWhenPiped(t *testing.T) {
|
||||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte(`[{"number":6,"title":"A very long issue title that keeps going and going to see how list output handles wrapping","state":"open","user":{"login":"alice"}},` +
|
||||
`{"number":5,"title":"支持中文路径 (support CJK paths)","state":"open","user":{"login":""}}]`))
|
||||
}))
|
||||
defer server.Close()
|
||||
for _, group := range []string{"issue", "pr"} {
|
||||
t.Run(group, func(t *testing.T) {
|
||||
deps, out, _ := serverDeps(t, server.URL)
|
||||
args := []string{group, "list", "-R", "alice/project"}
|
||||
require.NoError(t, Execute(context.Background(), deps, args))
|
||||
require.Equal(t, "6\tA very long issue title that keeps going and going to see how list output handles wrapping\topen\talice\n"+
|
||||
"5\t支持中文路径 (support CJK paths)\topen\t\n", out.String())
|
||||
|
||||
out.Reset()
|
||||
deps.Streams.OutTTY, deps.Streams.Width = true, 50
|
||||
deps.Getenv = serverEnv(server.URL, map[string]string{"NO_COLOR": "1"})
|
||||
require.NoError(t, Execute(context.Background(), deps, args))
|
||||
require.Equal(t, "ID TITLE STATE AUTHOR\n"+
|
||||
"#6 A very long issue title that k… open alice\n"+
|
||||
"#5 支持中文路径 (support CJK path… open\n", stripLinks(out.String()))
|
||||
for _, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
require.LessOrEqual(t, runewidth.StringWidth(stripLinks(line)), 50, line)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// stripLinks removes OSC 8 hyperlinks from rendered output.
|
||||
func stripLinks(s string) string {
|
||||
for {
|
||||
start := strings.Index(s, "\x1b]8;")
|
||||
if start < 0 {
|
||||
return s
|
||||
}
|
||||
end := strings.Index(s[start:], "\x1b\\")
|
||||
s = s[:start] + s[start+end+2:]
|
||||
}
|
||||
}
|
||||
|
||||
// outputApplication returns an application over deps and a command writing to its streams.
|
||||
func outputApplication(deps Dependencies, json string) (*application, *cobra.Command) {
|
||||
deps.Now = func() time.Time { return time.Date(2026, 10, 11, 20, 0, 0, 0, time.UTC) }
|
||||
a := &application{deps: deps, options: options{json: json}}
|
||||
cmd := &cobra.Command{Use: "list"}
|
||||
cmd.SetOut(deps.Streams.Out)
|
||||
cmd.SetErr(deps.Streams.ErrOut)
|
||||
cmd.SetContext(context.Background())
|
||||
return a, cmd
|
||||
}
|
||||
|
||||
type listed struct {
|
||||
Number int64 `json:"number"`
|
||||
Title string `json:"title"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
var listedSpec = listSpec[listed]{
|
||||
Columns: []terminal.Column{{Header: "ID", Prefix: "#"}, {Header: "Title", Flex: true}, {Header: "Updated"}},
|
||||
Row: func(item listed) []terminal.Cell {
|
||||
return []terminal.Cell{{Text: strconv.FormatInt(item.Number, 10)}, {Text: item.Title}, {Time: item.Updated}}
|
||||
},
|
||||
Empty: "no open issues in alice/x",
|
||||
Context: "open issues in alice/x",
|
||||
}
|
||||
|
||||
func TestWriteTable(t *testing.T) {
|
||||
updated := time.Date(2026, 10, 11, 18, 0, 0, 0, time.UTC)
|
||||
items := []listed{{12, "Emoji in title 🚀", updated}, {1, "Login fails", updated}}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
tty bool
|
||||
json string
|
||||
items []listed
|
||||
page listPage
|
||||
out, stderr string
|
||||
pagedHeadings bool
|
||||
}{
|
||||
{
|
||||
name: "context line and header on a terminal", tty: true, items: items, page: listPage{Number: 1, More: true, Total: 112, Counted: true},
|
||||
out: "Showing 2 of 112 open issues in alice/x · next: --page 2\n\n" +
|
||||
"ID TITLE UPDATED\n#12 Emoji in title 🚀 about 2 hours ago\n#1 Login fails about 2 hours ago\n",
|
||||
},
|
||||
{
|
||||
name: "counts a single page without a total", tty: true, items: items[:1], page: listPage{Number: 1},
|
||||
out: "Showing 1 of 1 open issues in alice/x\n\nID TITLE UPDATED\n#12 Emoji in title 🚀 about 2 hours ago\n",
|
||||
},
|
||||
{
|
||||
name: "tab-separated with the page hint on stderr when piped", items: items, page: listPage{Number: 2, More: true},
|
||||
out: "12\tEmoji in title 🚀\t2026-10-11T18:00:00Z\n1\tLogin fails\t2026-10-11T18:00:00Z\n",
|
||||
stderr: "more results available; use --page 3\n",
|
||||
},
|
||||
{name: "empty message on a terminal", tty: true, page: listPage{Number: 1}, stderr: "no open issues in alice/x\n"},
|
||||
{name: "nothing when empty and piped", page: listPage{Number: 1}},
|
||||
{name: "JSON", tty: true, json: "number", items: items[:1], page: listPage{Number: 1, More: true}, out: `[{"number":12}]` + "\n"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
deps, out, diagnostics := testDeps(t)
|
||||
deps.Streams.OutTTY = tc.tty
|
||||
deps.Getenv = envFunc(map[string]string{"NO_COLOR": "1"})
|
||||
a, cmd := outputApplication(deps, tc.json)
|
||||
require.NoError(t, writeTable(a, cmd, tc.items, listedSpec, tc.page))
|
||||
require.Equal(t, tc.out, out.String())
|
||||
require.Equal(t, tc.stderr, diagnostics.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteView(t *testing.T) {
|
||||
view := func(item listed) terminal.View {
|
||||
return terminal.View{
|
||||
Title: item.Title, ID: fmt.Sprintf("#%d", item.Number), Badges: []terminal.Cell{{Text: "Open", Color: "green"}},
|
||||
Meta: "alice opened " + terminal.RelativeTime(time.Date(2026, 10, 11, 20, 0, 0, 0, time.UTC), item.Updated), Body: "Steps", Kind: "issue", URL: "https://example.org/1",
|
||||
}
|
||||
}
|
||||
item := listed{1, "Login fails", time.Date(2026, 10, 11, 18, 0, 0, 0, time.UTC)}
|
||||
for _, tc := range []struct {
|
||||
name, json, want string
|
||||
tty bool
|
||||
}{
|
||||
{name: "terminal", tty: true, want: "Login fails #1\nOpen • alice opened about 2 hours ago\n\nSteps\n\nView this issue on Gitea: https://example.org/1\n"},
|
||||
{name: "piped", want: "title:\tLogin fails\nurl:\thttps://example.org/1\n--\nSteps\n"},
|
||||
{name: "JSON", tty: true, json: "title", want: `{"title":"Login fails"}` + "\n"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
deps, out, _ := testDeps(t)
|
||||
deps.Streams.OutTTY = tc.tty
|
||||
deps.Getenv = envFunc(map[string]string{"NO_COLOR": "1", "TEA_MARKDOWN": "raw", "TEA_HYPERLINKS": "never"})
|
||||
a, cmd := outputApplication(deps, tc.json)
|
||||
require.NoError(t, writeView(a, cmd, item, view))
|
||||
require.Equal(t, tc.want, out.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowing(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
shown int
|
||||
page listPage
|
||||
want string
|
||||
}{
|
||||
{30, listPage{Number: 1, More: true, Total: 112, Counted: true}, "Showing 30 of 112 open issues · next: --page 2"},
|
||||
{11, listPage{Number: 1, Total: 11, Counted: true}, "Showing 11 of 11 open issues"},
|
||||
{11, listPage{Number: 1}, "Showing 11 of 11 open issues"},
|
||||
{30, listPage{Number: 1, More: true}, "Showing 30 open issues · next: --page 2"},
|
||||
{5, listPage{Number: 3}, "Showing 5 open issues"},
|
||||
} {
|
||||
require.Equal(t, tc.want, showing(tc.shown, tc.page, "open issues"), "%+v", tc.page)
|
||||
}
|
||||
}
|
||||
@@ -97,12 +97,13 @@ func TestIssueListLinksNumbersOnColorTerminals(t *testing.T) {
|
||||
deps, out, _ := serverDeps(t, server.URL)
|
||||
args := []string{"issue", "list", "-R", "alice/project"}
|
||||
require.NoError(t, Execute(context.Background(), deps, args))
|
||||
require.Equal(t, "7 Short open alice\n12 Evil]52;c;aGk= Title closed bob\n", out.String())
|
||||
require.Equal(t, "7\tShort\topen\talice\n12\tEvil]52;c;aGk= Title\tclosed\tbob\n", out.String())
|
||||
out.Reset()
|
||||
deps.Streams.OutTTY = true
|
||||
require.NoError(t, Execute(context.Background(), deps, args))
|
||||
require.Equal(t, "\x1b]8;;https://example.org/i/7\x1b\\7\x1b]8;;\x1b\\ Short \x1b[38;2;45;164;78mopen\x1b[0m alice\n"+
|
||||
"\x1b]8;;https://example.org/i/12\x1b\\12\x1b]8;;\x1b\\ Evil]52;c;aGk= Title \x1b[38;2;207;34;46mclosed\x1b[0m bob\n", out.String())
|
||||
require.Equal(t, "\x1b[90mID\x1b[0m \x1b[90mTITLE\x1b[0m \x1b[90mSTATE\x1b[0m \x1b[90mAUTHOR\x1b[0m\n"+
|
||||
"\x1b]8;;https://example.org/i/7\x1b\\#7\x1b]8;;\x1b\\ Short \x1b[32mopen\x1b[0m alice\n"+
|
||||
"\x1b]8;;https://example.org/i/12\x1b\\#12\x1b]8;;\x1b\\ Evil]52;c;aGk= Title \x1b[31mclosed\x1b[0m bob\n", out.String())
|
||||
}
|
||||
|
||||
func TestIssueListKindFilter(t *testing.T) {
|
||||
|
||||
@@ -66,5 +66,5 @@ func TestLabelImportAcceptsColorPrefixedRows(t *testing.T) {
|
||||
require.Len(t, created, 2)
|
||||
requirePayload(t, `{"name":"bug","color":"ee0701","description":"Something is not working"}`, strings.NewReader(created[0]))
|
||||
requirePayload(t, `{"name":"enhancement","color":"84b6eb"}`, strings.NewReader(created[1]))
|
||||
require.Equal(t, "1 bug\n2 enhancement\n", out.String())
|
||||
require.Equal(t, "1\tbug\t\t\n2\tenhancement\t\t\n", out.String())
|
||||
}
|
||||
Reference in new issue
Block a user