chore(cli): replace lipgloss with coder/pretty (#9564)

This change will improve over CLI performance and "snappiness" as well as
substantially reduce our test times. Preliminary benchmarks show
`coder server --help` times cut from 300ms to 120ms on my dogfood
instance.

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.
This commit is contained in:
Ammar Bandukwala
2023-09-07 16:28:22 -05:00
committed by GitHub
parent 8421f56137
commit dd97fe2bce
80 changed files with 490 additions and 330 deletions
+2 -6
View File
@@ -11,11 +11,11 @@ import (
"strings"
"testing"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/config"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
@@ -53,13 +53,9 @@ func DefaultCases() []CommandHelpCase {
//
//nolint:tparallel,paralleltest
func TestCommandHelp(t *testing.T, getRoot func(t *testing.T) *clibase.Cmd, cases []CommandHelpCase) {
ogColorProfile := lipgloss.ColorProfile()
// ANSI256 escape codes are far easier for humans to parse in a diff,
// but TrueColor is probably more popular with modern terminals.
lipgloss.SetColorProfile(termenv.ANSI)
t.Cleanup(func() {
lipgloss.SetColorProfile(ogColorProfile)
})
cliui.TestColor(t, termenv.ANSI)
rootClient, replacements := prepareTestData(t)
root := getRoot(t)
+125 -36
View File
@@ -2,11 +2,13 @@ package cliui
import (
"os"
"testing"
"time"
"github.com/charmbracelet/charm/ui/common"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"golang.org/x/xerrors"
"github.com/coder/pretty"
)
var Canceled = xerrors.New("canceled")
@@ -15,55 +17,142 @@ var Canceled = xerrors.New("canceled")
var DefaultStyles Styles
type Styles struct {
Bold,
Checkmark,
Code,
Crossmark,
DateTimeStamp,
Error,
Field,
Keyword,
Paragraph,
Placeholder,
Prompt,
FocusedPrompt,
Fuchsia,
Logo,
Warn,
Wrap lipgloss.Style
Wrap pretty.Style
}
var color = termenv.NewOutput(os.Stdout).ColorProfile()
// TestColor sets the color profile to the given profile for the duration of the
// test.
// WARN: Must not be used in parallel tests.
func TestColor(t *testing.T, tprofile termenv.Profile) {
old := color
color = tprofile
t.Cleanup(func() {
color = old
})
}
var (
Green = color.Color("#04B575")
Red = color.Color("#ED567A")
Fuchsia = color.Color("#EE6FF8")
Yellow = color.Color("#ECFD65")
Blue = color.Color("#5000ff")
)
func isTerm() bool {
return color != termenv.Ascii
}
// Bold returns a formatter that renders text in bold
// if the terminal supports it.
func Bold(s string) string {
if !isTerm() {
return s
}
return pretty.Sprint(pretty.Bold(), s)
}
// BoldFmt returns a formatter that renders text in bold
// if the terminal supports it.
func BoldFmt() pretty.Formatter {
if !isTerm() {
return pretty.Style{}
}
return pretty.Bold()
}
// Timestamp formats a timestamp for display.
func Timestamp(t time.Time) string {
return pretty.Sprint(DefaultStyles.DateTimeStamp, t.Format(time.Stamp))
}
// Keyword formats a keyword for display.
func Keyword(s string) string {
return pretty.Sprint(DefaultStyles.Keyword, s)
}
// Placeholder formats a placeholder for display.
func Placeholder(s string) string {
return pretty.Sprint(DefaultStyles.Placeholder, s)
}
// Wrap prevents the text from overflowing the terminal.
func Wrap(s string) string {
return pretty.Sprint(DefaultStyles.Wrap, s)
}
// Code formats code for display.
func Code(s string) string {
return pretty.Sprint(DefaultStyles.Code, s)
}
// Field formats a field for display.
func Field(s string) string {
return pretty.Sprint(DefaultStyles.Field, s)
}
func ifTerm(fmt pretty.Formatter) pretty.Formatter {
if !isTerm() {
return pretty.Nop
}
return fmt
}
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()
// We do not adapt the color based on whether the terminal is light or dark.
// Doing so would require a round-trip between the program and the terminal
// due to the OSC query and response.
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),
Code: pretty.Style{
ifTerm(pretty.XPad(1, 1)),
pretty.FgColor(Red),
pretty.BgColor(color.Color("#2c2c2c")),
},
DateTimeStamp: pretty.Style{
pretty.FgColor(color.Color("#7571F9")),
},
Error: pretty.Style{
pretty.FgColor(Red),
},
Field: pretty.Style{
pretty.XPad(1, 1),
pretty.FgColor(color.Color("#FFFFFF")),
pretty.BgColor(color.Color("#2b2a2a")),
},
Keyword: pretty.Style{
pretty.FgColor(Green),
},
Placeholder: pretty.Style{
pretty.FgColor(color.Color("#4d46b3")),
},
Prompt: pretty.Style{
pretty.FgColor(color.Color("#5C5C5C")),
pretty.Wrap("> ", ""),
},
Warn: pretty.Style{
pretty.FgColor(Yellow),
},
Wrap: pretty.Style{
pretty.LineWrap(80),
},
}
DefaultStyles.FocusedPrompt = append(
DefaultStyles.Prompt,
pretty.FgColor(Blue),
)
}
// ValidateNotEmpty is a helper function to disallow empty inputs!
+7 -7
View File
@@ -5,12 +5,12 @@ import (
"io"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/coder/pretty"
)
// cliMessage provides a human-readable message for CLI errors and messages.
type cliMessage struct {
Style lipgloss.Style
Style pretty.Style
Header string
Prefix string
Lines []string
@@ -21,13 +21,13 @@ func (m cliMessage) String() string {
var str strings.Builder
if m.Prefix != "" {
_, _ = str.WriteString(m.Style.Bold(true).Render(m.Prefix))
_, _ = str.WriteString(Bold(m.Prefix))
}
_, _ = str.WriteString(m.Style.Bold(false).Render(m.Header))
pretty.Fprint(&str, m.Style, m.Header)
_, _ = str.WriteString("\r\n")
for _, line := range m.Lines {
_, _ = fmt.Fprintf(&str, " %s %s\r\n", m.Style.Render("|"), line)
_, _ = fmt.Fprintf(&str, " %s %s\r\n", pretty.Sprint(m.Style, "|"), line)
}
return str.String()
}
@@ -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: DefaultStyles.Warn.Copy(),
Style: DefaultStyles.Warn,
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: DefaultStyles.Error.Copy(),
Style: DefaultStyles.Error,
Prefix: "ERROR: ",
Header: header,
Lines: lines,
+9 -5
View File
@@ -7,6 +7,7 @@ import (
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
)
func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.TemplateVersionParameter) (string, error) {
@@ -16,10 +17,10 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
}
if templateVersionParameter.Ephemeral {
label += DefaultStyles.Warn.Render(" (build option)")
label += pretty.Sprint(DefaultStyles.Warn, " (build option)")
}
_, _ = fmt.Fprintln(inv.Stdout, DefaultStyles.Bold.Render(label))
_, _ = fmt.Fprintln(inv.Stdout, Bold(label))
if templateVersionParameter.DescriptionPlaintext != "" {
_, _ = 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
}
_, _ = fmt.Fprintln(inv.Stdout)
_, _ = fmt.Fprintln(inv.Stdout, " "+DefaultStyles.Prompt.String()+DefaultStyles.Field.Render(strings.Join(values, ", ")))
pretty.Fprintf(
inv.Stdout,
DefaultStyles.Prompt, "%s\n", strings.Join(values, ", "),
)
value = string(v)
}
} else if len(templateVersionParameter.Options) > 0 {
@@ -59,7 +63,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
})
if err == nil {
_, _ = fmt.Fprintln(inv.Stdout)
_, _ = fmt.Fprintln(inv.Stdout, " "+DefaultStyles.Prompt.String()+DefaultStyles.Field.Render(richParameterOption.Name))
pretty.Fprintf(inv.Stdout, DefaultStyles.Prompt, "%s\n", richParameterOption.Name)
value = richParameterOption.Value
}
} else {
@@ -70,7 +74,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
text += ":"
value, err = Prompt(inv, PromptOptions{
Text: DefaultStyles.Bold.Render(text),
Text: Bold(text),
Validate: func(value string) error {
return validateRichPrompt(value, templateVersionParameter)
},
+12 -8
View File
@@ -14,6 +14,7 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/pretty"
)
// PromptOptions supply a set of options to the prompt.
@@ -55,21 +56,24 @@ func Prompt(inv *clibase.Invocation, opts PromptOptions) (string, error) {
}
}
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.FocusedPrompt.String()+opts.Text+" ")
pretty.Fprintf(inv.Stdout, DefaultStyles.FocusedPrompt, "")
pretty.Fprintf(inv.Stdout, pretty.Nop, "%s ", opts.Text)
if opts.IsConfirm {
if len(opts.Default) == 0 {
opts.Default = ConfirmYes
}
renderedYes := DefaultStyles.Placeholder.Render(ConfirmYes)
renderedNo := DefaultStyles.Placeholder.Render(ConfirmNo)
var (
renderedYes = pretty.Sprint(DefaultStyles.Placeholder, ConfirmYes)
renderedNo = pretty.Sprint(DefaultStyles.Placeholder, ConfirmNo)
)
if opts.Default == ConfirmYes {
renderedYes = DefaultStyles.Bold.Render(ConfirmYes)
renderedYes = Bold(ConfirmYes)
} else {
renderedNo = DefaultStyles.Bold.Render(ConfirmNo)
renderedNo = Bold(ConfirmNo)
}
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.Placeholder.Render("("+renderedYes+DefaultStyles.Placeholder.Render("/"+renderedNo+DefaultStyles.Placeholder.Render(") "))))
pretty.Fprintf(inv.Stdout, DefaultStyles.Placeholder, "(%s/%s)", renderedYes, renderedNo)
} else if opts.Default != "" {
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.Placeholder.Render("("+opts.Default+") "))
_, _ = fmt.Fprint(inv.Stdout, pretty.Sprint(DefaultStyles.Placeholder, "("+opts.Default+") "))
}
interrupt := make(chan os.Signal, 1)
@@ -126,7 +130,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, pretty.Sprint(DefaultStyles.Error, err.Error()))
return Prompt(inv, opts)
}
}
+13 -8
View File
@@ -15,6 +15,7 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
)
func WorkspaceBuild(ctx context.Context, writer io.Writer, client *codersdk.Client, build uuid.UUID) error {
@@ -54,7 +55,7 @@ func (err *ProvisionerJobError) Error() string {
}
// ProvisionerJob renders a provisioner job with interactive cancellation.
func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOptions) error {
func ProvisionerJob(ctx context.Context, wr io.Writer, opts ProvisionerJobOptions) error {
if opts.FetchInterval == 0 {
opts.FetchInterval = time.Second
}
@@ -70,7 +71,7 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
jobMutex sync.Mutex
)
sw := &stageWriter{w: writer, verbose: opts.Verbose, silentLogs: opts.Silent}
sw := &stageWriter{w: wr, verbose: opts.Verbose, silentLogs: opts.Silent}
printStage := func() {
sw.Start(currentStage)
@@ -127,7 +128,11 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
return
}
}
_, _ = fmt.Fprintf(writer, DefaultStyles.FocusedPrompt.String()+DefaultStyles.Bold.Render("Gracefully canceling...")+"\n\n")
pretty.Fprintf(
wr,
DefaultStyles.FocusedPrompt.With(BoldFmt()),
"Gracefully canceling...\n\n",
)
err := opts.Cancel()
if err != nil {
errChan <- xerrors.Errorf("cancel: %w", err)
@@ -236,7 +241,7 @@ func (s *stageWriter) Log(createdAt time.Time, level codersdk.LogLevel, line str
w = &s.logBuf
}
render := func(s ...string) string { return strings.Join(s, " ") }
var style pretty.Style
var lines []string
if !createdAt.IsZero() {
@@ -249,14 +254,14 @@ func (s *stageWriter) Log(createdAt time.Time, level codersdk.LogLevel, line str
if !s.verbose {
return
}
render = DefaultStyles.Placeholder.Render
style = DefaultStyles.Placeholder
case codersdk.LogLevelError:
render = DefaultStyles.Error.Render
style = DefaultStyles.Error
case codersdk.LogLevelWarn:
render = DefaultStyles.Warn.Render
style = DefaultStyles.Warn
case codersdk.LogLevelInfo:
}
_, _ = fmt.Fprintf(w, "%s\n", render(lines...))
pretty.Fprintf(w, style, "%s\n", strings.Join(lines, " "))
}
func (s *stageWriter) flushLogs() {
+16 -15
View File
@@ -11,6 +11,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
)
type WorkspaceResourcesOptions struct {
@@ -78,7 +79,7 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
// Display a line for the resource.
tableWriter.AppendRow(table.Row{
DefaultStyles.Bold.Render(resourceAddress),
Bold(resourceAddress),
"",
"",
"",
@@ -107,7 +108,7 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
if totalAgents > 1 {
sshCommand += "." + agent.Name
}
sshCommand = DefaultStyles.Code.Render(sshCommand)
sshCommand = pretty.Sprint(DefaultStyles.Code, sshCommand)
row = append(row, sshCommand)
}
tableWriter.AppendRow(row)
@@ -122,31 +123,31 @@ func renderAgentStatus(agent codersdk.WorkspaceAgent) string {
switch agent.Status {
case codersdk.WorkspaceAgentConnecting:
since := dbtime.Now().Sub(agent.CreatedAt)
return DefaultStyles.Warn.Render("⦾ connecting") + " " +
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
return pretty.Sprint(DefaultStyles.Warn, "⦾ connecting") + " " +
pretty.Sprint(DefaultStyles.Placeholder, "["+strconv.Itoa(int(since.Seconds()))+"s]")
case codersdk.WorkspaceAgentDisconnected:
since := dbtime.Now().Sub(*agent.DisconnectedAt)
return DefaultStyles.Error.Render("⦾ disconnected") + " " +
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
return pretty.Sprint(DefaultStyles.Error, "⦾ disconnected") + " " +
pretty.Sprint(DefaultStyles.Placeholder, "["+strconv.Itoa(int(since.Seconds()))+"s]")
case codersdk.WorkspaceAgentTimeout:
since := dbtime.Now().Sub(agent.CreatedAt)
return fmt.Sprintf(
"%s %s",
DefaultStyles.Warn.Render("⦾ timeout"),
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]"),
pretty.Sprint(DefaultStyles.Warn, "⦾ timeout"),
pretty.Sprint(DefaultStyles.Placeholder, "["+strconv.Itoa(int(since.Seconds()))+"s]"),
)
case codersdk.WorkspaceAgentConnected:
return DefaultStyles.Keyword.Render("⦿ connected")
return pretty.Sprint(DefaultStyles.Keyword, "⦿ connected")
default:
return DefaultStyles.Warn.Render("○ unknown")
return pretty.Sprint(DefaultStyles.Warn, "○ unknown")
}
}
func renderAgentHealth(agent codersdk.WorkspaceAgent) string {
if agent.Health.Healthy {
return DefaultStyles.Keyword.Render("✔ healthy")
return pretty.Sprint(DefaultStyles.Keyword, "✔ healthy")
}
return DefaultStyles.Error.Render("✘ " + agent.Health.Reason)
return pretty.Sprint(DefaultStyles.Error, "✘ "+agent.Health.Reason)
}
func renderAgentVersion(agentVersion, serverVersion string) string {
@@ -154,11 +155,11 @@ func renderAgentVersion(agentVersion, serverVersion string) string {
agentVersion = "(unknown)"
}
if !semver.IsValid(serverVersion) || !semver.IsValid(agentVersion) {
return DefaultStyles.Placeholder.Render(agentVersion)
return pretty.Sprint(DefaultStyles.Placeholder, agentVersion)
}
outdated := semver.Compare(agentVersion, serverVersion) < 0
if outdated {
return DefaultStyles.Warn.Render(agentVersion + " (outdated)")
return pretty.Sprint(DefaultStyles.Warn, agentVersion+" (outdated)")
}
return DefaultStyles.Keyword.Render(agentVersion)
return pretty.Sprint(DefaultStyles.Keyword, agentVersion)
}
+10 -3
View File
@@ -10,6 +10,8 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/util/ptr"
@@ -75,7 +77,7 @@ func (r *RootCmd) create() *clibase.Cmd {
var template codersdk.Template
if templateName == "" {
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Wrap.Render("Select a template below to preview the provisioned infrastructure:"))
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Wrap, "Select a template below to preview the provisioned infrastructure:"))
templates, err := client.TemplatesByOrganization(inv.Context(), organization.ID)
if err != nil {
@@ -93,7 +95,7 @@ func (r *RootCmd) create() *clibase.Cmd {
templateName := template.Name
if template.ActiveUserCount > 0 {
templateName += cliui.DefaultStyles.Placeholder.Render(
templateName += cliui.Placeholder(
fmt.Sprintf(
" (used by %s)",
formatActiveDevelopers(template.ActiveUserCount),
@@ -177,7 +179,12 @@ func (r *RootCmd) create() *clibase.Cmd {
return xerrors.Errorf("watch build: %w", err)
}
_, _ = 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)))
_, _ = fmt.Fprintf(
inv.Stdout,
"\nThe %s workspace has been created at %s!\n",
cliui.Keyword(workspace.Name),
cliui.Timestamp(time.Now()),
)
return nil
},
}
+5 -1
View File
@@ -54,7 +54,11 @@ func (r *RootCmd) deleteWorkspace() *clibase.Cmd {
return err
}
_, _ = 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)))
_, _ = fmt.Fprintf(
inv.Stdout,
"\n%s has been deleted at %s!\n", cliui.Keyword(workspace.FullName()),
cliui.Timestamp(time.Now()),
)
return nil
},
}
+4 -2
View File
@@ -13,6 +13,8 @@ import (
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
)
@@ -143,7 +145,7 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
return err
}
// if the repo exists we soft fail the update operation and try to continue
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Error.Render("Failed to update repo, continuing..."))
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Error, "Failed to update repo, continuing..."))
}
if dotfilesExists && gitbranch != "" {
@@ -159,7 +161,7 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
if err != nil {
// Do not block on this error, just log it and continue
_, _ = fmt.Fprintln(inv.Stdout,
cliui.DefaultStyles.Error.Render(fmt.Sprintf("Failed to use branch %q (%s), continuing...", err.Error(), gitbranch)))
pretty.Sprint(cliui.DefaultStyles.Error, fmt.Sprintf("Failed to use branch %q (%s), continuing...", err.Error(), gitbranch)))
}
}
+9 -5
View File
@@ -16,6 +16,7 @@ import (
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/pretty"
)
func (r *RootCmd) gitssh() *clibase.Cmd {
@@ -90,12 +91,15 @@ func (r *RootCmd) gitssh() *clibase.Cmd {
exitErr := &exec.ExitError{}
if xerrors.As(err, &exitErr) && exitErr.ExitCode() == 255 {
_, _ = fmt.Fprintln(inv.Stderr,
"\n"+cliui.DefaultStyles.Wrap.Render("Coder authenticates with "+cliui.DefaultStyles.Field.Render("git")+
" using the public key below. All clones with SSH are authenticated automatically 🪄.")+"\n")
_, _ = fmt.Fprintln(inv.Stderr, cliui.DefaultStyles.Code.Render(strings.TrimSpace(key.PublicKey))+"\n")
"\n"+pretty.Sprintf(
cliui.DefaultStyles.Wrap,
"Coder authenticates with "+pretty.Sprint(cliui.DefaultStyles.Field, "git")+
" using the public key below. All clones with SSH are authenticated automatically 🪄.")+"\n",
)
_, _ = fmt.Fprintln(inv.Stderr, pretty.Sprint(cliui.DefaultStyles.Code, strings.TrimSpace(key.PublicKey))+"\n")
_, _ = fmt.Fprintln(inv.Stderr, "Add to GitHub and GitLab:")
_, _ = fmt.Fprintln(inv.Stderr, cliui.DefaultStyles.Prompt.String()+"https://github.com/settings/ssh/new")
_, _ = fmt.Fprintln(inv.Stderr, cliui.DefaultStyles.Prompt.String()+"https://gitlab.com/-/profile/keys")
pretty.Fprintf(inv.Stderr, cliui.DefaultStyles.Prompt, "%s", "https://github.com/settings/ssh/new\n\n")
pretty.Fprintf(inv.Stderr, cliui.DefaultStyles.Prompt, "%s", "https://gitlab.com/-/profile/keys\n\n")
_, _ = fmt.Fprintln(inv.Stderr)
return err
}
+1 -1
View File
@@ -127,7 +127,7 @@ var usageTemplate = template.Must(
return opt.Flag
},
"prettyHeader": func(s string) string {
return cliui.DefaultStyles.Bold.Render(s)
return cliui.Bold(s)
},
"isEnterprise": func(opt clibase.Option) bool {
return opt.Annotations.IsSet("enterprise")
+4 -2
View File
@@ -7,6 +7,8 @@ import (
"github.com/google/uuid"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/schedule/cron"
@@ -119,9 +121,9 @@ func (r *RootCmd) list() *clibase.Cmd {
return err
}
if len(res.Workspaces) == 0 {
_, _ = fmt.Fprintln(inv.Stderr, cliui.DefaultStyles.Prompt.String()+"No workspaces found! Create one:")
pretty.Fprintf(inv.Stderr, cliui.DefaultStyles.Prompt, "No workspaces found! Create one:\n")
_, _ = fmt.Fprintln(inv.Stderr)
_, _ = fmt.Fprintln(inv.Stderr, " "+cliui.DefaultStyles.Code.Render("coder create <name>"))
_, _ = fmt.Fprintln(inv.Stderr, " "+pretty.Sprint(cliui.DefaultStyles.Code, "coder create <name>"))
_, _ = fmt.Fprintln(inv.Stderr)
return nil
}
+14 -14
View File
@@ -16,6 +16,8 @@ import (
"github.com/pkg/browser"
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/userpassword"
@@ -43,7 +45,7 @@ func promptFirstUsername(inv *clibase.Invocation) (string, error) {
return "", xerrors.Errorf("get current user: %w", err)
}
username, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "What " + cliui.DefaultStyles.Field.Render("username") + " would you like?",
Text: "What " + pretty.Sprint(cliui.DefaultStyles.Field, "username") + " would you like?",
Default: currentUser.Username,
})
if errors.Is(err, cliui.Canceled) {
@@ -59,7 +61,7 @@ func promptFirstUsername(inv *clibase.Invocation) (string, error) {
func promptFirstPassword(inv *clibase.Invocation) (string, error) {
retry:
password, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Enter a " + cliui.DefaultStyles.Field.Render("password") + ":",
Text: "Enter a " + pretty.Sprint(cliui.DefaultStyles.Field, "password") + ":",
Secret: true,
Validate: func(s string) error {
return userpassword.Validate(s)
@@ -69,7 +71,7 @@ retry:
return "", xerrors.Errorf("specify password prompt: %w", err)
}
confirm, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Confirm " + cliui.DefaultStyles.Field.Render("password") + ":",
Text: "Confirm " + pretty.Sprint(cliui.DefaultStyles.Field, "password") + ":",
Secret: true,
Validate: cliui.ValidateNotEmpty,
})
@@ -78,7 +80,7 @@ retry:
}
if confirm != password {
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Error.Render("Passwords do not match"))
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Error, "Passwords do not match"))
goto retry
}
@@ -115,12 +117,8 @@ func (r *RootCmd) loginWithPassword(
_, _ = fmt.Fprintf(
inv.Stdout,
cliui.DefaultStyles.Paragraph.Render(
fmt.Sprintf(
"Welcome to Coder, %s! You're authenticated.",
cliui.DefaultStyles.Keyword.Render(u.Username),
),
)+"\n",
"Welcome to Coder, %s! You're authenticated.",
pretty.Sprint(cliui.DefaultStyles.Keyword, u.Username),
)
return nil
@@ -177,7 +175,7 @@ func (r *RootCmd) login() *clibase.Cmd {
if err != nil {
// Checking versions isn't a fatal error so we print a warning
// and proceed.
_, _ = fmt.Fprintln(inv.Stderr, cliui.DefaultStyles.Warn.Render(err.Error()))
_, _ = fmt.Fprintln(inv.Stderr, pretty.Sprint(cliui.DefaultStyles.Warn, err.Error()))
}
hasFirstUser, err := client.HasFirstUser(ctx)
@@ -209,7 +207,7 @@ func (r *RootCmd) login() *clibase.Cmd {
if email == "" {
email, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: "What's your " + cliui.DefaultStyles.Field.Render("email") + "?",
Text: "What's your " + pretty.Sprint(cliui.DefaultStyles.Field, "email") + "?",
Validate: func(s string) error {
err := validator.New().Var(s, "email")
if err != nil {
@@ -261,7 +259,9 @@ func (r *RootCmd) login() *clibase.Cmd {
_, _ = fmt.Fprintf(
inv.Stdout,
cliui.DefaultStyles.Paragraph.Render("Get started by creating a template: "+cliui.DefaultStyles.Code.Render("coder templates init"))+"\n")
"Get started by creating a template: %s\n",
pretty.Sprint(cliui.DefaultStyles.Code, "coder templates init"),
)
return nil
}
@@ -327,7 +327,7 @@ func (r *RootCmd) login() *clibase.Cmd {
return xerrors.Errorf("write server url: %w", err)
}
_, _ = fmt.Fprintf(inv.Stdout, Caret+"Welcome to Coder, %s! You're authenticated.\n", cliui.DefaultStyles.Keyword.Render(resp.Username))
_, _ = fmt.Fprintf(inv.Stdout, Caret+"Welcome to Coder, %s! You're authenticated.\n", pretty.Sprint(cliui.DefaultStyles.Keyword, resp.Username))
return nil
},
}
+3 -1
View File
@@ -8,6 +8,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/coderdtest"
@@ -141,7 +143,7 @@ func TestLogin(t *testing.T) {
// Validate that we reprompt for matching passwords.
pty.ExpectMatch("Passwords do not match")
pty.ExpectMatch("Enter a " + cliui.DefaultStyles.Field.Render("password"))
pty.ExpectMatch("Enter a " + pretty.Sprint(cliui.DefaultStyles.Field, "password"))
pty.WriteLine("SomeSecurePassword!")
pty.ExpectMatch("Confirm")
+3 -1
View File
@@ -5,6 +5,8 @@ import (
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
@@ -203,7 +205,7 @@ func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuild
Value: parameterValue,
})
} else if action == WorkspaceUpdate && !tvp.Mutable && !firstTimeUse {
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Warn.Render(fmt.Sprintf("Parameter %q is not mutable, and cannot be customized after workspace creation.", tvp.Name)))
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Warn, fmt.Sprintf("Parameter %q is not mutable, and cannot be customized after workspace creation.", tvp.Name)))
}
}
return resolved, nil
+9 -7
View File
@@ -10,6 +10,8 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
@@ -104,14 +106,14 @@ func (r *RootCmd) ping() *clibase.Cmd {
if p2p {
if !didP2p {
_, _ = fmt.Fprintln(inv.Stdout, "p2p connection established in",
cliui.DefaultStyles.DateTimeStamp.Render(time.Since(start).Round(time.Millisecond).String()),
pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Since(start).Round(time.Millisecond).String()),
)
}
didP2p = true
via = fmt.Sprintf("%s via %s",
cliui.DefaultStyles.Fuchsia.Render("p2p"),
cliui.DefaultStyles.Code.Render(pong.Endpoint),
pretty.Sprint(cliui.DefaultStyles.Fuchsia, "p2p"),
pretty.Sprint(cliui.DefaultStyles.Code, pong.Endpoint),
)
} else {
derpName := "unknown"
@@ -120,15 +122,15 @@ func (r *RootCmd) ping() *clibase.Cmd {
derpName = derpRegion.RegionName
}
via = fmt.Sprintf("%s via %s",
cliui.DefaultStyles.Fuchsia.Render("proxied"),
cliui.DefaultStyles.Code.Render(fmt.Sprintf("DERP(%s)", derpName)),
pretty.Sprint(cliui.DefaultStyles.Fuchsia, "proxied"),
pretty.Sprint(cliui.DefaultStyles.Code, fmt.Sprintf("DERP(%s)", derpName)),
)
}
_, _ = fmt.Fprintf(inv.Stdout, "pong from %s %s in %s\n",
cliui.DefaultStyles.Keyword.Render(workspaceName),
pretty.Sprint(cliui.DefaultStyles.Keyword, workspaceName),
via,
cliui.DefaultStyles.DateTimeStamp.Render(dur.String()),
pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, dur.String()),
)
if n == int(pingNum) {
+6 -4
View File
@@ -5,6 +5,8 @@ import (
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
@@ -44,13 +46,13 @@ func (r *RootCmd) publickey() *clibase.Cmd {
}
cliui.Infof(inv.Stdout,
"This is your public key for using "+cliui.DefaultStyles.Field.Render("git")+" in "+
"This is your public key for using "+pretty.Sprint(cliui.DefaultStyles.Field, "git")+" in "+
"Coder. All clones with SSH will be authenticated automatically 🪄.",
)
cliui.Infof(inv.Stdout, cliui.DefaultStyles.Code.Render(strings.TrimSpace(key.PublicKey))+"\n")
cliui.Infof(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Code, strings.TrimSpace(key.PublicKey))+"\n")
cliui.Infof(inv.Stdout, "Add to GitHub and GitLab:")
cliui.Infof(inv.Stdout, cliui.DefaultStyles.Prompt.String()+"https://github.com/settings/ssh/new")
cliui.Infof(inv.Stdout, cliui.DefaultStyles.Prompt.String()+"https://gitlab.com/-/profile/keys")
cliui.Infof(inv.Stdout, "> https://github.com/settings/ssh/new")
cliui.Infof(inv.Stdout, "> https://gitlab.com/-/profile/keys")
return nil
},
+3 -1
View File
@@ -5,6 +5,8 @@ import (
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
@@ -27,7 +29,7 @@ func (r *RootCmd) rename() *clibase.Cmd {
}
_, _ = fmt.Fprintf(inv.Stdout, "%s\n\n",
cliui.DefaultStyles.Wrap.Render("WARNING: A rename can result in data loss if a resource references the workspace name in the template (e.g volumes). Please backup any data before proceeding."),
pretty.Sprint(cliui.DefaultStyles.Wrap, "WARNING: A rename can result in data loss if a resource references the workspace name in the template (e.g volumes). Please backup any data before proceeding."),
)
_, _ = fmt.Fprintf(inv.Stdout, "See: %s\n\n", "https://coder.com/docs/coder-oss/latest/templates/resource-persistence#%EF%B8%8F-persistence-pitfalls")
_, err = cliui.Prompt(inv, cliui.PromptOptions{
+5 -3
View File
@@ -8,6 +8,8 @@ import (
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/database"
@@ -49,7 +51,7 @@ func (*RootCmd) resetPassword() *clibase.Cmd {
}
password, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Enter new " + cliui.DefaultStyles.Field.Render("password") + ":",
Text: "Enter new " + pretty.Sprint(cliui.DefaultStyles.Field, "password") + ":",
Secret: true,
Validate: func(s string) error {
return userpassword.Validate(s)
@@ -59,7 +61,7 @@ func (*RootCmd) resetPassword() *clibase.Cmd {
return xerrors.Errorf("password prompt: %w", err)
}
confirmedPassword, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Confirm " + cliui.DefaultStyles.Field.Render("password") + ":",
Text: "Confirm " + pretty.Sprint(cliui.DefaultStyles.Field, "password") + ":",
Secret: true,
Validate: cliui.ValidateNotEmpty,
})
@@ -83,7 +85,7 @@ func (*RootCmd) resetPassword() *clibase.Cmd {
return xerrors.Errorf("updating password: %w", err)
}
_, _ = fmt.Fprintf(inv.Stdout, "\nPassword has been reset for user %s!\n", cliui.DefaultStyles.Keyword.Render(user.Username))
_, _ = fmt.Fprintf(inv.Stdout, "\nPassword has been reset for user %s!\n", pretty.Sprint(cliui.DefaultStyles.Keyword, user.Username))
return nil
},
}
+6 -1
View File
@@ -6,6 +6,8 @@ import (
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
@@ -92,7 +94,10 @@ func (r *RootCmd) restart() *clibase.Cmd {
return err
}
_, _ = fmt.Fprintf(out, "\nThe %s workspace has been restarted at %s!\n", cliui.DefaultStyles.Keyword.Render(workspace.Name), cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp)))
_, _ = fmt.Fprintf(out,
"\nThe %s workspace has been restarted at %s!\n",
pretty.Sprint(cliui.DefaultStyles.Keyword, workspace.Name), cliui.Timestamp(time.Now()),
)
return nil
},
}
+12 -12
View File
@@ -30,6 +30,8 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"github.com/coder/pretty"
"cdr.dev/slog"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clibase"
@@ -42,7 +44,7 @@ import (
)
var (
Caret = cliui.DefaultStyles.Prompt.String()
Caret = pretty.Sprint(cliui.DefaultStyles.Prompt, "")
// Applied as annotations to workspace commands
// so they display in a separated "help" section.
@@ -581,15 +583,13 @@ func (r *RootCmd) initClientInternal(client *codersdk.Client, allowTokenMissing
if err = <-versionErr; err != nil {
// Just log the error here. We never want to fail a command
// due to a pre-run.
_, _ = fmt.Fprintf(inv.Stderr,
cliui.DefaultStyles.Warn.Render("check versions error: %s"), err)
pretty.Fprintf(inv.Stderr, cliui.DefaultStyles.Warn, "check versions error: %s", err)
_, _ = fmt.Fprintln(inv.Stderr)
}
if err = <-warningErr; err != nil {
// Same as above
_, _ = fmt.Fprintf(inv.Stderr,
cliui.DefaultStyles.Warn.Render("check entitlement warnings error: %s"), err)
pretty.Fprintf(inv.Stderr, cliui.DefaultStyles.Warn, "check entitlement warnings error: %s", err)
_, _ = fmt.Fprintln(inv.Stderr)
}
@@ -753,18 +753,18 @@ type example struct {
func formatExamples(examples ...example) string {
var sb strings.Builder
padStyle := cliui.DefaultStyles.Wrap.Copy().PaddingLeft(4)
padStyle := cliui.DefaultStyles.Wrap.With(pretty.XPad(4, 0))
for i, e := range examples {
if len(e.Description) > 0 {
wordwrap.WrapString(e.Description, 80)
_, _ = sb.WriteString(
" - " + padStyle.Render(e.Description + ":")[4:] + "\n\n ",
" - " + pretty.Sprint(padStyle, e.Description+":")[4:] + "\n\n ",
)
}
// We add 1 space here because `cliui.DefaultStyles.Code` adds an extra
// space. This makes the code block align at an even 2 or 6
// spaces for symmetry.
_, _ = sb.WriteString(" " + cliui.DefaultStyles.Code.Render(fmt.Sprintf("$ %s", e.Command)))
_, _ = sb.WriteString(" " + pretty.Sprint(cliui.DefaultStyles.Code, fmt.Sprintf("$ %s", e.Command)))
if i < len(examples)-1 {
_, _ = sb.WriteString("\n\n")
}
@@ -802,8 +802,8 @@ func (r *RootCmd) checkVersions(i *clibase.Invocation, client *codersdk.Client)
}
if !buildinfo.VersionsMatch(clientVersion, info.Version) {
warn := cliui.DefaultStyles.Warn.Copy().Align(lipgloss.Left)
_, _ = fmt.Fprintf(i.Stderr, warn.Render(fmtWarningText), clientVersion, info.Version, strings.TrimPrefix(info.CanonicalVersion(), "v"))
warn := cliui.DefaultStyles.Warn
_, _ = fmt.Fprintf(i.Stderr, pretty.Sprint(warn, fmtWarningText), clientVersion, info.Version, strings.TrimPrefix(info.CanonicalVersion(), "v"))
_, _ = fmt.Fprintln(i.Stderr)
}
@@ -821,7 +821,7 @@ func (r *RootCmd) checkWarnings(i *clibase.Invocation, client *codersdk.Client)
entitlements, err := client.Entitlements(ctx)
if err == nil {
for _, w := range entitlements.Warnings {
_, _ = fmt.Fprintln(i.Stderr, cliui.DefaultStyles.Warn.Render(w))
_, _ = fmt.Fprintln(i.Stderr, pretty.Sprint(cliui.DefaultStyles.Warn, w))
}
}
return nil
@@ -1022,7 +1022,7 @@ func (p *prettyErrorFormatter) printf(style lipgloss.Style, format string, a ...
//nolint:unused
func SlimUnsupported(w io.Writer, cmd string) {
_, _ = fmt.Fprintf(w, "You are using a 'slim' build of Coder, which does not support the %s subcommand.\n", cliui.DefaultStyles.Code.Render(cmd))
_, _ = fmt.Fprintf(w, "You are using a 'slim' build of Coder, which does not support the %s subcommand.\n", pretty.Sprint(cliui.DefaultStyles.Code, cmd))
_, _ = fmt.Fprintln(w, "")
_, _ = fmt.Fprintln(w, "Please use a build of Coder from GitHub releases:")
_, _ = fmt.Fprintln(w, " https://github.com/coder/coder/releases")
+7 -7
View File
@@ -52,6 +52,8 @@ import (
"gopkg.in/yaml.v3"
"tailscale.com/tailcfg"
"github.com/coder/pretty"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"cdr.dev/slog/sloggers/slogjson"
@@ -512,7 +514,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
cliui.Warnf(
inv.Stderr,
"The access URL %s %s, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n",
cliui.DefaultStyles.Field.Render(vals.AccessURL.String()), reason,
pretty.Sprint(cliui.DefaultStyles.Field, vals.AccessURL.String()), reason,
)
}
@@ -1026,9 +1028,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
select {
case <-notifyCtx.Done():
exitErr = notifyCtx.Err()
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Bold.Render(
"Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit",
))
_, _ = io.WriteString(inv.Stdout, cliui.Bold("Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit"))
case <-tunnelDone:
exitErr = xerrors.New("dev tunnel closed unexpectedly")
case exitErr = <-errCh:
@@ -1134,7 +1134,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
if pgRawURL {
_, _ = fmt.Fprintf(inv.Stdout, "%s\n", url)
} else {
_, _ = fmt.Fprintf(inv.Stdout, "%s\n", cliui.DefaultStyles.Code.Render(fmt.Sprintf("psql %q", url)))
_, _ = fmt.Fprintf(inv.Stdout, "%s\n", pretty.Sprint(cliui.DefaultStyles.Code, fmt.Sprintf("psql %q", url)))
}
return nil
},
@@ -1164,7 +1164,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
if pgRawURL {
_, _ = fmt.Fprintf(inv.Stdout, "%s\n", url)
} else {
_, _ = fmt.Fprintf(inv.Stdout, "%s\n", cliui.DefaultStyles.Code.Render(fmt.Sprintf("psql %q", url)))
_, _ = fmt.Fprintf(inv.Stdout, "%s\n", pretty.Sprint(cliui.DefaultStyles.Code, fmt.Sprintf("psql %q", url)))
}
<-ctx.Done()
@@ -1403,7 +1403,7 @@ func PrintLogo(inv *clibase.Invocation, daemonTitle string) {
return
}
versionString := cliui.DefaultStyles.Bold.Render(daemonTitle + " " + buildinfo.Version())
versionString := cliui.Bold(daemonTitle + " " + buildinfo.Version())
_, _ = fmt.Fprintf(inv.Stdout, "%s - Your Self-Hosted Remote Development Platform\n", versionString)
}
+4 -1
View File
@@ -71,7 +71,10 @@ func (r *RootCmd) start() *clibase.Cmd {
return err
}
_, _ = fmt.Fprintf(inv.Stdout, "\nThe %s workspace has been started at %s!\n", cliui.DefaultStyles.Keyword.Render(workspace.Name), cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp)))
_, _ = fmt.Fprintf(
inv.Stdout, "\nThe %s workspace has been started at %s!\n",
cliui.Keyword(workspace.Name), cliui.Timestamp(time.Now()),
)
return nil
},
}
+6 -1
View File
@@ -47,7 +47,12 @@ func (r *RootCmd) stop() *clibase.Cmd {
return err
}
_, _ = fmt.Fprintf(inv.Stdout, "\nThe %s workspace has been stopped at %s!\n", cliui.DefaultStyles.Keyword.Render(workspace.Name), cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp)))
_, _ = fmt.Fprintf(
inv.Stdout,
"\nThe %s workspace has been stopped at %s!\n", cliui.Keyword(workspace.Name),
cliui.Timestamp(time.Now()),
)
return nil
},
}
+12 -8
View File
@@ -14,6 +14,8 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/util/ptr"
@@ -144,11 +146,13 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
return err
}
_, _ = fmt.Fprintln(inv.Stdout, "\n"+cliui.DefaultStyles.Wrap.Render(
"The "+cliui.DefaultStyles.Keyword.Render(templateName)+" template has been created at "+cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp))+"! "+
_, _ = fmt.Fprintln(inv.Stdout, "\n"+pretty.Sprint(cliui.DefaultStyles.Wrap,
"The "+pretty.Sprint(
cliui.DefaultStyles.Keyword, templateName)+" template has been created at "+
pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp))+"! "+
"Developers can provision a workspace with this template using:")+"\n")
_, _ = fmt.Fprintln(inv.Stdout, " "+cliui.DefaultStyles.Code.Render(fmt.Sprintf("coder create --template=%q [workspace name]", templateName)))
_, _ = fmt.Fprintln(inv.Stdout, " "+pretty.Sprint(cliui.DefaultStyles.Code, fmt.Sprintf("coder create --template=%q [workspace name]", templateName)))
_, _ = fmt.Fprintln(inv.Stdout)
return nil
@@ -331,12 +335,12 @@ func prettyDirectoryPath(dir string) string {
if err != nil {
return dir
}
pretty := dir
if strings.HasPrefix(pretty, homeDir) {
pretty = strings.TrimPrefix(pretty, homeDir)
pretty = "~" + pretty
prettyDir := dir
if strings.HasPrefix(prettyDir, homeDir) {
prettyDir = strings.TrimPrefix(prettyDir, homeDir)
prettyDir = "~" + prettyDir
}
return pretty
return prettyDir
}
func ParseProvisionerTags(rawTags []string) (map[string]string, error) {
+6 -2
View File
@@ -7,6 +7,8 @@ import (
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
@@ -77,7 +79,7 @@ func (r *RootCmd) templateDelete() *clibase.Cmd {
// Confirm deletion of the template.
_, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: fmt.Sprintf("Delete these templates: %s?", cliui.DefaultStyles.Code.Render(strings.Join(templateNames, ", "))),
Text: fmt.Sprintf("Delete these templates: %s?", pretty.Sprint(cliui.DefaultStyles.Code, strings.Join(templateNames, ", "))),
IsConfirm: true,
Default: cliui.ConfirmNo,
})
@@ -91,7 +93,9 @@ func (r *RootCmd) templateDelete() *clibase.Cmd {
return xerrors.Errorf("delete template %q: %w", template.Name, err)
}
_, _ = fmt.Fprintln(inv.Stdout, "Deleted template "+cliui.DefaultStyles.Code.Render(template.Name)+" at "+cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp))+"!")
_, _ = fmt.Fprintln(
inv.Stdout, "Deleted template "+pretty.Sprint(cliui.DefaultStyles.Keyword, template.Name)+" at "+cliui.Timestamp(time.Now()),
)
}
return nil
+4 -2
View File
@@ -8,6 +8,8 @@ import (
"github.com/stretchr/testify/require"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/coderdtest"
@@ -37,7 +39,7 @@ func TestTemplateDelete(t *testing.T) {
execDone <- inv.Run()
}()
pty.ExpectMatch(fmt.Sprintf("Delete these templates: %s?", cliui.DefaultStyles.Code.Render(template.Name)))
pty.ExpectMatch(fmt.Sprintf("Delete these templates: %s?", pretty.Sprint(cliui.DefaultStyles.Code, template.Name)))
pty.WriteLine("yes")
require.NoError(t, <-execDone)
@@ -95,7 +97,7 @@ func TestTemplateDelete(t *testing.T) {
execDone <- inv.Run()
}()
pty.ExpectMatch(fmt.Sprintf("Delete these templates: %s?", cliui.DefaultStyles.Code.Render(strings.Join(templateNames, ", "))))
pty.ExpectMatch(fmt.Sprintf("Delete these templates: %s?", pretty.Sprint(cliui.DefaultStyles.Code, strings.Join(templateNames, ", "))))
pty.WriteLine("yes")
require.NoError(t, <-execDone)
+3 -1
View File
@@ -8,6 +8,8 @@ import (
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
@@ -114,7 +116,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
if err != nil {
return xerrors.Errorf("update template metadata: %w", err)
}
_, _ = fmt.Fprintf(inv.Stdout, "Updated template metadata at %s!\n", cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp)))
_, _ = fmt.Fprintf(inv.Stdout, "Updated template metadata at %s!\n", pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp)))
return nil
},
}
+19 -9
View File
@@ -17,6 +17,7 @@ import (
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/examples"
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/pretty"
)
func (*RootCmd) templateInit() *clibase.Cmd {
@@ -42,17 +43,21 @@ func (*RootCmd) templateInit() *clibase.Cmd {
for _, example := range exampleList {
name := fmt.Sprintf(
"%s\n%s\n%s\n",
cliui.DefaultStyles.Bold.Render(example.Name),
cliui.DefaultStyles.Wrap.Copy().PaddingLeft(6).Render(example.Description),
cliui.DefaultStyles.Keyword.Copy().PaddingLeft(6).Render(example.URL),
cliui.Bold(example.Name),
pretty.Sprint(cliui.DefaultStyles.Wrap.With(pretty.XPad(6, 0)), example.Description),
pretty.Sprint(cliui.DefaultStyles.Keyword.With(pretty.XPad(6, 0)), example.URL),
)
optsToID[name] = example.ID
}
opts := maps.Keys(optsToID)
sort.Strings(opts)
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Wrap.Render(
"A template defines infrastructure as code to be provisioned "+
"for individual developer workspaces. Select an example to be copied to the active directory:\n"))
_, _ = fmt.Fprintln(
inv.Stdout,
pretty.Sprint(
cliui.DefaultStyles.Wrap,
"A template defines infrastructure as code to be provisioned "+
"for individual developer workspaces. Select an example to be copied to the active directory:\n"),
)
selected, err := cliui.Select(inv, cliui.SelectOptions{
Options: opts,
})
@@ -94,7 +99,7 @@ func (*RootCmd) templateInit() *clibase.Cmd {
} else {
relPath = "./" + relPath
}
_, _ = fmt.Fprintf(inv.Stdout, "Extracting %s to %s...\n", cliui.DefaultStyles.Field.Render(selectedTemplate.ID), relPath)
_, _ = fmt.Fprintf(inv.Stdout, "Extracting %s to %s...\n", pretty.Sprint(cliui.DefaultStyles.Field, selectedTemplate.ID), relPath)
err = os.MkdirAll(directory, 0o700)
if err != nil {
return err
@@ -104,8 +109,13 @@ func (*RootCmd) templateInit() *clibase.Cmd {
return err
}
_, _ = fmt.Fprintln(inv.Stdout, "Create your template by running:")
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Paragraph.Render(cliui.DefaultStyles.Code.Render("cd "+relPath+" && coder templates create"))+"\n")
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Wrap.Render("Examples provide a starting point and are expected to be edited! 🎨"))
_, _ = fmt.Fprintln(
inv.Stdout,
pretty.Sprint(
cliui.DefaultStyles.Code,
"cd "+relPath+" && coder templates create"),
)
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Wrap, "\nExamples provide a starting point and are expected to be edited! 🎨"))
return nil
},
}
+9 -6
View File
@@ -11,6 +11,8 @@ import (
"github.com/briandowns/spinner"
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
@@ -85,7 +87,7 @@ func (pf *templateUploadFlags) upload(inv *clibase.Invocation, client *codersdk.
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond)
spin.Writer = inv.Stdout
spin.Suffix = cliui.DefaultStyles.Keyword.Render(" Uploading directory...")
spin.Suffix = pretty.Sprint(cliui.DefaultStyles.Keyword, " Uploading directory...")
spin.Start()
defer spin.Stop()
@@ -110,7 +112,7 @@ func (pf *templateUploadFlags) checkForLockfile(inv *clibase.Invocation) error {
if !hasLockfile {
cliui.Warn(inv.Stdout, "No .terraform.lock.hcl file found",
"When provisioning, Coder will be unable to cache providers without a lockfile and must download them from the internet each time.",
"Create one by running "+cliui.DefaultStyles.Code.Render("terraform init")+" in your template directory.",
"Create one by running "+pretty.Sprint(cliui.DefaultStyles.Code, "terraform init")+" in your template directory.",
)
}
return nil
@@ -246,9 +248,10 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
return err
}
_, _ = fmt.Fprintln(inv.Stdout, "\n"+cliui.DefaultStyles.Wrap.Render(
"The "+cliui.DefaultStyles.Keyword.Render(name)+" template has been created at "+cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp))+"! "+
"Developers can provision a workspace with this template using:")+"\n")
_, _ = fmt.Fprintln(
inv.Stdout, "\n"+cliui.Wrap(
"The "+cliui.Keyword(name)+" template has been created at "+cliui.Timestamp(time.Now())+"! "+
"Developers can provision a workspace with this template using:")+"\n")
} else if activate {
err = client.UpdateActiveTemplateVersion(inv.Context(), template.ID, codersdk.UpdateActiveTemplateVersion{
ID: job.ID,
@@ -258,7 +261,7 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
}
}
_, _ = fmt.Fprintf(inv.Stdout, "Updated version at %s!\n", cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp)))
_, _ = fmt.Fprintf(inv.Stdout, "Updated version at %s!\n", pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp)))
return nil
},
}
+3 -1
View File
@@ -5,6 +5,8 @@ import (
"github.com/google/uuid"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
@@ -75,7 +77,7 @@ func templatesToRows(templates ...codersdk.Template) []templateTableRow {
OrganizationID: template.OrganizationID,
Provisioner: template.Provisioner,
ActiveVersionID: template.ActiveVersionID,
UsedBy: cliui.DefaultStyles.Fuchsia.Render(formatActiveDevelopers(template.ActiveUserCount)),
UsedBy: pretty.Sprint(cliui.DefaultStyles.Fuchsia, formatActiveDevelopers(template.ActiveUserCount)),
DefaultTTL: (time.Duration(template.DefaultTTLMillis) * time.Millisecond),
}
}
+1 -1
View File
@@ -101,7 +101,7 @@ func templateVersionsToRows(activeVersionID uuid.UUID, templateVersions ...coder
for i, templateVersion := range templateVersions {
activeStatus := ""
if templateVersion.ID == activeVersionID {
activeStatus = cliui.DefaultStyles.Code.Render(cliui.DefaultStyles.Keyword.Render("Active"))
activeStatus = cliui.Keyword("Active")
}
rows[i] = templateVersionRow{
+4 -4
View File
@@ -1,13 +1,13 @@
Usage: coder [global-flags] <subcommand>
Coder v0.0.0-devel — A tool for provisioning self-hosted development environments with Terraform.
- Start a Coder server:
- Start a Coder server:
 $ coder server 
$ coder server
- Get started by creating a template from an example:
- Get started by creating a template from an example:
 $ coder templates init 
$ coder templates init
Subcommands
config-ssh Add an SSH Host entry for your workspaces "ssh
+5 -5
View File
@@ -2,14 +2,14 @@ Usage: coder config-ssh [flags]
Add an SSH Host entry for your workspaces "ssh coder.workspace"
- You can use -o (or --ssh-option) so set SSH options to be used for all your
workspaces:
- You can use -o (or --ssh-option) so set SSH options to be used for all your
workspaces:
 $ coder config-ssh -o ForwardAgent=yes 
$ coder config-ssh -o ForwardAgent=yes
- You can use --dry-run (or -n) to see the changes that would be made:
- You can use --dry-run (or -n) to see the changes that would be made:
 $ coder config-ssh --dry-run 
$ coder config-ssh --dry-run
Options
--coder-binary-path string, $CODER_SSH_CONFIG_BINARY_PATH
+2 -2
View File
@@ -2,9 +2,9 @@ Usage: coder create [flags] [name]
Create a workspace
- Create a workspace for another user (if you have permission):
- Create a workspace for another user (if you have permission):
 $ coder create <username>/<workspace_name> 
$ coder create <username>/<workspace_name>
Options
--parameter string-array, $CODER_RICH_PARAMETER
+2 -2
View File
@@ -2,9 +2,9 @@ Usage: coder dotfiles [flags] <git_repo_url>
Personalize your workspace by applying a canonical dotfiles repository
- Check out and install a dotfiles repository without prompts:
- Check out and install a dotfiles repository without prompts:
 $ coder dotfiles --yes git@github.com:example/dotfiles.git 
$ coder dotfiles --yes git@github.com:example/dotfiles.git
Options
-b, --branch string
+12 -12
View File
@@ -5,27 +5,27 @@ forwarding, use "coder ssh -R".
Aliases: tunnel
- Port forward a single TCP port from 1234 in the workspace to port 5678 on
your local machine:
- Port forward a single TCP port from 1234 in the workspace to port 5678 on your
local machine:
 $ coder port-forward <workspace> --tcp 5678:1234 
$ coder port-forward <workspace> --tcp 5678:1234
- Port forward a single UDP port from port 9000 to port 9000 on your local
machine:
- Port forward a single UDP port from port 9000 to port 9000 on your local
machine:
 $ coder port-forward <workspace> --udp 9000 
$ coder port-forward <workspace> --udp 9000
- Port forward multiple TCP ports and a UDP port:
- Port forward multiple TCP ports and a UDP port:
 $ coder port-forward <workspace> --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53 
$ coder port-forward <workspace> --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53
- Port forward multiple ports (TCP or UDP) in condensed syntax:
- Port forward multiple ports (TCP or UDP) in condensed syntax:
 $ coder port-forward <workspace> --tcp 8080,9000:3000,9090-9092,10000-10002:10010-10012 
$ coder port-forward <workspace> --tcp 8080,9000:3000,9090-9092,10000-10002:10010-10012
- Port forward specifying the local address to bind to:
- Port forward specifying the local address to bind to:
 $ coder port-forward <workspace> --tcp 1.2.3.4:8080:8080 
$ coder port-forward <workspace> --tcp 1.2.3.4:8080:8080
Options
-p, --tcp string-array, $CODER_PORT_FORWARD_TCP
+1 -1
View File
@@ -6,7 +6,7 @@ Override the stop time of a currently running workspace instance.
* The new stop time must be at least 30 minutes in the future.
* The workspace template may restrict the maximum workspace runtime.
 $ coder schedule override-stop my-workspace 90m 
$ coder schedule override-stop my-workspace 90m
---
Run `coder --help` for a list of global options.
+2 -2
View File
@@ -12,9 +12,9 @@ Schedule format: <start-time> [day-of-week] [location].
If omitted, we will fall back to either the TZ environment variable or /etc/localtime.
You can check your corresponding location by visiting https://ipinfo.io - it shows in the demo widget on the right.
- Set the workspace to start at 9:30am (in Dublin) from Monday to Friday:
- Set the workspace to start at 9:30am (in Dublin) from Monday to Friday:
 $ coder schedule start my-workspace 9:30AM Mon-Fri Europe/Dublin 
$ coder schedule start my-workspace 9:30AM Mon-Fri Europe/Dublin
---
Run `coder --help` for a list of global options.
+1 -1
View File
@@ -15,7 +15,7 @@ When enabling scheduled stop, enter a duration in one of the following formats:
* 2m (2 minutes)
* 2 (2 minutes)
 $ coder schedule stop my-workspace 2h30m 
$ coder schedule stop my-workspace 2h30m
---
Run `coder --help` for a list of global options.
+5 -5
View File
@@ -5,17 +5,17 @@ Manage templates
Aliases: template
Templates are written in standard Terraform and describe the infrastructure for workspaces
- Create a template for developers to create workspaces:
- Create a template for developers to create workspaces:
 $ coder templates create 
$ coder templates create
- Make changes to your template, and plan the changes:
- Make changes to your template, and plan the changes:
 $ coder templates plan my-template 
$ coder templates plan my-template
- Push an update to the template. Your developers can update their workspaces:
 $ coder templates push my-template 
$ coder templates push my-template
Subcommands
create Create a template from the current directory or as specified by
+2 -2
View File
@@ -4,9 +4,9 @@ Manage different versions of the specified template
Aliases: version
- List versions of a specific template:
- List versions of a specific template:
 $ coder templates versions list my-template 
$ coder templates versions list my-template
Subcommands
list List all the versions of the specified template
+6 -6
View File
@@ -5,17 +5,17 @@ Manage personal access tokens
Aliases: token
Tokens are used to authenticate automated clients to Coder.
- Create a token for automation:
- Create a token for automation:
 $ coder tokens create 
$ coder tokens create
- List your tokens:
- List your tokens:
 $ coder tokens ls 
$ coder tokens ls
- Remove a token by ID:
- Remove a token by ID:
 $ coder tokens rm WuoWs4ZsMX 
$ coder tokens rm WuoWs4ZsMX
Subcommands
create Create a token
+1 -1
View File
@@ -5,7 +5,7 @@ platform
Aliases: active
 $ coder users activate example_user 
$ coder users activate example_user
Options
-c, --column string-array (default: username,email,created_at,status)
+1 -1
View File
@@ -2,7 +2,7 @@ Usage: coder users show [flags] <username|user_id|'me'>
Show a single user. Use 'me' to indicate the currently authenticated user.
 $ coder users show me 
$ coder users show me
Options
-o, --output string (default: table)
+1 -1
View File
@@ -5,7 +5,7 @@ platform
Aliases: rm, delete
 $ coder users suspend example_user 
$ coder users suspend example_user
Options
-c, --column string-array (default: username,email,created_at,status)
+7 -5
View File
@@ -7,6 +7,8 @@ import (
"github.com/go-playground/validator/v10"
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
@@ -88,7 +90,7 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
authenticationMethod := ""
switch codersdk.LoginType(strings.ToLower(string(userLoginType))) {
case codersdk.LoginTypePassword:
authenticationMethod = `Your password is: ` + cliui.DefaultStyles.Field.Render(password)
authenticationMethod = `Your password is: ` + pretty.Sprint(cliui.DefaultStyles.Field, password)
case codersdk.LoginTypeNone:
authenticationMethod = "Login has been disabled for this user. Contact your administrator to authenticate."
case codersdk.LoginTypeGithub:
@@ -99,16 +101,16 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
_, _ = fmt.Fprintln(inv.Stderr, `A new user has been created!
Share the instructions below to get them started.
`+cliui.DefaultStyles.Placeholder.Render("—————————————————————————————————————————————————")+`
`+pretty.Sprint(cliui.DefaultStyles.Placeholder, "—————————————————————————————————————————————————")+`
Download the Coder command line for your operating system:
https://github.com/coder/coder/releases
Run `+cliui.DefaultStyles.Code.Render("coder login "+client.URL.String())+` to authenticate.
Run `+pretty.Sprint(cliui.DefaultStyles.Code, "coder login "+client.URL.String())+` to authenticate.
Your email is: `+cliui.DefaultStyles.Field.Render(email)+`
Your email is: `+pretty.Sprint(cliui.DefaultStyles.Field, email)+`
`+authenticationMethod+`
Create a workspace `+cliui.DefaultStyles.Code.Render("coder create")+`!`)
Create a workspace `+pretty.Sprint(cliui.DefaultStyles.Code, "coder create")+`!`)
return nil
},
}
+3 -1
View File
@@ -6,6 +6,8 @@ import (
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
@@ -89,7 +91,7 @@ func (r *RootCmd) createUserStatusCommand(sdkStatus codersdk.UserStatus) *clibas
return xerrors.Errorf("%s user: %w", verb, err)
}
_, _ = fmt.Fprintf(inv.Stdout, "\nUser %s has been %s!\n", cliui.DefaultStyles.Keyword.Render(user.Username), pastVerb)
_, _ = fmt.Fprintf(inv.Stdout, "\nUser %s has been %s!\n", pretty.Sprint(cliui.DefaultStyles.Keyword, user.Username), pastVerb)
return nil
},
}
+4 -2
View File
@@ -5,6 +5,8 @@ import (
"strings"
"time"
"github.com/coder/pretty"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
@@ -39,9 +41,9 @@ func (vi versionInfo) String() string {
_, _ = str.WriteString("\r\n" + vi.ExternalURL + "\r\n\r\n")
if vi.Slim {
_, _ = str.WriteString(fmt.Sprintf("Slim build of Coder, does not support the %s subcommand.", cliui.DefaultStyles.Code.Render("server")))
_, _ = str.WriteString(fmt.Sprintf("Slim build of Coder, does not support the %s subcommand.", pretty.Sprint(cliui.DefaultStyles.Code, "server")))
} else {
_, _ = str.WriteString(fmt.Sprintf("Full build of Coder, supports the %s subcommand.", cliui.DefaultStyles.Code.Render("server")))
_, _ = str.WriteString(fmt.Sprintf("Full build of Coder, supports the %s subcommand.", pretty.Sprint(cliui.DefaultStyles.Code, "server")))
}
return str.String()
}
+2 -11
View File
@@ -6,28 +6,19 @@ import (
"strings"
"testing"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/testutil"
)
// We need to override the global color profile to test escape codes.
//
//nolint:tparallel,paralleltest
func TestVersion(t *testing.T) {
ogColorProfile := lipgloss.ColorProfile()
lipgloss.SetColorProfile(termenv.ANSI)
t.Cleanup(func() {
lipgloss.SetColorProfile(ogColorProfile)
})
t.Parallel()
expectedText := `Coder v0.0.0-devel
https://github.com/coder/coder
Full build of Coder, supports the  server  subcommand.
Full build of Coder, supports the server subcommand.
`
expectedJSON := `{
"version": "v0.0.0-devel",