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 17:28:22 -04:00
committed by GitHub
parent 8421f56137
commit dd97fe2bce
80 changed files with 490 additions and 330 deletions

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!