From 46dd8fbcc3c4b62da61c282dcd6a0c438bc028ed Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Thu, 4 Mar 2021 12:54:40 -0500 Subject: [PATCH] Switch to a method-based API --- align.go | 2 +- color.go | 9 +- style.go | 383 +++++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 321 insertions(+), 73 deletions(-) diff --git a/align.go b/align.go index 4a0e9f6..e081b26 100644 --- a/align.go +++ b/align.go @@ -16,7 +16,7 @@ const ( // Perform text alignment. If the string is multi-lined, we also make all lines // the same width by padding them with spaces. -func align(s string, t Align) string { +func alignText(s string, t Align) string { if strings.Count(s, "\n") == 0 { return s } diff --git a/color.go b/color.go index f9f2a42..e817672 100644 --- a/color.go +++ b/color.go @@ -7,9 +7,9 @@ import ( ) var ( - hasDarkBackground bool - color func(string) termenv.Color - initTermenv sync.Once + hasDarkBackground bool + color func(string) termenv.Color = termenv.ColorProfile().Color + checkBackgroundColor sync.Once ) // ColorType is an interface used in color specifications. @@ -58,9 +58,8 @@ type AdaptiveColor struct { } func (a AdaptiveColor) value() string { - initTermenv.Do(func() { + checkBackgroundColor.Do(func() { hasDarkBackground = termenv.HasDarkBackground() - color = termenv.ColorProfile().Color }) if hasDarkBackground { diff --git a/style.go b/style.go index 3978a72..18bf342 100644 --- a/style.go +++ b/style.go @@ -13,55 +13,224 @@ import ( // ANSI reset sequence. const resetSeq = termenv.CSI + termenv.ResetSeq + "m" -// Style describes formatting instructions for a given string. +// Style contains formatting instructions for a given string. type Style struct { - Bold bool - Italic bool - Underline bool - Strikethrough bool - Reverse bool - Blink bool - Faint bool - Foreground ColorType - Background ColorType + bold *bool + italic *bool + underline *bool + strikethrough *bool + reverse *bool + blink *bool + faint *bool + foreground *ColorType + background *ColorType // If the string contains multiple lines, they'll wrap at this value. // Lines will also be padded with spaces so they'll all be the same width. - Width int + width *int // Text alignment. - Align Align + align *Align // Padding. This will be colored according to the Background value if // StylePadding is true. - LeftPadding int - RightPadding int - TopPadding int - BottomPadding int + leftPadding *int + rightPadding *int + topPadding *int + bottomPadding *int // Whether or not to apply styling to the padding. Most notably, this // determines whether or not the indent background color is styled. - StylePadding bool + stylePadding *bool // Margins. These will never be colored. - LeftMargin int - RightMargin int - TopMargin int - BottomMargin int + leftMargin *int + rightMargin *int + topMargin *int + bottomMargin *int // If set, we truncate lines at this value after all other style has been // applied. That is to say, the physical width of strings will not exceed // this value, so this can be handy when building user interfaces. - MaxWidth int + maxWidth *int // Whether or not to remove trailing spaces with no background color. By // default we leave them in. - RenderClearTrailingSpaces bool + renderClearTrailingSpaces *bool // Whether to draw underlines on spaces (like padding). We don't do this by // default as it's likely not what people want, but you can turn it on if // you so desire. - RenderUnderlinesOnSpaces bool + renderUnderlinesOnSpaces *bool +} + +func (s Style) Bold(v bool) Style { + s.bold = &v + return s +} + +func (s Style) Italic(v bool) Style { + s.italic = &v + return s +} + +func (s Style) Underline(v bool) Style { + s.underline = &v + return s +} + +func (s Style) Strikethrough(v bool) Style { + s.strikethrough = &v + return s +} + +func (s Style) Reverse(v bool) Style { + s.reverse = &v + return s +} + +func (s Style) Blink(v bool) Style { + s.blink = &v + return s +} + +func (s Style) Faint(v bool) Style { + s.faint = &v + return s +} + +func (s Style) Foreground(c ColorType) Style { + s.foreground = &c + return s +} + +func (s Style) Background(c ColorType) Style { + s.background = &c + return s +} + +func (s Style) Width(i int) Style { + s.width = &i + return s +} + +func (s Style) Align(a Align) Style { + s.align = &a + return s +} + +// Padding is a shorthand method for setting padding on all sides at once. +// +// With one argument, the value is applied to all sides. +// +// With two arguments, the value is applied to the vertical and horizontal sides, +// in that order. +// +// With three arguments, the value is applied to the top side, the horizontal +// sides, and the bottom side, in that order. +// +// With four arguments, the value is applied clockwise starting from the top +// side, followed by the right side, then the bottom, and finally the top. +// +// With more than four arguments no padding will be added. +func (s Style) Padding(i ...int) Style { + top, right, bottom, left, ok := whichSides(i...) + if !ok { + return s + } + + s.topPadding = &top + s.rightPadding = &right + s.bottomPadding = &bottom + s.leftPadding = &left + return s +} + +func (s Style) LeftPadding(i int) Style { + s.leftPadding = &i + return s +} + +func (s Style) RightPadding(i int) Style { + s.rightPadding = &i + return s +} + +func (s Style) TopPadding(i int) Style { + s.topPadding = &i + return s +} + +func (s Style) BottomPadding(i int) Style { + s.bottomPadding = &i + return s +} + +func (s Style) StylePadding(v bool) Style { + s.stylePadding = &v + return s +} + +// Margin is a shorthand method for setting margins on all sides at once. +// +// With one argument, the value is applied to all sides. +// +// With two arguments, the value is applied to the vertical and horizontal sides, +// in that order. +// +// With three arguments, the value is applied to the top side, the horizontal +// sides, and the bottom side, in that order. +// +// With four arguments, the value is applied clockwise starting from the top +// side, followed by the right side, then the bottom, and finally the top. +// +// With more than four arguments no padding will be added. +func (s Style) Margin(i ...int) Style { + top, right, bottom, left, ok := whichSides(i...) + if !ok { + return s + } + + s.topMargin = &top + s.rightMargin = &right + s.bottomMargin = &bottom + s.leftMargin = &left + return s +} + +func (s Style) LeftMargin(i int) Style { + s.leftMargin = &i + return s +} + +func (s Style) RightMargin(i int) Style { + s.rightMargin = &i + return s +} + +func (s Style) TopMargin(i int) Style { + s.topMargin = &i + return s +} + +func (s Style) BottomMargin(i int) Style { + s.bottomMargin = &i + return s +} + +func (s Style) MaxWidth(i int) Style { + s.maxWidth = &i + return s +} + +func (s Style) RenderClearTrailingSpaces(v bool) Style { + s.renderClearTrailingSpaces = &v + return s +} + +func (s Style) RenderUnderlinesOnSpaces(v bool) Style { + s.renderUnderlinesOnSpaces = &v + return s } // Apply applies formatting to a given string. @@ -87,7 +256,7 @@ func (s Style) ApplyInline(str string) string { // fmt.Println(userStyle.WithMaxWidth(16).Apply(userInput)) // func (s Style) WithMaxWidth(n int) Style { - s.MaxWidth = n + s.maxWidth = &n return s } @@ -102,37 +271,41 @@ func (s Style) apply(str string, singleLine bool) string { noUnderlineStyler *termenv.Style ) - if s.Bold { + if s.bold != nil && *s.bold { styler = styler.Bold() } - if s.Italic { + if s.italic != nil && *s.italic { styler = styler.Italic() } - if s.Strikethrough { + if s.strikethrough != nil && *s.strikethrough { styler = styler.CrossOut() } - if s.Reverse { + if s.reverse != nil && *s.reverse { styler = styler.Reverse() } - if s.Blink { + if s.blink != nil && *s.blink { styler = styler.Blink() } - if s.Faint { + if s.faint != nil && *s.faint { styler = styler.Faint() } - switch c := s.Foreground.(type) { - case Color, AdaptiveColor: - styler = styler.Foreground(color(c.value())) + if s.foreground != nil { + switch c := (*s.foreground).(type) { + case Color, AdaptiveColor: + styler = styler.Foreground(color(c.value())) + } } - switch c := s.Background.(type) { - case Color, AdaptiveColor: - styler = styler.Background(color(c.value())) + if s.background != nil { + switch c := (*s.background).(type) { + case Color, AdaptiveColor: + styler = styler.Background(color(c.value())) + } } - if s.Underline { - if s.Underline && !s.RenderUnderlinesOnSpaces { + if s.renderUnderlinesOnSpaces != nil && s.underline != nil && *s.underline { + if !*s.renderUnderlinesOnSpaces { stylerCopy := styler noUnderlineStyler = &stylerCopy } @@ -144,45 +317,79 @@ func (s Style) apply(str string, singleLine bool) string { str = strings.Replace(str, "\n", "", -1) } - if !s.StylePadding { + if s.stylePadding != nil && !*s.stylePadding { str = styler.Styled(str) } // Word wrap - if !singleLine && s.Width > 0 { - str = wordwrap.String(str, s.Width-s.LeftPadding-s.RightPadding) + if !singleLine && s.width != nil && *s.width > 0 { + var leftPadding, rightPadding int + if s.leftPadding != nil { + leftPadding = *s.leftPadding + } + if s.rightPadding != nil { + rightPadding = *s.rightPadding + } + str = wordwrap.String(str, *s.width-leftPadding-rightPadding) } // Is a background color set? - backgroundColorSet := true - switch s.Background.(type) { - case nil, noColor: - backgroundColorSet = false + var backgroundColorSet bool + if s.background != nil { + backgroundColorSet = true + switch (*s.background).(type) { + case noColor: + backgroundColorSet = false + } } // Left/right padding - str = padLeft(str, s.LeftPadding) - if !s.RenderClearTrailingSpaces || backgroundColorSet { - str = padRight(str, s.RightPadding, s.StylePadding) + if s.leftPadding != nil { + str = padLeft(str, *s.leftPadding) + } + + if (s.renderClearTrailingSpaces != nil && !*s.renderClearTrailingSpaces) || backgroundColorSet { + var rightPadding int + if s.rightPadding != nil { + rightPadding = *s.rightPadding + } + var stylePadding bool + if s.stylePadding != nil { + stylePadding = *s.stylePadding + } + str = padRight(str, rightPadding, stylePadding) } // Top/bottom padding - if !singleLine && s.TopPadding > 0 { - str = strings.Repeat("\n", s.TopPadding) + str + if s.topPadding != nil && *s.topPadding > 0 && !singleLine { + str = strings.Repeat("\n", *s.topPadding) + str } - if !singleLine && s.BottomPadding > 0 { - str += strings.Repeat("\n", s.BottomPadding) + if s.bottomPadding != nil && *s.bottomPadding > 0 && !singleLine { + str += strings.Repeat("\n", *s.bottomPadding) } numLines := strings.Count(str, "\n") // Set alignment. This will also pad short lines with spaces so that all - // lines are the same length. - if numLines > 0 && (!s.RenderClearTrailingSpaces || backgroundColorSet || s.Align != AlignLeft) { - str = align(str, s.Align) + // lines are the same length, so we run it under a few different conditions + // beyond alignment. + var renderClearTrailingSpaces bool + { + align := AlignLeft + if s.align != nil { + align = *s.align + } + + if s.renderClearTrailingSpaces != nil { + renderClearTrailingSpaces = *s.renderClearTrailingSpaces + } + + if numLines > 0 && (align != AlignLeft || !renderClearTrailingSpaces || backgroundColorSet) { + str = alignText(str, align) + } } - if s.StylePadding { + if s.stylePadding != nil && *s.stylePadding { // We have to do some extra work to not render underlines on spaces if noUnderlineStyler != nil { var b strings.Builder @@ -203,36 +410,38 @@ func (s Style) apply(str string, singleLine bool) string { } // Add left margin - str = padLeft(str, s.LeftMargin) + if s.leftMargin != nil { + str = padLeft(str, *s.leftMargin) + } // Add right margin - if !s.RenderClearTrailingSpaces { - str = padRight(str, s.RightMargin, false) + if s.rightMargin != nil && !renderClearTrailingSpaces { + str = padRight(str, *s.rightMargin, false) } // Top/bottom margin if !singleLine { var maybeSpaces string - if s.RenderClearTrailingSpaces { + if renderClearTrailingSpaces { _, width := getLines(str) maybeSpaces = strings.Repeat(" ", width) } - if s.TopMargin > 0 { - str = strings.Repeat(maybeSpaces+"\n", s.TopMargin) + str + if s.topMargin != nil && *s.topMargin > 0 { + str = strings.Repeat(maybeSpaces+"\n", *s.topMargin) + str } - if s.BottomMargin > 0 { - str += strings.Repeat("\n"+maybeSpaces, s.BottomMargin) + "\n" + if s.bottomMargin != nil && *s.bottomMargin > 0 { + str += strings.Repeat("\n"+maybeSpaces, *s.bottomMargin) + "\n" } } // Truncate accoridng to MaxWidth - if s.MaxWidth > 0 { + if s.maxWidth != nil && *s.maxWidth > 0 { lines := strings.Split(str, "\n") for i := range lines { - lines[i] = truncate.String(lines[i], uint(s.MaxWidth)) + lines[i] = truncate.String(lines[i], uint(*s.maxWidth)) } str = strings.Join(lines, "\n") @@ -269,3 +478,43 @@ func padRight(str string, n int, stylePadding bool) string { return strings.Join(lines, "\n") } + +// whichEdges 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: +// +// 0 args: do nothing +// 1 arg: all sides +// 2 args: top -> bottom +// 3 args: top -> horizontal -> bottom +// 4 args: top -> right -> bottom -> left +// 5+ args: do nothing +func whichSides(i ...int) (top, right, bottom, left int, ok bool) { + switch len(i) { + case 1: + top = i[0] + bottom = i[0] + left = i[0] + right = i[0] + ok = true + case 2: + top = i[0] + bottom = i[0] + left = i[1] + right = i[1] + ok = true + case 3: + top = i[0] + left = i[1] + right = i[1] + bottom = i[2] + ok = true + case 4: + top = i[0] + right = i[1] + bottom = i[2] + left = i[3] + ok = true + } + return +}