util.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  1. package tview
  2. import (
  3. "math"
  4. "os"
  5. "regexp"
  6. "sort"
  7. "strconv"
  8. "github.com/gdamore/tcell/v2"
  9. "github.com/rivo/uniseg"
  10. )
  11. // Text alignment within a box. Also used to align images.
  12. const (
  13. AlignLeft = iota
  14. AlignCenter
  15. AlignRight
  16. AlignTop = 0
  17. AlignBottom = 2
  18. )
  19. // Common regular expressions.
  20. var (
  21. colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([lbidrus]+|\-)?)?)?\]`)
  22. regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`)
  23. escapePattern = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`)
  24. nonEscapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`)
  25. boundaryPattern = regexp.MustCompile(`(([,\.\-:;!\?&#+]|\n)[ \t\f\r]*|([ \t\f\r]+))`)
  26. spacePattern = regexp.MustCompile(`\s+`)
  27. )
  28. // Positions of substrings in regular expressions.
  29. const (
  30. colorForegroundPos = 1
  31. colorBackgroundPos = 3
  32. colorFlagPos = 5
  33. )
  34. // The number of colors available in the terminal.
  35. var availableColors = 256
  36. // Predefined InputField acceptance functions.
  37. var (
  38. // InputFieldInteger accepts integers.
  39. InputFieldInteger func(text string, ch rune) bool
  40. // InputFieldFloat accepts floating-point numbers.
  41. InputFieldFloat func(text string, ch rune) bool
  42. // InputFieldMaxLength returns an input field accept handler which accepts
  43. // input strings up to a given length. Use it like this:
  44. //
  45. // inputField.SetAcceptanceFunc(InputFieldMaxLength(10)) // Accept up to 10 characters.
  46. InputFieldMaxLength func(maxLength int) func(text string, ch rune) bool
  47. )
  48. // Package initialization.
  49. func init() {
  50. // Initialize the predefined input field handlers.
  51. InputFieldInteger = func(text string, ch rune) bool {
  52. if text == "-" {
  53. return true
  54. }
  55. _, err := strconv.Atoi(text)
  56. return err == nil
  57. }
  58. InputFieldFloat = func(text string, ch rune) bool {
  59. if text == "-" || text == "." || text == "-." {
  60. return true
  61. }
  62. _, err := strconv.ParseFloat(text, 64)
  63. return err == nil
  64. }
  65. InputFieldMaxLength = func(maxLength int) func(text string, ch rune) bool {
  66. return func(text string, ch rune) bool {
  67. return len([]rune(text)) <= maxLength
  68. }
  69. }
  70. // Determine the number of colors available in the terminal.
  71. info, err := tcell.LookupTerminfo(os.Getenv("TERM"))
  72. if err == nil {
  73. availableColors = info.Colors
  74. }
  75. }
  76. // styleFromTag takes the given style, defined by a foreground color (fgColor),
  77. // a background color (bgColor), and style attributes, and modifies it based on
  78. // the substrings (tagSubstrings) extracted by the regular expression for color
  79. // tags. The new colors and attributes are returned where empty strings mean
  80. // "don't modify" and a dash ("-") means "reset to default".
  81. func styleFromTag(fgColor, bgColor, attributes string, tagSubstrings []string) (newFgColor, newBgColor, newAttributes string) {
  82. if tagSubstrings[colorForegroundPos] != "" {
  83. color := tagSubstrings[colorForegroundPos]
  84. if color == "-" {
  85. fgColor = "-"
  86. } else if color != "" {
  87. fgColor = color
  88. }
  89. }
  90. if tagSubstrings[colorBackgroundPos-1] != "" {
  91. color := tagSubstrings[colorBackgroundPos]
  92. if color == "-" {
  93. bgColor = "-"
  94. } else if color != "" {
  95. bgColor = color
  96. }
  97. }
  98. if tagSubstrings[colorFlagPos-1] != "" {
  99. flags := tagSubstrings[colorFlagPos]
  100. if flags == "-" {
  101. attributes = "-"
  102. } else if flags != "" {
  103. attributes = flags
  104. }
  105. }
  106. return fgColor, bgColor, attributes
  107. }
  108. // overlayStyle calculates a new style based on "style" and applying tag-based
  109. // colors/attributes to it (see also styleFromTag()).
  110. func overlayStyle(style tcell.Style, fgColor, bgColor, attributes string) tcell.Style {
  111. _, _, defAttr := style.Decompose()
  112. if fgColor != "" && fgColor != "-" {
  113. style = style.Foreground(tcell.GetColor(fgColor))
  114. }
  115. if bgColor != "" && bgColor != "-" {
  116. style = style.Background(tcell.GetColor(bgColor))
  117. }
  118. if attributes == "-" {
  119. style = style.Bold(defAttr&tcell.AttrBold > 0).
  120. Italic(defAttr&tcell.AttrItalic > 0).
  121. Blink(defAttr&tcell.AttrBlink > 0).
  122. Reverse(defAttr&tcell.AttrReverse > 0).
  123. Underline(defAttr&tcell.AttrUnderline > 0).
  124. Dim(defAttr&tcell.AttrDim > 0)
  125. } else if attributes != "" {
  126. style = style.Normal()
  127. for _, flag := range attributes {
  128. switch flag {
  129. case 'l':
  130. style = style.Blink(true)
  131. case 'b':
  132. style = style.Bold(true)
  133. case 'i':
  134. style = style.Italic(true)
  135. case 'd':
  136. style = style.Dim(true)
  137. case 'r':
  138. style = style.Reverse(true)
  139. case 'u':
  140. style = style.Underline(true)
  141. case 's':
  142. style = style.StrikeThrough(true)
  143. }
  144. }
  145. }
  146. return style
  147. }
  148. // decomposeString returns information about a string which may contain color
  149. // tags or region tags, depending on which ones are requested to be found. It
  150. // returns the indices of the color tags (as returned by
  151. // re.FindAllStringIndex()), the color tags themselves (as returned by
  152. // re.FindAllStringSubmatch()), the indices of region tags and the region tags
  153. // themselves, the indices of an escaped tags (only if at least color tags or
  154. // region tags are requested), the string stripped by any tags and escaped, and
  155. // the screen width of the stripped string.
  156. func decomposeString(text string, findColors, findRegions bool) (colorIndices [][]int, colors [][]string, regionIndices [][]int, regions [][]string, escapeIndices [][]int, stripped string, width int) {
  157. // Shortcut for the trivial case.
  158. if !findColors && !findRegions {
  159. return nil, nil, nil, nil, nil, text, uniseg.StringWidth(text)
  160. }
  161. // Get positions of any tags.
  162. if findColors {
  163. colorIndices = colorPattern.FindAllStringIndex(text, -1)
  164. colors = colorPattern.FindAllStringSubmatch(text, -1)
  165. }
  166. if findRegions {
  167. regionIndices = regionPattern.FindAllStringIndex(text, -1)
  168. regions = regionPattern.FindAllStringSubmatch(text, -1)
  169. }
  170. escapeIndices = escapePattern.FindAllStringIndex(text, -1)
  171. // Because the color pattern detects empty tags, we need to filter them out.
  172. for i := len(colorIndices) - 1; i >= 0; i-- {
  173. if colorIndices[i][1]-colorIndices[i][0] == 2 {
  174. colorIndices = append(colorIndices[:i], colorIndices[i+1:]...)
  175. colors = append(colors[:i], colors[i+1:]...)
  176. }
  177. }
  178. // Make a (sorted) list of all tags.
  179. allIndices := make([][3]int, 0, len(colorIndices)+len(regionIndices)+len(escapeIndices))
  180. for indexType, index := range [][][]int{colorIndices, regionIndices, escapeIndices} {
  181. for _, tag := range index {
  182. allIndices = append(allIndices, [3]int{tag[0], tag[1], indexType})
  183. }
  184. }
  185. sort.Slice(allIndices, func(i int, j int) bool {
  186. return allIndices[i][0] < allIndices[j][0]
  187. })
  188. // Remove the tags from the original string.
  189. var from int
  190. buf := make([]byte, 0, len(text))
  191. for _, indices := range allIndices {
  192. if indices[2] == 2 { // Escape sequences are not simply removed.
  193. buf = append(buf, []byte(text[from:indices[1]-2])...)
  194. buf = append(buf, ']')
  195. from = indices[1]
  196. } else {
  197. buf = append(buf, []byte(text[from:indices[0]])...)
  198. from = indices[1]
  199. }
  200. }
  201. buf = append(buf, text[from:]...)
  202. stripped = string(buf)
  203. // Get the width of the stripped string.
  204. width = uniseg.StringWidth(stripped)
  205. return
  206. }
  207. // Print prints text onto the screen into the given box at (x,y,maxWidth,1),
  208. // not exceeding that box. "align" is one of AlignLeft, AlignCenter, or
  209. // AlignRight. The screen's background color will not be changed.
  210. //
  211. // You can change the colors and text styles mid-text by inserting a color tag.
  212. // See the package description for details.
  213. //
  214. // Returns the number of actual bytes of the text printed (including color tags)
  215. // and the actual width used for the printed runes.
  216. func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tcell.Color) (int, int) {
  217. bytes, width, _, _ := printWithStyle(screen, text, x, y, 0, maxWidth, align, tcell.StyleDefault.Foreground(color), true)
  218. return bytes, width
  219. }
  220. // printWithStyle works like Print() but it takes a style instead of just a
  221. // foreground color. The skipWidth parameter specifies the number of cells
  222. // skipped at the beginning of the text. It also returns the start and end index
  223. // (exclusively) of the text actually printed. If maintainBackground is "true",
  224. // The existing screen background is not changed (i.e. the style's background
  225. // color is ignored).
  226. func printWithStyle(screen tcell.Screen, text string, x, y, skipWidth, maxWidth, align int, style tcell.Style, maintainBackground bool) (int, int, int, int) {
  227. totalWidth, totalHeight := screen.Size()
  228. if maxWidth <= 0 || len(text) == 0 || y < 0 || y >= totalHeight {
  229. return 0, 0, 0, 0
  230. }
  231. // Decompose the text.
  232. colorIndices, colors, _, _, escapeIndices, strippedText, strippedWidth := decomposeString(text, true, false)
  233. // We want to reduce all alignments to AlignLeft.
  234. if align == AlignRight {
  235. if strippedWidth-skipWidth <= maxWidth {
  236. // There's enough space for the entire text.
  237. return printWithStyle(screen, text, x+maxWidth-strippedWidth+skipWidth, y, skipWidth, maxWidth, AlignLeft, style, maintainBackground)
  238. }
  239. // Trim characters off the beginning.
  240. var (
  241. bytes, width, colorPos, escapePos, tagOffset, from, to int
  242. foregroundColor, backgroundColor, attributes string
  243. )
  244. originalStyle := style
  245. iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
  246. // Update color/escape tag offset and style.
  247. if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
  248. foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
  249. style = overlayStyle(originalStyle, foregroundColor, backgroundColor, attributes)
  250. tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
  251. colorPos++
  252. }
  253. if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
  254. tagOffset++
  255. escapePos++
  256. }
  257. if strippedWidth-screenPos <= maxWidth {
  258. // We chopped off enough.
  259. if escapePos > 0 && textPos+tagOffset-1 >= escapeIndices[escapePos-1][0] && textPos+tagOffset-1 < escapeIndices[escapePos-1][1] {
  260. // Unescape open escape sequences.
  261. escapeCharPos := escapeIndices[escapePos-1][1] - 2
  262. text = text[:escapeCharPos] + text[escapeCharPos+1:]
  263. }
  264. // Print and return.
  265. bytes, width, from, to = printWithStyle(screen, text[textPos+tagOffset:], x, y, 0, maxWidth, AlignLeft, style, maintainBackground)
  266. from += textPos + tagOffset
  267. to += textPos + tagOffset
  268. return true
  269. }
  270. return false
  271. })
  272. return bytes, width, from, to
  273. } else if align == AlignCenter {
  274. if strippedWidth-skipWidth == maxWidth {
  275. // Use the exact space.
  276. return printWithStyle(screen, text, x, y, skipWidth, maxWidth, AlignLeft, style, maintainBackground)
  277. } else if strippedWidth-skipWidth < maxWidth {
  278. // We have more space than we need.
  279. half := (maxWidth - strippedWidth + skipWidth) / 2
  280. return printWithStyle(screen, text, x+half, y, skipWidth, maxWidth-half, AlignLeft, style, maintainBackground)
  281. } else {
  282. // Chop off runes until we have a perfect fit.
  283. var choppedLeft, choppedRight, leftIndex, rightIndex int
  284. rightIndex = len(strippedText)
  285. for rightIndex-1 > leftIndex && strippedWidth-skipWidth-choppedLeft-choppedRight > maxWidth {
  286. if skipWidth > 0 || choppedLeft < choppedRight {
  287. // Iterate on the left by one character.
  288. iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
  289. if skipWidth > 0 {
  290. skipWidth -= screenWidth
  291. strippedWidth -= screenWidth
  292. } else {
  293. choppedLeft += screenWidth
  294. }
  295. leftIndex += textWidth
  296. return true
  297. })
  298. } else {
  299. // Iterate on the right by one character.
  300. iterateStringReverse(strippedText[leftIndex:rightIndex], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
  301. choppedRight += screenWidth
  302. rightIndex -= textWidth
  303. return true
  304. })
  305. }
  306. }
  307. // Add tag offsets and determine start style.
  308. var (
  309. colorPos, escapePos, tagOffset int
  310. foregroundColor, backgroundColor, attributes string
  311. )
  312. originalStyle := style
  313. for index := range strippedText {
  314. // We only need the offset of the left index.
  315. if index > leftIndex {
  316. // We're done.
  317. if escapePos > 0 && leftIndex+tagOffset-1 >= escapeIndices[escapePos-1][0] && leftIndex+tagOffset-1 < escapeIndices[escapePos-1][1] {
  318. // Unescape open escape sequences.
  319. escapeCharPos := escapeIndices[escapePos-1][1] - 2
  320. text = text[:escapeCharPos] + text[escapeCharPos+1:]
  321. }
  322. break
  323. }
  324. // Update color/escape tag offset.
  325. if colorPos < len(colorIndices) && index+tagOffset >= colorIndices[colorPos][0] && index+tagOffset < colorIndices[colorPos][1] {
  326. if index <= leftIndex {
  327. foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
  328. style = overlayStyle(originalStyle, foregroundColor, backgroundColor, attributes)
  329. }
  330. tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
  331. colorPos++
  332. }
  333. if escapePos < len(escapeIndices) && index+tagOffset >= escapeIndices[escapePos][0] && index+tagOffset < escapeIndices[escapePos][1] {
  334. tagOffset++
  335. escapePos++
  336. }
  337. }
  338. bytes, width, from, to := printWithStyle(screen, text[leftIndex+tagOffset:], x, y, 0, maxWidth, AlignLeft, style, maintainBackground)
  339. from += leftIndex + tagOffset
  340. to += leftIndex + tagOffset
  341. return bytes, width, from, to
  342. }
  343. }
  344. // Draw text.
  345. var (
  346. drawn, drawnWidth, colorPos, escapePos, tagOffset, from, to int
  347. foregroundColor, backgroundColor, attributes string
  348. )
  349. iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth, boundaries int) bool {
  350. // Skip character if necessary.
  351. if skipWidth > 0 {
  352. skipWidth -= screenWidth
  353. from = textPos + length
  354. to = from
  355. return false
  356. }
  357. // Only continue if there is still space.
  358. if drawnWidth+screenWidth > maxWidth || x+drawnWidth >= totalWidth {
  359. return true
  360. }
  361. // Handle color tags.
  362. for colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
  363. foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
  364. tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
  365. colorPos++
  366. }
  367. // Handle escape tags.
  368. if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
  369. if textPos+tagOffset == escapeIndices[escapePos][1]-2 {
  370. tagOffset++
  371. escapePos++
  372. }
  373. }
  374. // Memorize positions.
  375. to = textPos + length
  376. // Print the rune sequence.
  377. finalX := x + drawnWidth
  378. finalStyle := style
  379. if maintainBackground {
  380. _, _, existingStyle, _ := screen.GetContent(finalX, y)
  381. _, background, _ := existingStyle.Decompose()
  382. finalStyle = finalStyle.Background(background)
  383. }
  384. finalStyle = overlayStyle(finalStyle, foregroundColor, backgroundColor, attributes)
  385. for offset := screenWidth - 1; offset >= 0; offset-- {
  386. // To avoid undesired effects, we populate all cells.
  387. if offset == 0 {
  388. screen.SetContent(finalX+offset, y, main, comb, finalStyle)
  389. } else {
  390. screen.SetContent(finalX+offset, y, ' ', nil, finalStyle)
  391. }
  392. }
  393. // Advance.
  394. drawn += length
  395. drawnWidth += screenWidth
  396. return false
  397. })
  398. return drawn + tagOffset + len(escapeIndices), drawnWidth, from, to
  399. }
  400. // PrintSimple prints white text to the screen at the given position.
  401. func PrintSimple(screen tcell.Screen, text string, x, y int) {
  402. Print(screen, text, x, y, math.MaxInt32, AlignLeft, Styles.PrimaryTextColor)
  403. }
  404. // TaggedStringWidth returns the width of the given string needed to print it on
  405. // screen. The text may contain color tags which are not counted.
  406. func TaggedStringWidth(text string) int {
  407. _, _, _, _, _, _, width := decomposeString(text, true, false)
  408. return width
  409. }
  410. // WordWrap splits a text such that each resulting line does not exceed the
  411. // given screen width. Possible split points are after any punctuation or
  412. // whitespace. Whitespace after split points will be dropped.
  413. //
  414. // This function considers color tags to have no width.
  415. //
  416. // Text is always split at newline characters ('\n').
  417. func WordWrap(text string, width int) (lines []string) {
  418. colorTagIndices, _, _, _, escapeIndices, strippedText, _ := decomposeString(text, true, false)
  419. // Find candidate breakpoints.
  420. breakpoints := boundaryPattern.FindAllStringSubmatchIndex(strippedText, -1)
  421. // Results in one entry for each candidate. Each entry is an array a of
  422. // indices into strippedText where a[6] < 0 for newline/punctuation matches
  423. // and a[4] < 0 for whitespace matches.
  424. // Process stripped text one character at a time.
  425. var (
  426. colorPos, escapePos, breakpointPos, tagOffset int
  427. lastBreakpoint, lastContinuation, currentLineStart int
  428. lineWidth, overflow int
  429. forceBreak bool
  430. )
  431. unescape := func(substr string, startIndex int) string {
  432. // A helper function to unescape escaped tags.
  433. for index := escapePos; index >= 0; index-- {
  434. if index < len(escapeIndices) && startIndex > escapeIndices[index][0] && startIndex < escapeIndices[index][1]-1 {
  435. pos := escapeIndices[index][1] - 2 - startIndex
  436. return substr[:pos] + substr[pos+1:]
  437. }
  438. }
  439. return substr
  440. }
  441. iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
  442. // Handle tags.
  443. for {
  444. if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
  445. // Colour tags.
  446. tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
  447. colorPos++
  448. } else if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
  449. // Escape tags.
  450. tagOffset++
  451. escapePos++
  452. } else {
  453. break
  454. }
  455. }
  456. // Is this a breakpoint?
  457. if breakpointPos < len(breakpoints) && textPos+tagOffset == breakpoints[breakpointPos][0] {
  458. // Yes, it is. Set up breakpoint infos depending on its type.
  459. lastBreakpoint = breakpoints[breakpointPos][0] + tagOffset
  460. lastContinuation = breakpoints[breakpointPos][1] + tagOffset
  461. overflow = 0
  462. forceBreak = main == '\n'
  463. if breakpoints[breakpointPos][6] < 0 && !forceBreak {
  464. lastBreakpoint++ // Don't skip punctuation.
  465. }
  466. breakpointPos++
  467. }
  468. // Check if a break is warranted.
  469. if forceBreak || lineWidth > 0 && lineWidth+screenWidth > width {
  470. breakpoint := lastBreakpoint
  471. continuation := lastContinuation
  472. if forceBreak {
  473. breakpoint = textPos + tagOffset
  474. continuation = textPos + tagOffset + 1
  475. lastBreakpoint = 0
  476. overflow = 0
  477. } else if lastBreakpoint <= currentLineStart {
  478. breakpoint = textPos + tagOffset
  479. continuation = textPos + tagOffset
  480. overflow = 0
  481. }
  482. lines = append(lines, unescape(text[currentLineStart:breakpoint], currentLineStart))
  483. currentLineStart, lineWidth, forceBreak = continuation, overflow, false
  484. }
  485. // Remember the characters since the last breakpoint.
  486. if lastBreakpoint > 0 && lastContinuation <= textPos+tagOffset {
  487. overflow += screenWidth
  488. }
  489. // Advance.
  490. lineWidth += screenWidth
  491. // But if we're still inside a breakpoint, skip next character (whitespace).
  492. if textPos+tagOffset < currentLineStart {
  493. lineWidth -= screenWidth
  494. }
  495. return false
  496. })
  497. // Flush the rest.
  498. if currentLineStart < len(text) {
  499. lines = append(lines, unescape(text[currentLineStart:], currentLineStart))
  500. }
  501. return
  502. }
  503. // Escape escapes the given text such that color and/or region tags are not
  504. // recognized and substituted by the print functions of this package. For
  505. // example, to include a tag-like string in a box title or in a TextView:
  506. //
  507. // box.SetTitle(tview.Escape("[squarebrackets]"))
  508. // fmt.Fprint(textView, tview.Escape(`["quoted"]`))
  509. func Escape(text string) string {
  510. return nonEscapePattern.ReplaceAllString(text, "$1[]")
  511. }
  512. // iterateString iterates through the given string one printed character at a
  513. // time. For each such character, the callback function is called with the
  514. // Unicode code points of the character (the first rune and any combining runes
  515. // which may be nil if there aren't any), the starting position (in bytes)
  516. // within the original string, its length in bytes, the screen position of the
  517. // character, the screen width of it, and a boundaries value which includes
  518. // word/sentence boundary or line break information (see the
  519. // github.com/rivo/uniseg package, Step() function, for more information). The
  520. // iteration stops if the callback returns true. This function returns true if
  521. // the iteration was stopped before the last character.
  522. func iterateString(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool) bool {
  523. var screenPos, textPos, boundaries int
  524. state := -1
  525. for len(text) > 0 {
  526. var cluster string
  527. cluster, text, boundaries, state = uniseg.StepString(text, state)
  528. width := boundaries >> uniseg.ShiftWidth
  529. runes := []rune(cluster)
  530. var comb []rune
  531. if len(runes) > 1 {
  532. comb = runes[1:]
  533. }
  534. if callback(runes[0], comb, textPos, len(cluster), screenPos, width, boundaries) {
  535. return true
  536. }
  537. screenPos += width
  538. textPos += len(cluster)
  539. }
  540. return false
  541. }
  542. // iterateStringReverse iterates through the given string in reverse, starting
  543. // from the end of the string, one printed character at a time. For each such
  544. // character, the callback function is called with the Unicode code points of
  545. // the character (the first rune and any combining runes which may be nil if
  546. // there aren't any), the starting position (in bytes) within the original
  547. // string, its length in bytes, the screen position of the character, and the
  548. // screen width of it. The iteration stops if the callback returns true. This
  549. // function returns true if the iteration was stopped before the last character.
  550. func iterateStringReverse(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
  551. type cluster struct {
  552. main rune
  553. comb []rune
  554. textPos, textWidth, screenPos, screenWidth int
  555. }
  556. // Create the grapheme clusters.
  557. var clusters []cluster
  558. iterateString(text, func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth, boundaries int) bool {
  559. clusters = append(clusters, cluster{
  560. main: main,
  561. comb: comb,
  562. textPos: textPos,
  563. textWidth: textWidth,
  564. screenPos: screenPos,
  565. screenWidth: screenWidth,
  566. })
  567. return false
  568. })
  569. // Iterate in reverse.
  570. for index := len(clusters) - 1; index >= 0; index-- {
  571. if callback(
  572. clusters[index].main,
  573. clusters[index].comb,
  574. clusters[index].textPos,
  575. clusters[index].textWidth,
  576. clusters[index].screenPos,
  577. clusters[index].screenWidth,
  578. ) {
  579. return true
  580. }
  581. }
  582. return false
  583. }
  584. // stripTags strips colour tags from the given string. (Region tags are not
  585. // stripped.)
  586. func stripTags(text string) string {
  587. stripped := colorPattern.ReplaceAllStringFunc(text, func(match string) string {
  588. if len(match) > 2 {
  589. return ""
  590. }
  591. return match
  592. })
  593. return escapePattern.ReplaceAllString(stripped, `[$1$2]`)
  594. }