Skip to content

Commit 5eaf809

Browse files
authored
fix(cli): speed up CLI over SSH (#7885)
By caching the terminal's color profile, we avoid myriad round trips during command execution.
1 parent 1288a83 commit 5eaf809

Some content is hidden

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

46 files changed

+190
-172
lines changed

cli/cliui/agent.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ type message struct {
179179

180180
func waitingMessage(agent codersdk.WorkspaceAgent, opts AgentOptions) (m *message) {
181181
m = &message{
182-
Spin: fmt.Sprintf("Waiting for connection from %s...", Styles.Field.Render(agent.Name)),
182+
Spin: fmt.Sprintf("Waiting for connection from %s...", DefaultStyles.Field.Render(agent.Name)),
183183
Prompt: "Don't panic, your workspace is booting up!",
184184
}
185185
defer func() {
@@ -192,7 +192,7 @@ func waitingMessage(agent codersdk.WorkspaceAgent, opts AgentOptions) (m *messag
192192

193193
// We don't want to wrap the troubleshooting URL, so we'll handle word
194194
// wrapping ourselves (vs using lipgloss).
195-
w := wordwrap.NewWriter(Styles.Paragraph.GetWidth() - Styles.Paragraph.GetMarginLeft()*2)
195+
w := wordwrap.NewWriter(DefaultStyles.Paragraph.GetWidth() - DefaultStyles.Paragraph.GetMarginLeft()*2)
196196
w.Breakpoints = []rune{' ', '\n'}
197197

198198
_, _ = fmt.Fprint(w, m.Prompt)
@@ -208,7 +208,7 @@ func waitingMessage(agent codersdk.WorkspaceAgent, opts AgentOptions) (m *messag
208208
// We want to prefix the prompt with a caret, but we want text on the
209209
// following lines to align with the text on the first line (i.e. added
210210
// spacing).
211-
ind := " " + Styles.Prompt.String()
211+
ind := " " + DefaultStyles.Prompt.String()
212212
iw := indent.NewWriter(1, func(w io.Writer) {
213213
_, _ = w.Write([]byte(ind))
214214
ind = " " // Set indentation to space after initial prompt.
@@ -223,7 +223,7 @@ func waitingMessage(agent codersdk.WorkspaceAgent, opts AgentOptions) (m *messag
223223
case codersdk.WorkspaceAgentDisconnected:
224224
m.Prompt = "The workspace agent lost connection!"
225225
case codersdk.WorkspaceAgentConnected:
226-
m.Spin = fmt.Sprintf("Waiting for %s to become ready...", Styles.Field.Render(agent.Name))
226+
m.Spin = fmt.Sprintf("Waiting for %s to become ready...", DefaultStyles.Field.Render(agent.Name))
227227
m.Prompt = "Don't panic, your workspace agent has connected and the workspace is getting ready!"
228228
if opts.NoWait {
229229
m.Prompt = "Your workspace is still getting ready, it may be in an incomplete state."

cli/cliui/cliui.go

+48-33
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,20 @@
11
package cliui
22

33
import (
4+
"os"
5+
46
"github.com/charmbracelet/charm/ui/common"
57
"github.com/charmbracelet/lipgloss"
8+
"github.com/muesli/termenv"
69
"golang.org/x/xerrors"
710
)
811

9-
var (
10-
Canceled = xerrors.New("canceled")
11-
12-
defaultStyles = common.DefaultStyles()
13-
)
12+
var Canceled = xerrors.New("canceled")
1413

15-
// ValidateNotEmpty is a helper function to disallow empty inputs!
16-
func ValidateNotEmpty(s string) error {
17-
if s == "" {
18-
return xerrors.New("Must be provided!")
19-
}
20-
return nil
21-
}
14+
// DefaultStyles compose visual elements of the UI.
15+
var DefaultStyles Styles
2216

23-
// Styles compose visual elements of the UI!
24-
var Styles = struct {
17+
type Styles struct {
2518
Bold,
2619
Checkmark,
2720
Code,
@@ -38,23 +31,45 @@ var Styles = struct {
3831
Logo,
3932
Warn,
4033
Wrap lipgloss.Style
41-
}{
42-
Bold: lipgloss.NewStyle().Bold(true),
43-
Checkmark: defaultStyles.Checkmark,
44-
Code: defaultStyles.Code,
45-
Crossmark: defaultStyles.Error.Copy().SetString("✘"),
46-
DateTimeStamp: defaultStyles.LabelDim,
47-
Error: defaultStyles.Error,
48-
Field: defaultStyles.Code.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FFFFFF"}),
49-
Keyword: defaultStyles.Keyword,
50-
Paragraph: defaultStyles.Paragraph,
51-
Placeholder: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#585858", Dark: "#4d46b3"}),
52-
Prompt: defaultStyles.Prompt.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"}),
53-
FocusedPrompt: defaultStyles.FocusedPrompt.Copy().Foreground(lipgloss.Color("#651fff")),
54-
Fuchsia: defaultStyles.SelectedMenuItem.Copy(),
55-
Logo: defaultStyles.Logo.Copy().SetString("Coder"),
56-
Warn: lipgloss.NewStyle().Foreground(
57-
lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"},
58-
),
59-
Wrap: lipgloss.NewStyle().Width(80),
34+
}
35+
36+
func init() {
37+
lipgloss.SetDefaultRenderer(
38+
lipgloss.NewRenderer(os.Stdout, termenv.WithColorCache(true)),
39+
)
40+
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()
46+
47+
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),
66+
}
67+
}
68+
69+
// ValidateNotEmpty is a helper function to disallow empty inputs!
70+
func ValidateNotEmpty(s string) error {
71+
if s == "" {
72+
return xerrors.New("Must be provided!")
73+
}
74+
return nil
6075
}

cli/cliui/log.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func (m cliMessage) String() string {
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: Styles.Warn.Copy(),
38+
Style: DefaultStyles.Warn.Copy(),
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: Styles.Error.Copy(),
66+
Style: DefaultStyles.Error.Copy(),
6767
Prefix: "ERROR: ",
6868
Header: header,
6969
Lines: lines,

cli/cliui/parameter.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
1515
label = templateVersionParameter.DisplayName
1616
}
1717

18-
_, _ = fmt.Fprintln(inv.Stdout, Styles.Bold.Render(label))
18+
_, _ = fmt.Fprintln(inv.Stdout, DefaultStyles.Bold.Render(label))
1919
if templateVersionParameter.DescriptionPlaintext != "" {
2020
_, _ = fmt.Fprintln(inv.Stdout, " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.DescriptionPlaintext, "\n"), "\n "))+"\n")
2121
}
@@ -40,7 +40,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
4040
}
4141

4242
_, _ = fmt.Fprintln(inv.Stdout)
43-
_, _ = fmt.Fprintln(inv.Stdout, " "+Styles.Prompt.String()+Styles.Field.Render(strings.Join(values, ", ")))
43+
_, _ = fmt.Fprintln(inv.Stdout, " "+DefaultStyles.Prompt.String()+DefaultStyles.Field.Render(strings.Join(values, ", ")))
4444
value = string(v)
4545
}
4646
} else if len(templateVersionParameter.Options) > 0 {
@@ -54,7 +54,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
5454
})
5555
if err == nil {
5656
_, _ = fmt.Fprintln(inv.Stdout)
57-
_, _ = fmt.Fprintln(inv.Stdout, " "+Styles.Prompt.String()+Styles.Field.Render(richParameterOption.Name))
57+
_, _ = fmt.Fprintln(inv.Stdout, " "+DefaultStyles.Prompt.String()+DefaultStyles.Field.Render(richParameterOption.Name))
5858
value = richParameterOption.Value
5959
}
6060
} else {
@@ -65,7 +65,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
6565
text += ":"
6666

6767
value, err = Prompt(inv, PromptOptions{
68-
Text: Styles.Bold.Render(text),
68+
Text: DefaultStyles.Bold.Render(text),
6969
Validate: func(value string) error {
7070
return validateRichPrompt(value, templateVersionParameter)
7171
},

cli/cliui/prompt.go

+8-8
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,21 @@ func Prompt(inv *clibase.Invocation, opts PromptOptions) (string, error) {
5555
}
5656
}
5757

58-
_, _ = fmt.Fprint(inv.Stdout, Styles.FocusedPrompt.String()+opts.Text+" ")
58+
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.FocusedPrompt.String()+opts.Text+" ")
5959
if opts.IsConfirm {
6060
if len(opts.Default) == 0 {
6161
opts.Default = ConfirmYes
6262
}
63-
renderedYes := Styles.Placeholder.Render(ConfirmYes)
64-
renderedNo := Styles.Placeholder.Render(ConfirmNo)
63+
renderedYes := DefaultStyles.Placeholder.Render(ConfirmYes)
64+
renderedNo := DefaultStyles.Placeholder.Render(ConfirmNo)
6565
if opts.Default == ConfirmYes {
66-
renderedYes = Styles.Bold.Render(ConfirmYes)
66+
renderedYes = DefaultStyles.Bold.Render(ConfirmYes)
6767
} else {
68-
renderedNo = Styles.Bold.Render(ConfirmNo)
68+
renderedNo = DefaultStyles.Bold.Render(ConfirmNo)
6969
}
70-
_, _ = fmt.Fprint(inv.Stdout, Styles.Placeholder.Render("("+renderedYes+Styles.Placeholder.Render("/"+renderedNo+Styles.Placeholder.Render(") "))))
70+
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.Placeholder.Render("("+renderedYes+DefaultStyles.Placeholder.Render("/"+renderedNo+DefaultStyles.Placeholder.Render(") "))))
7171
} else if opts.Default != "" {
72-
_, _ = fmt.Fprint(inv.Stdout, Styles.Placeholder.Render("("+opts.Default+") "))
72+
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.Placeholder.Render("("+opts.Default+") "))
7373
}
7474
interrupt := make(chan os.Signal, 1)
7575

@@ -126,7 +126,7 @@ func Prompt(inv *clibase.Invocation, opts PromptOptions) (string, error) {
126126
if opts.Validate != nil {
127127
err := opts.Validate(line)
128128
if err != nil {
129-
_, _ = fmt.Fprintln(inv.Stdout, defaultStyles.Error.Render(err.Error()))
129+
_, _ = fmt.Fprintln(inv.Stdout, DefaultStyles.Error.Render(err.Error()))
130130
return Prompt(inv, opts)
131131
}
132132
}

cli/cliui/provisionerjob.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
7171
)
7272

7373
printStage := func() {
74-
_, _ = fmt.Fprintf(writer, Styles.Prompt.Render("⧗")+"%s\n", Styles.Field.Render(currentStage))
74+
_, _ = fmt.Fprintf(writer, DefaultStyles.Prompt.Render("⧗")+"%s\n", DefaultStyles.Field.Render(currentStage))
7575
}
7676

7777
updateStage := func(stage string, startedAt time.Time) {
@@ -80,11 +80,11 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
8080
if !didLogBetweenStage {
8181
prefix = "\033[1A\r"
8282
}
83-
mark := Styles.Checkmark
83+
mark := DefaultStyles.Checkmark
8484
if job.CompletedAt != nil && job.Status != codersdk.ProvisionerJobSucceeded {
85-
mark = Styles.Crossmark
85+
mark = DefaultStyles.Crossmark
8686
}
87-
_, _ = fmt.Fprintf(writer, prefix+mark.String()+Styles.Placeholder.Render(" %s [%dms]")+"\n", currentStage, startedAt.Sub(currentStageStartedAt).Milliseconds())
87+
_, _ = fmt.Fprintf(writer, prefix+mark.String()+DefaultStyles.Placeholder.Render(" %s [%dms]")+"\n", currentStage, startedAt.Sub(currentStageStartedAt).Milliseconds())
8888
}
8989
if stage == "" {
9090
return
@@ -129,7 +129,7 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
129129
return
130130
}
131131
}
132-
_, _ = fmt.Fprintf(writer, "\033[2K\r\n"+Styles.FocusedPrompt.String()+Styles.Bold.Render("Gracefully canceling...")+"\n\n")
132+
_, _ = fmt.Fprintf(writer, "\033[2K\r\n"+DefaultStyles.FocusedPrompt.String()+DefaultStyles.Bold.Render("Gracefully canceling...")+"\n\n")
133133
err := opts.Cancel()
134134
if err != nil {
135135
errChan <- xerrors.Errorf("cancel: %w", err)
@@ -207,11 +207,11 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
207207
if !opts.Verbose {
208208
continue
209209
}
210-
output = Styles.Placeholder.Render(log.Output)
210+
output = DefaultStyles.Placeholder.Render(log.Output)
211211
case codersdk.LogLevelError:
212-
output = defaultStyles.Error.Render(log.Output)
212+
output = DefaultStyles.Error.Render(log.Output)
213213
case codersdk.LogLevelWarn:
214-
output = Styles.Warn.Render(log.Output)
214+
output = DefaultStyles.Warn.Render(log.Output)
215215
case codersdk.LogLevelInfo:
216216
output = log.Output
217217
}
@@ -222,7 +222,7 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
222222
jobMutex.Unlock()
223223
continue
224224
}
225-
_, _ = fmt.Fprintf(logOutput, "%s %s\n", Styles.Placeholder.Render(" "), output)
225+
_, _ = fmt.Fprintf(logOutput, "%s %s\n", DefaultStyles.Placeholder.Render(" "), output)
226226
if !opts.Silent {
227227
didLogBetweenStage = true
228228
}

cli/cliui/resources.go

+13-13
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
7878

7979
// Display a line for the resource.
8080
tableWriter.AppendRow(table.Row{
81-
Styles.Bold.Render(resourceAddress),
81+
DefaultStyles.Bold.Render(resourceAddress),
8282
"",
8383
"",
8484
})
@@ -106,7 +106,7 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
106106
if totalAgents > 1 {
107107
sshCommand += "." + agent.Name
108108
}
109-
sshCommand = Styles.Code.Render(sshCommand)
109+
sshCommand = DefaultStyles.Code.Render(sshCommand)
110110
row = append(row, sshCommand)
111111
}
112112
tableWriter.AppendRow(row)
@@ -121,23 +121,23 @@ func renderAgentStatus(agent codersdk.WorkspaceAgent) string {
121121
switch agent.Status {
122122
case codersdk.WorkspaceAgentConnecting:
123123
since := database.Now().Sub(agent.CreatedAt)
124-
return Styles.Warn.Render("⦾ connecting") + " " +
125-
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
124+
return DefaultStyles.Warn.Render("⦾ connecting") + " " +
125+
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
126126
case codersdk.WorkspaceAgentDisconnected:
127127
since := database.Now().Sub(*agent.DisconnectedAt)
128-
return Styles.Error.Render("⦾ disconnected") + " " +
129-
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
128+
return DefaultStyles.Error.Render("⦾ disconnected") + " " +
129+
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
130130
case codersdk.WorkspaceAgentTimeout:
131131
since := database.Now().Sub(agent.CreatedAt)
132132
return fmt.Sprintf(
133133
"%s %s",
134-
Styles.Warn.Render("⦾ timeout"),
135-
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]"),
134+
DefaultStyles.Warn.Render("⦾ timeout"),
135+
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]"),
136136
)
137137
case codersdk.WorkspaceAgentConnected:
138-
return Styles.Keyword.Render("⦿ connected")
138+
return DefaultStyles.Keyword.Render("⦿ connected")
139139
default:
140-
return Styles.Warn.Render("○ unknown")
140+
return DefaultStyles.Warn.Render("○ unknown")
141141
}
142142
}
143143

@@ -146,11 +146,11 @@ func renderAgentVersion(agentVersion, serverVersion string) string {
146146
agentVersion = "(unknown)"
147147
}
148148
if !semver.IsValid(serverVersion) || !semver.IsValid(agentVersion) {
149-
return Styles.Placeholder.Render(agentVersion)
149+
return DefaultStyles.Placeholder.Render(agentVersion)
150150
}
151151
outdated := semver.Compare(agentVersion, serverVersion) < 0
152152
if outdated {
153-
return Styles.Warn.Render(agentVersion + " (outdated)")
153+
return DefaultStyles.Warn.Render(agentVersion + " (outdated)")
154154
}
155-
return Styles.Keyword.Render(agentVersion)
155+
return DefaultStyles.Keyword.Render(agentVersion)
156156
}

0 commit comments

Comments
 (0)