| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497 |
- package lipgloss
- import (
- "strings"
- "unicode"
- "github.com/muesli/reflow/truncate"
- "github.com/muesli/reflow/wordwrap"
- "github.com/muesli/reflow/wrap"
- "github.com/muesli/termenv"
- )
- // Property for a key.
- type propKey int
- // Available properties.
- const (
- boldKey propKey = iota
- italicKey
- underlineKey
- strikethroughKey
- reverseKey
- blinkKey
- faintKey
- foregroundKey
- backgroundKey
- widthKey
- heightKey
- alignHorizontalKey
- alignVerticalKey
- // Padding.
- paddingTopKey
- paddingRightKey
- paddingBottomKey
- paddingLeftKey
- colorWhitespaceKey
- // Margins.
- marginTopKey
- marginRightKey
- marginBottomKey
- marginLeftKey
- marginBackgroundKey
- // Border runes.
- borderStyleKey
- // Border edges.
- borderTopKey
- borderRightKey
- borderBottomKey
- borderLeftKey
- // Border foreground colors.
- borderTopForegroundKey
- borderRightForegroundKey
- borderBottomForegroundKey
- borderLeftForegroundKey
- // Border background colors.
- borderTopBackgroundKey
- borderRightBackgroundKey
- borderBottomBackgroundKey
- borderLeftBackgroundKey
- inlineKey
- maxWidthKey
- maxHeightKey
- underlineSpacesKey
- strikethroughSpacesKey
- )
- // A set of properties.
- type rules map[propKey]interface{}
- // 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 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
- }
- // Style contains a set of rules that comprise a style as a whole.
- type Style struct {
- r *Renderer
- rules map[propKey]interface{}
- value string
- }
- // joinString joins a list of strings into a single string separated with a
- // space.
- func joinString(strs ...string) string {
- return strings.Join(strs, " ")
- }
- // SetString sets the underlying string value for this style. To render once
- // the underlying string is set, use the Style.String. This method is
- // a convenience for cases when having a stringer implementation is handy, such
- // as when using fmt.Sprintf. You can also simply define a style and render out
- // strings directly with Style.Render.
- func (s Style) SetString(strs ...string) Style {
- s.value = joinString(strs...)
- return s
- }
- // Value returns the raw, unformatted, underlying string value for this style.
- func (s Style) Value() string {
- return s.value
- }
- // String implements stringer for a Style, returning the rendered result based
- // on the rules in this style. An underlying string value must be set with
- // Style.SetString prior to using this method.
- func (s Style) String() string {
- return s.Render()
- }
- // Copy returns a copy of this style, including any underlying string values.
- func (s Style) Copy() Style {
- o := NewStyle()
- o.init()
- for k, v := range s.rules {
- o.rules[k] = v
- }
- o.r = s.r
- o.value = s.value
- return o
- }
- // Inherit overlays the style in the argument onto this style by copying each explicitly
- // set value from the argument style onto this style if it is not already explicitly set.
- // Existing set values are kept intact and not overwritten.
- //
- // Margins, padding, and underlying string values are not inherited.
- func (s Style) Inherit(i Style) Style {
- s.init()
- for k, v := range i.rules {
- switch k {
- case marginTopKey, marginRightKey, marginBottomKey, marginLeftKey:
- // Margins are not inherited
- continue
- case paddingTopKey, paddingRightKey, paddingBottomKey, paddingLeftKey:
- // Padding is not inherited
- continue
- case backgroundKey:
- // The margins also inherit the background color
- if !s.isSet(marginBackgroundKey) && !i.isSet(marginBackgroundKey) {
- s.rules[marginBackgroundKey] = v
- }
- }
- if _, exists := s.rules[k]; exists {
- continue
- }
- s.rules[k] = v
- }
- return s
- }
- // 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...)
- }
- var (
- str = joinString(strs...)
- te = s.r.ColorProfile().String()
- teSpace = s.r.ColorProfile().String()
- teWhitespace = s.r.ColorProfile().String()
- bold = s.getAsBool(boldKey, false)
- italic = s.getAsBool(italicKey, false)
- underline = s.getAsBool(underlineKey, false)
- strikethrough = s.getAsBool(strikethroughKey, false)
- reverse = s.getAsBool(reverseKey, false)
- blink = s.getAsBool(blinkKey, false)
- faint = s.getAsBool(faintKey, false)
- fg = s.getAsColor(foregroundKey)
- bg = s.getAsColor(backgroundKey)
- width = s.getAsInt(widthKey)
- height = s.getAsInt(heightKey)
- horizontalAlign = s.getAsPosition(alignHorizontalKey)
- verticalAlign = s.getAsPosition(alignVerticalKey)
- topPadding = s.getAsInt(paddingTopKey)
- rightPadding = s.getAsInt(paddingRightKey)
- bottomPadding = s.getAsInt(paddingBottomKey)
- leftPadding = s.getAsInt(paddingLeftKey)
- colorWhitespace = s.getAsBool(colorWhitespaceKey, true)
- inline = s.getAsBool(inlineKey, false)
- maxWidth = s.getAsInt(maxWidthKey)
- maxHeight = s.getAsInt(maxHeightKey)
- underlineSpaces = underline && s.getAsBool(underlineSpacesKey, true)
- strikethroughSpaces = strikethrough && s.getAsBool(strikethroughSpacesKey, true)
- // Do we need to style whitespace (padding and space outside
- // paragraphs) separately?
- styleWhitespace = reverse
- // Do we need to style spaces separately?
- useSpaceStyler = underlineSpaces || strikethroughSpaces
- )
- if len(s.rules) == 0 {
- return 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()
- }
- if italic {
- te = te.Italic()
- }
- if underline {
- te = te.Underline()
- }
- if reverse {
- if reverse {
- teWhitespace = teWhitespace.Reverse()
- }
- te = te.Reverse()
- }
- if blink {
- te = te.Blink()
- }
- if faint {
- te = te.Faint()
- }
- if fg != noColor {
- te = te.Foreground(fg.color(s.r))
- if styleWhitespace {
- teWhitespace = teWhitespace.Foreground(fg.color(s.r))
- }
- if useSpaceStyler {
- teSpace = teSpace.Foreground(fg.color(s.r))
- }
- }
- if bg != noColor {
- te = te.Background(bg.color(s.r))
- if colorWhitespace {
- teWhitespace = teWhitespace.Background(bg.color(s.r))
- }
- if useSpaceStyler {
- teSpace = teSpace.Background(bg.color(s.r))
- }
- }
- if underline {
- te = te.Underline()
- }
- if strikethrough {
- te = te.CrossOut()
- }
- if underlineSpaces {
- teSpace = teSpace.Underline()
- }
- if strikethroughSpaces {
- teSpace = teSpace.CrossOut()
- }
- // Strip newlines in single line mode
- if inline {
- str = strings.ReplaceAll(str, "\n", "")
- }
- // Word wrap
- if !inline && width > 0 {
- wrapAt := width - leftPadding - rightPadding
- str = wordwrap.String(str, wrapAt)
- str = wrap.String(str, wrapAt) // force-wrap long strings
- }
- // Render core text
- {
- var b strings.Builder
- l := strings.Split(str, "\n")
- for i := range l {
- if useSpaceStyler {
- // Look for spaces and apply a different styler
- for _, r := range l[i] {
- if unicode.IsSpace(r) {
- b.WriteString(teSpace.Styled(string(r)))
- continue
- }
- b.WriteString(te.Styled(string(r)))
- }
- } else {
- b.WriteString(te.Styled(l[i]))
- }
- if i != len(l)-1 {
- b.WriteRune('\n')
- }
- }
- str = b.String()
- }
- // Padding
- if !inline {
- if leftPadding > 0 {
- var st *termenv.Style
- if colorWhitespace || styleWhitespace {
- st = &teWhitespace
- }
- str = padLeft(str, leftPadding, st)
- }
- if rightPadding > 0 {
- var st *termenv.Style
- if colorWhitespace || styleWhitespace {
- st = &teWhitespace
- }
- str = padRight(str, rightPadding, st)
- }
- if topPadding > 0 {
- str = strings.Repeat("\n", topPadding) + str
- }
- if bottomPadding > 0 {
- str += strings.Repeat("\n", bottomPadding)
- }
- }
- // Height
- if height > 0 {
- str = alignTextVertical(str, verticalAlign, height, nil)
- }
- // Set alignment. This will also pad short lines with spaces so that all
- // lines are the same length, so we run it under a few different conditions
- // beyond alignment.
- {
- numLines := strings.Count(str, "\n")
- if !(numLines == 0 && width == 0) {
- var st *termenv.Style
- if colorWhitespace || styleWhitespace {
- st = &teWhitespace
- }
- str = alignTextHorizontal(str, horizontalAlign, width, st)
- }
- }
- if !inline {
- str = s.applyBorder(str)
- str = s.applyMargins(str, inline)
- }
- // Truncate according to MaxWidth
- if maxWidth > 0 {
- lines := strings.Split(str, "\n")
- for i := range lines {
- lines[i] = truncate.String(lines[i], uint(maxWidth))
- }
- str = strings.Join(lines, "\n")
- }
- // Truncate according to MaxHeight
- if maxHeight > 0 {
- lines := strings.Split(str, "\n")
- str = strings.Join(lines[:min(maxHeight, len(lines))], "\n")
- }
- return str
- }
- func (s Style) applyMargins(str string, inline bool) string {
- var (
- topMargin = s.getAsInt(marginTopKey)
- rightMargin = s.getAsInt(marginRightKey)
- bottomMargin = s.getAsInt(marginBottomKey)
- leftMargin = s.getAsInt(marginLeftKey)
- styler termenv.Style
- )
- bgc := s.getAsColor(marginBackgroundKey)
- if bgc != noColor {
- styler = styler.Background(bgc.color(s.r))
- }
- // Add left and right margin
- str = padLeft(str, leftMargin, &styler)
- str = padRight(str, rightMargin, &styler)
- // Top/bottom margin
- if !inline {
- _, width := getLines(str)
- spaces := strings.Repeat(" ", width)
- if topMargin > 0 {
- str = styler.Styled(strings.Repeat(spaces+"\n", topMargin)) + str
- }
- if bottomMargin > 0 {
- str += styler.Styled(strings.Repeat("\n"+spaces, bottomMargin))
- }
- }
- return str
- }
- // Apply left padding.
- func padLeft(str string, n int, style *termenv.Style) string {
- if n == 0 {
- return str
- }
- sp := strings.Repeat(" ", n)
- if style != nil {
- sp = style.Styled(sp)
- }
- b := strings.Builder{}
- l := strings.Split(str, "\n")
- for i := range l {
- b.WriteString(sp)
- b.WriteString(l[i])
- if i != len(l)-1 {
- b.WriteRune('\n')
- }
- }
- return b.String()
- }
- // Apply right padding.
- func padRight(str string, n int, style *termenv.Style) string {
- if n == 0 || str == "" {
- return str
- }
- sp := strings.Repeat(" ", n)
- if style != nil {
- sp = style.Styled(sp)
- }
- b := strings.Builder{}
- l := strings.Split(str, "\n")
- for i := range l {
- b.WriteString(l[i])
- b.WriteString(sp)
- if i != len(l)-1 {
- b.WriteRune('\n')
- }
- }
- return b.String()
- }
- func max(a, b int) int {
- if a > b {
- return a
- }
- return b
- }
- func min(a, b int) int {
- if a < b {
- return a
- }
- return b
- }
|