Skip to content

Commit 10dccc1

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 2dae600 commit 10dccc1

Some content is hidden

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

45 files changed

+238
-153
lines changed

cli/cliui/cliui.go

+49-35
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ package cliui
33
import (
44
"os"
55

6-
"github.com/charmbracelet/charm/ui/common"
7-
"github.com/charmbracelet/lipgloss"
6+
"github.com/coder/pretty"
87
"github.com/muesli/termenv"
98
"golang.org/x/xerrors"
109
)
@@ -15,55 +14,70 @@ var Canceled = xerrors.New("canceled")
1514
var DefaultStyles Styles
1615

1716
type Styles struct {
18-
Bold,
19-
Checkmark,
2017
Code,
21-
Crossmark,
2218
DateTimeStamp,
2319
Error,
2420
Field,
2521
Keyword,
26-
Paragraph,
2722
Placeholder,
2823
Prompt,
2924
FocusedPrompt,
3025
Fuchsia,
31-
Logo,
3226
Warn,
33-
Wrap lipgloss.Style
27+
Wrap pretty.Style
3428
}
3529

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

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.
44-
45-
charmStyles := common.DefaultStyles()
32+
var (
33+
Green = color.Color("#04B575")
34+
Red = color.Color("#ED567A")
35+
Fuschia = color.Color("#EE6FF8")
36+
Yellow = color.Color("#ECFD65")
37+
)
4638

39+
func init() {
40+
// We do not adapt the color based on whether the terminal is light or dark.
41+
// Doing so would require a round-trip between the program and the terminal
42+
// due to the OSC query and response.
4743
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),
44+
Code: pretty.Style{
45+
pretty.XPad(1, 1),
46+
pretty.FgColor(Red),
47+
},
48+
DateTimeStamp: pretty.Style{
49+
pretty.FgColor(color.Color("#7571F9")),
50+
},
51+
Error: pretty.Style{
52+
pretty.FgColor(Red),
53+
},
54+
Field: pretty.Style{
55+
pretty.XPad(1, 1),
56+
pretty.FgColor(color.Color("#FFFFFF")),
57+
pretty.BgColor(color.Color("#2b2a2a")),
58+
},
59+
Keyword: pretty.Style{
60+
pretty.FgColor(Green),
61+
},
62+
Placeholder: pretty.Style{
63+
pretty.FgColor(color.Color("#4d46b3")),
64+
},
65+
Prompt: pretty.Style{
66+
pretty.FgColor(color.Color("#5C5C5C")),
67+
pretty.Wrap(">", ""),
68+
},
69+
Warn: pretty.Style{
70+
pretty.FgColor(Yellow),
71+
},
72+
Wrap: pretty.Style{
73+
pretty.LineWrap(80),
74+
},
6675
}
76+
77+
DefaultStyles.FocusedPrompt = append(
78+
DefaultStyles.Prompt,
79+
pretty.FgColor(Fuschia),
80+
)
6781
}
6882

6983
// 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(pretty.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

