Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit dd97fe2

Browse files
authored
chore(cli): replace lipgloss with coder/pretty (#9564)
This change will improve over CLI performance and "snappiness" as well as substantially reduce our test times. Preliminary benchmarks show `coder server --help` times cut from 300ms to 120ms on my dogfood instance. The inefficiency of lipgloss disproportionately impacts our system, as all help text for every command is generated whenever any command is invoked. The `pretty` API could clean up a lot of the code (e.g., by replacing complex string concatenations with Printf), but this commit is too expansive as is so that work will be done in a follow up.
1 parent 8421f56 commit dd97fe2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+488
-328
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ docs/admin/prometheus.md: scripts/metricsdocgen/main.go scripts/metricsdocgen/me
559559
pnpm run format:write:only ./docs/admin/prometheus.md
560560

561561
docs/cli.md: scripts/clidocgen/main.go examples/examples.gen.json $(GO_SRC_FILES)
562-
BASE_PATH="." go run ./scripts/clidocgen
562+
CI=true BASE_PATH="." go run ./scripts/clidocgen
563563
pnpm run format:write:only ./docs/cli.md ./docs/cli/*.md ./docs/manifest.json
564564

565565
docs/admin/audit-logs.md: scripts/auditdocgen/main.go enterprise/audit/table.go coderd/rbac/object_gen.go

cli/clitest/golden.go

+2-6
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import (
1111
"strings"
1212
"testing"
1313

14-
"github.com/charmbracelet/lipgloss"
1514
"github.com/muesli/termenv"
1615
"github.com/stretchr/testify/require"
1716

1817
"github.com/coder/coder/v2/cli/clibase"
18+
"github.com/coder/coder/v2/cli/cliui"
1919
"github.com/coder/coder/v2/cli/config"
2020
"github.com/coder/coder/v2/coderd/coderdtest"
2121
"github.com/coder/coder/v2/coderd/database/dbtestutil"
@@ -53,13 +53,9 @@ func DefaultCases() []CommandHelpCase {
5353
//
5454
//nolint:tparallel,paralleltest
5555
func TestCommandHelp(t *testing.T, getRoot func(t *testing.T) *clibase.Cmd, cases []CommandHelpCase) {
56-
ogColorProfile := lipgloss.ColorProfile()
5756
// ANSI256 escape codes are far easier for humans to parse in a diff,
5857
// but TrueColor is probably more popular with modern terminals.
59-
lipgloss.SetColorProfile(termenv.ANSI)
60-
t.Cleanup(func() {
61-
lipgloss.SetColorProfile(ogColorProfile)
62-
})
58+
cliui.TestColor(t, termenv.ANSI)
6359
rootClient, replacements := prepareTestData(t)
6460

6561
root := getRoot(t)

cli/cliui/cliui.go

+123-34
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package cliui
22

33
import (
44
"os"
5+
"testing"
6+
"time"
57

6-
"github.com/charmbracelet/charm/ui/common"
7-
"github.com/charmbracelet/lipgloss"
88
"github.com/muesli/termenv"
99
"golang.org/x/xerrors"
10+
11+
"github.com/coder/pretty"
1012
)
1113

1214
var Canceled = xerrors.New("canceled")
@@ -15,55 +17,142 @@ var Canceled = xerrors.New("canceled")
1517
var DefaultStyles Styles
1618

1719
type Styles struct {
18-
Bold,
19-
Checkmark,
2020
Code,
21-
Crossmark,
2221
DateTimeStamp,
2322
Error,
2423
Field,
2524
Keyword,
26-
Paragraph,
2725
Placeholder,
2826
Prompt,
2927
FocusedPrompt,
3028
Fuchsia,
31-
Logo,
3229
Warn,
33-
Wrap lipgloss.Style
30+
Wrap pretty.Style
3431
}
3532

36-
func init() {
37-
lipgloss.SetDefaultRenderer(
38-
lipgloss.NewRenderer(os.Stdout, termenv.WithColorCache(true)),
39-
)
33+
var color = termenv.NewOutput(os.Stdout).ColorProfile()
34+
35+
// TestColor sets the color profile to the given profile for the duration of the
36+
// test.
37+
// WARN: Must not be used in parallel tests.
38+
func TestColor(t *testing.T, tprofile termenv.Profile) {
39+
old := color
40+
color = tprofile
41+
t.Cleanup(func() {
42+
color = old
43+
})
44+
}
45+
46+
var (
47+
Green = color.Color("#04B575")
48+
Red = color.Color("#ED567A")
49+
Fuchsia = color.Color("#EE6FF8")
50+
Yellow = color.Color("#ECFD65")
51+
Blue = color.Color("#5000ff")
52+
)
53+
54+
func isTerm() bool {
55+
return color != termenv.Ascii
56+
}
57+
58+
// Bold returns a formatter that renders text in bold
59+
// if the terminal supports it.
60+
func Bold(s string) string {
61+
if !isTerm() {
62+
return s
63+
}
64+
return pretty.Sprint(pretty.Bold(), s)
65+
}
4066

41-
// All Styles are set after we change the DefaultRenderer so that the ColorCache
42-
// is in effect, mitigating the severe performance issue seen here:
43-
// https://github.com/coder/coder/issues/7884.
67+
// BoldFmt returns a formatter that renders text in bold
68+
// if the terminal supports it.
69+
func BoldFmt() pretty.Formatter {
70+
if !isTerm() {
71+
return pretty.Style{}
72+
}
73+
return pretty.Bold()
74+
}
4475

45-
charmStyles := common.DefaultStyles()
76+
// Timestamp formats a timestamp for display.
77+
func Timestamp(t time.Time) string {
78+
return pretty.Sprint(DefaultStyles.DateTimeStamp, t.Format(time.Stamp))
79+
}
4680

81+
// Keyword formats a keyword for display.
82+
func Keyword(s string) string {
83+
return pretty.Sprint(DefaultStyles.Keyword, s)
84+
}
85+
86+
// Placeholder formats a placeholder for display.
87+
func Placeholder(s string) string {
88+
return pretty.Sprint(DefaultStyles.Placeholder, s)
89+
}
90+
91+
// Wrap prevents the text from overflowing the terminal.
92+
func Wrap(s string) string {
93+
return pretty.Sprint(DefaultStyles.Wrap, s)
94+
}
95+
96+
// Code formats code for display.
97+
func Code(s string) string {
98+
return pretty.Sprint(DefaultStyles.Code, s)
99+
}
100+
101+
// Field formats a field for display.
102+
func Field(s string) string {
103+
return pretty.Sprint(DefaultStyles.Field, s)
104+
}
105+
106+
func ifTerm(fmt pretty.Formatter) pretty.Formatter {
107+
if !isTerm() {
108+
return pretty.Nop
109+
}
110+
return fmt
111+
}
112+
113+
func init() {
114+
// We do not adapt the color based on whether the terminal is light or dark.
115+
// Doing so would require a round-trip between the program and the terminal
116+
// due to the OSC query and response.
47117
DefaultStyles = Styles{
48-
Bold: lipgloss.NewStyle().Bold(true),
49-
Checkmark: charmStyles.Checkmark,
50-
Code: charmStyles.Code,
51-
Crossmark: charmStyles.Error.Copy().SetString("✘"),
52-
DateTimeStamp: charmStyles.LabelDim,
53-
Error: charmStyles.Error,
54-
Field: charmStyles.Code.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FFFFFF"}),
55-
Keyword: charmStyles.Keyword,
56-
Paragraph: charmStyles.Paragraph,
57-
Placeholder: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#585858", Dark: "#4d46b3"}),
58-
Prompt: charmStyles.Prompt.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"}),
59-
FocusedPrompt: charmStyles.FocusedPrompt.Copy().Foreground(lipgloss.Color("#651fff")),
60-
Fuchsia: charmStyles.SelectedMenuItem.Copy(),
61-
Logo: charmStyles.Logo.Copy().SetString("Coder"),
62-
Warn: lipgloss.NewStyle().Foreground(
63-
lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"},
64-
),
65-
Wrap: lipgloss.NewStyle().Width(80),
118+
Code: pretty.Style{
119+
ifTerm(pretty.XPad(1, 1)),
120+
pretty.FgColor(Red),
121+
pretty.BgColor(color.Color("#2c2c2c")),
122+
},
123+
DateTimeStamp: pretty.Style{
124+
pretty.FgColor(color.Color("#7571F9")),
125+
},
126+
Error: pretty.Style{
127+
pretty.FgColor(Red),
128+
},
129+
Field: pretty.Style{
130+
pretty.XPad(1, 1),
131+
pretty.FgColor(color.Color("#FFFFFF")),
132+
pretty.BgColor(color.Color("#2b2a2a")),
133+
},
134+
Keyword: pretty.Style{
135+
pretty.FgColor(Green),
136+
},
137+
Placeholder: pretty.Style{
138+
pretty.FgColor(color.Color("#4d46b3")),
139+
},
140+
Prompt: pretty.Style{
141+
pretty.FgColor(color.Color("#5C5C5C")),
142+
pretty.Wrap("> ", ""),
143+
},
144+
Warn: pretty.Style{
145+
pretty.FgColor(Yellow),
146+
},
147+
Wrap: pretty.Style{
148+
pretty.LineWrap(80),
149+
},
66150
}
151+
152+
DefaultStyles.FocusedPrompt = append(
153+
DefaultStyles.Prompt,
154+
pretty.FgColor(Blue),
155+
)
67156
}
68157

69158
// ValidateNotEmpty is a helper function to disallow empty inputs!

cli/cliui/log.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import (
55
"io"
66
"strings"
77

8-
"github.com/charmbracelet/lipgloss"
8+
"github.com/coder/pretty"
99
)
1010

1111
// cliMessage provides a human-readable message for CLI errors and messages.
1212
type cliMessage struct {
13-
Style lipgloss.Style
13+
Style pretty.Style
1414
Header string
1515
Prefix string
1616
Lines []string
@@ -21,21 +21,21 @@ func (m cliMessage) String() string {
2121
var str strings.Builder
2222

2323
if m.Prefix != "" {
24-
_, _ = str.WriteString(m.Style.Bold(true).Render(m.Prefix))
24+
_, _ = str.WriteString(Bold(m.Prefix))
2525
}
2626

27-
_, _ = str.WriteString(m.Style.Bold(false).Render(m.Header))
27+
pretty.Fprint(&str, m.Style, m.Header)
2828
_, _ = str.WriteString("\r\n")
2929
for _, line := range m.Lines {
30-
_, _ = fmt.Fprintf(&str, " %s %s\r\n", m.Style.Render("|"), line)
30+
_, _ = fmt.Fprintf(&str, " %s %s\r\n", pretty.Sprint(m.Style, "|"), line)
3131
}
3232
return str.String()
3333
}
3434

3535
// Warn writes a log to the writer provided.
3636
func Warn(wtr io.Writer, header string, lines ...string) {
3737
_, _ = fmt.Fprint(wtr, cliMessage{
38-
Style: DefaultStyles.Warn.Copy(),
38+
Style: DefaultStyles.Warn,
3939
Prefix: "WARN: ",
4040
Header: header,
4141
Lines: lines,
@@ -63,7 +63,7 @@ func Infof(wtr io.Writer, fmtStr string, args ...interface{}) {
6363
// Error writes a log to the writer provided.
6464
func Error(wtr io.Writer, header string, lines ...string) {
6565
_, _ = fmt.Fprint(wtr, cliMessage{
66-
Style: DefaultStyles.Error.Copy(),
66+
Style: DefaultStyles.Error,
6767
Prefix: "ERROR: ",
6868
Header: header,
6969
Lines: lines,

cli/cliui/parameter.go

+9-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/coder/coder/v2/cli/clibase"
99
"github.com/coder/coder/v2/codersdk"
10+
"github.com/coder/pretty"
1011
)
1112

1213
func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.TemplateVersionParameter) (string, error) {
@@ -16,10 +17,10 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
1617
}
1718

1819
if templateVersionParameter.Ephemeral {
19-
label += DefaultStyles.Warn.Render(" (build option)")
20+
label += pretty.Sprint(DefaultStyles.Warn, " (build option)")
2021
}
2122

22-
_, _ = fmt.Fprintln(inv.Stdout, DefaultStyles.Bold.Render(label))
23+
_, _ = fmt.Fprintln(inv.Stdout, Bold(label))
2324

2425
if templateVersionParameter.DescriptionPlaintext != "" {
2526
_, _ = fmt.Fprintln(inv.Stdout, " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.DescriptionPlaintext, "\n"), "\n "))+"\n")
@@ -45,7 +46,10 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
4546
}
4647

4748
_, _ = fmt.Fprintln(inv.Stdout)
48-
_, _ = fmt.Fprintln(inv.Stdout, " "+DefaultStyles.Prompt.String()+DefaultStyles.Field.Render(strings.Join(values, ", ")))
49+
pretty.Fprintf(
50+
inv.Stdout,
51+
DefaultStyles.Prompt, "%s\n", strings.Join(values, ", "),
52+
)
4953
value = string(v)
5054
}
5155
} else if len(templateVersionParameter.Options) > 0 {
@@ -59,7 +63,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
5963
})
6064
if err == nil {
6165
_, _ = fmt.Fprintln(inv.Stdout)
62-
_, _ = fmt.Fprintln(inv.Stdout, " "+DefaultStyles.Prompt.String()+DefaultStyles.Field.Render(richParameterOption.Name))
66+
pretty.Fprintf(inv.Stdout, DefaultStyles.Prompt, "%s\n", richParameterOption.Name)
6367
value = richParameterOption.Value
6468
}
6569
} else {
@@ -70,7 +74,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
7074
text += ":"
7175

7276
value, err = Prompt(inv, PromptOptions{
73-
Text: DefaultStyles.Bold.Render(text),
77+
Text: Bold(text),
7478
Validate: func(value string) error {
7579
return validateRichPrompt(value, templateVersionParameter)
7680
},

cli/cliui/prompt.go

+12-8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"golang.org/x/xerrors"
1515

1616
"github.com/coder/coder/v2/cli/clibase"
17+
"github.com/coder/pretty"
1718
)
1819

1920
// PromptOptions supply a set of options to the prompt.
@@ -55,21 +56,24 @@ func Prompt(inv *clibase.Invocation, opts PromptOptions) (string, error) {
5556
}
5657
}
5758

58-
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.FocusedPrompt.String()+opts.Text+" ")
59+
pretty.Fprintf(inv.Stdout, DefaultStyles.FocusedPrompt, "")
60+
pretty.Fprintf(inv.Stdout, pretty.Nop, "%s ", opts.Text)
5961
if opts.IsConfirm {
6062
if len(opts.Default) == 0 {
6163
opts.Default = ConfirmYes
6264
}
63-
renderedYes := DefaultStyles.Placeholder.Render(ConfirmYes)
64-
renderedNo := DefaultStyles.Placeholder.Render(ConfirmNo)
65+
var (
66+
renderedYes = pretty.Sprint(DefaultStyles.Placeholder, ConfirmYes)
67+
renderedNo = pretty.Sprint(DefaultStyles.Placeholder, ConfirmNo)
68+
)
6569
if opts.Default == ConfirmYes {
66-
renderedYes = DefaultStyles.Bold.Render(ConfirmYes)
70+
renderedYes = Bold(ConfirmYes)
6771
} else {
68-
renderedNo = DefaultStyles.Bold.Render(ConfirmNo)
72+
renderedNo = Bold(ConfirmNo)
6973
}
70-
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.Placeholder.Render("("+renderedYes+DefaultStyles.Placeholder.Render("/"+renderedNo+DefaultStyles.Placeholder.Render(") "))))
74+
pretty.Fprintf(inv.Stdout, DefaultStyles.Placeholder, "(%s/%s)", renderedYes, renderedNo)
7175
} else if opts.Default != "" {
72-
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.Placeholder.Render("("+opts.Default+") "))
76+
_, _ = fmt.Fprint(inv.Stdout, pretty.Sprint(DefaultStyles.Placeholder, "("+opts.Default+") "))
7377
}
7478
interrupt := make(chan os.Signal, 1)
7579

@@ -126,7 +130,7 @@ func Prompt(inv *clibase.Invocation, opts PromptOptions) (string, error) {
126130
if opts.Validate != nil {
127131
err := opts.Validate(line)
128132
if err != nil {
129-
_, _ = fmt.Fprintln(inv.Stdout, DefaultStyles.Error.Render(err.Error()))
133+
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(DefaultStyles.Error, err.Error()))
130134
return Prompt(inv, opts)
131135
}
132136
}

0 commit comments

Comments
 (0)