Skip to content
112 changes: 40 additions & 72 deletions cli/root.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cli

import (
"bufio"
"context"
"errors"
"flag"
Expand All @@ -14,14 +13,11 @@ import (
"os"
"os/signal"
"path/filepath"
"regexp"
"runtime"
"strings"
"syscall"
"time"
"unicode/utf8"

"golang.org/x/crypto/ssh/terminal"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"

Expand Down Expand Up @@ -822,89 +818,61 @@ func isConnectionError(err error) bool {
}

type prettyErrorFormatter struct {
level int
w io.Writer
}

func (prettyErrorFormatter) prefixLines(spaces int, s string) string {
twidth, _, err := terminal.GetSize(0)
if err != nil {
twidth = 80
}

s = lipgloss.NewStyle().Width(twidth - spaces).Render(s)

var b strings.Builder
scanner := bufio.NewScanner(strings.NewReader(s))
for i := 0; scanner.Scan(); i++ {
// The first line is already padded.
if i == 0 {
_, _ = fmt.Fprintf(&b, "%s\n", scanner.Text())
continue
}
_, _ = fmt.Fprintf(&b, "%s%s\n", strings.Repeat(" ", spaces), scanner.Text())
}
return strings.TrimSuffix(strings.TrimSuffix(b.String(), "\n"), " ")
w io.Writer
}

func (p *prettyErrorFormatter) format(err error) {
underErr := errors.Unwrap(err)

arrowStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#515151"))
errTail := errors.Unwrap(err)

//nolint:errorlint
if _, ok := err.(*clibase.RunCommandError); ok && p.level == 0 && underErr != nil {
// We can do a better job now.
p.format(underErr)
if _, ok := err.(*clibase.RunCommandError); ok && errTail != nil {
// Avoid extra nesting.
p.format(errTail)
return
}

var (
padding string
arrowWidth int
)
if p.level > 0 {
const arrow = "┗━ "
arrowWidth = utf8.RuneCount([]byte(arrow))
padding = strings.Repeat(" ", arrowWidth*p.level)
_, _ = fmt.Fprintf(p.w, "%v%v", padding, arrowStyle.Render(arrow))
}

if underErr != nil {
header := strings.TrimSuffix(err.Error(), ": "+underErr.Error())
_, _ = fmt.Fprintf(p.w, "%s\n", p.prefixLines(len(padding)+arrowWidth, header))
p.level++
p.format(underErr)
return
var headErr string
if errTail != nil {
headErr = strings.TrimSuffix(err.Error(), ": "+errTail.Error())
} else {
headErr = err.Error()
}

{
style := lipgloss.NewStyle().Foreground(lipgloss.Color("#D16644")).Background(lipgloss.Color("#000000")).Bold(false)
// This is the last error in a tree.
p.wrappedPrintf(
"%s\n",
p.prefixLines(
len(padding)+arrowWidth,
fmt.Sprintf(
"%s%s%s",
lipgloss.NewStyle().Inherit(style).Underline(true).Render("ERROR"),
lipgloss.NewStyle().Inherit(style).Foreground(arrowStyle.GetForeground()).Render(" ► "),
style.Render(err.Error()),
),
),
)
var msg string
var sdkError *codersdk.Error
if errors.As(err, &sdkError) {
// We don't want to repeat the same error message twice, so we
// only show the SDK error on the top of the stack.
msg = sdkError.Message
if sdkError.Helper != "" {
msg = msg + "\n" + sdkError.Helper
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newline here makes sense and makes this look nice. 👍🏻

}
// The SDK error is usually good enough, and we don't want to overwhelm
// the user with output.
errTail = nil
} else {
msg = headErr
}
}

func (p *prettyErrorFormatter) wrappedPrintf(format string, a ...interface{}) {
s := lipgloss.NewStyle().Width(ttyWidth()).Render(
fmt.Sprintf(format, a...),
headStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#D16644"))
p.printf(
headStyle,
"%s",
msg,
)

// Not sure why, but lipgloss is adding extra spaces we need to remove.
excessSpaceRe := regexp.MustCompile(`[[:blank:]]*\n[[:blank:]]*$`)
s = excessSpaceRe.ReplaceAllString(s, "\n")
tailStyle := headStyle.Copy().Foreground(lipgloss.Color("#969696"))

if errTail != nil {
p.printf(headStyle, ": ")
// Grey out the less important, deep errors.
p.printf(tailStyle, "%s", errTail.Error())
}
p.printf(tailStyle, "\n")
}

func (p *prettyErrorFormatter) printf(style lipgloss.Style, format string, a ...interface{}) {
s := style.Render(fmt.Sprintf(format, a...))
_, _ = p.w.Write(
[]byte(
s,
Expand Down