Skip to content

Commit 737c80f

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 737c80f

Some content is hidden

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

48 files changed

+386
-227
lines changed

cli/cliui/cliui.go

+102-34
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ package cliui
22

33
import (
44
"os"
5+
"time"
56

6-
"github.com/charmbracelet/charm/ui/common"
7-
"github.com/charmbracelet/lipgloss"
87
"github.com/muesli/termenv"
98
"golang.org/x/xerrors"
9+
10+
"github.com/coder/pretty"
1011
)
1112

1213
var Canceled = xerrors.New("canceled")
@@ -15,55 +16,122 @@ var Canceled = xerrors.New("canceled")
1516
var DefaultStyles Styles
1617

1718
type Styles struct {
18-
Bold,
19-
Checkmark,
2019
Code,
21-
Crossmark,
2220
DateTimeStamp,
2321
Error,
2422
Field,
2523
Keyword,
26-
Paragraph,
2724
Placeholder,
2825
Prompt,
2926
FocusedPrompt,
3027
Fuchsia,
31-
Logo,
3228
Warn,
33-
Wrap lipgloss.Style
29+
Wrap pretty.Style
3430
}
3531

36-
func init() {
37-
lipgloss.SetDefaultRenderer(
38-
lipgloss.NewRenderer(os.Stdout, termenv.WithColorCache(true)),
39-
)
32+
var color = termenv.NewOutput(os.Stdout).ColorProfile()
33+
34+
var (
35+
Green = color.Color("#04B575")
36+
Red = color.Color("#ED567A")
37+
Fuschia = color.Color("#EE6FF8")
38+
Yellow = color.Color("#ECFD65")
39+
)
40+
41+
func isTerm() bool {
42+
return color != termenv.Ascii
43+
}
44+
45+
// Bold returns a formatter that renders text in bold
46+
// if the terminal supports it.
47+
func Bold(s string) string {
48+
if !isTerm() {
49+
return s
50+
}
51+
return pretty.Sprint(pretty.Bold(), s)
52+
}
53+
54+
// BoldFmt returns a formatter that renders text in bold
55+
// if the terminal supports it.
56+
func BoldFmt() pretty.Formatter {
57+
if !isTerm() {
58+
return pretty.Style{}
59+
}
60+
return pretty.Bold()
61+
}
4062

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.
63+
// Timestamp formats a timestamp for display.
64+
func Timestamp(t time.Time) string {
65+
return pretty.Sprint(DefaultStyles.DateTimeStamp, t.Format(time.Stamp))
66+
}
67+
68+
// Keyword formats a keyword for display.
69+
func Keyword(s string) string {
70+
return pretty.Sprint(DefaultStyles.Keyword, s)
71+
}
4472

45-
charmStyles := common.DefaultStyles()
73+
// Placeholder formats a placeholder for display.
74+
func Placeholder(s string) string {
75+
return pretty.Sprint(DefaultStyles.Placeholder, s)
76+
}
4677

78+
// Wrap prevents the text from overflowing the terminal.
79+
func Wrap(s string) string {
80+
return pretty.Sprint(DefaultStyles.Wrap, s)
81+
}
82+
83+
// Code formats code for display.
84+
func Code(s string) string {
85+
return pretty.Sprint(DefaultStyles.Code, s)
86+
}
87+
88+
// Field formats a field for display.
89+
func Field(s string) string {
90+
return pretty.Sprint(DefaultStyles.Field, s)
91+
}
92+
93+
func init() {
94+
// We do not adapt the color based on whether the terminal is light or dark.
95+
// Doing so would require a round-trip between the program and the terminal
96+
// due to the OSC query and response.
4797
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),
98+
Code: pretty.Style{
99+
pretty.XPad(1, 1),
100+
pretty.FgColor(Red),
101+
},
102+
DateTimeStamp: pretty.Style{
103+
pretty.FgColor(color.Color("#7571F9")),
104+
},
105+
Error: pretty.Style{
106+
pretty.FgColor(Red),
107+
},
108+
Field: pretty.Style{
109+
pretty.XPad(1, 1),
110+
pretty.FgColor(color.Color("#FFFFFF")),
111+
pretty.BgColor(color.Color("#2b2a2a")),
112+
},
113+
Keyword: pretty.Style{
114+
pretty.FgColor(Green),
115+
},
116+
Placeholder: pretty.Style{
117+
pretty.FgColor(color.Color("#4d46b3")),
118+
},
119+
Prompt: pretty.Style{
120+
pretty.FgColor(color.Color("#5C5C5C")),
121+
pretty.Wrap(">", ""),
122+
},
123+
Warn: pretty.Style{
124+
pretty.FgColor(Yellow),
125+
},
126+
Wrap: pretty.Style{
127+
pretty.LineWrap(80),
128+
},
66129
}
130+
131+
DefaultStyles.FocusedPrompt = append(
132+
DefaultStyles.Prompt,
133+
pretty.FgColor(Fuschia),
134+
)
67135
}
68136

69137
// 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

+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 = Bold(ConfirmYes)
6770
} else {
68-
renderedNo = DefaultStyles.Bold.Render(ConfirmNo)
71+
renderedNo = 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(BoldFmt()),
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)