mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
+4
-4
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user