Skip to content

Commit 3231854

Browse files
committed
chore(cli): replace lipgloss with coder/pretty
This change will improve over CLI performance and "snapiness" as well as substantially reduce our test times (benchmark TODO). 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 869d040 commit 3231854

Some content is hidden

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

47 files changed

+250
-196
lines changed

cli/cliui/cliui.go

+62-34
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ package cliui
33
import (
44
"os"
55

6-
"github.com/charmbracelet/charm/ui/common"
7-
"github.com/charmbracelet/lipgloss"
86
"github.com/muesli/termenv"
97
"golang.org/x/xerrors"
8+
9+
"github.com/coder/pretty"
1010
)
1111

1212
var Canceled = xerrors.New("canceled")
@@ -15,55 +15,83 @@ var Canceled = xerrors.New("canceled")
1515
var DefaultStyles Styles
1616

1717
type Styles struct {
18-
Bold,
19-
Checkmark,
2018
Code,
21-
Crossmark,
2219
DateTimeStamp,
2320
Error,
2421
Field,
2522
Keyword,
26-
Paragraph,
2723
Placeholder,
2824
Prompt,
2925
FocusedPrompt,
3026
Fuchsia,
31-
Logo,
3227
Warn,
33-
Wrap lipgloss.Style
28+
Wrap pretty.Style
3429
}
3530

36-
func init() {
37-
lipgloss.SetDefaultRenderer(
38-
lipgloss.NewRenderer(os.Stdout, termenv.WithColorCache(true)),
39-
)
31+
var color = termenv.NewOutput(os.Stdout).ColorProfile()
4032

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.
33+
var (
34+
Green = color.Color("#04B575")
35+
Red = color.Color("#ED567A")
36+
Fuschia = color.Color("#EE6FF8")
37+
Yellow = color.Color("#ECFD65")
38+
)
4439

45-
charmStyles := common.DefaultStyles()
40+
func ifTerm(f pretty.Formatter) pretty.Formatter {
41+
if color != termenv.Ascii {
42+
return f
43+
}
44+
return pretty.Nop
45+
}
4646

47+
// Bold returns a formatter that renders text in bold
48+
// if the terminal supports it.
49+
func Bold() pretty.Formatter {
50+
return ifTerm(Bold())
51+
}
52+
53+
func init() {
54+
// We do not adapt the color based on whether the terminal is light or dark.
55+
// Doing so would require a round-trip between the program and the terminal
56+
// due to the OSC query and response.
4757
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),
58+
Code: pretty.Style{
59+
pretty.XPad(1, 1),
60+
pretty.FgColor(Red),
61+
},
62+
DateTimeStamp: pretty.Style{
63+
pretty.FgColor(color.Color("#7571F9")),
64+
},
65+
Error: pretty.Style{
66+
pretty.FgColor(Red),
67+
},
68+
Field: pretty.Style{
69+
pretty.XPad(1, 1),
70+
pretty.FgColor(color.Color("#FFFFFF")),
71+
pretty.BgColor(color.Color("#2b2a2a")),
72+
},
73+
Keyword: pretty.Style{
74+
pretty.FgColor(Green),
75+
},
76+
Placeholder: pretty.Style{
77+
pretty.FgColor(color.Color("#4d46b3")),
78+
},
79+
Prompt: pretty.Style{
80+
pretty.FgColor(color.Color("#5C5C5C")),
81+
pretty.Wrap(">", ""),
82+
},
83+
Warn: pretty.Style{
84+
pretty.FgColor(Yellow),
85+
},
86+
Wrap: pretty.Style{
87+
pretty.LineWrap(80),
88+
},
6689
}
90+
91+
DefaultStyles.FocusedPrompt = append(
92+
DefaultStyles.Prompt,
93+
pretty.FgColor(Fuschia),
94+
)
6795
}
6896

