mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
refactor(terminal): inline the column fitting and shorten doc comments
Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
6 files changed
+37
-59
No files matched your search
@@ -163,10 +163,8 @@ var (
|
|||||||
wikiTitle = regexp.MustCompile(`\[title: (.+?)(?:\.md)?\]`)
|
wikiTitle = regexp.MustCompile(`\[title: (.+?)(?:\.md)?\]`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Classify names the resource of a failed request for the user: a 404 becomes a NotFoundError
|
// Classify names the resource of a failed request: a 404 or an unknown user becomes a
|
||||||
// for kind key in scope, a refusal Gitea explains (archived repository, merged pull request,
|
// NotFoundError, a refusal Gitea explains a ConflictError. Other errors are returned unchanged.
|
||||||
// existing resource, ...) becomes a ConflictError, and an unknown user the request named becomes
|
|
||||||
// a NotFoundError for that user. Other errors are returned unchanged.
|
|
||||||
func Classify(err error, kind, key, scope string) error {
|
func Classify(err error, kind, key, scope string) error {
|
||||||
apiErr, ok := errors.AsType[*transport.APIError](err)
|
apiErr, ok := errors.AsType[*transport.APIError](err)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -41,10 +41,9 @@ func (s Labels) Find(ctx context.Context, raws []string, byName bool) ([]Label,
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUnique creates a label unless existing, the labels of the scope, has one with its name,
|
// CreateUnique creates a label unless existing has one with its name, ignoring case: Gitea would
|
||||||
// ignoring case. Then update edits that label with the supplied fields, keeping its name, and
|
// create a second label that name filters cannot tell apart. Then update edits that label, keeping
|
||||||
// otherwise CreateUnique fails: Gitea would create a second label that name filters cannot tell
|
// its name, or CreateUnique fails. It reports whether it updated a label.
|
||||||
// apart. It reports whether it updated a label.
|
|
||||||
func (s Labels) CreateUnique(ctx context.Context, existing []Label, opts LabelOptions, update bool) (Label, bool, error) {
|
func (s Labels) CreateUnique(ctx context.Context, existing []Label, opts LabelOptions, update bool) (Label, bool, error) {
|
||||||
current, err := s.finder(true).Find(existing, deref(opts.Name))
|
current, err := s.finder(true).Find(existing, deref(opts.Name))
|
||||||
if errors.As(err, new(*NotFoundError)) {
|
if errors.As(err, new(*NotFoundError)) {
|
||||||
|
|||||||
@@ -66,8 +66,7 @@ func (p Policy) Validate(s Streams) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrCanceled reports a declined confirmation or an aborted prompt. It exits with code 3 and is
|
// ErrCanceled reports a declined confirmation or an aborted prompt; it exits with code 3.
|
||||||
// meant to be reported as "Canceled." rather than as an error.
|
|
||||||
var ErrCanceled = clierr.Wrap(clierr.Canceled, errors.New("canceled"))
|
var ErrCanceled = clierr.Wrap(clierr.Canceled, errors.New("canceled"))
|
||||||
|
|
||||||
// Prompter abstracts cancellable input collection and confirmation.
|
// Prompter abstracts cancellable input collection and confirmation.
|
||||||
@@ -99,9 +98,8 @@ func promptError(ctx context.Context, err error) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// errNoTerminalSize refuses forms on a terminal without a size, such as a pty that `script` opens
|
// errNoTerminalSize refuses forms on a terminal without a size, such as a pty that `script` opens:
|
||||||
// without a terminal. Forms size their fields from it, and a zero size panics in bubbles; nobody
|
// a zero size panics in bubbles.
|
||||||
// could see the form there anyway.
|
|
||||||
var errNoTerminalSize = clierr.Wrap(clierr.Usage, errors.New("terminal size unknown; pass the values as flags or use --no-input"))
|
var errNoTerminalSize = clierr.Wrap(clierr.Usage, errors.New("terminal size unknown; pass the values as flags or use --no-input"))
|
||||||
|
|
||||||
func (p *FormPrompter) runForm(ctx context.Context, form *huh.Form) error {
|
func (p *FormPrompter) runForm(ctx context.Context, form *huh.Form) error {
|
||||||
|
|||||||
+25
-39
@@ -11,9 +11,8 @@ import (
|
|||||||
"github.com/mattn/go-runewidth"
|
"github.com/mattn/go-runewidth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Display describes the output that tables and views render for. On a terminal (TTY) they align,
|
// Display describes the output that tables and views render for: a terminal (TTY), or plain text
|
||||||
// truncate and style text for people; otherwise they write plain tab-separated or key/value text
|
// for scripts.
|
||||||
// for scripts, without header, color or links.
|
|
||||||
type Display struct {
|
type Display struct {
|
||||||
TTY bool
|
TTY bool
|
||||||
// Color enables SGR colors and emphasis on a TTY.
|
// Color enables SGR colors and emphasis on a TTY.
|
||||||
@@ -42,9 +41,9 @@ type Column struct {
|
|||||||
Prefix string
|
Prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cell is one value in a Table. On color terminals Color (a name such as green, or six-digit
|
// Cell is one value in a Table. Color (a name such as green), Bold and Muted style the text after
|
||||||
// RGB hex), Bold and Muted style the text after alignment, so escape sequences never count
|
// alignment, so escape sequences never count toward the column width. Link applies to the first
|
||||||
// toward the column width. Link applies to the first column only.
|
// column only.
|
||||||
type Cell struct {
|
type Cell struct {
|
||||||
Text, Link, Color string
|
Text, Link, Color string
|
||||||
Bold, Muted bool
|
Bold, Muted bool
|
||||||
@@ -87,10 +86,8 @@ func (c Cell) style(text string, d Display, link bool) string {
|
|||||||
func (c Cell) Render(d Display) string { return c.style(SafeLine(c.Text), d, false) }
|
func (c Cell) Render(d Display) string { return c.style(SafeLine(c.Text), d, false) }
|
||||||
|
|
||||||
// Table renders rows under columns; rows may have more cells than there are columns. On a TTY it
|
// Table renders rows under columns; rows may have more cells than there are columns. On a TTY it
|
||||||
// prints an uppercase header row when any column has a header, aligns columns two spaces apart,
|
// aligns columns under an uppercase header and shrinks Flex columns to the display width;
|
||||||
// and shrinks Flex columns until rows fit the display width. Otherwise it writes one line of
|
// otherwise it writes tab-separated lines. Cell text passes through SafeLine either way.
|
||||||
// tab-separated cells per row. Cell text passes through SafeLine either way, so server data can
|
|
||||||
// neither break the layout nor reach the terminal as escape sequences. No rows render as nothing.
|
|
||||||
func Table(columns []Column, rows [][]Cell, d Display) string {
|
func Table(columns []Column, rows [][]Cell, d Display) string {
|
||||||
if len(rows) == 0 {
|
if len(rows) == 0 {
|
||||||
return ""
|
return ""
|
||||||
@@ -144,10 +141,24 @@ func Table(columns []Column, rows [][]Cell, d Display) string {
|
|||||||
widths[j] = max(widths[j], runewidth.StringWidth(line[j]))
|
widths[j] = max(widths[j], runewidth.StringWidth(line[j]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fitWidth(widths, d.Width, func(j int) (bool, int) {
|
// Narrow the widest flexible column above its floor, one cell at a time, until the row fits.
|
||||||
c := column(shown[j])
|
excess := 2*(len(widths)-1) - d.Width
|
||||||
return c.Flex, max(c.Min, runewidth.StringWidth(headers[j]), 8)
|
for _, w := range widths {
|
||||||
})
|
excess += w
|
||||||
|
}
|
||||||
|
for ; d.Width > 0 && excess > 0; excess-- {
|
||||||
|
widest := -1
|
||||||
|
for j, w := range widths {
|
||||||
|
c := column(shown[j])
|
||||||
|
if c.Flex && w > max(c.Min, runewidth.StringWidth(headers[j]), 8) && (widest < 0 || w > widths[widest]) {
|
||||||
|
widest = j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if widest < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
widths[widest]--
|
||||||
|
}
|
||||||
|
|
||||||
writeLine := func(cells []string, style func(j int, text string) string) {
|
writeLine := func(cells []string, style func(j int, text string) string) {
|
||||||
var line strings.Builder
|
var line strings.Builder
|
||||||
@@ -177,31 +188,6 @@ func Table(columns []Column, rows [][]Cell, d Display) string {
|
|||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// fitWidth narrows flexible columns, always the widest one first, until the row and its two-space
|
|
||||||
// gaps fit limit or every flexible column reached its floor. A limit of 0 disables fitting.
|
|
||||||
func fitWidth(widths []int, limit int, flex func(j int) (bool, int)) {
|
|
||||||
if limit <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
excess := 2 * (len(widths) - 1)
|
|
||||||
for _, w := range widths {
|
|
||||||
excess += w
|
|
||||||
}
|
|
||||||
excess -= limit
|
|
||||||
for ; excess > 0; excess-- {
|
|
||||||
widest := -1
|
|
||||||
for j, w := range widths {
|
|
||||||
if ok, floor := flex(j); ok && w > floor && (widest < 0 || w > widths[widest]) {
|
|
||||||
widest = j
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if widest < 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
widths[widest]--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SafeLine reduces server text to one line: SafeText, with tabs and line breaks as spaces.
|
// SafeLine reduces server text to one line: SafeText, with tabs and line breaks as spaces.
|
||||||
func SafeLine(s string) string {
|
func SafeLine(s string) string {
|
||||||
return strings.Map(func(r rune) rune {
|
return strings.Map(func(r rune) rune {
|
||||||
|
|||||||
@@ -35,12 +35,9 @@ func (f Field) text(d Display) string {
|
|||||||
return strings.Join(values, ", ")
|
return strings.Join(values, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// View is one resource for human readers, such as an issue, a release or a repository.
|
// View is one resource for human readers, such as an issue. On a TTY it shows the title, badges,
|
||||||
//
|
// fields, body and a "View this <kind> on Gitea" footer; piped, "key:\tvalue" lines, "--" and the
|
||||||
// On a TTY it shows the bold title with its muted ID, a line of badges and muted metadata, the
|
// raw body.
|
||||||
// aligned non-empty fields, the body (markdown rendered when the display allows), and the footer
|
|
||||||
// "View this <kind> on Gitea: <url>". Otherwise it writes "key:\tvalue" lines for the title, the
|
|
||||||
// fields and the URL, then "--" and the raw body, for scripts.
|
|
||||||
type View struct {
|
type View struct {
|
||||||
Title, ID string
|
Title, ID string
|
||||||
// Badges, such as the state, start the line under the title; Meta completes it, for example
|
// Badges, such as the state, start the line under the title; Meta completes it, for example
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func (t debugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// failureMessage returns the sanitized server message of a failed response and keeps its body
|
// failureMessage returns the sanitized server message of a failed response and keeps its body
|
||||||
// readable, so --debug shows what the server said when tea explains the failure in its own words.
|
// readable for the caller.
|
||||||
func failureMessage(req *http.Request, resp *http.Response) string {
|
func failureMessage(req *http.Request, resp *http.Response) string {
|
||||||
if resp.StatusCode < 400 {
|
if resp.StatusCode < 400 {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
Reference in new issue
Block a user