mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01: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
@@ -0,0 +1,103 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func issueView() View {
|
||||
return View{
|
||||
Title: "Login fails\x1b[2J", ID: "#1",
|
||||
Badges: []Cell{{Text: "Open", Color: "green"}},
|
||||
Meta: "alice opened about 1 hour ago • 2 comments",
|
||||
Fields: []Field{
|
||||
{Label: "State", Value: "open", PipedOnly: true},
|
||||
{Label: "Labels", Cells: []Cell{{Text: "bug", Color: "ee0701"}, {Text: "priority/high"}}},
|
||||
{Label: "Milestone", Value: "v1.0"},
|
||||
{Label: "Blocked by"},
|
||||
{Label: "Comments", Value: "2", TTYOnly: true},
|
||||
},
|
||||
Body: "Steps:\r\n1. Use `p@ss!`\x1b]52;c;aGk=\x07\n",
|
||||
Kind: "issue", URL: "http://localhost:3001/alice/project/issues/1",
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewRender(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
view View
|
||||
d Display
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"key and value lines when piped",
|
||||
issueView(),
|
||||
Display{Color: true},
|
||||
"title:\tLogin fails[2J\nstate:\topen\nlabels:\tbug, priority/high\nmilestone:\tv1.0\nblocked by:\t\n" +
|
||||
"url:\thttp://localhost:3001/alice/project/issues/1\n--\nSteps:\n1. Use `p@ss!`]52;c;aGk=\n",
|
||||
},
|
||||
{
|
||||
"summary for people",
|
||||
issueView(),
|
||||
Display{TTY: true},
|
||||
"Login fails[2J #1\nOpen • alice opened about 1 hour ago • 2 comments\n" +
|
||||
"Labels: bug, priority/high\nMilestone: v1.0\nComments: 2\n\n" +
|
||||
"Steps:\n1. Use `p@ss!`]52;c;aGk=\n\nView this issue on Gitea: http://localhost:3001/alice/project/issues/1\n",
|
||||
},
|
||||
{
|
||||
"styled for color terminals",
|
||||
View{Title: "v1.0", Badges: []Cell{{Text: "Latest", Color: "green"}}, Fields: []Field{{Label: "Labels", Cells: []Cell{{Text: "bug", Color: "ee0701"}}}}, URL: "https://example.org/r"},
|
||||
Display{TTY: true, Color: true, TrueColor: true},
|
||||
"\x1b[1mv1.0\x1b[0m\n\x1b[32mLatest\x1b[0m\n\x1b[1mLabels:\x1b[0m \x1b[38;2;238;7;1mbug\x1b[0m\n\n" +
|
||||
"\x1b[90mNo description provided\x1b[0m\n\n\x1b[90mView on Gitea: https://example.org/r\x1b[0m\n",
|
||||
},
|
||||
{
|
||||
"minimal",
|
||||
View{Title: "Home"},
|
||||
Display{TTY: true},
|
||||
"Home\n\nNo description provided\n",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, tc.view.Render(tc.d))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewRendersMarkdownOnlyWhenEnabled(t *testing.T) {
|
||||
view := View{Title: "Docs", Body: "# Setup\n\nRun **tea** [now](https://example.org).\n\n- one\n- two"}
|
||||
require.Contains(t, view.Render(Display{TTY: true}), "\n# Setup\n\nRun **tea** [now](https://example.org).\n")
|
||||
require.Contains(t, view.Render(Display{Markdown: true}), "--\n# Setup\n\nRun **tea** [now](https://example.org).\n")
|
||||
|
||||
plain := view.Render(Display{TTY: true, Markdown: true, Width: 40})
|
||||
require.Contains(t, plain, "\n # Setup\n\n Run **tea** now https://example.org.\n\n • one\n • two\n")
|
||||
require.NotContains(t, plain, "\x1b")
|
||||
|
||||
colored := view.Render(Display{TTY: true, Markdown: true, Color: true})
|
||||
require.Contains(t, colored, "Run \x1b[1mtea\x1b[m")
|
||||
require.NotContains(t, colored, "\x1b]8;")
|
||||
require.Contains(t, view.Render(Display{TTY: true, Markdown: true, Color: true, Links: true}), "\x1b]8;id=")
|
||||
}
|
||||
|
||||
func TestMarkdownSanitizesServerText(t *testing.T) {
|
||||
rendered := Markdown("Hello \x1b]0;PWNED\x07world\n\n`\x1b[2Jcode`", Display{Color: true, Links: true})
|
||||
require.NotContains(t, rendered, "\x1b]0;")
|
||||
require.NotContains(t, rendered, "\x1b[2J")
|
||||
require.Contains(t, rendered, "]0;PWNEDworld")
|
||||
for _, line := range strings.Split(Markdown(strings.Repeat("word ", 50), Display{Width: 40}), "\n") {
|
||||
require.LessOrEqual(t, len(line), 40, line)
|
||||
require.Equal(t, strings.TrimRight(line, " "), line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownEnabled(t *testing.T) {
|
||||
require.True(t, MarkdownEnabled("auto", true))
|
||||
require.True(t, MarkdownEnabled("", true))
|
||||
require.False(t, MarkdownEnabled("raw", true))
|
||||
require.False(t, MarkdownEnabled("auto", false))
|
||||
}
|
||||
Reference in new issue
Block a user