mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
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>
58 lines
1.4 KiB
Go
58 lines
1.4 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package terminal
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// RelativeTime describes t relative to now like gh: "less than a minute ago", "about 3 hours ago",
|
|
// and "in about 2 months" for future times such as due dates.
|
|
func RelativeTime(now, t time.Time) string {
|
|
d, prefix, suffix := now.Sub(t), "", " ago"
|
|
if d < 0 {
|
|
d, prefix, suffix = -d, "in ", ""
|
|
}
|
|
const day = 24 * time.Hour
|
|
var n time.Duration
|
|
var unit string
|
|
switch {
|
|
case d < time.Minute:
|
|
return prefix + "less than a minute" + suffix
|
|
case d < time.Hour:
|
|
n, unit = d/time.Minute, "minute"
|
|
case d < day:
|
|
n, unit = d/time.Hour, "hour"
|
|
case d < 30*day:
|
|
n, unit = d/day, "day"
|
|
case d < 365*day:
|
|
n, unit = d/(30*day), "month"
|
|
default:
|
|
n, unit = d/(365*day), "year"
|
|
}
|
|
return prefix + "about " + plural(int(n), unit) + suffix
|
|
}
|
|
|
|
// Timestamp formats t for scripts: RFC3339 in UTC at second precision.
|
|
func Timestamp(t time.Time) string {
|
|
return t.UTC().Truncate(time.Second).Format(time.RFC3339)
|
|
}
|
|
|
|
// ParseTime parses an API timestamp, returning the zero time for an empty or malformed value.
|
|
func ParseTime(raw string) time.Time {
|
|
t, err := time.Parse(time.RFC3339, raw)
|
|
if err != nil {
|
|
return time.Time{}
|
|
}
|
|
return t
|
|
}
|
|
|
|
func plural(n int, unit string) string {
|
|
if n == 1 {
|
|
return "1 " + unit
|
|
}
|
|
return fmt.Sprintf("%d %ss", n, unit)
|
|
}
|