6997
// 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+
pretty.Fprint(&str, m.Style.With(Bold()), "%s", 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, pretty.Sprint(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: pretty.Sprint(Bold(), text),
7478
Validate: func(value string) error {
7579
return validateRichPrompt(value, templateVersionParameter)
7680
},

cli/cliui/prompt.go

+11-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,23 @@ 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, "%s ", opts.Text)
5960
if opts.IsConfirm {
6061
if len(opts.Default) == 0 {
6162
opts.Default = ConfirmYes
6263
}
63-
renderedYes := DefaultStyles.Placeholder.Render(ConfirmYes)
64-
renderedNo := DefaultStyles.Placeholder.Render(ConfirmNo)
64+
var (
65+
renderedYes = pretty.Sprint(DefaultStyles.Placeholder, ConfirmYes)
66+
renderedNo = pretty.Sprint(DefaultStyles.Placeholder, ConfirmNo)
67+
)
6568
if opts.Default == ConfirmYes {
66-
renderedYes = DefaultStyles.Bold.Render(ConfirmYes)
69+
renderedYes = pretty.Sprint(Bold(), ConfirmYes)
6770
} else {
68-
renderedNo = DefaultStyles.Bold.Render(ConfirmNo)
71+
renderedNo = pretty.Sprint(Bold(), ConfirmNo)
6972
}
70-
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.Placeholder.Render("("+renderedYes+DefaultStyles.Placeholder.Render("/"+renderedNo+DefaultStyles.Placeholder.Render(") "))))
73+
pretty.Fprintf(inv.Stdout, DefaultStyles.Placeholder, "(%s/%s)", renderedYes, renderedNo)
7174
} else if opts.Default != "" {
72-
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.Placeholder.Render("("+opts.Default+") "))
75+
_, _ = fmt.Fprint(inv.Stdout, pretty.Sprint(DefaultStyles.Placeholder, "("+opts.Default+") "))
7376
}
7477
interrupt := make(chan os.Signal, 1)
7578

@@ -126,7 +129,7 @@ func Prompt(inv *clibase.Invocation, opts PromptOptions) (string, error) {
126129
if opts.Validate != nil {
127130
err := opts.Validate(line)
128131
if err != nil {
129-
_, _ = fmt.Fprintln(inv.Stdout, DefaultStyles.Error.Render(err.Error()))
132+
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(DefaultStyles.Error, err.Error()))
130133
return Prompt(inv, opts)
131134
}
132135
}

cli/cliui/provisionerjob.go

+13-8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"golang.org/x/xerrors"
1616

1717
"github.com/coder/coder/v2/codersdk"
18+
"github.com/coder/pretty"
1819
)
1920

2021
func WorkspaceBuild(ctx context.Context, writer io.Writer, client *codersdk.Client, build uuid.UUID) error {
@@ -54,7 +55,7 @@ func (err *ProvisionerJobError) Error() string {
5455
}
5556

5657
// ProvisionerJob renders a provisioner job with interactive cancellation.
57-
func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOptions) error {
58+
func ProvisionerJob(ctx context.Context, wr io.Writer, opts ProvisionerJobOptions) error {
5859
if opts.FetchInterval == 0 {
5960
opts.FetchInterval = time.Second
6061
}
@@ -70,7 +71,7 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
7071
jobMutex sync.Mutex
7172
)
7273

73-
sw := &stageWriter{w: writer, verbose: opts.Verbose, silentLogs: opts.Silent}
74+
sw := &stageWriter{w: wr, verbose: opts.Verbose, silentLogs: opts.Silent}
7475

7576
printStage := func() {
7677
sw.Start(currentStage)
@@ -127,7 +128,11 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
127128
return
128129
}
129130
}
130-
_, _ = fmt.Fprintf(writer, DefaultStyles.FocusedPrompt.String()+DefaultStyles.Bold.Render("Gracefully canceling...")+"\n\n")
131+
pretty.Fprintf(
132+
wr,
133+
DefaultStyles.FocusedPrompt.With(Bold()),
134+
"Gracefully canceling...\n\n",
135+
)
131136
err := opts.Cancel()
132137
if err != nil {
133138
errChan <- xerrors.Errorf("cancel: %w", err)
@@ -236,7 +241,7 @@ func (s *stageWriter) Log(createdAt time.Time, level codersdk.LogLevel, line str
236241
w = &s.logBuf
237242
}
238243

239-
render := func(s ...string) string { return strings.Join(s, " ") }
244+
var style pretty.Style
240245

241246
var lines []string
242247
if !createdAt.IsZero() {
@@ -249,14 +254,14 @@ func (s *stageWriter) Log(createdAt time.Time, level codersdk.LogLevel, line str
249254
if !s.verbose {
250255
return
251256
}
252-
render = DefaultStyles.Placeholder.Render
257+
style = DefaultStyles.Placeholder
253258
case codersdk.LogLevelError:
254-
render = DefaultStyles.Error.Render
259+
style = DefaultStyles.Error
255260
case codersdk.LogLevelWarn:
256-
render = DefaultStyles.Warn.Render
261+
style = DefaultStyles.Warn
257262
case codersdk.LogLevelInfo:
258263
}
259-
_, _ = fmt.Fprintf(w, "%s\n", render(lines...))
264+
pretty.Fprintf(w, style, "%s\n", strings.Join(lines, " "))
260265
}
261266

262267
func (s *stageWriter) flushLogs() {

0 commit comments

Comments
 (0)