diff --git a/internal/services/errors.go b/internal/services/errors.go index a0589489..21e27cd8 100644 --- a/internal/services/errors.go +++ b/internal/services/errors.go @@ -163,10 +163,8 @@ var ( wikiTitle = regexp.MustCompile(`\[title: (.+?)(?:\.md)?\]`) ) -// Classify names the resource of a failed request for the user: a 404 becomes a NotFoundError -// for kind key in scope, a refusal Gitea explains (archived repository, merged pull request, -// existing resource, ...) becomes a ConflictError, and an unknown user the request named becomes -// a NotFoundError for that user. Other errors are returned unchanged. +// Classify names the resource of a failed request: a 404 or an unknown user becomes a +// NotFoundError, a refusal Gitea explains a ConflictError. Other errors are returned unchanged. func Classify(err error, kind, key, scope string) error { apiErr, ok := errors.AsType[*transport.APIError](err) if !ok { diff --git a/internal/services/label_selection.go b/internal/services/label_selection.go index 23aa239e..e09c4418 100644 --- a/internal/services/label_selection.go +++ b/internal/services/label_selection.go @@ -41,10 +41,9 @@ func (s Labels) Find(ctx context.Context, raws []string, byName bool) ([]Label, return result, nil } -// CreateUnique creates a label unless existing, the labels of the scope, has one with its name, -// ignoring case. Then update edits that label with the supplied fields, keeping its name, and -// otherwise CreateUnique fails: Gitea would create a second label that name filters cannot tell -// apart. It reports whether it updated a label. +// CreateUnique creates a label unless existing has one with its name, ignoring case: Gitea would +// create a second label that name filters cannot tell apart. Then update edits that label, keeping +// its name, or CreateUnique fails. It reports whether it updated a label. 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)) if errors.As(err, new(*NotFoundError)) { diff --git a/internal/terminal/streams.go b/internal/terminal/streams.go index a43bd4e8..60393c29 100644 --- a/internal/terminal/streams.go +++ b/internal/terminal/streams.go @@ -66,8 +66,7 @@ func (p Policy) Validate(s Streams) error { return nil } -// ErrCanceled reports a declined confirmation or an aborted prompt. It exits with code 3 and is -// meant to be reported as "Canceled." rather than as an error. +// ErrCanceled reports a declined confirmation or an aborted prompt; it exits with code 3. var ErrCanceled = clierr.Wrap(clierr.Canceled, errors.New("canceled")) // Prompter abstracts cancellable input collection and confirmation. @@ -99,9 +98,8 @@ func promptError(ctx context.Context, err error) error { return err } -// 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 -// could see the form there anyway. +// errNoTerminalSize refuses forms on a terminal without a size, such as a pty that `script` opens: +// a zero size panics in bubbles. 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 { diff --git a/internal/terminal/table.go b/internal/terminal/table.go index a71d78a3..64738a3d 100644 --- a/internal/terminal/table.go +++ b/internal/terminal/table.go @@ -11,9 +11,8 @@ import ( "github.com/mattn/go-runewidth" ) -// Display describes the output that tables and views render for. On a terminal (TTY) they align, -// truncate and style text for people; otherwise they write plain tab-separated or key/value text -// for scripts, without header, color or links. +// Display describes the output that tables and views render for: a terminal (TTY), or plain text +// for scripts. type Display struct { TTY bool // Color enables SGR colors and emphasis on a TTY. @@ -42,9 +41,9 @@ type Column struct { Prefix string } -// Cell is one value in a Table. On color terminals Color (a name such as green, or six-digit -// RGB hex), Bold and Muted style the text after alignment, so escape sequences never count -// toward the column width. Link applies to the first column only. +// Cell is one value in a Table. Color (a name such as green), Bold and Muted style the text after +// alignment, so escape sequences never count toward the column width. Link applies to the first +// column only. type Cell struct { Text, Link, Color string 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) } // 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, -// and shrinks Flex columns until rows fit the display width. Otherwise it writes one line of -// 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. +// aligns columns under an uppercase header and shrinks Flex columns to the display width; +// otherwise it writes tab-separated lines. Cell text passes through SafeLine either way. func Table(columns []Column, rows [][]Cell, d Display) string { if len(rows) == 0 { return "" @@ -144,10 +141,24 @@ func Table(columns []Column, rows [][]Cell, d Display) string { widths[j] = max(widths[j], runewidth.StringWidth(line[j])) } } - fitWidth(widths, d.Width, func(j int) (bool, int) { - c := column(shown[j]) - return c.Flex, max(c.Min, runewidth.StringWidth(headers[j]), 8) - }) + // Narrow the widest flexible column above its floor, one cell at a time, until the row fits. + excess := 2*(len(widths)-1) - d.Width + 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) { var line strings.Builder @@ -177,31 +188,6 @@ func Table(columns []Column, rows [][]Cell, d Display) 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. func SafeLine(s string) string { return strings.Map(func(r rune) rune { diff --git a/internal/terminal/view.go b/internal/terminal/view.go index eba183b5..220d69e8 100644 --- a/internal/terminal/view.go +++ b/internal/terminal/view.go @@ -35,12 +35,9 @@ func (f Field) text(d Display) string { return strings.Join(values, ", ") } -// View is one resource for human readers, such as an issue, a release or a repository. -// -// On a TTY it shows the bold title with its muted ID, a line of badges and muted metadata, the -// aligned non-empty fields, the body (markdown rendered when the display allows), and the footer -// "View this on Gitea: ". Otherwise it writes "key:\tvalue" lines for the title, the -// fields and the URL, then "--" and the raw body, for scripts. +// 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 on Gitea" footer; piped, "key:\tvalue" lines, "--" and the +// raw body. type View struct { Title, ID string // Badges, such as the state, start the line under the title; Meta completes it, for example diff --git a/internal/transport/debug.go b/internal/transport/debug.go index 141594c8..f46bc342 100644 --- a/internal/transport/debug.go +++ b/internal/transport/debug.go @@ -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 -// 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 { if resp.StatusCode < 400 { return ""