fix(cli): speed up CLI over SSH (#7885)

By caching the terminal's color profile, we avoid myriad round trips during command execution.
This commit is contained in:
Ammar Bandukwala
2023-06-07 05:22:58 +00:00
committed by GitHub
parent 1288a83e42
commit 5eaf809851
46 changed files with 190 additions and 172 deletions
+4 -4
View File
@@ -179,7 +179,7 @@ type message struct {
func waitingMessage(agent codersdk.WorkspaceAgent, opts AgentOptions) (m *message) {
m = &message{
Spin: fmt.Sprintf("Waiting for connection from %s...", Styles.Field.Render(agent.Name)),
Spin: fmt.Sprintf("Waiting for connection from %s...", DefaultStyles.Field.Render(agent.Name)),
Prompt: "Don't panic, your workspace is booting up!",
}
defer func() {
@@ -192,7 +192,7 @@ func waitingMessage(agent codersdk.WorkspaceAgent, opts AgentOptions) (m *messag
// We don't want to wrap the troubleshooting URL, so we'll handle word
// wrapping ourselves (vs using lipgloss).
w := wordwrap.NewWriter(Styles.Paragraph.GetWidth() - Styles.Paragraph.GetMarginLeft()*2)
w := wordwrap.NewWriter(DefaultStyles.Paragraph.GetWidth() - DefaultStyles.Paragraph.GetMarginLeft()*2)
w.Breakpoints = []rune{' ', '\n'}
_, _ = fmt.Fprint(w, m.Prompt)
@@ -208,7 +208,7 @@ func waitingMessage(agent codersdk.WorkspaceAgent, opts AgentOptions) (m *messag
// We want to prefix the prompt with a caret, but we want text on the
// following lines to align with the text on the first line (i.e. added
// spacing).
ind := " " + Styles.Prompt.String()
ind := " " + DefaultStyles.Prompt.String()
iw := indent.NewWriter(1, func(w io.Writer) {
_, _ = w.Write([]byte(ind))
ind = " " // Set indentation to space after initial prompt.
@@ -223,7 +223,7 @@ func waitingMessage(agent codersdk.WorkspaceAgent, opts AgentOptions) (m *messag
case codersdk.WorkspaceAgentDisconnected:
m.Prompt = "The workspace agent lost connection!"
case codersdk.WorkspaceAgentConnected:
m.Spin = fmt.Sprintf("Waiting for %s to become ready...", Styles.Field.Render(agent.Name))
m.Spin = fmt.Sprintf("Waiting for %s to become ready...", DefaultStyles.Field.Render(agent.Name))
m.Prompt = "Don't panic, your workspace agent has connected and the workspace is getting ready!"
if opts.NoWait {
m.Prompt = "Your workspace is still getting ready, it may be in an incomplete state."
+48 -33
View File
@@ -1,27 +1,20 @@
package cliui
import (
"os"
"github.com/charmbracelet/charm/ui/common"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"golang.org/x/xerrors"
)
var (
Canceled = xerrors.New("canceled")
var Canceled = xerrors.New("canceled")
defaultStyles = common.DefaultStyles()
)
// DefaultStyles compose visual elements of the UI.
var DefaultStyles Styles
// ValidateNotEmpty is a helper function to disallow empty inputs!
func ValidateNotEmpty(s string) error {
if s == "" {
return xerrors.New("Must be provided!")
}
return nil
}
// Styles compose visual elements of the UI!
var Styles = struct {
type Styles struct {
Bold,
Checkmark,
Code,
@@ -38,23 +31,45 @@ var Styles = struct {
Logo,
Warn,
Wrap lipgloss.Style
}{
Bold: lipgloss.NewStyle().Bold(true),
Checkmark: defaultStyles.Checkmark,
Code: defaultStyles.Code,
Crossmark: defaultStyles.Error.Copy().SetString("✘"),
DateTimeStamp: defaultStyles.LabelDim,
Error: defaultStyles.Error,
Field: defaultStyles.Code.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FFFFFF"}),
Keyword: defaultStyles.Keyword,
Paragraph: defaultStyles.Paragraph,
Placeholder: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#585858", Dark: "#4d46b3"}),
Prompt: defaultStyles.Prompt.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"}),
FocusedPrompt: defaultStyles.FocusedPrompt.Copy().Foreground(lipgloss.Color("#651fff")),
Fuchsia: defaultStyles.SelectedMenuItem.Copy(),
Logo: defaultStyles.Logo.Copy().SetString("Coder"),
Warn: lipgloss.NewStyle().Foreground(
lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"},
),
Wrap: lipgloss.NewStyle().Width(80),
}
func init() {
lipgloss.SetDefaultRenderer(
lipgloss.NewRenderer(os.Stdout, termenv.WithColorCache(true)),
)
// All Styles are set after we change the DefaultRenderer so that the ColorCache
// is in effect, mitigating the severe performance issue seen here:
// https://github.com/coder/coder/issues/7884.
charmStyles := common.DefaultStyles()
DefaultStyles = Styles{
Bold: lipgloss.NewStyle().Bold(true),
Checkmark: charmStyles.Checkmark,
Code: charmStyles.Code,
Crossmark: charmStyles.Error.Copy().SetString("✘"),
DateTimeStamp: charmStyles.LabelDim,
Error: charmStyles.Error,
Field: charmStyles.Code.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FFFFFF"}),
Keyword: charmStyles.Keyword,
Paragraph: charmStyles.Paragraph,
Placeholder: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#585858", Dark: "#4d46b3"}),
Prompt: charmStyles.Prompt.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"}),
FocusedPrompt: charmStyles.FocusedPrompt.Copy().Foreground(lipgloss.Color("#651fff")),
Fuchsia: charmStyles.SelectedMenuItem.Copy(),
Logo: charmStyles.Logo.Copy().SetString("Coder"),
Warn: lipgloss.NewStyle().Foreground(
lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"},
),
Wrap: lipgloss.NewStyle().Width(80),
}
}
// ValidateNotEmpty is a helper function to disallow empty inputs!
func ValidateNotEmpty(s string) error {
if s == "" {
return xerrors.New("Must be provided!")
}
return nil
}
+2 -2
View File
@@ -35,7 +35,7 @@ func (m cliMessage) String() string {
// Warn writes a log to the writer provided.
func Warn(wtr io.Writer, header string, lines ...string) {
_, _ = fmt.Fprint(wtr, cliMessage{
Style: Styles.Warn.Copy(),
Style: DefaultStyles.Warn.Copy(),
Prefix: "WARN: ",
Header: header,
Lines: lines,
@@ -63,7 +63,7 @@ func Infof(wtr io.Writer, fmtStr string, args ...interface{}) {
// Error writes a log to the writer provided.
func Error(wtr io.Writer, header string, lines ...string) {
_, _ = fmt.Fprint(wtr, cliMessage{
Style: Styles.Error.Copy(),
Style: DefaultStyles.Error.Copy(),
Prefix: "ERROR: ",
Header: header,
Lines: lines,
+4 -4
View File
@@ -15,7 +15,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
label = templateVersionParameter.DisplayName
}
_, _ = fmt.Fprintln(inv.Stdout, Styles.Bold.Render(label))
_, _ = fmt.Fprintln(inv.Stdout, DefaultStyles.Bold.Render(label))
if templateVersionParameter.DescriptionPlaintext != "" {
_, _ = fmt.Fprintln(inv.Stdout, " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.DescriptionPlaintext, "\n"), "\n "))+"\n")
}
@@ -40,7 +40,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
}
_, _ = fmt.Fprintln(inv.Stdout)
_, _ = fmt.Fprintln(inv.Stdout, " "+Styles.Prompt.String()+Styles.Field.Render(strings.Join(values, ", ")))
_, _ = fmt.Fprintln(inv.Stdout, " "+DefaultStyles.Prompt.String()+DefaultStyles.Field.Render(strings.Join(values, ", ")))
value = string(v)
}
} else if len(templateVersionParameter.Options) > 0 {
@@ -54,7 +54,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
})
if err == nil {
_, _ = fmt.Fprintln(inv.Stdout)
_, _ = fmt.Fprintln(inv.Stdout, " "+Styles.Prompt.String()+Styles.Field.Render(richParameterOption.Name))
_, _ = fmt.Fprintln(inv.Stdout, " "+DefaultStyles.Prompt.String()+DefaultStyles.Field.Render(richParameterOption.Name))
value = richParameterOption.Value
}
} else {
@@ -65,7 +65,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
text += ":"
value, err = Prompt(inv, PromptOptions{
Text: Styles.Bold.Render(text),
Text: DefaultStyles.Bold.Render(text),
Validate: func(value string) error {
return validateRichPrompt(value, templateVersionParameter)
},
+8 -8
View File
@@ -55,21 +55,21 @@ func Prompt(inv *clibase.Invocation, opts PromptOptions) (string, error) {
}
}
_, _ = fmt.Fprint(inv.Stdout, Styles.FocusedPrompt.String()+opts.Text+" ")
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.FocusedPrompt.String()+opts.Text+" ")
if opts.IsConfirm {
if len(opts.Default) == 0 {
opts.Default = ConfirmYes
}
renderedYes := Styles.Placeholder.Render(ConfirmYes)
renderedNo := Styles.Placeholder.Render(ConfirmNo)
renderedYes := DefaultStyles.Placeholder.Render(ConfirmYes)
renderedNo := DefaultStyles.Placeholder.Render(ConfirmNo)
if opts.Default == ConfirmYes {
renderedYes = Styles.Bold.Render(ConfirmYes)
renderedYes = DefaultStyles.Bold.Render(ConfirmYes)
} else {
renderedNo = Styles.Bold.Render(ConfirmNo)
renderedNo = DefaultStyles.Bold.Render(ConfirmNo)
}
_, _ = fmt.Fprint(inv.Stdout, Styles.Placeholder.Render("("+renderedYes+Styles.Placeholder.Render("/"+renderedNo+Styles.Placeholder.Render(") "))))
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.Placeholder.Render("("+renderedYes+DefaultStyles.Placeholder.Render("/"+renderedNo+DefaultStyles.Placeholder.Render(") "))))
} else if opts.Default != "" {
_, _ = fmt.Fprint(inv.Stdout, Styles.Placeholder.Render("("+opts.Default+") "))
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.Placeholder.Render("("+opts.Default+") "))
}
interrupt := make(chan os.Signal, 1)
@@ -126,7 +126,7 @@ func Prompt(inv *clibase.Invocation, opts PromptOptions) (string, error) {
if opts.Validate != nil {
err := opts.Validate(line)
if err != nil {
_, _ = fmt.Fprintln(inv.Stdout, defaultStyles.Error.Render(err.Error()))
_, _ = fmt.Fprintln(inv.Stdout, DefaultStyles.Error.Render(err.Error()))
return Prompt(inv, opts)
}
}
+9 -9
View File
@@ -71,7 +71,7 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
)
printStage := func() {
_, _ = fmt.Fprintf(writer, Styles.Prompt.Render("⧗")+"%s\n", Styles.Field.Render(currentStage))
_, _ = fmt.Fprintf(writer, DefaultStyles.Prompt.Render("⧗")+"%s\n", DefaultStyles.Field.Render(currentStage))
}
updateStage := func(stage string, startedAt time.Time) {
@@ -80,11 +80,11 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
if !didLogBetweenStage {
prefix = "\033[1A\r"
}
mark := Styles.Checkmark
mark := DefaultStyles.Checkmark
if job.CompletedAt != nil && job.Status != codersdk.ProvisionerJobSucceeded {
mark = Styles.Crossmark
mark = DefaultStyles.Crossmark
}
_, _ = fmt.Fprintf(writer, prefix+mark.String()+Styles.Placeholder.Render(" %s [%dms]")+"\n", currentStage, startedAt.Sub(currentStageStartedAt).Milliseconds())
_, _ = fmt.Fprintf(writer, prefix+mark.String()+DefaultStyles.Placeholder.Render(" %s [%dms]")+"\n", currentStage, startedAt.Sub(currentStageStartedAt).Milliseconds())
}
if stage == "" {
return
@@ -129,7 +129,7 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
return
}
}
_, _ = fmt.Fprintf(writer, "\033[2K\r\n"+Styles.FocusedPrompt.String()+Styles.Bold.Render("Gracefully canceling...")+"\n\n")
_, _ = fmt.Fprintf(writer, "\033[2K\r\n"+DefaultStyles.FocusedPrompt.String()+DefaultStyles.Bold.Render("Gracefully canceling...")+"\n\n")
err := opts.Cancel()
if err != nil {
errChan <- xerrors.Errorf("cancel: %w", err)
@@ -207,11 +207,11 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
if !opts.Verbose {
continue
}
output = Styles.Placeholder.Render(log.Output)
output = DefaultStyles.Placeholder.Render(log.Output)
case codersdk.LogLevelError:
output = defaultStyles.Error.Render(log.Output)
output = DefaultStyles.Error.Render(log.Output)
case codersdk.LogLevelWarn:
output = Styles.Warn.Render(log.Output)
output = DefaultStyles.Warn.Render(log.Output)
case codersdk.LogLevelInfo:
output = log.Output
}
@@ -222,7 +222,7 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
jobMutex.Unlock()
continue
}
_, _ = fmt.Fprintf(logOutput, "%s %s\n", Styles.Placeholder.Render(" "), output)
_, _ = fmt.Fprintf(logOutput, "%s %s\n", DefaultStyles.Placeholder.Render(" "), output)
if !opts.Silent {
didLogBetweenStage = true
}
+13 -13
View File
@@ -78,7 +78,7 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
// Display a line for the resource.
tableWriter.AppendRow(table.Row{
Styles.Bold.Render(resourceAddress),
DefaultStyles.Bold.Render(resourceAddress),
"",
"",
})
@@ -106,7 +106,7 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
if totalAgents > 1 {
sshCommand += "." + agent.Name
}
sshCommand = Styles.Code.Render(sshCommand)
sshCommand = DefaultStyles.Code.Render(sshCommand)
row = append(row, sshCommand)
}
tableWriter.AppendRow(row)
@@ -121,23 +121,23 @@ func renderAgentStatus(agent codersdk.WorkspaceAgent) string {
switch agent.Status {
case codersdk.WorkspaceAgentConnecting:
since := database.Now().Sub(agent.CreatedAt)
return Styles.Warn.Render("⦾ connecting") + " " +
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
return DefaultStyles.Warn.Render("⦾ connecting") + " " +
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
case codersdk.WorkspaceAgentDisconnected:
since := database.Now().Sub(*agent.DisconnectedAt)
return Styles.Error.Render("⦾ disconnected") + " " +
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
return DefaultStyles.Error.Render("⦾ disconnected") + " " +
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
case codersdk.WorkspaceAgentTimeout:
since := database.Now().Sub(agent.CreatedAt)
return fmt.Sprintf(
"%s %s",
Styles.Warn.Render("⦾ timeout"),
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]"),
DefaultStyles.Warn.Render("⦾ timeout"),
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]"),
)
case codersdk.WorkspaceAgentConnected:
return Styles.Keyword.Render("⦿ connected")
return DefaultStyles.Keyword.Render("⦿ connected")
default:
return Styles.Warn.Render("○ unknown")
return DefaultStyles.Warn.Render("○ unknown")
}
}
@@ -146,11 +146,11 @@ func renderAgentVersion(agentVersion, serverVersion string) string {
agentVersion = "(unknown)"
}
if !semver.IsValid(serverVersion) || !semver.IsValid(agentVersion) {
return Styles.Placeholder.Render(agentVersion)
return DefaultStyles.Placeholder.Render(agentVersion)
}
outdated := semver.Compare(agentVersion, serverVersion) < 0
if outdated {
return Styles.Warn.Render(agentVersion + " (outdated)")
return DefaultStyles.Warn.Render(agentVersion + " (outdated)")
}
return Styles.Keyword.Render(agentVersion)
return DefaultStyles.Keyword.Render(agentVersion)
}