+6-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(pretty.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,7 @@ 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+
_, _ = fmt.Fprintln(inv.Stdout, " "+pretty.Sprint(DefaultStyles.Prompt.String()+DefaultStyles.Field, strings.Join(values, ", ")))
4950
value = string(v)
5051
}
5152
} else if len(templateVersionParameter.Options) > 0 {
@@ -59,7 +60,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
5960
})
6061
if err == nil {
6162
_, _ = fmt.Fprintln(inv.Stdout)
62-
_, _ = fmt.Fprintln(inv.Stdout, " "+DefaultStyles.Prompt.String()+DefaultStyles.Field.Render(richParameterOption.Name))
63+
_, _ = fmt.Fprintln(inv.Stdout, " "+pretty.Sprint(DefaultStyles.Prompt.String()+DefaultStyles.Field, richParameterOption.Name))
6364
value = richParameterOption.Value
6465
}
6566
} else {
@@ -70,7 +71,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
7071
text += ":"
7172

7273
value, err = Prompt(inv, PromptOptions{
73-
Text: DefaultStyles.Bold.Render(text),
74+
Text: pretty.Sprint(DefaultStyles.Bold, text),
7475
Validate: func(value string) error {
7576
return validateRichPrompt(value, templateVersionParameter)
7677
},

cli/cliui/prompt.go

+8-7
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.
@@ -60,16 +61,16 @@ func Prompt(inv *clibase.Invocation, opts PromptOptions) (string, error) {
6061
if len(opts.Default) == 0 {
6162
opts.Default = ConfirmYes
6263
}
63-
renderedYes := DefaultStyles.Placeholder.Render(ConfirmYes)
64-
renderedNo := DefaultStyles.Placeholder.Render(ConfirmNo)
64+
renderedYes := pretty.Sprint(DefaultStyles.Placeholder, ConfirmYes)
65+
renderedNo := pretty.Sprint(DefaultStyles.Placeholder, ConfirmNo)
6566
if opts.Default == ConfirmYes {
66-
renderedYes = DefaultStyles.Bold.Render(ConfirmYes)
67+
renderedYes = pretty.Sprint(DefaultStyles.Bold, ConfirmYes)
6768
} else {
68-
renderedNo = DefaultStyles.Bold.Render(ConfirmNo)
69+
renderedNo = pretty.Sprint(DefaultStyles.Bold, ConfirmNo)
6970
}
70-
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.Placeholder.Render("("+renderedYes+DefaultStyles.Placeholder.Render("/"+renderedNo+DefaultStyles.Placeholder.Render(") "))))
71+
_, _ = fmt.Fprint(inv.Stdout, pretty.Sprint(DefaultStyles.Placeholder.Render("("+renderedYes+DefaultStyles.Placeholder.Render("/"+renderedNo+DefaultStyles.Placeholder, ") "))))
7172
} else if opts.Default != "" {
72-
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.Placeholder.Render("("+opts.Default+") "))
73+
_, _ = fmt.Fprint(inv.Stdout, pretty.Sprint(DefaultStyles.Placeholder, "("+opts.Default+") "))
7374
}
7475
interrupt := make(chan os.Signal, 1)
7576

@@ -126,7 +127,7 @@ func Prompt(inv *clibase.Invocation, opts PromptOptions) (string, error) {
126127
if opts.Validate != nil {
127128
err := opts.Validate(line)
128129
if err != nil {
129-
_, _ = fmt.Fprintln(inv.Stdout, DefaultStyles.Error.Render(err.Error()))
130+
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(DefaultStyles.Error, err.Error()))
130131
return Prompt(inv, opts)
131132
}
132133
}

cli/cliui/provisionerjob.go

+2-1
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 {
@@ -127,7 +128,7 @@ 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+
_, _ = fmt.Fprintf(writer, pretty.Sprint(DefaultStyles.FocusedPrompt.String()+DefaultStyles.Bold, "Gracefully canceling...")+"\n\n")
131132
err := opts.Cancel()
132133
if err != nil {
133134
errChan <- xerrors.Errorf("cancel: %w", err)

cli/cliui/resources.go

+16-15
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/coder/coder/v2/coderd/database/dbtime"
1313
"github.com/coder/coder/v2/codersdk"
14+
"github.com/coder/pretty"
1415
)
1516

1617
type WorkspaceResourcesOptions struct {
@@ -78,7 +79,7 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
7879

7980
// Display a line for the resource.
8081
tableWriter.AppendRow(table.Row{
81-
DefaultStyles.Bold.Render(resourceAddress),
82+
pretty.Sprint(DefaultStyles.Bold, resourceAddress),
8283
"",
8384
"",
8485
"",
@@ -107,7 +108,7 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
107108
if totalAgents > 1 {
108109
sshCommand += "." + agent.Name
109110
}
110-
sshCommand = DefaultStyles.Code.Render(sshCommand)
111+
sshCommand = pretty.Sprint(DefaultStyles.Code, sshCommand)
111112
row = append(row, sshCommand)
112113
}
113114
tableWriter.AppendRow(row)
@@ -122,43 +123,43 @@ func renderAgentStatus(agent codersdk.WorkspaceAgent) string {
122123
switch agent.Status {
123124
case codersdk.WorkspaceAgentConnecting:
124125
since := dbtime.Now().Sub(agent.CreatedAt)
125-
return DefaultStyles.Warn.Render("⦾ connecting") + " " +
126-
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
126+
return pretty.Sprint(DefaultStyles.Warn, "⦾ connecting") + " " +
127+
pretty.Sprint(DefaultStyles.Placeholder, "["+strconv.Itoa(int(since.Seconds()))+"s]")
127128
case codersdk.WorkspaceAgentDisconnected:
128129
since := dbtime.Now().Sub(*agent.DisconnectedAt)
129-
return DefaultStyles.Error.Render("⦾ disconnected") + " " +
130-
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
130+
return pretty.Sprint(DefaultStyles.Error, "⦾ disconnected") + " " +
131+
pretty.Sprint(DefaultStyles.Placeholder, "["+strconv.Itoa(int(since.Seconds()))+"s]")
131132
case codersdk.WorkspaceAgentTimeout:
132133
since := dbtime.Now().Sub(agent.CreatedAt)
133134
return fmt.Sprintf(
134135
"%s %s",
135-
DefaultStyles.Warn.Render("⦾ timeout"),
136-
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]"),
136+
pretty.Sprint(DefaultStyles.Warn, "⦾ timeout"),
137+
pretty.Sprint(DefaultStyles.Placeholder, "["+strconv.Itoa(int(since.Seconds()))+"s]"),
137138
)
138139
case codersdk.WorkspaceAgentConnected:
139-
return DefaultStyles.Keyword.Render("⦿ connected")
140+
return pretty.Sprint(DefaultStyles.Keyword, "⦿ connected")
140141
default:
141-
return DefaultStyles.Warn.Render("○ unknown")
142+
return pretty.Sprint(DefaultStyles.Warn, "○ unknown")
142143
}
143144
}
144145

145146
func renderAgentHealth(agent codersdk.WorkspaceAgent) string {
146147
if agent.Health.Healthy {
147-
return DefaultStyles.Keyword.Render("✔ healthy")
148+
return pretty.Sprint(DefaultStyles.Keyword, "✔ healthy")
148149
}
149-
return DefaultStyles.Error.Render("✘ " + agent.Health.Reason)
150+
return pretty.Sprint(DefaultStyles.Error, "✘ "+agent.Health.Reason)
150151
}
151152

152153
func renderAgentVersion(agentVersion, serverVersion string) string {
153154
if agentVersion == "" {
154155
agentVersion = "(unknown)"
155156
}
156157
if !semver.IsValid(serverVersion) || !semver.IsValid(agentVersion) {
157-
return DefaultStyles.Placeholder.Render(agentVersion)
158+
return pretty.Sprint(DefaultStyles.Placeholder, agentVersion)
158159
}
159160
outdated := semver.Compare(agentVersion, serverVersion) < 0
160161
if outdated {
161-
return DefaultStyles.Warn.Render(agentVersion + " (outdated)")
162+
return pretty.Sprint(DefaultStyles.Warn, agentVersion+" (outdated)")
162163
}
163-
return DefaultStyles.Keyword.Render(agentVersion)
164+
return pretty.Sprint(DefaultStyles.Keyword, agentVersion)
164165
}

cli/create.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"time"
88

99
"github.com/google/uuid"
10+
"github.com/kr/pretty"
1011
"golang.org/x/exp/slices"
1112
"golang.org/x/xerrors"
1213

@@ -75,7 +76,7 @@ func (r *RootCmd) create() *clibase.Cmd {
7576

7677
var template codersdk.Template
7778
if templateName == "" {
78-
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Wrap.Render("Select a template below to preview the provisioned infrastructure:"))
79+
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Wrap, "Select a template below to preview the provisioned infrastructure:"))
7980

8081
templates, err := client.TemplatesByOrganization(inv.Context(), organization.ID)
8182
if err != nil {
@@ -177,7 +178,7 @@ func (r *RootCmd) create() *clibase.Cmd {
177178
return xerrors.Errorf("watch build: %w", err)
178179
}
179180

180-
_, _ = fmt.Fprintf(inv.Stdout, "\nThe %s workspace has been created at %s!\n", cliui.DefaultStyles.Keyword.Render(workspace.Name), cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp)))
181+
_, _ = fmt.Fprintf(inv.Stdout, "\nThe %s workspace has been created at %s!\n", pretty.Sprint(cliui.DefaultStyles.Keyword.Render(workspace.Name), cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp)))
181182
return nil
182183
},
183184
}

cli/delete.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/coder/coder/v2/cli/clibase"
88
"github.com/coder/coder/v2/cli/cliui"
99
"github.com/coder/coder/v2/codersdk"
10+
"github.com/kr/pretty"
1011
)
1112

1213
// nolint
@@ -54,7 +55,7 @@ func (r *RootCmd) deleteWorkspace() *clibase.Cmd {
5455
return err
5556
}
5657

57-
_, _ = fmt.Fprintf(inv.Stdout, "\n%s has been deleted at %s!\n", cliui.DefaultStyles.Keyword.Render(workspace.FullName()), cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp)))
58+
_, _ = fmt.Fprintf(inv.Stdout, "\n%s has been deleted at %s!\n", pretty.Sprint(cliui.DefaultStyles.Keyword.Render(workspace.FullName()), cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp)))
5859
return nil
5960
},
6061
}

0 commit comments

Comments
 (0)