From 2390dea254e83341cdb062be81842444059af79e Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Mon, 22 Jul 2024 12:24:23 -0400 Subject: [PATCH] feat!: simplify color interface and style --- align.go | 9 +- ansi_unix.go | 6 +- ansi_windows.go | 31 +++--- borders.go | 8 +- color.go | 206 ++++++++++++++++------------------------ color_test.go | 58 ++++++----- examples/layout/main.go | 17 ++-- examples/ssh/main.go | 116 ++++++---------------- get.go | 1 + go.mod | 4 +- go.sum | 4 - position.go | 26 +---- runes_test.go | 10 +- set.go | 7 -- style.go | 72 +++++--------- style_test.go | 47 ++++----- whitespace.go | 26 ++--- 17 files changed, 246 insertions(+), 402 deletions(-) diff --git a/align.go b/align.go index 805d1e0d..8e3cf4c2 100644 --- a/align.go +++ b/align.go @@ -4,13 +4,12 @@ import ( "strings" "github.com/charmbracelet/x/ansi" - "github.com/muesli/termenv" ) // Perform text alignment. If the string is multi-lined, we also make all lines -// the same width by padding them with spaces. If a termenv style is passed, -// use that to style the spaces added. -func alignTextHorizontal(str string, pos Position, width int, style *termenv.Style) string { +// the same width by padding them with spaces. If a style is passed, use that +// to style the spaces added. +func alignTextHorizontal(str string, pos Position, width int, style *ansi.Style) string { lines, widestLine := getLines(str) var b strings.Builder @@ -59,7 +58,7 @@ func alignTextHorizontal(str string, pos Position, width int, style *termenv.Sty return b.String() } -func alignTextVertical(str string, pos Position, height int, _ *termenv.Style) string { +func alignTextVertical(str string, pos Position, height int, _ *ansi.Style) string { strHeight := strings.Count(str, "\n") + 1 if height < strHeight { return str diff --git a/ansi_unix.go b/ansi_unix.go index d416b8c9..12822f76 100644 --- a/ansi_unix.go +++ b/ansi_unix.go @@ -3,5 +3,7 @@ package lipgloss -// enableLegacyWindowsANSI is only needed on Windows. -func enableLegacyWindowsANSI() {} +import "os" + +// EnableLegacyWindowsANSI is only needed on Windows. +func EnableLegacyWindowsANSI(*os.File) {} diff --git a/ansi_windows.go b/ansi_windows.go index 0cf56e4c..1b8e3195 100644 --- a/ansi_windows.go +++ b/ansi_windows.go @@ -4,19 +4,28 @@ package lipgloss import ( - "sync" + "os" - "github.com/muesli/termenv" + "golang.org/x/sys/windows" ) -var enableANSI sync.Once +// EnableLegacyWindowsANSI enables support for ANSI color sequences in the +// Windows default console (cmd.exe and the PowerShell application). Note that +// this only works with Windows 10 and greater. Also note that Windows Terminal +// supports colors by default. +func EnableLegacyWindowsANSI(f *os.File) { + var mode uint32 + handle := windows.Handle(f.Fd()) + err := windows.GetConsoleMode(handle, &mode) + if err != nil { + return + } -// enableANSIColors enables support for ANSI color sequences in the Windows -// default console (cmd.exe and the PowerShell application). Note that this -// only works with Windows 10. Also note that Windows Terminal supports colors -// by default. -func enableLegacyWindowsANSI() { - enableANSI.Do(func() { - _, _ = termenv.EnableWindowsANSIConsole() - }) + // See https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences + if mode&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING { + vtpmode := mode | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING + if err := windows.SetConsoleMode(handle, vtpmode); err != nil { + return + } + } } diff --git a/borders.go b/borders.go index deb6b35a..226a0a86 100644 --- a/borders.go +++ b/borders.go @@ -4,7 +4,6 @@ import ( "strings" "github.com/charmbracelet/x/ansi" - "github.com/muesli/termenv" "github.com/rivo/uniseg" ) @@ -407,13 +406,12 @@ func (s Style) styleBorder(border string, fg, bg TerminalColor) string { return border } - style := termenv.Style{} - + var style ansi.Style if fg != noColor { - style = style.Foreground(fg.color(s.r)) + style = style.ForegroundColor(fg) } if bg != noColor { - style = style.Background(bg.color(s.r)) + style = style.BackgroundColor(bg) } return style.Styled(border) diff --git a/color.go b/color.go index 5dfb3cfe..bd0636b3 100644 --- a/color.go +++ b/color.go @@ -3,13 +3,34 @@ package lipgloss import ( "strconv" - "github.com/muesli/termenv" + "github.com/charmbracelet/x/ansi" + "github.com/lucasb-eyer/go-colorful" +) + +// 4-bit color constants. +const ( + Black ansi.BasicColor = iota + Red + Green + Yellow + Blue + Magenta + Cyan + White + + BrightBlack + BrightRed + BrightGreen + BrightYellow + BrightBlue + BrightMagenta + BrightCyan + BrightWhite ) // TerminalColor is a color intended to be rendered in the terminal. type TerminalColor interface { - color(*Renderer) termenv.Color - RGBA() (r, g, b, a uint32) + ansi.Color } var noColor = NoColor{} @@ -23,150 +44,89 @@ var noColor = NoColor{} // var style = someStyle.Background(lipgloss.NoColor{}) type NoColor struct{} -func (NoColor) color(*Renderer) termenv.Color { - return termenv.NoColor{} -} - // RGBA returns the RGBA value of this color. Because we have to return // something, despite this color being the absence of color, we're returning // black with 100% opacity. // // Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. -// -// Deprecated. func (n NoColor) RGBA() (r, g, b, a uint32) { return 0x0, 0x0, 0x0, 0xFFFF //nolint:gomnd } -// Color specifies a color by hex or ANSI value. For example: +// Color specifies a color by hex or ANSI256 value. For example: // -// ansiColor := lipgloss.Color("21") +// ansiColor := lipgloss.Color(21) // hexColor := lipgloss.Color("#0000ff") -type Color string - -func (c Color) color(r *Renderer) termenv.Color { - return r.ColorProfile().Color(string(c)) -} - -// RGBA returns the RGBA value of this color. This satisfies the Go Color -// interface. Note that on error we return black with 100% opacity, or: -// -// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. -// -// Deprecated. -func (c Color) RGBA() (r, g, b, a uint32) { - return termenv.ConvertToRGB(c.color(renderer)).RGBA() +// uint32Color := lipgloss.Color(0xff0000) +func Color[T string | int](c T) TerminalColor { + var col TerminalColor = noColor + switch c := any(c).(type) { + case string: + if len(c) == 0 { + return col + } + if h, err := colorful.Hex(c); err == nil { + return h + } else if i, err := strconv.Atoi(c); err == nil { + if i < 16 { + return ansi.BasicColor(i) + } else if i < 256 { + return ansi.ExtendedColor(i) + } + return ansi.TrueColor(i) + } + case int: + if c < 16 { + return ansi.BasicColor(c) + } else if c < 256 { + return ansi.ExtendedColor(c) + } + return ansi.TrueColor(c) + } + return col } -// ANSIColor is a color specified by an ANSI color value. It's merely syntactic -// sugar for the more general Color function. Invalid colors will render as -// black. -// -// Example usage: -// -// // These two statements are equivalent. -// colorA := lipgloss.ANSIColor(21) -// colorB := lipgloss.Color("21") -type ANSIColor uint - -func (ac ANSIColor) color(r *Renderer) termenv.Color { - return Color(strconv.FormatUint(uint64(ac), 10)).color(r) +// RGBColor is a color specified by red, green, and blue values. +type RGBColor struct { + R uint8 + G uint8 + B uint8 } // RGBA returns the RGBA value of this color. This satisfies the Go Color -// interface. Note that on error we return black with 100% opacity, or: -// -// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. -// -// Deprecated. -func (ac ANSIColor) RGBA() (r, g, b, a uint32) { - cf := Color(strconv.FormatUint(uint64(ac), 10)) - return cf.RGBA() +// interface. +func (c RGBColor) RGBA() (r, g, b, a uint32) { + r |= uint32(c.R) << 8 + g |= uint32(c.G) << 8 + b |= uint32(c.B) << 8 + a = 0xFFFF //nolint:gomnd + return } -// AdaptiveColor provides color options for light and dark backgrounds. The -// appropriate color will be returned at runtime based on the darkness of the -// terminal background color. +// ANSIColor is a color specified by an ANSI256 color value. // // Example usage: // -// color := lipgloss.AdaptiveColor{Light: "#0000ff", Dark: "#000099"} -type AdaptiveColor struct { - Light string - Dark string -} - -func (ac AdaptiveColor) color(r *Renderer) termenv.Color { - if r.HasDarkBackground() { - return Color(ac.Dark).color(r) - } - return Color(ac.Light).color(r) -} +// colorA := lipgloss.ANSIColor(8) +// colorB := lipgloss.ANSIColor(134) +type ANSIColor = ansi.ExtendedColor -// RGBA returns the RGBA value of this color. This satisfies the Go Color -// interface. Note that on error we return black with 100% opacity, or: -// -// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. -// -// Deprecated. -func (ac AdaptiveColor) RGBA() (r, g, b, a uint32) { - return termenv.ConvertToRGB(ac.color(renderer)).RGBA() -} - -// CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color -// profiles. Automatic color degradation will not be performed. -type CompleteColor struct { - TrueColor string - ANSI256 string - ANSI string -} - -func (c CompleteColor) color(r *Renderer) termenv.Color { - p := r.ColorProfile() - switch p { //nolint:exhaustive - case termenv.TrueColor: - return p.Color(c.TrueColor) - case termenv.ANSI256: - return p.Color(c.ANSI256) - case termenv.ANSI: - return p.Color(c.ANSI) - default: - return termenv.NoColor{} - } -} - -// RGBA returns the RGBA value of this color. This satisfies the Go Color -// interface. Note that on error we return black with 100% opacity, or: +// IsDarkColor returns whether the given color is dark. // -// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. -// CompleteAdaptiveColor specifies exact values for truecolor, ANSI256, and ANSI color +// Example usage: // -// Deprecated. -func (c CompleteColor) RGBA() (r, g, b, a uint32) { - return termenv.ConvertToRGB(c.color(renderer)).RGBA() -} - -// CompleteAdaptiveColor specifies exact values for truecolor, ANSI256, and ANSI color -// profiles, with separate options for light and dark backgrounds. Automatic -// color degradation will not be performed. -type CompleteAdaptiveColor struct { - Light CompleteColor - Dark CompleteColor -} - -func (cac CompleteAdaptiveColor) color(r *Renderer) termenv.Color { - if r.HasDarkBackground() { - return cac.Dark.color(r) +// color := lipgloss.Color("#0000ff") +// if lipgloss.IsDarkColor(color) { +// fmt.Println("It's dark!") +// } else { +// fmt.Println("It's light!") +// } +func IsDarkColor(c TerminalColor) bool { + col, ok := colorful.MakeColor(c) + if !ok { + return true } - return cac.Light.color(r) -} -// RGBA returns the RGBA value of this color. This satisfies the Go Color -// interface. Note that on error we return black with 100% opacity, or: -// -// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. -// -// Deprecated. -func (cac CompleteAdaptiveColor) RGBA() (r, g, b, a uint32) { - return termenv.ConvertToRGB(cac.color(renderer)).RGBA() + _, _, l := col.Hsl() + return l < 0.5 } diff --git a/color_test.go b/color_test.go index 0881076c..2825a298 100644 --- a/color_test.go +++ b/color_test.go @@ -3,38 +3,36 @@ package lipgloss import ( "image/color" "testing" - - "github.com/muesli/termenv" ) func TestSetColorProfile(t *testing.T) { - r := renderer + r := DefaultRenderer() input := "hello" tt := []struct { name string - profile termenv.Profile + profile Profile expected string }{ { "ascii", - termenv.Ascii, + Ascii, "hello", }, { "ansi", - termenv.ANSI, - "\x1b[94mhello\x1b[0m", + ANSI, + "\x1b[94mhello\x1b[m", }, { "ansi256", - termenv.ANSI256, - "\x1b[38;5;62mhello\x1b[0m", + ANSI256, + "\x1b[38;5;62mhello\x1b[m", }, { "truecolor", - termenv.TrueColor, - "\x1b[38;2;89;86;224mhello\x1b[0m", + TrueColor, + "\x1b[38;2;89;86;224mhello\x1b[m", }, } @@ -89,76 +87,76 @@ func TestHexToColor(t *testing.T) { func TestRGBA(t *testing.T) { tt := []struct { - profile termenv.Profile + profile Profile darkBg bool input TerminalColor expected uint }{ // lipgloss.Color { - termenv.TrueColor, + TrueColor, true, Color("#FF0000"), 0xFF0000, }, { - termenv.TrueColor, + TrueColor, true, Color("9"), 0xFF0000, }, { - termenv.TrueColor, + TrueColor, true, Color("21"), 0x0000FF, }, // lipgloss.AdaptiveColor { - termenv.TrueColor, + TrueColor, true, AdaptiveColor{Light: "#0000FF", Dark: "#FF0000"}, 0xFF0000, }, { - termenv.TrueColor, + TrueColor, false, AdaptiveColor{Light: "#0000FF", Dark: "#FF0000"}, 0x0000FF, }, { - termenv.TrueColor, + TrueColor, true, AdaptiveColor{Light: "21", Dark: "9"}, 0xFF0000, }, { - termenv.TrueColor, + TrueColor, false, AdaptiveColor{Light: "21", Dark: "9"}, 0x0000FF, }, // lipgloss.CompleteColor { - termenv.TrueColor, + TrueColor, true, CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"}, 0xFF0000, }, { - termenv.ANSI256, + ANSI256, true, CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"}, 0xFFFFFF, }, { - termenv.ANSI, + ANSI, true, CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "12"}, 0x0000FF, }, { - termenv.TrueColor, + TrueColor, true, CompleteColor{TrueColor: "", ANSI256: "231", ANSI: "12"}, 0x000000, @@ -166,7 +164,7 @@ func TestRGBA(t *testing.T) { // lipgloss.CompleteAdaptiveColor // dark { - termenv.TrueColor, + TrueColor, true, CompleteAdaptiveColor{ Light: CompleteColor{TrueColor: "#0000FF", ANSI256: "231", ANSI: "12"}, @@ -175,7 +173,7 @@ func TestRGBA(t *testing.T) { 0xFF0000, }, { - termenv.ANSI256, + ANSI256, true, CompleteAdaptiveColor{ Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "21", ANSI: "12"}, @@ -184,7 +182,7 @@ func TestRGBA(t *testing.T) { 0xFFFFFF, }, { - termenv.ANSI, + ANSI, true, CompleteAdaptiveColor{ Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "9"}, @@ -194,7 +192,7 @@ func TestRGBA(t *testing.T) { }, // light { - termenv.TrueColor, + TrueColor, false, CompleteAdaptiveColor{ Light: CompleteColor{TrueColor: "#0000FF", ANSI256: "231", ANSI: "12"}, @@ -203,7 +201,7 @@ func TestRGBA(t *testing.T) { 0x0000FF, }, { - termenv.ANSI256, + ANSI256, false, CompleteAdaptiveColor{ Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "21", ANSI: "12"}, @@ -212,7 +210,7 @@ func TestRGBA(t *testing.T) { 0x0000FF, }, { - termenv.ANSI, + ANSI, false, CompleteAdaptiveColor{ Light: CompleteColor{TrueColor: "#FF0000", ANSI256: "231", ANSI: "9"}, @@ -227,7 +225,7 @@ func TestRGBA(t *testing.T) { r.SetColorProfile(tc.profile) r.SetHasDarkBackground(tc.darkBg) - r, g, b, _ := tc.input.RGBA() + r, g, b, _ := tc.input.color(r).RGBA() o := uint(r/256)<<16 + uint(g/256)<<8 + uint(b/256) if o != tc.expected { diff --git a/examples/layout/main.go b/examples/layout/main.go index e3281c6b..d329961e 100644 --- a/examples/layout/main.go +++ b/examples/layout/main.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/adaptive" "github.com/lucasb-eyer/go-colorful" "golang.org/x/term" ) @@ -27,9 +28,9 @@ var ( // General. - subtle = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"} - highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"} - special = lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"} + subtle = adaptive.AdaptiveColor("#D9DCCF", "#383838") + highlight = adaptive.AdaptiveColor("#874BFD", "#7D56F4") + special = adaptive.AdaptiveColor("#43BF6D", "#73F59F") divider = lipgloss.NewStyle(). SetString("•"). @@ -141,7 +142,7 @@ var ( listDone = func(s string) string { return checkMark + lipgloss.NewStyle(). Strikethrough(true). - Foreground(lipgloss.AdaptiveColor{Light: "#969B86", Dark: "#696969"}). + Foreground(adaptive.AdaptiveColor("#969B86", "#696969")). Render(s) } @@ -163,8 +164,8 @@ var ( Padding(0, 1) statusBarStyle = lipgloss.NewStyle(). - Foreground(lipgloss.AdaptiveColor{Light: "#343433", Dark: "#C1C6B2"}). - Background(lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#353533"}) + Foreground(adaptive.AdaptiveColor("#343433", "#C1C6B2")). + Background(adaptive.AdaptiveColor("#D9DCCF", "#353533")) statusStyle = lipgloss.NewStyle(). Inherit(statusBarStyle). @@ -243,7 +244,7 @@ func main() { lipgloss.Center, lipgloss.Center, dialogBoxStyle.Render(ui), lipgloss.WithWhitespaceChars("猫咪"), - lipgloss.WithWhitespaceForeground(subtle), + lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Foreground(subtle)), ) doc.WriteString(dialog + "\n\n") @@ -334,7 +335,7 @@ func main() { } // Okay, let's print it - fmt.Println(docStyle.Render(doc.String())) + lipgloss.Println(docStyle.Render(doc.String())) } func colorGrid(xSteps, ySteps int) [][]string { diff --git a/examples/ssh/main.go b/examples/ssh/main.go index e8c697be..6a6dbf33 100644 --- a/examples/ssh/main.go +++ b/examples/ssh/main.go @@ -12,15 +12,13 @@ package main import ( "fmt" "log" - "os" "strings" "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/adaptive" "github.com/charmbracelet/ssh" "github.com/charmbracelet/wish" - lm "github.com/charmbracelet/wish/logging" - "github.com/creack/pty" - "github.com/muesli/termenv" + "github.com/lucasb-eyer/go-colorful" ) // Available styles. @@ -40,100 +38,44 @@ type styles struct { } // Create new styles against a given renderer. -func makeStyles(r *lipgloss.Renderer) styles { +func makeStyles() styles { return styles{ - bold: r.NewStyle().SetString("bold").Bold(true), - faint: r.NewStyle().SetString("faint").Faint(true), - italic: r.NewStyle().SetString("italic").Italic(true), - underline: r.NewStyle().SetString("underline").Underline(true), - strikethrough: r.NewStyle().SetString("strikethrough").Strikethrough(true), - red: r.NewStyle().SetString("red").Foreground(lipgloss.Color("#E88388")), - green: r.NewStyle().SetString("green").Foreground(lipgloss.Color("#A8CC8C")), - yellow: r.NewStyle().SetString("yellow").Foreground(lipgloss.Color("#DBAB79")), - blue: r.NewStyle().SetString("blue").Foreground(lipgloss.Color("#71BEF2")), - magenta: r.NewStyle().SetString("magenta").Foreground(lipgloss.Color("#D290E4")), - cyan: r.NewStyle().SetString("cyan").Foreground(lipgloss.Color("#66C2CD")), - gray: r.NewStyle().SetString("gray").Foreground(lipgloss.Color("#B9BFCA")), + bold: lipgloss.NewStyle().SetString("bold").Bold(true), + faint: lipgloss.NewStyle().SetString("faint").Faint(true), + italic: lipgloss.NewStyle().SetString("italic").Italic(true), + underline: lipgloss.NewStyle().SetString("underline").Underline(true), + strikethrough: lipgloss.NewStyle().SetString("strikethrough").Strikethrough(true), + red: lipgloss.NewStyle().SetString("red").Foreground(lipgloss.Color("#E88388")), + green: lipgloss.NewStyle().SetString("green").Foreground(lipgloss.Color("#A8CC8C")), + yellow: lipgloss.NewStyle().SetString("yellow").Foreground(lipgloss.Color("#DBAB79")), + blue: lipgloss.NewStyle().SetString("blue").Foreground(lipgloss.Color("#71BEF2")), + magenta: lipgloss.NewStyle().SetString("magenta").Foreground(lipgloss.Color("#D290E4")), + cyan: lipgloss.NewStyle().SetString("cyan").Foreground(lipgloss.Color("#66C2CD")), + gray: lipgloss.NewStyle().SetString("gray").Foreground(lipgloss.Color("#B9BFCA")), } } -// Bridge Wish and Termenv so we can query for a user's terminal capabilities. -type sshOutput struct { - ssh.Session - tty *os.File -} - -func (s *sshOutput) Write(p []byte) (int, error) { - return s.Session.Write(p) -} - -func (s *sshOutput) Read(p []byte) (int, error) { - return s.Session.Read(p) -} - -func (s *sshOutput) Fd() uintptr { - return s.tty.Fd() -} - -type sshEnviron struct { - environ []string -} - -func (s *sshEnviron) Getenv(key string) string { - for _, v := range s.environ { - if strings.HasPrefix(v, key+"=") { - return v[len(key)+1:] - } - } - return "" -} - -func (s *sshEnviron) Environ() []string { - return s.environ -} - -// Create a termenv.Output from the session. -func outputFromSession(sess ssh.Session) *termenv.Output { - sshPty, _, _ := sess.Pty() - _, tty, err := pty.Open() - if err != nil { - log.Fatal(err) - } - o := &sshOutput{ - Session: sess, - tty: tty, - } - environ := sess.Environ() - environ = append(environ, fmt.Sprintf("TERM=%s", sshPty.Term)) - e := &sshEnviron{environ: environ} - // We need to use unsafe mode here because the ssh session is not running - // locally and we already know that the session is a TTY. - return termenv.NewOutput(o, termenv.WithUnsafe(), termenv.WithEnvironment(e)) -} - // Handle SSH requests. func handler(next ssh.Handler) ssh.Handler { return func(sess ssh.Session) { - // Get client's output. - clientOutput := outputFromSession(sess) - pty, _, active := sess.Pty() if !active { next(sess) return } - width := pty.Window.Width - // Initialize new renderer for the client. - renderer := lipgloss.NewRenderer(sess) - renderer.SetOutput(clientOutput) + environ := sess.Environ() + environ = append(environ, fmt.Sprintf("TERM=%s", pty.Term)) + output := adaptive.NewOutput(pty.Slave, pty.Slave, environ) + width := pty.Window.Width // Initialize new styles against the renderer. - styles := makeStyles(renderer) + styles := makeStyles() str := strings.Builder{} - fmt.Fprintf(&str, "\n\n%s %s %s %s %s", + fmt.Fprintf(&str, "\n\nProfile: %s\n%s %s %s %s %s", + output.ColorProfile().String(), styles.bold, styles.faint, styles.italic, @@ -161,18 +103,19 @@ func handler(next ssh.Handler) ssh.Handler { styles.gray, ) + col, _ := colorful.MakeColor(output.BackgroundColor) fmt.Fprintf(&str, "%s %t %s\n\n", styles.bold.UnsetString().Render("Has dark background?"), - renderer.HasDarkBackground(), - renderer.Output().BackgroundColor()) + output.HasDarkBackground(), + col.Hex()) - block := renderer.Place(width, + block := lipgloss.Place(width, lipgloss.Height(str.String()), lipgloss.Center, lipgloss.Center, str.String(), lipgloss.WithWhitespaceChars("/"), - lipgloss.WithWhitespaceForeground(lipgloss.AdaptiveColor{Light: "250", Dark: "236"}), + lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Foreground(output.AdaptiveColor(lipgloss.Color(250), lipgloss.Color(236)))), ) // Render to client. - wish.WriteString(sess, block) + output.Println(block) next(sess) } @@ -181,9 +124,10 @@ func handler(next ssh.Handler) ssh.Handler { func main() { port := 3456 s, err := wish.NewServer( + ssh.AllocatePty(), wish.WithAddress(fmt.Sprintf(":%d", port)), wish.WithHostKeyPath("ssh_example"), - wish.WithMiddleware(handler, lm.Middleware()), + wish.WithMiddleware(handler), ) if err != nil { log.Fatal(err) diff --git a/get.go b/get.go index 9c2f06fe..7afd0834 100644 --- a/get.go +++ b/get.go @@ -529,6 +529,7 @@ func (s Style) getAsTransform(propKey) func(string) string { // Split a string into lines, additionally returning the size of the widest // line. func getLines(s string) (lines []string, widest int) { + s = strings.ReplaceAll(s, "\t", " ") lines = strings.Split(s, "\n") for _, l := range lines { diff --git a/go.mod b/go.mod index de9c8269..0911a674 100644 --- a/go.mod +++ b/go.mod @@ -9,14 +9,14 @@ go 1.18 require ( github.com/aymanbagabas/go-udiff v0.2.0 github.com/charmbracelet/x/ansi v0.1.4 + github.com/lucasb-eyer/go-colorful v1.2.0 github.com/muesli/termenv v0.15.2 github.com/rivo/uniseg v0.4.7 + golang.org/x/sys v0.19.0 ) require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - golang.org/x/sys v0.19.0 // indirect ) diff --git a/go.sum b/go.sum index 1b8d1dc9..fee272a6 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,6 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= -github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk= -github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/ansi v0.1.3 h1:RBh/eleNWML5R524mjUF0yVRePTwqN9tPtV+DPgO5Lw= -github.com/charmbracelet/x/ansi v0.1.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= diff --git a/position.go b/position.go index 185f5af3..5144d487 100644 --- a/position.go +++ b/position.go @@ -34,26 +34,13 @@ const ( // Place places a string or text block vertically in an unstyled box of a given // width or height. func Place(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption) string { - return renderer.Place(width, height, hPos, vPos, str, opts...) -} - -// Place places a string or text block vertically in an unstyled box of a given -// width or height. -func (r *Renderer) Place(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption) string { - return r.PlaceVertical(height, vPos, r.PlaceHorizontal(width, hPos, str, opts...), opts...) + return PlaceVertical(height, vPos, PlaceHorizontal(width, hPos, str, opts...), opts...) } // PlaceHorizontal places a string or text block horizontally in an unstyled // block of a given width. If the given width is shorter than the max width of // the string (measured by its longest line) this will be a noop. func PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOption) string { - return renderer.PlaceHorizontal(width, pos, str, opts...) -} - -// PlaceHorizontal places a string or text block horizontally in an unstyled -// block of a given width. If the given width is shorter than the max width of -// the string (measured by its longest line) this will be a noöp. -func (r *Renderer) PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOption) string { lines, contentWidth := getLines(str) gap := width - contentWidth @@ -61,7 +48,7 @@ func (r *Renderer) PlaceHorizontal(width int, pos Position, str string, opts ... return str } - ws := newWhitespace(r, opts...) + ws := newWhitespace(opts...) var b strings.Builder for i, l := range lines { @@ -101,13 +88,6 @@ func (r *Renderer) PlaceHorizontal(width int, pos Position, str string, opts ... // of a given height. If the given height is shorter than the height of the // string (measured by its newlines) then this will be a noop. func PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOption) string { - return renderer.PlaceVertical(height, pos, str, opts...) -} - -// PlaceVertical places a string or text block vertically in an unstyled block -// of a given height. If the given height is shorter than the height of the -// string (measured by its newlines) then this will be a noöp. -func (r *Renderer) PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOption) string { contentHeight := strings.Count(str, "\n") + 1 gap := height - contentHeight @@ -115,7 +95,7 @@ func (r *Renderer) PlaceVertical(height int, pos Position, str string, opts ...W return str } - ws := newWhitespace(r, opts...) + ws := newWhitespace(opts...) _, width := getLines(str) emptyLine := ws.render(width) diff --git a/runes_test.go b/runes_test.go index 44f3963e..be36150d 100644 --- a/runes_test.go +++ b/runes_test.go @@ -19,31 +19,31 @@ func TestStyleRunes(t *testing.T) { "hello 0", "hello", []int{0}, - "\x1b[7mh\x1b[0mello", + "\x1b[7mh\x1b[mello", }, { "你好 1", "你好", []int{1}, - "你\x1b[7m好\x1b[0m", + "你\x1b[7m好\x1b[m", }, { "hello 你好 6,7", "hello 你好", []int{6, 7}, - "hello \x1b[7m你好\x1b[0m", + "hello \x1b[7m你好\x1b[m", }, { "hello 1,3", "hello", []int{1, 3}, - "h\x1b[7me\x1b[0ml\x1b[7ml\x1b[0mo", + "h\x1b[7me\x1b[ml\x1b[7ml\x1b[mo", }, { "你好 0,1", "你好", []int{0, 1}, - "\x1b[7m你好\x1b[0m", + "\x1b[7m你好\x1b[m", }, } diff --git a/set.go b/set.go index ed6e272c..a5dd2354 100644 --- a/set.go +++ b/set.go @@ -685,13 +685,6 @@ func (s Style) Transform(fn func(string) string) Style { return s } -// Renderer sets the renderer for the style. This is useful for changing the -// renderer for a style that is being used in a different context. -func (s Style) Renderer(r *Renderer) Style { - s.r = r - return s -} - // whichSidesInt is a helper method for setting values on sides of a block based // on the number of arguments. It follows the CSS shorthand rules for blocks // like margin, padding. and borders. Here are how the rules work: diff --git a/style.go b/style.go index 28ddccbe..a9c8294f 100644 --- a/style.go +++ b/style.go @@ -5,7 +5,6 @@ import ( "unicode" "github.com/charmbracelet/x/ansi" - "github.com/muesli/termenv" ) const tabWidthDefault = 4 @@ -97,24 +96,13 @@ func (p props) has(k propKey) bool { // NewStyle returns a new, empty Style. While it's syntactic sugar for the // Style{} primitive, it's recommended to use this function for creating styles -// in case the underlying implementation changes. It takes an optional string -// value to be set as the underlying string value for this style. +// in case the underlying implementation changes. func NewStyle() Style { - return renderer.NewStyle() -} - -// NewStyle returns a new, empty Style. While it's syntactic sugar for the -// Style{} primitive, it's recommended to use this function for creating styles -// in case the underlying implementation changes. It takes an optional string -// value to be set as the underlying string value for this style. -func (r *Renderer) NewStyle() Style { - s := Style{r: r} - return s + return Style{} } // Style contains a set of rules that comprise a style as a whole. type Style struct { - r *Renderer props props value string @@ -231,9 +219,6 @@ func (s Style) Inherit(i Style) Style { // Render applies the defined style formatting to a given string. func (s Style) Render(strs ...string) string { - if s.r == nil { - s.r = renderer - } if s.value != "" { strs = append([]string{s.value}, strs...) } @@ -241,10 +226,9 @@ func (s Style) Render(strs ...string) string { var ( str = joinString(strs...) - p = s.r.ColorProfile() - te = p.String() - teSpace = p.String() - teWhitespace = p.String() + te ansi.Style + teSpace ansi.Style + teWhitespace ansi.Style bold = s.getAsBool(boldKey, false) italic = s.getAsBool(italicKey, false) @@ -293,10 +277,6 @@ func (s Style) Render(strs ...string) string { return s.maybeConvertTabs(str) } - // Enable support for ANSI on the legacy Windows cmd.exe console. This is a - // no-op on non-Windows systems and on Windows runs only once. - enableLegacyWindowsANSI() - if bold { te = te.Bold() } @@ -313,29 +293,29 @@ func (s Style) Render(strs ...string) string { te = te.Reverse() } if blink { - te = te.Blink() + te = te.SlowBlink() } if faint { te = te.Faint() } if fg != noColor { - te = te.Foreground(fg.color(s.r)) + te = te.ForegroundColor(fg) if styleWhitespace { - teWhitespace = teWhitespace.Foreground(fg.color(s.r)) + teWhitespace = teWhitespace.ForegroundColor(fg) } if useSpaceStyler { - teSpace = teSpace.Foreground(fg.color(s.r)) + teSpace = teSpace.ForegroundColor(fg) } } if bg != noColor { - te = te.Background(bg.color(s.r)) + te = te.BackgroundColor(bg) if colorWhitespace { - teWhitespace = teWhitespace.Background(bg.color(s.r)) + teWhitespace = teWhitespace.BackgroundColor(bg) } if useSpaceStyler { - teSpace = teSpace.Background(bg.color(s.r)) + teSpace = teSpace.BackgroundColor(bg) } } @@ -343,14 +323,14 @@ func (s Style) Render(strs ...string) string { te = te.Underline() } if strikethrough { - te = te.CrossOut() + te = te.Strikethrough() } if underlineSpaces { teSpace = teSpace.Underline() } if strikethroughSpaces { - teSpace = teSpace.CrossOut() + teSpace = teSpace.Strikethrough() } // Potentially convert tabs to spaces @@ -396,7 +376,7 @@ func (s Style) Render(strs ...string) string { // Padding if !inline { //nolint:nestif if leftPadding > 0 { - var st *termenv.Style + var st *ansi.Style if colorWhitespace || styleWhitespace { st = &teWhitespace } @@ -404,7 +384,7 @@ func (s Style) Render(strs ...string) string { } if rightPadding > 0 { - var st *termenv.Style + var st *ansi.Style if colorWhitespace || styleWhitespace { st = &teWhitespace } @@ -432,7 +412,7 @@ func (s Style) Render(strs ...string) string { numLines := strings.Count(str, "\n") if !(numLines == 0 && width == 0) { - var st *termenv.Style + var st *ansi.Style if colorWhitespace || styleWhitespace { st = &teWhitespace } @@ -490,17 +470,17 @@ func (s Style) applyMargins(str string, inline bool) string { bottomMargin = s.getAsInt(marginBottomKey) leftMargin = s.getAsInt(marginLeftKey) - styler termenv.Style + style ansi.Style ) bgc := s.getAsColor(marginBackgroundKey) if bgc != noColor { - styler = styler.Background(bgc.color(s.r)) + style = style.BackgroundColor(bgc) } // Add left and right margin - str = padLeft(str, leftMargin, &styler) - str = padRight(str, rightMargin, &styler) + str = padLeft(str, leftMargin, &style) + str = padRight(str, rightMargin, &style) // Top/bottom margin if !inline { @@ -508,10 +488,10 @@ func (s Style) applyMargins(str string, inline bool) string { spaces := strings.Repeat(" ", width) if topMargin > 0 { - str = styler.Styled(strings.Repeat(spaces+"\n", topMargin)) + str + str = style.Styled(strings.Repeat(spaces+"\n", topMargin)) + str } if bottomMargin > 0 { - str += styler.Styled(strings.Repeat("\n"+spaces, bottomMargin)) + str += style.Styled(strings.Repeat("\n"+spaces, bottomMargin)) } } @@ -519,19 +499,19 @@ func (s Style) applyMargins(str string, inline bool) string { } // Apply left padding. -func padLeft(str string, n int, style *termenv.Style) string { +func padLeft(str string, n int, style *ansi.Style) string { return pad(str, -n, style) } // Apply right padding. -func padRight(str string, n int, style *termenv.Style) string { +func padRight(str string, n int, style *ansi.Style) string { return pad(str, n, style) } // pad adds padding to either the left or right side of a string. // Positive values add to the right side while negative values // add to the left side. -func pad(str string, n int, style *termenv.Style) string { +func pad(str string, n int, style *ansi.Style) string { if n == 0 { return str } diff --git a/style_test.go b/style_test.go index eacaadf5..6ce4537f 100644 --- a/style_test.go +++ b/style_test.go @@ -1,12 +1,10 @@ package lipgloss import ( - "io" + "os" "reflect" "strings" "testing" - - "github.com/muesli/termenv" ) func TestUnderline(t *testing.T) { @@ -88,8 +86,7 @@ func TestStrikethrough(t *testing.T) { } func TestStyleRender(t *testing.T) { - r := NewRenderer(io.Discard) - r.SetColorProfile(termenv.TrueColor) + r := NewRenderer(TrueColor, true) r.SetHasDarkBackground(true) t.Parallel() @@ -99,31 +96,31 @@ func TestStyleRender(t *testing.T) { }{ { r.NewStyle().Foreground(Color("#5A56E0")), - "\x1b[38;2;89;86;224mhello\x1b[0m", + "\x1b[38;2;89;86;224mhello\x1b[m", }, { r.NewStyle().Foreground(AdaptiveColor{Light: "#fffe12", Dark: "#5A56E0"}), - "\x1b[38;2;89;86;224mhello\x1b[0m", + "\x1b[38;2;89;86;224mhello\x1b[m", }, { r.NewStyle().Bold(true), - "\x1b[1mhello\x1b[0m", + "\x1b[1mhello\x1b[m", }, { r.NewStyle().Italic(true), - "\x1b[3mhello\x1b[0m", + "\x1b[3mhello\x1b[m", }, { r.NewStyle().Underline(true), - "\x1b[4;4mh\x1b[0m\x1b[4;4me\x1b[0m\x1b[4;4ml\x1b[0m\x1b[4;4ml\x1b[0m\x1b[4;4mo\x1b[0m", + "\x1b[4;4mh\x1b[m\x1b[4;4me\x1b[m\x1b[4;4ml\x1b[m\x1b[4;4ml\x1b[m\x1b[4;4mo\x1b[m", }, { r.NewStyle().Blink(true), - "\x1b[5mhello\x1b[0m", + "\x1b[5mhello\x1b[m", }, { r.NewStyle().Faint(true), - "\x1b[2mhello\x1b[0m", + "\x1b[2mhello\x1b[m", }, } @@ -139,44 +136,42 @@ func TestStyleRender(t *testing.T) { } func TestStyleCustomRender(t *testing.T) { - r := NewRenderer(io.Discard) - r.SetHasDarkBackground(false) - r.SetColorProfile(termenv.TrueColor) + r := NewRenderer(TrueColor, false) tt := []struct { style Style expected string }{ { r.NewStyle().Foreground(Color("#5A56E0")), - "\x1b[38;2;89;86;224mhello\x1b[0m", + "\x1b[38;2;89;86;224mhello\x1b[m", }, { r.NewStyle().Foreground(AdaptiveColor{Light: "#fffe12", Dark: "#5A56E0"}), - "\x1b[38;2;255;254;18mhello\x1b[0m", + "\x1b[38;2;255;254;18mhello\x1b[m", }, { r.NewStyle().Bold(true), - "\x1b[1mhello\x1b[0m", + "\x1b[1mhello\x1b[m", }, { r.NewStyle().Italic(true), - "\x1b[3mhello\x1b[0m", + "\x1b[3mhello\x1b[m", }, { r.NewStyle().Underline(true), - "\x1b[4;4mh\x1b[0m\x1b[4;4me\x1b[0m\x1b[4;4ml\x1b[0m\x1b[4;4ml\x1b[0m\x1b[4;4mo\x1b[0m", + "\x1b[4;4mh\x1b[m\x1b[4;4me\x1b[m\x1b[4;4ml\x1b[m\x1b[4;4ml\x1b[m\x1b[4;4mo\x1b[m", }, { r.NewStyle().Blink(true), - "\x1b[5mhello\x1b[0m", + "\x1b[5mhello\x1b[m", }, { r.NewStyle().Faint(true), - "\x1b[2mhello\x1b[0m", + "\x1b[2mhello\x1b[m", }, { NewStyle().Faint(true).Renderer(r), - "\x1b[2mhello\x1b[0m", + "\x1b[2mhello\x1b[m", }, } @@ -192,7 +187,7 @@ func TestStyleCustomRender(t *testing.T) { } func TestStyleRenderer(t *testing.T) { - r := NewRenderer(io.Discard) + r := NewRenderer(DetectColorProfile(os.Stdout, nil), true) s1 := NewStyle().Bold(true) s2 := s1.Renderer(r) if s1.r == s2.r { @@ -441,7 +436,7 @@ func TestStyleValue(t *testing.T) { name: "set string with bold", text: "foo", style: NewStyle().SetString("bar").Bold(true), - expected: "\x1b[1mbar foo\x1b[0m", + expected: "\x1b[1mbar foo\x1b[m", }, { name: "new style with string", @@ -534,7 +529,7 @@ func TestStringTransform(t *testing.T) { }, } { res := NewStyle().Bold(true).Transform(tc.fn).Render(tc.input) - expected := "\x1b[1m" + tc.expected + "\x1b[0m" + expected := "\x1b[1m" + tc.expected + "\x1b[m" if res != expected { t.Errorf("Test #%d:\nExpected: %q\nGot: %q", i+1, expected, res) } diff --git a/whitespace.go b/whitespace.go index 040dc98e..ec5ae9e0 100644 --- a/whitespace.go +++ b/whitespace.go @@ -4,24 +4,19 @@ import ( "strings" "github.com/charmbracelet/x/ansi" - "github.com/muesli/termenv" ) // whitespace is a whitespace renderer. type whitespace struct { - re *Renderer - style termenv.Style chars string + style Style } // newWhitespace creates a new whitespace renderer. The order of the options // matters, if you're using WithWhitespaceRenderer, make sure it comes first as // other options might depend on it. -func newWhitespace(r *Renderer, opts ...WhitespaceOption) *whitespace { - w := &whitespace{ - re: r, - style: r.ColorProfile().String(), - } +func newWhitespace(opts ...WhitespaceOption) *whitespace { + w := &whitespace{} for _, opt := range opts { opt(w) } @@ -55,23 +50,16 @@ func (w whitespace) render(width int) string { b.WriteString(strings.Repeat(" ", short)) } - return w.style.Styled(b.String()) + return w.style.Render(b.String()) } // WhitespaceOption sets a styling rule for rendering whitespace. type WhitespaceOption func(*whitespace) -// WithWhitespaceForeground sets the color of the characters in the whitespace. -func WithWhitespaceForeground(c TerminalColor) WhitespaceOption { - return func(w *whitespace) { - w.style = w.style.Foreground(c.color(w.re)) - } -} - -// WithWhitespaceBackground sets the background color of the whitespace. -func WithWhitespaceBackground(c TerminalColor) WhitespaceOption { +// WithWhitespaceStyle sets the style for the whitespace. +func WithWhitespaceStyle(s Style) WhitespaceOption { return func(w *whitespace) { - w.style = w.style.Background(c.color(w.re)) + w.style = s } }