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:
bircni committed 2026-10-11 22:43:10 +02:00
1 parent 1c9cc32d28
commit 55574a9fd0
17 files changed
+1253 -103

No files matched your search

+20 -1
View File
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"os"
"strconv"
"strings"
"charm.land/huh/v2"
@@ -24,11 +25,29 @@ type Streams struct {
InTTY bool
OutTTY bool
ErrTTY bool
// Width is the number of columns human output on Out fits in; 0 means unlimited.
Width int
}
// System binds streams to the current process and detects each terminal.
func System() Streams {
return Streams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr, InTTY: term.IsTerminal(int(os.Stdin.Fd())), OutTTY: term.IsTerminal(int(os.Stdout.Fd())), ErrTTY: term.IsTerminal(int(os.Stderr.Fd()))}
return Streams{
In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr,
InTTY: term.IsTerminal(int(os.Stdin.Fd())), OutTTY: term.IsTerminal(int(os.Stdout.Fd())), ErrTTY: term.IsTerminal(int(os.Stderr.Fd())),
Width: terminalWidth(int(os.Stdout.Fd()), os.Getenv),
}
}
// terminalWidth returns the width of the terminal at fd, then $COLUMNS, then 80. A pty without a
// window size, such as `script` run without a terminal, reports a width of 0.
func terminalWidth(fd int, getenv func(string) string) int {
if width, _, err := term.GetSize(fd); err == nil && width > 0 {
return width
}
if width, err := strconv.Atoi(getenv("COLUMNS")); err == nil && width > 0 {
return width
}
return 80
}
// Policy controls input interaction independently of supplied flag count.