mirror of
https://github.com/charmbracelet/lipgloss.git
synced 2024-08-17 09:50:31 +03:00
When I create binaries for each OS using goreleaser, the propKeys generates an int overflow since the iota duplicates for each propKey This commit changes the int type to works well with binaries generated for arm and i386 archs.
587 lines
12 KiB
Go
587 lines
12 KiB
Go
package lipgloss
|
|
|
|
import (
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/charmbracelet/x/exp/term/ansi"
|
|
"github.com/muesli/termenv"
|
|
)
|
|
|
|
const tabWidthDefault = 4
|
|
|
|
// Property for a key.
|
|
type propKey int64
|
|
|
|
// Available properties.
|
|
const (
|
|
// Boolean props come first.
|
|
boldKey propKey = 1 << iota
|
|
italicKey
|
|
underlineKey
|
|
strikethroughKey
|
|
reverseKey
|
|
blinkKey
|
|
faintKey
|
|
underlineSpacesKey
|
|
strikethroughSpacesKey
|
|
colorWhitespaceKey
|
|
|
|
// Non-boolean props.
|
|
foregroundKey
|
|
backgroundKey
|
|
widthKey
|
|
heightKey
|
|
alignHorizontalKey
|
|
alignVerticalKey
|
|
|
|
// Padding.
|
|
paddingTopKey
|
|
paddingRightKey
|
|
paddingBottomKey
|
|
paddingLeftKey
|
|
|
|
// 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
|
|
tabWidthKey
|
|
|
|
transformKey
|
|
)
|
|
|
|
// props is a set of properties.
|
|
type props int64
|
|
|
|
// set sets a property.
|
|
func (p props) set(k propKey) props {
|
|
return p | props(k)
|
|
}
|
|
|
|
// unset unsets a property.
|
|
func (p props) unset(k propKey) props {
|
|
return p &^ props(k)
|
|
}
|
|
|
|
// has checks if a property is set.
|
|
func (p props) has(k propKey) bool {
|
|
return p&props(k) != 0
|
|
}
|
|
|
|
// 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
|
|
props props
|
|
value string
|
|
|
|
// we store bool props values here
|
|
attrs int
|
|
|
|
// props that have values
|
|
fgColor TerminalColor
|
|
bgColor TerminalColor
|
|
|
|
width int
|
|
height int
|
|
|
|
alignHorizontal Position
|
|
alignVertical Position
|
|
|
|
paddingTop int
|
|
paddingRight int
|
|
paddingBottom int
|
|
paddingLeft int
|
|
|
|
marginTop int
|
|
marginRight int
|
|
marginBottom int
|
|
marginLeft int
|
|
marginBgColor TerminalColor
|
|
|
|
borderStyle Border
|
|
borderTopFgColor TerminalColor
|
|
borderRightFgColor TerminalColor
|
|
borderBottomFgColor TerminalColor
|
|
borderLeftFgColor TerminalColor
|
|
borderTopBgColor TerminalColor
|
|
borderRightBgColor TerminalColor
|
|
borderBottomBgColor TerminalColor
|
|
borderLeftBgColor TerminalColor
|
|
|
|
maxWidth int
|
|
maxHeight int
|
|
tabWidth int
|
|
|
|
transform func(string) 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.
|
|
//
|
|
// Deprecated: to copy just use assignment (i.e. a := b). All methods also return a new style.
|
|
func (s Style) Copy() Style {
|
|
return s
|
|
}
|
|
|
|
// 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 {
|
|
for k := boldKey; k <= transformKey; k <<= 1 {
|
|
if !i.isSet(k) {
|
|
continue
|
|
}
|
|
|
|
switch k { //nolint:exhaustive
|
|
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.set(marginBackgroundKey, i.bgColor)
|
|
}
|
|
}
|
|
|
|
if s.isSet(k) {
|
|
continue
|
|
}
|
|
|
|
s.setFrom(k, i)
|
|
}
|
|
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...)
|
|
|
|
p = s.r.ColorProfile()
|
|
te = p.String()
|
|
teSpace = p.String()
|
|
teWhitespace = p.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
|
|
|
|
transform = s.getAsTransform(transformKey)
|
|
)
|
|
|
|
if transform != nil {
|
|
str = transform(str)
|
|
}
|
|
|
|
if s.props == 0 {
|
|
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()
|
|
}
|
|
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()
|
|
}
|
|
|
|
// Potentially convert tabs to spaces
|
|
str = s.maybeConvertTabs(str)
|
|
|
|
// Strip newlines in single line mode
|
|
if inline {
|
|
str = strings.ReplaceAll(str, "\n", "")
|
|
}
|
|
|
|
// Word wrap
|
|
if !inline && width > 0 {
|
|
wrapAt := width - leftPadding - rightPadding
|
|
str = ansi.Wrap(str, wrapAt, "")
|
|
}
|
|
|
|
// 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 { //nolint:nestif
|
|
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] = ansi.Truncate(lines[i], maxWidth, "")
|
|
}
|
|
|
|
str = strings.Join(lines, "\n")
|
|
}
|
|
|
|
// Truncate according to MaxHeight
|
|
if maxHeight > 0 {
|
|
lines := strings.Split(str, "\n")
|
|
height := min(maxHeight, len(lines))
|
|
if len(lines) > 0 {
|
|
str = strings.Join(lines[:height], "\n")
|
|
}
|
|
}
|
|
|
|
return str
|
|
}
|
|
|
|
func (s Style) maybeConvertTabs(str string) string {
|
|
tw := tabWidthDefault
|
|
if s.isSet(tabWidthKey) {
|
|
tw = s.getAsInt(tabWidthKey)
|
|
}
|
|
switch tw {
|
|
case -1:
|
|
return str
|
|
case 0:
|
|
return strings.ReplaceAll(str, "\t", "")
|
|
default:
|
|
return strings.ReplaceAll(str, "\t", strings.Repeat(" ", tw))
|
|
}
|
|
}
|
|
|
|
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 {
|
|
return pad(str, -n, style)
|
|
}
|
|
|
|
// Apply right padding.
|
|
func padRight(str string, n int, style *termenv.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 {
|
|
if n == 0 {
|
|
return str
|
|
}
|
|
|
|
sp := strings.Repeat(" ", abs(n))
|
|
if style != nil {
|
|
sp = style.Styled(sp)
|
|
}
|
|
|
|
b := strings.Builder{}
|
|
l := strings.Split(str, "\n")
|
|
|
|
for i := range l {
|
|
switch {
|
|
// pad right
|
|
case n > 0:
|
|
b.WriteString(l[i])
|
|
b.WriteString(sp)
|
|
// pad left
|
|
default:
|
|
b.WriteString(sp)
|
|
b.WriteString(l[i])
|
|
}
|
|
|
|
if i != len(l)-1 {
|
|
b.WriteRune('\n')
|
|
}
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func max(a, b int) int { //nolint:unparam
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func abs(a int) int {
|
|
if a < 0 {
|
|
return -a
|
|
}
|
|
|
|
return a
|
|
}
|