From 1d82be316d17e2a21b9c1eb181233d17c5968a88 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Fri, 29 Mar 2024 14:32:02 -0400 Subject: [PATCH] refactor: introduce NoTTY profile and export Sequence The NoTTY profile generates no sequences while the Ascii profile generates sequences with no colors (bold, italic, underline, etc) --- align.go | 4 +- borders.go | 2 +- color.go | 21 +++++------ env.go | 14 +++---- profile.go | 101 +++++++++++++++++++++++++++++++++++++++++++------- style.go | 72 +++++------------------------------ whitespace.go | 4 +- 7 files changed, 118 insertions(+), 100 deletions(-) diff --git a/align.go b/align.go index d20eeb4f..4b4ce501 100644 --- a/align.go +++ b/align.go @@ -9,7 +9,7 @@ import ( // Perform text alignment. If the string is multi-lined, we also make all lines // 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 *style) string { +func alignTextHorizontal(str string, pos Position, width int, style *Sequence) string { lines, widestLine := getLines(str) var b strings.Builder @@ -58,7 +58,7 @@ func alignTextHorizontal(str string, pos Position, width int, style *style) stri return b.String() } -func alignTextVertical(str string, pos Position, height int, _ *style) string { +func alignTextVertical(str string, pos Position, height int, _ *Sequence) string { strHeight := strings.Count(str, "\n") + 1 if height < strHeight { return str diff --git a/borders.go b/borders.go index 240fe7b7..f14dcd2d 100644 --- a/borders.go +++ b/borders.go @@ -406,7 +406,7 @@ func (s Style) styleBorder(border string, fg, bg TerminalColor) string { return border } - style := s.r.ColorProfile().string() + style := s.r.ColorProfile().Sequence() if fg != noColor { style = style.ForegroundColor(fg.color(s.r)) diff --git a/color.go b/color.go index a9f18a70..24ccf5f9 100644 --- a/color.go +++ b/color.go @@ -46,7 +46,7 @@ func (n NoColor) RGBA() (r, g, b, a uint32) { type Color string func (c Color) color(r *Renderer) ansi.Color { - return r.ColorProfile().color(string(c)) + return r.ColorProfile().Color(string(c)) } // RGBA returns the RGBA value of this color. This satisfies the Go Color @@ -59,19 +59,16 @@ func (c Color) RGBA() (r, g, b, a uint32) { return c.color(DefaultRenderer()).RGBA() } -// 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. +// ANSIColor is a color specified by an ANSI256 color value. // // Example usage: // -// // These two statements are equivalent. -// colorA := lipgloss.ANSIColor(21) -// colorB := lipgloss.Color("21") -type ANSIColor uint +// colorA := lipgloss.ANSIColor(8) +// colorB := lipgloss.ANSIColor(134) +type ANSIColor uint8 func (ac ANSIColor) color(r *Renderer) ansi.Color { - return Color(strconv.FormatUint(uint64(ac), 10)).color(r) + return r.ColorProfile().Convert(ansi.ExtendedColor(ac)) } // RGBA returns the RGBA value of this color. This satisfies the Go Color @@ -126,11 +123,11 @@ func (c CompleteColor) color(r *Renderer) ansi.Color { p := r.ColorProfile() switch p { //nolint:exhaustive case TrueColor: - return p.color(c.TrueColor) + return p.Color(c.TrueColor) case ANSI256: - return p.color(c.ANSI256) + return p.Color(c.ANSI256) case ANSI: - return p.color(c.ANSI) + return p.Color(c.ANSI) default: return NoColor{} } diff --git a/env.go b/env.go index b4ef0feb..d4af4811 100644 --- a/env.go +++ b/env.go @@ -25,15 +25,19 @@ func envNoColor(env map[string]string) bool { // If the terminal does not support any colors, but CLICOLOR_FORCE is set and not "0" // then the ANSI color profile will be returned. func EnvColorProfile(stdout *os.File, environ []string) Profile { + if stdout == nil || !term.IsTerminal(stdout.Fd()) { + return NoTTY + } if environ == nil { environ = os.Environ() } + env := environMap(environ) if envNoColor(env) { return Ascii } - p := detectColorProfile(stdout, env) - if cliColorForced(env) && p == Ascii { + p := detectColorProfile(env) + if cliColorForced(env) && p <= NoTTY { return ANSI } return p @@ -48,11 +52,7 @@ func cliColorForced(env map[string]string) bool { // detectColorProfile returns the supported color profile: // Ascii, ANSI, ANSI256, or TrueColor. -func detectColorProfile(stdout *os.File, env map[string]string) (p Profile) { - if stdout == nil || !term.IsTerminal(stdout.Fd()) { - return Ascii - } - +func detectColorProfile(env map[string]string) (p Profile) { setProfile := func(profile Profile) { if profile > p { p = profile diff --git a/profile.go b/profile.go index 43880a4d..e8e463ef 100644 --- a/profile.go +++ b/profile.go @@ -14,8 +14,10 @@ import ( type Profile int const ( + // NoTTY, no terminal profile + NoTTY Profile = iota // Ascii, uncolored profile - Ascii Profile = iota //nolint:revive + Ascii // nolint: revive // ANSI, 4-bit color profile ANSI // ANSI256, 8-bit color profile @@ -24,14 +26,85 @@ const ( TrueColor ) -func (p Profile) string() style { - return style{Profile: p} +// Sequence returns a style sequence for the profile. +func (p Profile) Sequence() Sequence { + return Sequence{Profile: p} } -// convert transforms a given Color to a Color supported within the Profile. -func (p Profile) convert(c ansi.Color) ansi.Color { - if p == Ascii { - return NoColor{} +// Sequence represents a text ANSI sequence style. +type Sequence struct { + ansi.Style + Profile +} + +// Styled returns a styled string. +func (s Sequence) Styled(str string) string { + if s.Profile <= NoTTY { + return str + } + return s.Style.Styled(str) +} + +// Bold returns a sequence with bold enabled. +func (s Sequence) Bold() Sequence { + return Sequence{s.Style.Bold(), s.Profile} +} + +// Italic returns a sequence with italic enabled. +func (s Sequence) Italic() Sequence { + return Sequence{s.Style.Italic(), s.Profile} +} + +// Underline returns a sequence with underline enabled. +func (s Sequence) Underline() Sequence { + return Sequence{s.Style.Underline(), s.Profile} +} + +// Strikethrough returns a sequence with strikethrough enabled. +func (s Sequence) Strikethrough() Sequence { + return Sequence{s.Style.Strikethrough(), s.Profile} +} + +// Inverse returns a sequence with inverse enabled. +func (s Sequence) Reverse() Sequence { + return Sequence{s.Style.Reverse(), s.Profile} +} + +// SlowBlink returns a sequence with slow blink enabled. +func (s Sequence) SlowBlink() Sequence { + return Sequence{s.Style.SlowBlink(), s.Profile} +} + +// RapidBlink returns a sequence with rapid blink enabled. +func (s Sequence) RapidBlink() Sequence { + return Sequence{s.Style.RapidBlink(), s.Profile} +} + +// Faint returns a sequence with faint enabled. +func (s Sequence) Faint() Sequence { + return Sequence{s.Style.Faint(), s.Profile} +} + +// ForegroundColor returns a sequence with the foreground color set. +func (s Sequence) ForegroundColor(c ansi.Color) Sequence { + if s.Profile <= Ascii { + return s + } + return Sequence{s.Style.ForegroundColor(c), s.Profile} +} + +// BackgroundColor returns a sequence with the background color set. +func (s Sequence) BackgroundColor(c ansi.Color) Sequence { + if s.Profile <= Ascii { + return s + } + return Sequence{s.Style.BackgroundColor(c), s.Profile} +} + +// Convert transforms a given Color to a Color supported within the Profile. +func (p Profile) Convert(c ansi.Color) ansi.Color { + if p <= Ascii { + return noColor } switch v := c.(type) { @@ -62,25 +135,25 @@ func (p Profile) convert(c ansi.Color) ansi.Color { return c } -// color creates a color from a string. Valid inputs are hex colors, as well as -// ANSI color codes (0-15, 16-255). -func (p Profile) color(s string) ansi.Color { +// Color creates a Color from a string. Valid inputs are hex colors, as well as +// ANSI Color codes (0-15, 16-255). +func (p Profile) Color(s string) ansi.Color { if len(s) == 0 { - return color.Black + return noColor } var c ansi.Color if strings.HasPrefix(s, "#") { h, err := colorful.Hex(s) if err != nil { - return nil + return noColor } tc := uint32(h.R*255)<<16 + uint32(h.G*255)<<8 + uint32(h.B*255) c = ansi.TrueColor(tc) } else { i, err := strconv.Atoi(s) if err != nil { - return nil + return noColor } if i < 16 { @@ -90,7 +163,7 @@ func (p Profile) color(s string) ansi.Color { } } - return p.convert(c) + return p.Convert(c) } func hexToANSI256Color(c colorful.Color) ansi.ExtendedColor { diff --git a/style.go b/style.go index 91d277af..8af399e9 100644 --- a/style.go +++ b/style.go @@ -185,9 +185,9 @@ func (s Style) Render(strs ...string) string { str = joinString(strs...) p = s.r.ColorProfile() - te = p.string() - teSpace = p.string() - teWhitespace = p.string() + te = p.Sequence() + teSpace = p.Sequence() + teWhitespace = p.Sequence() bold = s.getAsBool(boldKey, false) italic = s.getAsBool(italicKey, false) @@ -335,7 +335,7 @@ func (s Style) Render(strs ...string) string { // Padding if !inline { if leftPadding > 0 { - var st *style + var st *Sequence if colorWhitespace || styleWhitespace { st = &teWhitespace } @@ -343,7 +343,7 @@ func (s Style) Render(strs ...string) string { } if rightPadding > 0 { - var st *style + var st *Sequence if colorWhitespace || styleWhitespace { st = &teWhitespace } @@ -371,7 +371,7 @@ func (s Style) Render(strs ...string) string { numLines := strings.Count(str, "\n") if !(numLines == 0 && width == 0) { - var st *style + var st *Sequence if colorWhitespace || styleWhitespace { st = &teWhitespace } @@ -429,7 +429,7 @@ func (s Style) applyMargins(str string, inline bool) string { bottomMargin = s.getAsInt(marginBottomKey) leftMargin = s.getAsInt(marginLeftKey) - styler = s.r.ColorProfile().string() + styler = s.r.ColorProfile().Sequence() ) bgc := s.getAsColor(marginBackgroundKey) @@ -458,19 +458,19 @@ func (s Style) applyMargins(str string, inline bool) string { } // Apply left padding. -func padLeft(str string, n int, style *style) string { +func padLeft(str string, n int, style *Sequence) string { return pad(str, -n, style) } // Apply right padding. -func padRight(str string, n int, style *style) string { +func padRight(str string, n int, style *Sequence) 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 *style) string { +func pad(str string, n int, style *Sequence) string { if n == 0 { return str } @@ -524,55 +524,3 @@ func abs(a int) int { return a } - -type style struct { - ansi.Style - Profile -} - -func (s style) Styled(str string) string { - if s.Profile == Ascii { - return str - } - return s.Style.Styled(str) -} - -func (s style) Bold() style { - return style{s.Style.Bold(), s.Profile} -} - -func (s style) Italic() style { - return style{s.Style.Italic(), s.Profile} -} - -func (s style) Underline() style { - return style{s.Style.Underline(), s.Profile} -} - -func (s style) Strikethrough() style { - return style{s.Style.Strikethrough(), s.Profile} -} - -func (s style) Reverse() style { - return style{s.Style.Reverse(), s.Profile} -} - -func (s style) SlowBlink() style { - return style{s.Style.SlowBlink(), s.Profile} -} - -func (s style) RapidBlink() style { - return style{s.Style.RapidBlink(), s.Profile} -} - -func (s style) Faint() style { - return style{s.Style.Faint(), s.Profile} -} - -func (s style) ForegroundColor(c ansi.Color) style { - return style{s.Style.ForegroundColor(c), s.Profile} -} - -func (s style) BackgroundColor(c ansi.Color) style { - return style{s.Style.BackgroundColor(c), s.Profile} -} diff --git a/whitespace.go b/whitespace.go index b1d6b40a..d1c3c398 100644 --- a/whitespace.go +++ b/whitespace.go @@ -10,7 +10,7 @@ import ( type whitespace struct { re *Renderer chars string - style style + style Sequence } // newWhitespace creates a new whitespace renderer. The order of the options @@ -19,7 +19,7 @@ type whitespace struct { func newWhitespace(r *Renderer, opts ...WhitespaceOption) *whitespace { w := &whitespace{ re: r, - style: r.ColorProfile().string(), + style: r.ColorProfile().Sequence(), } for _, opt := range opts { opt(w)