style.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. package lipgloss
  2. import (
  3. "strings"
  4. "unicode"
  5. "github.com/charmbracelet/x/ansi"
  6. "github.com/muesli/termenv"
  7. )
  8. const tabWidthDefault = 4
  9. // Property for a key.
  10. type propKey int64
  11. // Available properties.
  12. const (
  13. // Boolean props come first.
  14. boldKey propKey = 1 << iota
  15. italicKey
  16. underlineKey
  17. strikethroughKey
  18. reverseKey
  19. blinkKey
  20. faintKey
  21. underlineSpacesKey
  22. strikethroughSpacesKey
  23. colorWhitespaceKey
  24. // Non-boolean props.
  25. foregroundKey
  26. backgroundKey
  27. widthKey
  28. heightKey
  29. alignHorizontalKey
  30. alignVerticalKey
  31. // Padding.
  32. paddingTopKey
  33. paddingRightKey
  34. paddingBottomKey
  35. paddingLeftKey
  36. // Margins.
  37. marginTopKey
  38. marginRightKey
  39. marginBottomKey
  40. marginLeftKey
  41. marginBackgroundKey
  42. // Border runes.
  43. borderStyleKey
  44. // Border edges.
  45. borderTopKey
  46. borderRightKey
  47. borderBottomKey
  48. borderLeftKey
  49. // Border foreground colors.
  50. borderTopForegroundKey
  51. borderRightForegroundKey
  52. borderBottomForegroundKey
  53. borderLeftForegroundKey
  54. // Border background colors.
  55. borderTopBackgroundKey
  56. borderRightBackgroundKey
  57. borderBottomBackgroundKey
  58. borderLeftBackgroundKey
  59. inlineKey
  60. maxWidthKey
  61. maxHeightKey
  62. tabWidthKey
  63. transformKey
  64. )
  65. // props is a set of properties.
  66. type props int64
  67. // set sets a property.
  68. func (p props) set(k propKey) props {
  69. return p | props(k)
  70. }
  71. // unset unsets a property.
  72. func (p props) unset(k propKey) props {
  73. return p &^ props(k)
  74. }
  75. // has checks if a property is set.
  76. func (p props) has(k propKey) bool {
  77. return p&props(k) != 0
  78. }
  79. // NewStyle returns a new, empty Style. While it's syntactic sugar for the
  80. // Style{} primitive, it's recommended to use this function for creating styles
  81. // in case the underlying implementation changes. It takes an optional string
  82. // value to be set as the underlying string value for this style.
  83. func NewStyle() Style {
  84. return renderer.NewStyle()
  85. }
  86. // NewStyle returns a new, empty Style. While it's syntactic sugar for the
  87. // Style{} primitive, it's recommended to use this function for creating styles
  88. // in case the underlying implementation changes. It takes an optional string
  89. // value to be set as the underlying string value for this style.
  90. func (r *Renderer) NewStyle() Style {
  91. s := Style{r: r}
  92. return s
  93. }
  94. // Style contains a set of rules that comprise a style as a whole.
  95. type Style struct {
  96. r *Renderer
  97. props props
  98. value string
  99. // we store bool props values here
  100. attrs int
  101. // props that have values
  102. fgColor TerminalColor
  103. bgColor TerminalColor
  104. width int
  105. height int
  106. alignHorizontal Position
  107. alignVertical Position
  108. paddingTop int
  109. paddingRight int
  110. paddingBottom int
  111. paddingLeft int
  112. marginTop int
  113. marginRight int
  114. marginBottom int
  115. marginLeft int
  116. marginBgColor TerminalColor
  117. borderStyle Border
  118. borderTopFgColor TerminalColor
  119. borderRightFgColor TerminalColor
  120. borderBottomFgColor TerminalColor
  121. borderLeftFgColor TerminalColor
  122. borderTopBgColor TerminalColor
  123. borderRightBgColor TerminalColor
  124. borderBottomBgColor TerminalColor
  125. borderLeftBgColor TerminalColor
  126. maxWidth int
  127. maxHeight int
  128. tabWidth int
  129. transform func(string) string
  130. }
  131. // joinString joins a list of strings into a single string separated with a
  132. // space.
  133. func joinString(strs ...string) string {
  134. return strings.Join(strs, " ")
  135. }
  136. // SetString sets the underlying string value for this style. To render once
  137. // the underlying string is set, use the Style.String. This method is
  138. // a convenience for cases when having a stringer implementation is handy, such
  139. // as when using fmt.Sprintf. You can also simply define a style and render out
  140. // strings directly with Style.Render.
  141. func (s Style) SetString(strs ...string) Style {
  142. s.value = joinString(strs...)
  143. return s
  144. }
  145. // Value returns the raw, unformatted, underlying string value for this style.
  146. func (s Style) Value() string {
  147. return s.value
  148. }
  149. // String implements stringer for a Style, returning the rendered result based
  150. // on the rules in this style. An underlying string value must be set with
  151. // Style.SetString prior to using this method.
  152. func (s Style) String() string {
  153. return s.Render()
  154. }
  155. // Copy returns a copy of this style, including any underlying string values.
  156. //
  157. // Deprecated: to copy just use assignment (i.e. a := b). All methods also
  158. // return a new style.
  159. func (s Style) Copy() Style {
  160. return s
  161. }
  162. // Inherit overlays the style in the argument onto this style by copying each explicitly
  163. // set value from the argument style onto this style if it is not already explicitly set.
  164. // Existing set values are kept intact and not overwritten.
  165. //
  166. // Margins, padding, and underlying string values are not inherited.
  167. func (s Style) Inherit(i Style) Style {
  168. for k := boldKey; k <= transformKey; k <<= 1 {
  169. if !i.isSet(k) {
  170. continue
  171. }
  172. switch k { //nolint:exhaustive
  173. case marginTopKey, marginRightKey, marginBottomKey, marginLeftKey:
  174. // Margins are not inherited
  175. continue
  176. case paddingTopKey, paddingRightKey, paddingBottomKey, paddingLeftKey:
  177. // Padding is not inherited
  178. continue
  179. case backgroundKey:
  180. // The margins also inherit the background color
  181. if !s.isSet(marginBackgroundKey) && !i.isSet(marginBackgroundKey) {
  182. s.set(marginBackgroundKey, i.bgColor)
  183. }
  184. }
  185. if s.isSet(k) {
  186. continue
  187. }
  188. s.setFrom(k, i)
  189. }
  190. return s
  191. }
  192. // Render applies the defined style formatting to a given string.
  193. func (s Style) Render(strs ...string) string {
  194. if s.r == nil {
  195. s.r = renderer
  196. }
  197. if s.value != "" {
  198. strs = append([]string{s.value}, strs...)
  199. }
  200. var (
  201. str = joinString(strs...)
  202. p = s.r.ColorProfile()
  203. te = p.String()
  204. teSpace = p.String()
  205. teWhitespace = p.String()
  206. bold = s.getAsBool(boldKey, false)
  207. italic = s.getAsBool(italicKey, false)
  208. underline = s.getAsBool(underlineKey, false)
  209. strikethrough = s.getAsBool(strikethroughKey, false)
  210. reverse = s.getAsBool(reverseKey, false)
  211. blink = s.getAsBool(blinkKey, false)
  212. faint = s.getAsBool(faintKey, false)
  213. fg = s.getAsColor(foregroundKey)
  214. bg = s.getAsColor(backgroundKey)
  215. width = s.getAsInt(widthKey)
  216. height = s.getAsInt(heightKey)
  217. horizontalAlign = s.getAsPosition(alignHorizontalKey)
  218. verticalAlign = s.getAsPosition(alignVerticalKey)
  219. topPadding = s.getAsInt(paddingTopKey)
  220. rightPadding = s.getAsInt(paddingRightKey)
  221. bottomPadding = s.getAsInt(paddingBottomKey)
  222. leftPadding = s.getAsInt(paddingLeftKey)
  223. colorWhitespace = s.getAsBool(colorWhitespaceKey, true)
  224. inline = s.getAsBool(inlineKey, false)
  225. maxWidth = s.getAsInt(maxWidthKey)
  226. maxHeight = s.getAsInt(maxHeightKey)
  227. underlineSpaces = s.getAsBool(underlineSpacesKey, false) || (underline && s.getAsBool(underlineSpacesKey, true))
  228. strikethroughSpaces = s.getAsBool(strikethroughSpacesKey, false) || (strikethrough && s.getAsBool(strikethroughSpacesKey, true))
  229. // Do we need to style whitespace (padding and space outside
  230. // paragraphs) separately?
  231. styleWhitespace = reverse
  232. // Do we need to style spaces separately?
  233. useSpaceStyler = (underline && !underlineSpaces) || (strikethrough && !strikethroughSpaces) || underlineSpaces || strikethroughSpaces
  234. transform = s.getAsTransform(transformKey)
  235. )
  236. if transform != nil {
  237. str = transform(str)
  238. }
  239. if s.props == 0 {
  240. return s.maybeConvertTabs(str)
  241. }
  242. // Enable support for ANSI on the legacy Windows cmd.exe console. This is a
  243. // no-op on non-Windows systems and on Windows runs only once.
  244. enableLegacyWindowsANSI()
  245. if bold {
  246. te = te.Bold()
  247. }
  248. if italic {
  249. te = te.Italic()
  250. }
  251. if underline {
  252. te = te.Underline()
  253. }
  254. if reverse {
  255. teWhitespace = teWhitespace.Reverse()
  256. te = te.Reverse()
  257. }
  258. if blink {
  259. te = te.Blink()
  260. }
  261. if faint {
  262. te = te.Faint()
  263. }
  264. if fg != noColor {
  265. te = te.Foreground(fg.color(s.r))
  266. if styleWhitespace {
  267. teWhitespace = teWhitespace.Foreground(fg.color(s.r))
  268. }
  269. if useSpaceStyler {
  270. teSpace = teSpace.Foreground(fg.color(s.r))
  271. }
  272. }
  273. if bg != noColor {
  274. te = te.Background(bg.color(s.r))
  275. if colorWhitespace {
  276. teWhitespace = teWhitespace.Background(bg.color(s.r))
  277. }
  278. if useSpaceStyler {
  279. teSpace = teSpace.Background(bg.color(s.r))
  280. }
  281. }
  282. if underline {
  283. te = te.Underline()
  284. }
  285. if strikethrough {
  286. te = te.CrossOut()
  287. }
  288. if underlineSpaces {
  289. teSpace = teSpace.Underline()
  290. }
  291. if strikethroughSpaces {
  292. teSpace = teSpace.CrossOut()
  293. }
  294. // Potentially convert tabs to spaces
  295. str = s.maybeConvertTabs(str)
  296. // carriage returns can cause strange behaviour when rendering.
  297. str = strings.ReplaceAll(str, "\r\n", "\n")
  298. // Strip newlines in single line mode
  299. if inline {
  300. str = strings.ReplaceAll(str, "\n", "")
  301. }
  302. // Word wrap
  303. if !inline && width > 0 {
  304. wrapAt := width - leftPadding - rightPadding
  305. str = ansi.Wrap(str, wrapAt, "")
  306. }
  307. // Render core text
  308. {
  309. var b strings.Builder
  310. l := strings.Split(str, "\n")
  311. for i := range l {
  312. if useSpaceStyler {
  313. // Look for spaces and apply a different styler
  314. for _, r := range l[i] {
  315. if unicode.IsSpace(r) {
  316. b.WriteString(teSpace.Styled(string(r)))
  317. continue
  318. }
  319. b.WriteString(te.Styled(string(r)))
  320. }
  321. } else {
  322. b.WriteString(te.Styled(l[i]))
  323. }
  324. if i != len(l)-1 {
  325. b.WriteRune('\n')
  326. }
  327. }
  328. str = b.String()
  329. }
  330. // Padding
  331. if !inline { //nolint:nestif
  332. if leftPadding > 0 {
  333. var st *termenv.Style
  334. if colorWhitespace || styleWhitespace {
  335. st = &teWhitespace
  336. }
  337. str = padLeft(str, leftPadding, st)
  338. }
  339. if rightPadding > 0 {
  340. var st *termenv.Style
  341. if colorWhitespace || styleWhitespace {
  342. st = &teWhitespace
  343. }
  344. str = padRight(str, rightPadding, st)
  345. }
  346. if topPadding > 0 {
  347. str = strings.Repeat("\n", topPadding) + str
  348. }
  349. if bottomPadding > 0 {
  350. str += strings.Repeat("\n", bottomPadding)
  351. }
  352. }
  353. // Height
  354. if height > 0 {
  355. str = alignTextVertical(str, verticalAlign, height, nil)
  356. }
  357. // Set alignment. This will also pad short lines with spaces so that all
  358. // lines are the same length, so we run it under a few different conditions
  359. // beyond alignment.
  360. {
  361. numLines := strings.Count(str, "\n")
  362. if !(numLines == 0 && width == 0) {
  363. var st *termenv.Style
  364. if colorWhitespace || styleWhitespace {
  365. st = &teWhitespace
  366. }
  367. str = alignTextHorizontal(str, horizontalAlign, width, st)
  368. }
  369. }
  370. if !inline {
  371. str = s.applyBorder(str)
  372. str = s.applyMargins(str, inline)
  373. }
  374. // Truncate according to MaxWidth
  375. if maxWidth > 0 {
  376. lines := strings.Split(str, "\n")
  377. for i := range lines {
  378. lines[i] = ansi.Truncate(lines[i], maxWidth, "")
  379. }
  380. str = strings.Join(lines, "\n")
  381. }
  382. // Truncate according to MaxHeight
  383. if maxHeight > 0 {
  384. lines := strings.Split(str, "\n")
  385. height := min(maxHeight, len(lines))
  386. if len(lines) > 0 {
  387. str = strings.Join(lines[:height], "\n")
  388. }
  389. }
  390. return str
  391. }
  392. func (s Style) maybeConvertTabs(str string) string {
  393. tw := tabWidthDefault
  394. if s.isSet(tabWidthKey) {
  395. tw = s.getAsInt(tabWidthKey)
  396. }
  397. switch tw {
  398. case -1:
  399. return str
  400. case 0:
  401. return strings.ReplaceAll(str, "\t", "")
  402. default:
  403. return strings.ReplaceAll(str, "\t", strings.Repeat(" ", tw))
  404. }
  405. }
  406. func (s Style) applyMargins(str string, inline bool) string {
  407. var (
  408. topMargin = s.getAsInt(marginTopKey)
  409. rightMargin = s.getAsInt(marginRightKey)
  410. bottomMargin = s.getAsInt(marginBottomKey)
  411. leftMargin = s.getAsInt(marginLeftKey)
  412. styler termenv.Style
  413. )
  414. bgc := s.getAsColor(marginBackgroundKey)
  415. if bgc != noColor {
  416. styler = styler.Background(bgc.color(s.r))
  417. }
  418. // Add left and right margin
  419. str = padLeft(str, leftMargin, &styler)
  420. str = padRight(str, rightMargin, &styler)
  421. // Top/bottom margin
  422. if !inline {
  423. _, width := getLines(str)
  424. spaces := strings.Repeat(" ", width)
  425. if topMargin > 0 {
  426. str = styler.Styled(strings.Repeat(spaces+"\n", topMargin)) + str
  427. }
  428. if bottomMargin > 0 {
  429. str += styler.Styled(strings.Repeat("\n"+spaces, bottomMargin))
  430. }
  431. }
  432. return str
  433. }
  434. // Apply left padding.
  435. func padLeft(str string, n int, style *termenv.Style) string {
  436. return pad(str, -n, style)
  437. }
  438. // Apply right padding.
  439. func padRight(str string, n int, style *termenv.Style) string {
  440. return pad(str, n, style)
  441. }
  442. // pad adds padding to either the left or right side of a string.
  443. // Positive values add to the right side while negative values
  444. // add to the left side.
  445. func pad(str string, n int, style *termenv.Style) string {
  446. if n == 0 {
  447. return str
  448. }
  449. sp := strings.Repeat(" ", abs(n))
  450. if style != nil {
  451. sp = style.Styled(sp)
  452. }
  453. b := strings.Builder{}
  454. l := strings.Split(str, "\n")
  455. for i := range l {
  456. switch {
  457. // pad right
  458. case n > 0:
  459. b.WriteString(l[i])
  460. b.WriteString(sp)
  461. // pad left
  462. default:
  463. b.WriteString(sp)
  464. b.WriteString(l[i])
  465. }
  466. if i != len(l)-1 {
  467. b.WriteRune('\n')
  468. }
  469. }
  470. return b.String()
  471. }
  472. func max(a, b int) int { //nolint:unparam,predeclared
  473. if a > b {
  474. return a
  475. }
  476. return b
  477. }
  478. func min(a, b int) int { //nolint:predeclared
  479. if a < b {
  480. return a
  481. }
  482. return b
  483. }
  484. func abs(a int) int {
  485. if a < 0 {
  486. return -a
  487. }
  488. return a
  489. }