textview.go 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190
  1. package tview
  2. import (
  3. "bytes"
  4. "fmt"
  5. "regexp"
  6. "strings"
  7. "sync"
  8. "unicode/utf8"
  9. "github.com/gdamore/tcell"
  10. colorful "github.com/lucasb-eyer/go-colorful"
  11. runewidth "github.com/mattn/go-runewidth"
  12. "github.com/rivo/uniseg"
  13. )
  14. var (
  15. openColorRegex = regexp.MustCompile(`\[([a-zA-Z]*|#[0-9a-zA-Z]*)$`)
  16. openRegionRegex = regexp.MustCompile(`\["[a-zA-Z0-9_,;: \-\.]*"?$`)
  17. newLineRegex = regexp.MustCompile(`\r?\n`)
  18. // TabSize is the number of spaces with which a tab character will be replaced.
  19. TabSize = 4
  20. )
  21. // textViewIndex contains information about each line displayed in the text
  22. // view.
  23. type textViewIndex struct {
  24. Line int // The index into the "buffer" variable.
  25. Pos int // The index into the "buffer" string (byte position).
  26. NextPos int // The (byte) index of the next character in this buffer line.
  27. Width int // The screen width of this line.
  28. ForegroundColor string // The starting foreground color ("" = don't change, "-" = reset).
  29. BackgroundColor string // The starting background color ("" = don't change, "-" = reset).
  30. Attributes string // The starting attributes ("" = don't change, "-" = reset).
  31. Region string // The starting region ID.
  32. }
  33. // textViewRegion contains information about a region.
  34. type textViewRegion struct {
  35. // The region ID.
  36. ID string
  37. // The starting and end screen position of the region as determined the last
  38. // time Draw() was called. A negative value indicates out-of-rect positions.
  39. FromX, FromY, ToX, ToY int
  40. }
  41. // TextView is a box which displays text. It implements the io.Writer interface
  42. // so you can stream text to it. This does not trigger a redraw automatically
  43. // but if a handler is installed via SetChangedFunc(), you can cause it to be
  44. // redrawn. (See SetChangedFunc() for more details.)
  45. //
  46. // Navigation
  47. //
  48. // If the text view is scrollable (the default), text is kept in a buffer which
  49. // may be larger than the screen and can be navigated similarly to Vim:
  50. //
  51. // - h, left arrow: Move left.
  52. // - l, right arrow: Move right.
  53. // - j, down arrow: Move down.
  54. // - k, up arrow: Move up.
  55. // - g, home: Move to the top.
  56. // - G, end: Move to the bottom.
  57. // - Ctrl-F, page down: Move down by one page.
  58. // - Ctrl-B, page up: Move up by one page.
  59. //
  60. // If the text is not scrollable, any text above the top visible line is
  61. // discarded.
  62. //
  63. // Use SetInputCapture() to override or modify keyboard input.
  64. //
  65. // Colors
  66. //
  67. // If dynamic colors are enabled via SetDynamicColors(), text color can be
  68. // changed dynamically by embedding color strings in square brackets. This works
  69. // the same way as anywhere else. Please see the package documentation for more
  70. // information.
  71. //
  72. // Regions and Highlights
  73. //
  74. // If regions are enabled via SetRegions(), you can define text regions within
  75. // the text and assign region IDs to them. Text regions start with region tags.
  76. // Region tags are square brackets that contain a region ID in double quotes,
  77. // for example:
  78. //
  79. // We define a ["rg"]region[""] here.
  80. //
  81. // A text region ends with the next region tag. Tags with no region ID ([""])
  82. // don't start new regions. They can therefore be used to mark the end of a
  83. // region. Region IDs must satisfy the following regular expression:
  84. //
  85. // [a-zA-Z0-9_,;: \-\.]+
  86. //
  87. // Regions can be highlighted by calling the Highlight() function with one or
  88. // more region IDs. This can be used to display search results, for example.
  89. //
  90. // The ScrollToHighlight() function can be used to jump to the currently
  91. // highlighted region once when the text view is drawn the next time.
  92. //
  93. // See https://github.com/rivo/tview/wiki/TextView for an example.
  94. type TextView struct {
  95. sync.Mutex
  96. *Box
  97. // The text buffer.
  98. buffer []string
  99. // The last bytes that have been received but are not part of the buffer yet.
  100. recentBytes []byte
  101. // The processed line index. This is nil if the buffer has changed and needs
  102. // to be re-indexed.
  103. index []*textViewIndex
  104. // The text alignment, one of AlignLeft, AlignCenter, or AlignRight.
  105. align int
  106. // Information about visible regions as of the last call to Draw().
  107. regionInfos []*textViewRegion
  108. // Indices into the "index" slice which correspond to the first line of the
  109. // first highlight and the last line of the last highlight. This is calculated
  110. // during re-indexing. Set to -1 if there is no current highlight.
  111. fromHighlight, toHighlight int
  112. // The screen space column of the highlight in its first line. Set to -1 if
  113. // there is no current highlight.
  114. posHighlight int
  115. // A set of region IDs that are currently highlighted.
  116. highlights map[string]struct{}
  117. // The last width for which the current table is drawn.
  118. lastWidth int
  119. // The screen width of the longest line in the index (not the buffer).
  120. longestLine int
  121. // The index of the first line shown in the text view.
  122. lineOffset int
  123. // If set to true, the text view will always remain at the end of the content.
  124. trackEnd bool
  125. // The number of characters to be skipped on each line (not in wrap mode).
  126. columnOffset int
  127. // The height of the content the last time the text view was drawn.
  128. pageSize int
  129. // If set to true, the text view will keep a buffer of text which can be
  130. // navigated when the text is longer than what fits into the box.
  131. scrollable bool
  132. // If set to true, lines that are longer than the available width are wrapped
  133. // onto the next line. If set to false, any characters beyond the available
  134. // width are discarded.
  135. wrap bool
  136. // If set to true and if wrap is also true, lines are split at spaces or
  137. // after punctuation characters.
  138. wordWrap bool
  139. // The (starting) color of the text.
  140. textColor tcell.Color
  141. // If set to true, the text color can be changed dynamically by piping color
  142. // strings in square brackets to the text view.
  143. dynamicColors bool
  144. // If set to true, region tags can be used to define regions.
  145. regions bool
  146. // A temporary flag which, when true, will automatically bring the current
  147. // highlight(s) into the visible screen.
  148. scrollToHighlights bool
  149. // If true, setting new highlights will be a XOR instead of an overwrite
  150. // operation.
  151. toggleHighlights bool
  152. // An optional function which is called when the content of the text view has
  153. // changed.
  154. changed func()
  155. // An optional function which is called when the user presses one of the
  156. // following keys: Escape, Enter, Tab, Backtab.
  157. done func(tcell.Key)
  158. // An optional function which is called when one or more regions were
  159. // highlighted.
  160. highlighted func(added, removed, remaining []string)
  161. }
  162. // NewTextView returns a new text view.
  163. func NewTextView() *TextView {
  164. return &TextView{
  165. Box: NewBox(),
  166. highlights: make(map[string]struct{}),
  167. lineOffset: -1,
  168. scrollable: true,
  169. align: AlignLeft,
  170. wrap: true,
  171. textColor: Styles.PrimaryTextColor,
  172. regions: false,
  173. dynamicColors: false,
  174. }
  175. }
  176. // SetScrollable sets the flag that decides whether or not the text view is
  177. // scrollable. If true, text is kept in a buffer and can be navigated. If false,
  178. // the last line will always be visible.
  179. func (t *TextView) SetScrollable(scrollable bool) *TextView {
  180. t.scrollable = scrollable
  181. if !scrollable {
  182. t.trackEnd = true
  183. }
  184. return t
  185. }
  186. // SetWrap sets the flag that, if true, leads to lines that are longer than the
  187. // available width being wrapped onto the next line. If false, any characters
  188. // beyond the available width are not displayed.
  189. func (t *TextView) SetWrap(wrap bool) *TextView {
  190. if t.wrap != wrap {
  191. t.index = nil
  192. }
  193. t.wrap = wrap
  194. return t
  195. }
  196. // SetWordWrap sets the flag that, if true and if the "wrap" flag is also true
  197. // (see SetWrap()), wraps the line at spaces or after punctuation marks. Note
  198. // that trailing spaces will not be printed.
  199. //
  200. // This flag is ignored if the "wrap" flag is false.
  201. func (t *TextView) SetWordWrap(wrapOnWords bool) *TextView {
  202. if t.wordWrap != wrapOnWords {
  203. t.index = nil
  204. }
  205. t.wordWrap = wrapOnWords
  206. return t
  207. }
  208. // SetTextAlign sets the text alignment within the text view. This must be
  209. // either AlignLeft, AlignCenter, or AlignRight.
  210. func (t *TextView) SetTextAlign(align int) *TextView {
  211. if t.align != align {
  212. t.index = nil
  213. }
  214. t.align = align
  215. return t
  216. }
  217. // SetTextColor sets the initial color of the text (which can be changed
  218. // dynamically by sending color strings in square brackets to the text view if
  219. // dynamic colors are enabled).
  220. func (t *TextView) SetTextColor(color tcell.Color) *TextView {
  221. t.textColor = color
  222. return t
  223. }
  224. // SetText sets the text of this text view to the provided string. Previously
  225. // contained text will be removed.
  226. func (t *TextView) SetText(text string) *TextView {
  227. t.Clear()
  228. fmt.Fprint(t, text)
  229. return t
  230. }
  231. // GetText returns the current text of this text view. If "stripTags" is set
  232. // to true, any region/color tags are stripped from the text.
  233. func (t *TextView) GetText(stripTags bool) string {
  234. // Get the buffer.
  235. buffer := t.buffer
  236. if !stripTags {
  237. buffer = append(buffer, string(t.recentBytes))
  238. }
  239. // Add newlines again.
  240. text := strings.Join(buffer, "\n")
  241. // Strip from tags if required.
  242. if stripTags {
  243. if t.regions {
  244. text = regionPattern.ReplaceAllString(text, "")
  245. }
  246. if t.dynamicColors {
  247. text = colorPattern.ReplaceAllStringFunc(text, func(match string) string {
  248. if len(match) > 2 {
  249. return ""
  250. }
  251. return match
  252. })
  253. }
  254. if t.regions || t.dynamicColors {
  255. text = escapePattern.ReplaceAllString(text, `[$1$2]`)
  256. }
  257. }
  258. return text
  259. }
  260. // SetDynamicColors sets the flag that allows the text color to be changed
  261. // dynamically. See class description for details.
  262. func (t *TextView) SetDynamicColors(dynamic bool) *TextView {
  263. if t.dynamicColors != dynamic {
  264. t.index = nil
  265. }
  266. t.dynamicColors = dynamic
  267. return t
  268. }
  269. // SetRegions sets the flag that allows to define regions in the text. See class
  270. // description for details.
  271. func (t *TextView) SetRegions(regions bool) *TextView {
  272. if t.regions != regions {
  273. t.index = nil
  274. }
  275. t.regions = regions
  276. return t
  277. }
  278. // SetChangedFunc sets a handler function which is called when the text of the
  279. // text view has changed. This is useful when text is written to this io.Writer
  280. // in a separate goroutine. Doing so does not automatically cause the screen to
  281. // be refreshed so you may want to use the "changed" handler to redraw the
  282. // screen.
  283. //
  284. // Note that to avoid race conditions or deadlocks, there are a few rules you
  285. // should follow:
  286. //
  287. // - You can call Application.Draw() from this handler.
  288. // - You can call TextView.HasFocus() from this handler.
  289. // - During the execution of this handler, access to any other variables from
  290. // this primitive or any other primitive must be queued using
  291. // Application.QueueUpdate().
  292. //
  293. // See package description for details on dealing with concurrency.
  294. func (t *TextView) SetChangedFunc(handler func()) *TextView {
  295. t.changed = handler
  296. return t
  297. }
  298. // SetDoneFunc sets a handler which is called when the user presses on the
  299. // following keys: Escape, Enter, Tab, Backtab. The key is passed to the
  300. // handler.
  301. func (t *TextView) SetDoneFunc(handler func(key tcell.Key)) *TextView {
  302. t.done = handler
  303. return t
  304. }
  305. // SetHighlightedFunc sets a handler which is called when the list of currently
  306. // highlighted regions change. It receives a list of region IDs which were newly
  307. // highlighted, those that are not highlighted anymore, and those that remain
  308. // highlighted.
  309. //
  310. // Note that because regions are only determined during drawing, this function
  311. // can only fire for regions that have existed during the last call to Draw().
  312. func (t *TextView) SetHighlightedFunc(handler func(added, removed, remaining []string)) *TextView {
  313. t.highlighted = handler
  314. return t
  315. }
  316. // ScrollTo scrolls to the specified row and column (both starting with 0).
  317. func (t *TextView) ScrollTo(row, column int) *TextView {
  318. if !t.scrollable {
  319. return t
  320. }
  321. t.lineOffset = row
  322. t.columnOffset = column
  323. t.trackEnd = false
  324. return t
  325. }
  326. // ScrollToBeginning scrolls to the top left corner of the text if the text view
  327. // is scrollable.
  328. func (t *TextView) ScrollToBeginning() *TextView {
  329. if !t.scrollable {
  330. return t
  331. }
  332. t.trackEnd = false
  333. t.lineOffset = 0
  334. t.columnOffset = 0
  335. return t
  336. }
  337. // ScrollToEnd scrolls to the bottom left corner of the text if the text view
  338. // is scrollable. Adding new rows to the end of the text view will cause it to
  339. // scroll with the new data.
  340. func (t *TextView) ScrollToEnd() *TextView {
  341. if !t.scrollable {
  342. return t
  343. }
  344. t.trackEnd = true
  345. t.columnOffset = 0
  346. return t
  347. }
  348. // GetScrollOffset returns the number of rows and columns that are skipped at
  349. // the top left corner when the text view has been scrolled.
  350. func (t *TextView) GetScrollOffset() (row, column int) {
  351. return t.lineOffset, t.columnOffset
  352. }
  353. // Clear removes all text from the buffer.
  354. func (t *TextView) Clear() *TextView {
  355. t.buffer = nil
  356. t.recentBytes = nil
  357. t.index = nil
  358. return t
  359. }
  360. // Highlight specifies which regions should be highlighted. If highlight
  361. // toggling is set to true (see SetToggleHighlights()), the highlight of the
  362. // provided regions is toggled (highlighted regions are un-highlighted and vice
  363. // versa). If toggling is set to false, the provided regions are highlighted and
  364. // all other regions will not be highlighted (you may also provide nil to turn
  365. // off all highlights).
  366. //
  367. // For more information on regions, see class description. Empty region strings
  368. // are ignored.
  369. //
  370. // Text in highlighted regions will be drawn inverted, i.e. with their
  371. // background and foreground colors swapped.
  372. func (t *TextView) Highlight(regionIDs ...string) *TextView {
  373. // Toggle highlights.
  374. if t.toggleHighlights {
  375. var newIDs []string
  376. HighlightLoop:
  377. for regionID := range t.highlights {
  378. for _, id := range regionIDs {
  379. if regionID == id {
  380. continue HighlightLoop
  381. }
  382. }
  383. newIDs = append(newIDs, regionID)
  384. }
  385. for _, regionID := range regionIDs {
  386. if _, ok := t.highlights[regionID]; !ok {
  387. newIDs = append(newIDs, regionID)
  388. }
  389. }
  390. regionIDs = newIDs
  391. } // Now we have a list of region IDs that end up being highlighted.
  392. // Determine added and removed regions.
  393. var added, removed, remaining []string
  394. if t.highlighted != nil {
  395. for _, regionID := range regionIDs {
  396. if _, ok := t.highlights[regionID]; ok {
  397. remaining = append(remaining, regionID)
  398. delete(t.highlights, regionID)
  399. } else {
  400. added = append(added, regionID)
  401. }
  402. }
  403. for regionID := range t.highlights {
  404. removed = append(removed, regionID)
  405. }
  406. }
  407. // Make new selection.
  408. t.highlights = make(map[string]struct{})
  409. for _, id := range regionIDs {
  410. if id == "" {
  411. continue
  412. }
  413. t.highlights[id] = struct{}{}
  414. }
  415. t.index = nil
  416. // Notify.
  417. if t.highlighted != nil && len(added) > 0 || len(removed) > 0 {
  418. t.highlighted(added, removed, remaining)
  419. }
  420. return t
  421. }
  422. // GetHighlights returns the IDs of all currently highlighted regions.
  423. func (t *TextView) GetHighlights() (regionIDs []string) {
  424. for id := range t.highlights {
  425. regionIDs = append(regionIDs, id)
  426. }
  427. return
  428. }
  429. // SetToggleHighlights sets a flag to determine how regions are highlighted.
  430. // When set to true, the Highlight() function (or a mouse click) will toggle the
  431. // provided/selected regions. When set to false, Highlight() (or a mouse click)
  432. // will simply highlight the provided regions.
  433. func (t *TextView) SetToggleHighlights(toggle bool) *TextView {
  434. t.toggleHighlights = toggle
  435. return t
  436. }
  437. // ScrollToHighlight will cause the visible area to be scrolled so that the
  438. // highlighted regions appear in the visible area of the text view. This
  439. // repositioning happens the next time the text view is drawn. It happens only
  440. // once so you will need to call this function repeatedly to always keep
  441. // highlighted regions in view.
  442. //
  443. // Nothing happens if there are no highlighted regions or if the text view is
  444. // not scrollable.
  445. func (t *TextView) ScrollToHighlight() *TextView {
  446. if len(t.highlights) == 0 || !t.scrollable || !t.regions {
  447. return t
  448. }
  449. t.index = nil
  450. t.scrollToHighlights = true
  451. t.trackEnd = false
  452. return t
  453. }
  454. // GetRegionText returns the text of the region with the given ID. If dynamic
  455. // colors are enabled, color tags are stripped from the text. Newlines are
  456. // always returned as '\n' runes.
  457. //
  458. // If the region does not exist or if regions are turned off, an empty string
  459. // is returned.
  460. func (t *TextView) GetRegionText(regionID string) string {
  461. if !t.regions || regionID == "" {
  462. return ""
  463. }
  464. var (
  465. buffer bytes.Buffer
  466. currentRegionID string
  467. )
  468. for _, str := range t.buffer {
  469. // Find all color tags in this line.
  470. var colorTagIndices [][]int
  471. if t.dynamicColors {
  472. colorTagIndices = colorPattern.FindAllStringIndex(str, -1)
  473. }
  474. // Find all regions in this line.
  475. var (
  476. regionIndices [][]int
  477. regions [][]string
  478. )
  479. if t.regions {
  480. regionIndices = regionPattern.FindAllStringIndex(str, -1)
  481. regions = regionPattern.FindAllStringSubmatch(str, -1)
  482. }
  483. // Analyze this line.
  484. var currentTag, currentRegion int
  485. for pos, ch := range str {
  486. // Skip any color tags.
  487. if currentTag < len(colorTagIndices) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] {
  488. if pos == colorTagIndices[currentTag][1]-1 {
  489. currentTag++
  490. }
  491. if colorTagIndices[currentTag][1]-colorTagIndices[currentTag][0] > 2 {
  492. continue
  493. }
  494. }
  495. // Skip any regions.
  496. if currentRegion < len(regionIndices) && pos >= regionIndices[currentRegion][0] && pos < regionIndices[currentRegion][1] {
  497. if pos == regionIndices[currentRegion][1]-1 {
  498. if currentRegionID == regionID {
  499. // This is the end of the requested region. We're done.
  500. return buffer.String()
  501. }
  502. currentRegionID = regions[currentRegion][1]
  503. currentRegion++
  504. }
  505. continue
  506. }
  507. // Add this rune.
  508. if currentRegionID == regionID {
  509. buffer.WriteRune(ch)
  510. }
  511. }
  512. // Add newline.
  513. if currentRegionID == regionID {
  514. buffer.WriteRune('\n')
  515. }
  516. }
  517. return escapePattern.ReplaceAllString(buffer.String(), `[$1$2]`)
  518. }
  519. // Focus is called when this primitive receives focus.
  520. func (t *TextView) Focus(delegate func(p Primitive)) {
  521. // Implemented here with locking because this is used by layout primitives.
  522. t.Lock()
  523. defer t.Unlock()
  524. t.hasFocus = true
  525. }
  526. // HasFocus returns whether or not this primitive has focus.
  527. func (t *TextView) HasFocus() bool {
  528. // Implemented here with locking because this may be used in the "changed"
  529. // callback.
  530. t.Lock()
  531. defer t.Unlock()
  532. return t.hasFocus
  533. }
  534. // Write lets us implement the io.Writer interface. Tab characters will be
  535. // replaced with TabSize space characters. A "\n" or "\r\n" will be interpreted
  536. // as a new line.
  537. func (t *TextView) Write(p []byte) (n int, err error) {
  538. // Notify at the end.
  539. t.Lock()
  540. changed := t.changed
  541. t.Unlock()
  542. if changed != nil {
  543. defer func() {
  544. // We always call the "changed" function in a separate goroutine to avoid
  545. // deadlocks.
  546. go changed()
  547. }()
  548. }
  549. t.Lock()
  550. defer t.Unlock()
  551. // Copy data over.
  552. newBytes := append(t.recentBytes, p...)
  553. t.recentBytes = nil
  554. // If we have a trailing invalid UTF-8 byte, we'll wait.
  555. if r, _ := utf8.DecodeLastRune(p); r == utf8.RuneError {
  556. t.recentBytes = newBytes
  557. return len(p), nil
  558. }
  559. // If we have a trailing open dynamic color, exclude it.
  560. if t.dynamicColors {
  561. location := openColorRegex.FindIndex(newBytes)
  562. if location != nil {
  563. t.recentBytes = newBytes[location[0]:]
  564. newBytes = newBytes[:location[0]]
  565. }
  566. }
  567. // If we have a trailing open region, exclude it.
  568. if t.regions {
  569. location := openRegionRegex.FindIndex(newBytes)
  570. if location != nil {
  571. t.recentBytes = newBytes[location[0]:]
  572. newBytes = newBytes[:location[0]]
  573. }
  574. }
  575. // Transform the new bytes into strings.
  576. newBytes = bytes.Replace(newBytes, []byte{'\t'}, bytes.Repeat([]byte{' '}, TabSize), -1)
  577. for index, line := range newLineRegex.Split(string(newBytes), -1) {
  578. if index == 0 {
  579. if len(t.buffer) == 0 {
  580. t.buffer = []string{line}
  581. } else {
  582. t.buffer[len(t.buffer)-1] += line
  583. }
  584. } else {
  585. t.buffer = append(t.buffer, line)
  586. }
  587. }
  588. // Reset the index.
  589. t.index = nil
  590. return len(p), nil
  591. }
  592. // reindexBuffer re-indexes the buffer such that we can use it to easily draw
  593. // the buffer onto the screen. Each line in the index will contain a pointer
  594. // into the buffer from which on we will print text. It will also contain the
  595. // color with which the line starts.
  596. func (t *TextView) reindexBuffer(width int) {
  597. if t.index != nil {
  598. return // Nothing has changed. We can still use the current index.
  599. }
  600. t.index = nil
  601. t.fromHighlight, t.toHighlight, t.posHighlight = -1, -1, -1
  602. // If there's no space, there's no index.
  603. if width < 1 {
  604. return
  605. }
  606. // Initial states.
  607. regionID := ""
  608. var (
  609. highlighted bool
  610. foregroundColor, backgroundColor, attributes string
  611. )
  612. // Go through each line in the buffer.
  613. for bufferIndex, str := range t.buffer {
  614. colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedStr, _ := decomposeString(str, t.dynamicColors, t.regions)
  615. // Split the line if required.
  616. var splitLines []string
  617. str = strippedStr
  618. if t.wrap && len(str) > 0 {
  619. for len(str) > 0 {
  620. extract := runewidth.Truncate(str, width, "")
  621. if len(extract) == 0 {
  622. // We'll extract at least one grapheme cluster.
  623. gr := uniseg.NewGraphemes(str)
  624. gr.Next()
  625. _, to := gr.Positions()
  626. extract = str[:to]
  627. }
  628. if t.wordWrap && len(extract) < len(str) {
  629. // Add any spaces from the next line.
  630. if spaces := spacePattern.FindStringIndex(str[len(extract):]); spaces != nil && spaces[0] == 0 {
  631. extract = str[:len(extract)+spaces[1]]
  632. }
  633. // Can we split before the mandatory end?
  634. matches := boundaryPattern.FindAllStringIndex(extract, -1)
  635. if len(matches) > 0 {
  636. // Yes. Let's split there.
  637. extract = extract[:matches[len(matches)-1][1]]
  638. }
  639. }
  640. splitLines = append(splitLines, extract)
  641. str = str[len(extract):]
  642. }
  643. } else {
  644. // No need to split the line.
  645. splitLines = []string{str}
  646. }
  647. // Create index from split lines.
  648. var originalPos, colorPos, regionPos, escapePos int
  649. for _, splitLine := range splitLines {
  650. line := &textViewIndex{
  651. Line: bufferIndex,
  652. Pos: originalPos,
  653. ForegroundColor: foregroundColor,
  654. BackgroundColor: backgroundColor,
  655. Attributes: attributes,
  656. Region: regionID,
  657. }
  658. // Shift original position with tags.
  659. lineLength := len(splitLine)
  660. remainingLength := lineLength
  661. tagEnd := originalPos
  662. totalTagLength := 0
  663. for {
  664. // Which tag comes next?
  665. nextTag := make([][3]int, 0, 3)
  666. if colorPos < len(colorTagIndices) {
  667. nextTag = append(nextTag, [3]int{colorTagIndices[colorPos][0], colorTagIndices[colorPos][1], 0}) // 0 = color tag.
  668. }
  669. if regionPos < len(regionIndices) {
  670. nextTag = append(nextTag, [3]int{regionIndices[regionPos][0], regionIndices[regionPos][1], 1}) // 1 = region tag.
  671. }
  672. if escapePos < len(escapeIndices) {
  673. nextTag = append(nextTag, [3]int{escapeIndices[escapePos][0], escapeIndices[escapePos][1], 2}) // 2 = escape tag.
  674. }
  675. minPos := -1
  676. tagIndex := -1
  677. for index, pair := range nextTag {
  678. if minPos < 0 || pair[0] < minPos {
  679. minPos = pair[0]
  680. tagIndex = index
  681. }
  682. }
  683. // Is the next tag in range?
  684. if tagIndex < 0 || minPos > tagEnd+remainingLength {
  685. break // No. We're done with this line.
  686. }
  687. // Advance.
  688. strippedTagStart := nextTag[tagIndex][0] - originalPos - totalTagLength
  689. tagEnd = nextTag[tagIndex][1]
  690. tagLength := tagEnd - nextTag[tagIndex][0]
  691. if nextTag[tagIndex][2] == 2 {
  692. tagLength = 1
  693. }
  694. totalTagLength += tagLength
  695. remainingLength = lineLength - (tagEnd - originalPos - totalTagLength)
  696. // Process the tag.
  697. switch nextTag[tagIndex][2] {
  698. case 0:
  699. // Process color tags.
  700. foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
  701. colorPos++
  702. case 1:
  703. // Process region tags.
  704. regionID = regions[regionPos][1]
  705. _, highlighted = t.highlights[regionID]
  706. // Update highlight range.
  707. if highlighted {
  708. line := len(t.index)
  709. if t.fromHighlight < 0 {
  710. t.fromHighlight, t.toHighlight = line, line
  711. t.posHighlight = stringWidth(splitLine[:strippedTagStart])
  712. } else if line > t.toHighlight {
  713. t.toHighlight = line
  714. }
  715. }
  716. regionPos++
  717. case 2:
  718. // Process escape tags.
  719. escapePos++
  720. }
  721. }
  722. // Advance to next line.
  723. originalPos += lineLength + totalTagLength
  724. // Append this line.
  725. line.NextPos = originalPos
  726. line.Width = stringWidth(splitLine)
  727. t.index = append(t.index, line)
  728. }
  729. // Word-wrapped lines may have trailing whitespace. Remove it.
  730. if t.wrap && t.wordWrap {
  731. for _, line := range t.index {
  732. str := t.buffer[line.Line][line.Pos:line.NextPos]
  733. spaces := spacePattern.FindAllStringIndex(str, -1)
  734. if spaces != nil && spaces[len(spaces)-1][1] == len(str) {
  735. oldNextPos := line.NextPos
  736. line.NextPos -= spaces[len(spaces)-1][1] - spaces[len(spaces)-1][0]
  737. line.Width -= stringWidth(t.buffer[line.Line][line.NextPos:oldNextPos])
  738. }
  739. }
  740. }
  741. }
  742. // Calculate longest line.
  743. t.longestLine = 0
  744. for _, line := range t.index {
  745. if line.Width > t.longestLine {
  746. t.longestLine = line.Width
  747. }
  748. }
  749. }
  750. // Draw draws this primitive onto the screen.
  751. func (t *TextView) Draw(screen tcell.Screen) {
  752. t.Lock()
  753. defer t.Unlock()
  754. t.Box.Draw(screen)
  755. totalWidth, totalHeight := screen.Size()
  756. // Get the available size.
  757. x, y, width, height := t.GetInnerRect()
  758. t.pageSize = height
  759. // If the width has changed, we need to reindex.
  760. if width != t.lastWidth && t.wrap {
  761. t.index = nil
  762. }
  763. t.lastWidth = width
  764. // Re-index.
  765. t.reindexBuffer(width)
  766. if t.regions {
  767. t.regionInfos = nil
  768. }
  769. // If we don't have an index, there's nothing to draw.
  770. if t.index == nil {
  771. return
  772. }
  773. // Move to highlighted regions.
  774. if t.regions && t.scrollToHighlights && t.fromHighlight >= 0 {
  775. // Do we fit the entire height?
  776. if t.toHighlight-t.fromHighlight+1 < height {
  777. // Yes, let's center the highlights.
  778. t.lineOffset = (t.fromHighlight + t.toHighlight - height) / 2
  779. } else {
  780. // No, let's move to the start of the highlights.
  781. t.lineOffset = t.fromHighlight
  782. }
  783. // If the highlight is too far to the right, move it to the middle.
  784. if t.posHighlight-t.columnOffset > 3*width/4 {
  785. t.columnOffset = t.posHighlight - width/2
  786. }
  787. // If the highlight is off-screen on the left, move it on-screen.
  788. if t.posHighlight-t.columnOffset < 0 {
  789. t.columnOffset = t.posHighlight - width/4
  790. }
  791. }
  792. t.scrollToHighlights = false
  793. // Adjust line offset.
  794. if t.lineOffset+height > len(t.index) {
  795. t.trackEnd = true
  796. }
  797. if t.trackEnd {
  798. t.lineOffset = len(t.index) - height
  799. }
  800. if t.lineOffset < 0 {
  801. t.lineOffset = 0
  802. }
  803. // Adjust column offset.
  804. if t.align == AlignLeft {
  805. if t.columnOffset+width > t.longestLine {
  806. t.columnOffset = t.longestLine - width
  807. }
  808. if t.columnOffset < 0 {
  809. t.columnOffset = 0
  810. }
  811. } else if t.align == AlignRight {
  812. if t.columnOffset-width < -t.longestLine {
  813. t.columnOffset = width - t.longestLine
  814. }
  815. if t.columnOffset > 0 {
  816. t.columnOffset = 0
  817. }
  818. } else { // AlignCenter.
  819. half := (t.longestLine - width) / 2
  820. if half > 0 {
  821. if t.columnOffset > half {
  822. t.columnOffset = half
  823. }
  824. if t.columnOffset < -half {
  825. t.columnOffset = -half
  826. }
  827. } else {
  828. t.columnOffset = 0
  829. }
  830. }
  831. // Draw the buffer.
  832. defaultStyle := tcell.StyleDefault.Foreground(t.textColor)
  833. for line := t.lineOffset; line < len(t.index); line++ {
  834. // Are we done?
  835. if line-t.lineOffset >= height || y+line-t.lineOffset >= totalHeight {
  836. break
  837. }
  838. // Get the text for this line.
  839. index := t.index[line]
  840. text := t.buffer[index.Line][index.Pos:index.NextPos]
  841. foregroundColor := index.ForegroundColor
  842. backgroundColor := index.BackgroundColor
  843. attributes := index.Attributes
  844. regionID := index.Region
  845. if t.regions && regionID != "" && (len(t.regionInfos) == 0 || t.regionInfos[len(t.regionInfos)-1].ID != regionID) {
  846. t.regionInfos = append(t.regionInfos, &textViewRegion{
  847. ID: regionID,
  848. FromX: x,
  849. FromY: y + line - t.lineOffset,
  850. ToX: -1,
  851. ToY: -1,
  852. })
  853. }
  854. // Process tags.
  855. colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedText, _ := decomposeString(text, t.dynamicColors, t.regions)
  856. // Calculate the position of the line.
  857. var skip, posX int
  858. if t.align == AlignLeft {
  859. posX = -t.columnOffset
  860. } else if t.align == AlignRight {
  861. posX = width - index.Width - t.columnOffset
  862. } else { // AlignCenter.
  863. posX = (width-index.Width)/2 - t.columnOffset
  864. }
  865. if posX < 0 {
  866. skip = -posX
  867. posX = 0
  868. }
  869. // Print the line.
  870. if y+line-t.lineOffset >= 0 {
  871. var colorPos, regionPos, escapePos, tagOffset, skipped int
  872. iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
  873. // Process tags.
  874. for {
  875. if colorPos < len(colorTags) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
  876. // Get the color.
  877. foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
  878. tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
  879. colorPos++
  880. } else if regionPos < len(regionIndices) && textPos+tagOffset >= regionIndices[regionPos][0] && textPos+tagOffset < regionIndices[regionPos][1] {
  881. // Get the region.
  882. if regionID != "" && len(t.regionInfos) > 0 && t.regionInfos[len(t.regionInfos)-1].ID == regionID {
  883. // End last region.
  884. t.regionInfos[len(t.regionInfos)-1].ToX = x + posX
  885. t.regionInfos[len(t.regionInfos)-1].ToY = y + line - t.lineOffset
  886. }
  887. regionID = regions[regionPos][1]
  888. if regionID != "" {
  889. // Start new region.
  890. t.regionInfos = append(t.regionInfos, &textViewRegion{
  891. ID: regionID,
  892. FromX: x + posX,
  893. FromY: y + line - t.lineOffset,
  894. ToX: -1,
  895. ToY: -1,
  896. })
  897. }
  898. tagOffset += regionIndices[regionPos][1] - regionIndices[regionPos][0]
  899. regionPos++
  900. } else {
  901. break
  902. }
  903. }
  904. // Skip the second-to-last character of an escape tag.
  905. if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
  906. tagOffset++
  907. escapePos++
  908. }
  909. // Mix the existing style with the new style.
  910. _, _, existingStyle, _ := screen.GetContent(x+posX, y+line-t.lineOffset)
  911. _, background, _ := existingStyle.Decompose()
  912. style := overlayStyle(background, defaultStyle, foregroundColor, backgroundColor, attributes)
  913. // Do we highlight this character?
  914. var highlighted bool
  915. if regionID != "" {
  916. if _, ok := t.highlights[regionID]; ok {
  917. highlighted = true
  918. }
  919. }
  920. if highlighted {
  921. fg, bg, _ := style.Decompose()
  922. if bg == tcell.ColorDefault {
  923. r, g, b := fg.RGB()
  924. c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255}
  925. _, _, li := c.Hcl()
  926. if li < .5 {
  927. bg = tcell.ColorWhite
  928. } else {
  929. bg = tcell.ColorBlack
  930. }
  931. }
  932. style = style.Background(fg).Foreground(bg)
  933. }
  934. // Skip to the right.
  935. if !t.wrap && skipped < skip {
  936. skipped += screenWidth
  937. return false
  938. }
  939. // Stop at the right border.
  940. if posX+screenWidth > width || x+posX >= totalWidth {
  941. return true
  942. }
  943. // Draw the character.
  944. for offset := screenWidth - 1; offset >= 0; offset-- {
  945. if offset == 0 {
  946. screen.SetContent(x+posX+offset, y+line-t.lineOffset, main, comb, style)
  947. } else {
  948. screen.SetContent(x+posX+offset, y+line-t.lineOffset, ' ', nil, style)
  949. }
  950. }
  951. // Advance.
  952. posX += screenWidth
  953. return false
  954. })
  955. }
  956. }
  957. // If this view is not scrollable, we'll purge the buffer of lines that have
  958. // scrolled out of view.
  959. if !t.scrollable && t.lineOffset > 0 {
  960. if t.lineOffset >= len(t.index) {
  961. t.buffer = nil
  962. } else {
  963. t.buffer = t.buffer[t.index[t.lineOffset].Line:]
  964. }
  965. t.index = nil
  966. t.lineOffset = 0
  967. }
  968. }
  969. // InputHandler returns the handler for this primitive.
  970. func (t *TextView) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
  971. return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
  972. key := event.Key()
  973. if key == tcell.KeyEscape || key == tcell.KeyEnter || key == tcell.KeyTab || key == tcell.KeyBacktab {
  974. if t.done != nil {
  975. t.done(key)
  976. }
  977. return
  978. }
  979. if !t.scrollable {
  980. return
  981. }
  982. switch key {
  983. case tcell.KeyRune:
  984. switch event.Rune() {
  985. case 'g': // Home.
  986. t.trackEnd = false
  987. t.lineOffset = 0
  988. t.columnOffset = 0
  989. case 'G': // End.
  990. t.trackEnd = true
  991. t.columnOffset = 0
  992. case 'j': // Down.
  993. t.lineOffset++
  994. case 'k': // Up.
  995. t.trackEnd = false
  996. t.lineOffset--
  997. case 'h': // Left.
  998. t.columnOffset--
  999. case 'l': // Right.
  1000. t.columnOffset++
  1001. }
  1002. case tcell.KeyHome:
  1003. t.trackEnd = false
  1004. t.lineOffset = 0
  1005. t.columnOffset = 0
  1006. case tcell.KeyEnd:
  1007. t.trackEnd = true
  1008. t.columnOffset = 0
  1009. case tcell.KeyUp:
  1010. t.trackEnd = false
  1011. t.lineOffset--
  1012. case tcell.KeyDown:
  1013. t.lineOffset++
  1014. case tcell.KeyLeft:
  1015. t.columnOffset--
  1016. case tcell.KeyRight:
  1017. t.columnOffset++
  1018. case tcell.KeyPgDn, tcell.KeyCtrlF:
  1019. t.lineOffset += t.pageSize
  1020. case tcell.KeyPgUp, tcell.KeyCtrlB:
  1021. t.trackEnd = false
  1022. t.lineOffset -= t.pageSize
  1023. }
  1024. })
  1025. }
  1026. // MouseHandler returns the mouse handler for this primitive.
  1027. func (t *TextView) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
  1028. return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
  1029. x, y := event.Position()
  1030. if !t.InRect(x, y) {
  1031. return false, nil
  1032. }
  1033. switch action {
  1034. case MouseLeftClick:
  1035. if t.regions {
  1036. // Find a region to highlight.
  1037. for _, region := range t.regionInfos {
  1038. if y == region.FromY && x < region.FromX ||
  1039. y == region.ToY && x >= region.ToX ||
  1040. region.FromY >= 0 && y < region.FromY ||
  1041. region.ToY >= 0 && y > region.ToY {
  1042. continue
  1043. }
  1044. t.Highlight(region.ID)
  1045. break
  1046. }
  1047. }
  1048. consumed = true
  1049. setFocus(t)
  1050. case MouseScrollUp:
  1051. t.trackEnd = false
  1052. t.lineOffset--
  1053. consumed = true
  1054. case MouseScrollDown:
  1055. t.lineOffset++
  1056. consumed = true
  1057. }
  1058. return
  1059. })
  1060. }