mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
refactor: remove features beyond the UX plan
Drop additions that the UX plan does not ask for: - the interactive label edit form; label edit now requires a flag - label clone and the skip mode of label creation it needed - repo rename rewriting the Git remote or tea.repository; it now warns that the remote still points to the old name - the extra "source" remote on clones of pull mirrors - run artifact download, which duplicates run download --zip - 256-color and 24-bit label swatches; colors stay ANSI 16 - config list --show-source (config get keeps it) - the membership and access GETs before team member/repo add and remove; Gitea's PUT and DELETE are idempotent label edit/delete keep --by-name: it is the only way to address a label whose name is a number that is also another label's ID. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
22 files changed
+69
-560
No files matched your search
@@ -4,8 +4,6 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
@@ -39,80 +37,17 @@ func HyperlinksEnabled(preference string, outTTY bool, getenv func(string) strin
|
||||
}
|
||||
}
|
||||
|
||||
// TrueColor reports whether the terminal advertises 24-bit color through COLORTERM.
|
||||
func TrueColor(getenv func(string) string) bool {
|
||||
if getenv == nil {
|
||||
return false
|
||||
}
|
||||
value := getenv("COLORTERM")
|
||||
return value == "truecolor" || value == "24bit"
|
||||
}
|
||||
|
||||
// namedColors are the ANSI 16 colors for states and status, so they follow the terminal's own theme.
|
||||
var namedColors = map[string]string{"red": "31", "green": "32", "yellow": "33", "blue": "34", "magenta": "35", "cyan": "36", "muted": "90"}
|
||||
|
||||
// Colorize colors text by name (red, green, yellow, blue, magenta, cyan or muted) from the terminal's palette,
|
||||
// or by six-digit RGB hex with 24-bit color. Unknown colors leave the text plain.
|
||||
// Colorize colors text by name (red, green, yellow, blue, magenta, cyan or muted) from the terminal's
|
||||
// ANSI 16 palette. Other colors, such as label hex colors, leave the text plain.
|
||||
func Colorize(color, text string, enabled bool) string {
|
||||
if !enabled {
|
||||
code, ok := namedColors[color]
|
||||
if !enabled || !ok || text == "" {
|
||||
return text
|
||||
}
|
||||
return paint(color, text, true)
|
||||
}
|
||||
|
||||
// paint is Colorize for enabled color; without trueColor, hex colors use the nearest 256-color index.
|
||||
func paint(color, text string, trueColor bool) string {
|
||||
if text == "" {
|
||||
return text
|
||||
}
|
||||
if code, ok := namedColors[color]; ok {
|
||||
return "\x1b[" + code + "m" + text + "\x1b[0m"
|
||||
}
|
||||
r, g, b, ok := parseHex(color)
|
||||
switch {
|
||||
case !ok:
|
||||
return text
|
||||
case trueColor:
|
||||
return fmt.Sprintf("\x1b[38;2;%d;%d;%dm%s\x1b[0m", r, g, b, text)
|
||||
default:
|
||||
return fmt.Sprintf("\x1b[38;5;%dm%s\x1b[0m", ansi256(r, g, b), text)
|
||||
}
|
||||
}
|
||||
|
||||
func parseHex(hex string) (r, g, b int, ok bool) {
|
||||
hex = strings.TrimPrefix(strings.TrimSpace(hex), "#")
|
||||
if len(hex) != 6 {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
value, err := strconv.ParseUint(hex, 16, 24)
|
||||
if err != nil {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
return int(value >> 16), int(value >> 8 & 0xff), int(value & 0xff), true
|
||||
}
|
||||
|
||||
// ansi256 returns the 256-color palette index nearest to an RGB color: one of the 6×6×6 cube colors
|
||||
// (16–231) or one of the 24 grays (232–255).
|
||||
func ansi256(r, g, b int) int {
|
||||
levels := [6]int{0, 95, 135, 175, 215, 255}
|
||||
level := func(v int) int {
|
||||
switch {
|
||||
case v < 48:
|
||||
return 0
|
||||
case v < 115:
|
||||
return 1
|
||||
default:
|
||||
return (v - 35) / 40
|
||||
}
|
||||
}
|
||||
ri, gi, bi := level(r), level(g), level(b)
|
||||
gray := min(max((r+g+b)/3-3, 0)/10, 23)
|
||||
grayValue := 8 + 10*gray
|
||||
distance := func(cr, cg, cb int) int { return (r-cr)*(r-cr) + (g-cg)*(g-cg) + (b-cb)*(b-cb) }
|
||||
if distance(levels[ri], levels[gi], levels[bi]) <= distance(grayValue, grayValue, grayValue) {
|
||||
return 16 + 36*ri + 6*gi + bi
|
||||
}
|
||||
return 232 + gray
|
||||
return "\x1b[" + code + "m" + text + "\x1b[0m"
|
||||
}
|
||||
|
||||
// Emphasis applies simple bold styling for TTY human tables.
|
||||
|
||||
@@ -42,29 +42,16 @@ func TestHyperlinksEnabledIgnoresColorPolicy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrueColor(t *testing.T) {
|
||||
require.True(t, TrueColor(env(map[string]string{"COLORTERM": "truecolor"})))
|
||||
require.True(t, TrueColor(env(map[string]string{"COLORTERM": "24bit"})))
|
||||
require.False(t, TrueColor(env(map[string]string{"COLORTERM": "yes"})))
|
||||
require.False(t, TrueColor(nil))
|
||||
}
|
||||
|
||||
func TestColorizeAndEmphasis(t *testing.T) {
|
||||
require.Equal(t, "bug", Colorize("ff0000", "bug", false))
|
||||
require.Equal(t, "bug", Colorize("bad", "bug", true))
|
||||
require.Equal(t, "\x1b[38;2;255;0;0mbug\x1b[0m", Colorize("#ff0000", "bug", true))
|
||||
require.Equal(t, "bug", Colorize("#ff0000", "bug", true))
|
||||
require.Equal(t, "\x1b[32mopen\x1b[0m", Colorize("green", "open", true))
|
||||
require.Equal(t, "\x1b[90mdraft\x1b[0m", Colorize("muted", "draft", true))
|
||||
require.Equal(t, "open", Emphasis("open", false))
|
||||
require.Equal(t, "\x1b[1mopen\x1b[0m", Emphasis("open", true))
|
||||
}
|
||||
|
||||
func TestPaintApproximatesHexColorsWithoutTrueColor(t *testing.T) {
|
||||
for hex, index := range map[string]string{"ff0000": "196", "00ff00": "46", "000000": "16", "ffffff": "231", "808080": "244", "d73a4a": "167", "7057ff": "63"} {
|
||||
require.Equal(t, "\x1b[38;5;"+index+"mx\x1b[0m", paint(hex, "x", false), hex)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyperlink(t *testing.T) {
|
||||
require.Equal(t, "#7", Hyperlink("https://example.org/i/7", "#7", false))
|
||||
require.Equal(t, "\x1b]8;;https://example.org/i/7\x1b\\#7\x1b]8;;\x1b\\", Hyperlink("https://example.org/i/7", "#7", true))
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
)
|
||||
|
||||
// LabelFormValues holds interactively edited label fields.
|
||||
type LabelFormValues struct {
|
||||
Name, Color, Description string
|
||||
Exclusive, Archived bool
|
||||
}
|
||||
|
||||
// LabelForm collects interactive label input.
|
||||
type LabelForm interface {
|
||||
// EditLabel asks for the fields of a label, starting from current, under heading, such as
|
||||
// `Editing label "bug" in alice/x`.
|
||||
EditLabel(ctx context.Context, heading string, current LabelFormValues) (LabelFormValues, error)
|
||||
}
|
||||
|
||||
var hexColor = regexp.MustCompile(`^#?[0-9a-fA-F]{6}$`)
|
||||
|
||||
// EditLabel runs the interactive label form on stderr.
|
||||
func (p *FormPrompter) EditLabel(ctx context.Context, heading string, current LabelFormValues) (LabelFormValues, error) {
|
||||
values := current
|
||||
if err := p.runForm(ctx, huh.NewForm(huh.NewGroup(
|
||||
huh.NewInput().Title("Label name (required):").Value(&values.Name).Validate(huh.ValidateNotEmpty()),
|
||||
huh.NewInput().Title("Color (hex, e.g. d73a4a):").Value(&values.Color).Validate(func(s string) error {
|
||||
if !hexColor.MatchString(strings.TrimSpace(s)) {
|
||||
return errors.New("use six hexadecimal digits")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewInput().Title("Description (optional):").Value(&values.Description),
|
||||
huh.NewConfirm().Title("Exclusive (one label of its scope per issue):").Value(&values.Exclusive),
|
||||
huh.NewConfirm().Title("Archived:").Value(&values.Archived),
|
||||
).Title(heading))); err != nil {
|
||||
return LabelFormValues{}, err
|
||||
}
|
||||
values.Name, values.Color = strings.TrimSpace(values.Name), strings.TrimPrefix(strings.TrimSpace(values.Color), "#")
|
||||
return values, nil
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLabelFormStartsFromCurrentValues(t *testing.T) {
|
||||
p, _ := testPrompter(strings.NewReader("\r\r\r\r\r"))
|
||||
current := LabelFormValues{Name: "bug", Color: "#EE0701", Description: "Broken", Exclusive: true}
|
||||
values, err := p.EditLabel(context.Background(), `Editing label "bug" in alice/x`, current)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, LabelFormValues{Name: "bug", Color: "EE0701", Description: "Broken", Exclusive: true}, values)
|
||||
}
|
||||
|
||||
func TestLabelFormMarksNameRequired(t *testing.T) {
|
||||
p, rendered := accessiblePrompter(t, "bug\nee0701\n\nn\nn\n")
|
||||
values, err := p.EditLabel(context.Background(), "", LabelFormValues{})
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, rendered.String(), "Label name (required):")
|
||||
require.Equal(t, "bug", values.Name)
|
||||
}
|
||||
@@ -16,9 +16,8 @@ import (
|
||||
// for scripts, without header, color or links.
|
||||
type Display struct {
|
||||
TTY bool
|
||||
// Color enables SGR colors and emphasis on a TTY; TrueColor writes hex colors in 24-bit instead
|
||||
// of the nearest of 256.
|
||||
Color, TrueColor bool
|
||||
// Color enables SGR colors and emphasis on a TTY.
|
||||
Color bool
|
||||
// Links enables OSC 8 links on the first table column on a TTY.
|
||||
Links bool
|
||||
// Markdown renders view bodies on a TTY.
|
||||
@@ -79,7 +78,7 @@ func (c Cell) style(text string, d Display, link bool) string {
|
||||
if c.Muted {
|
||||
color = "muted"
|
||||
}
|
||||
text = Emphasis(paint(color, text, d.TrueColor), c.Bold)
|
||||
text = Emphasis(Colorize(color, text, true), c.Bold)
|
||||
}
|
||||
return Hyperlink(c.Link, text, link && d.Links)
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ func TestTable(t *testing.T) {
|
||||
[][]Cell{{{Text: "7", Link: "https://example.org/7"}, {Text: "bug", Color: "ff0000", Link: "https://example.org/bug"}, {Text: "open", Color: "green", Bold: true}}, row("12", "feature", "closed")},
|
||||
Display{TTY: true, Color: true, Links: true},
|
||||
"\x1b[90mID\x1b[0m \x1b[90mNAME\x1b[0m \x1b[90mSTATE\x1b[0m\n" +
|
||||
"\x1b]8;;https://example.org/7\x1b\\7\x1b]8;;\x1b\\ \x1b[38;5;196mbug\x1b[0m \x1b[1m\x1b[32mopen\x1b[0m\x1b[0m\n" +
|
||||
"\x1b]8;;https://example.org/7\x1b\\7\x1b]8;;\x1b\\ bug \x1b[1m\x1b[32mopen\x1b[0m\x1b[0m\n" +
|
||||
"12 feature closed\n",
|
||||
},
|
||||
{
|
||||
@@ -109,11 +109,11 @@ func TestTable(t *testing.T) {
|
||||
"\x1b]8;;https://example.org/7\x1b\\7\x1b]8;;\x1b\\ bug\n",
|
||||
},
|
||||
{
|
||||
"writes 24-bit color on true-color terminals",
|
||||
"leaves hex colors plain",
|
||||
nil,
|
||||
[][]Cell{{{Text: "bug", Color: "#ff0000"}, {Text: "old", Muted: true}}},
|
||||
Display{TTY: true, Color: true, TrueColor: true},
|
||||
"\x1b[38;2;255;0;0mbug\x1b[0m \x1b[90mold\x1b[0m\n",
|
||||
Display{TTY: true, Color: true},
|
||||
"bug \x1b[90mold\x1b[0m\n",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
@@ -52,8 +52,8 @@ func TestViewRender(t *testing.T) {
|
||||
{
|
||||
"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" +
|
||||
Display{TTY: true, Color: true},
|
||||
"\x1b[1mv1.0\x1b[0m\n\x1b[32mLatest\x1b[0m\n\x1b[1mLabels:\x1b[0m bug\n\n" +
|
||||
"\x1b[90mNo description provided\x1b[0m\n\n\x1b[90mView on Gitea: https://example.org/r\x1b[0m\n",
|
||||
},
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user