textview.go 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503
  1. package tview
  2. import (
  3. "bytes"
  4. "fmt"
  5. "regexp"
  6. "strings"
  7. "sync"
  8. "unicode/utf8"
  9. "github.com/gdamore/tcell/v2"
  10. colorful "github.com/lucasb-eyer/go-colorful"
  11. "github.com/rivo/uniseg"
  12. )
  13. var (
  14. openColorRegex = regexp.MustCompile(`\[([a-zA-Z]*|#[0-9a-zA-Z]*)$`)
  15. openRegionRegex = regexp.MustCompile(`\["[a-zA-Z0-9_,;: \-\.]*"?$`)
  16. newLineRegex = regexp.MustCompile(`\r?\n`)
  17. // TabSize is the number of spaces with which a tab character will be replaced.
  18. TabSize = 4
  19. )
  20. // textViewIndex contains information about a line displayed in the text view.
  21. type textViewIndex struct {
  22. Line int // The index into the "buffer" slice.
  23. Pos int // The index into the "buffer" string (byte position).
  24. NextPos int // The (byte) index of the next line start within this buffer string.
  25. Width int // The screen width of this line.
  26. ForegroundColor string // The starting foreground color ("" = don't change, "-" = reset).
  27. BackgroundColor string // The starting background color ("" = don't change, "-" = reset).
  28. Attributes string // The starting attributes ("" = don't change, "-" = reset).
  29. Region string // The starting region ID.
  30. }
  31. // textViewRegion contains information about a region.
  32. type textViewRegion struct {
  33. // The region ID.
  34. ID string
  35. // The starting and end screen position of the region as determined the last
  36. // time Draw() was called. A negative value indicates out-of-rect positions.
  37. FromX, FromY, ToX, ToY int
  38. }
  39. // TextViewWriter is a writer that can be used to write to and clear a TextView
  40. // in batches, i.e. multiple writes with the lock only being acquired once. Don't
  41. // instantiated this class directly but use the TextView's BatchWriter method
  42. // instead.
  43. type TextViewWriter struct {
  44. t *TextView
  45. }
  46. // Close implements io.Closer for the writer by unlocking the original TextView.
  47. func (w TextViewWriter) Close() error {
  48. w.t.Unlock()
  49. return nil
  50. }
  51. // Clear removes all text from the buffer.
  52. func (w TextViewWriter) Clear() {
  53. w.t.clear()
  54. }
  55. // Write implements the io.Writer interface. It behaves like the TextView's
  56. // Write() method except that it does not acquire the lock.
  57. func (w TextViewWriter) Write(p []byte) (n int, err error) {
  58. return w.t.write(p)
  59. }
  60. // HasFocus returns whether the underlying TextView has focus.
  61. func (w TextViewWriter) HasFocus() bool {
  62. return w.t.hasFocus
  63. }
  64. // TextView is a box which displays text. While the text to be displayed can be
  65. // changed or appended to, there is no functionality that allows the user to
  66. // edit text. For that, TextArea should be used.
  67. //
  68. // TextView implements the io.Writer interface so you can stream text to it,
  69. // appending to the existing text. This does not trigger a redraw automatically
  70. // but if a handler is installed via SetChangedFunc(), you can cause it to be
  71. // redrawn. (See SetChangedFunc() for more details.)
  72. //
  73. // # Navigation
  74. //
  75. // If the text view is scrollable (the default), text is kept in a buffer which
  76. // may be larger than the screen and can be navigated similarly to Vim:
  77. //
  78. // - h, left arrow: Move left.
  79. // - l, right arrow: Move right.
  80. // - j, down arrow: Move down.
  81. // - k, up arrow: Move up.
  82. // - g, home: Move to the top.
  83. // - G, end: Move to the bottom.
  84. // - Ctrl-F, page down: Move down by one page.
  85. // - Ctrl-B, page up: Move up by one page.
  86. //
  87. // If the text is not scrollable, any text above the top visible line is
  88. // discarded.
  89. //
  90. // Use SetInputCapture() to override or modify keyboard input.
  91. //
  92. // # Colors
  93. //
  94. // If dynamic colors are enabled via SetDynamicColors(), text color can be
  95. // changed dynamically by embedding color strings in square brackets. This works
  96. // the same way as anywhere else. Please see the package documentation for more
  97. // information.
  98. //
  99. // # Regions and Highlights
  100. //
  101. // If regions are enabled via SetRegions(), you can define text regions within
  102. // the text and assign region IDs to them. Text regions start with region tags.
  103. // Region tags are square brackets that contain a region ID in double quotes,
  104. // for example:
  105. //
  106. // We define a ["rg"]region[""] here.
  107. //
  108. // A text region ends with the next region tag. Tags with no region ID ([""])
  109. // don't start new regions. They can therefore be used to mark the end of a
  110. // region. Region IDs must satisfy the following regular expression:
  111. //
  112. // [a-zA-Z0-9_,;: \-\.]+
  113. //
  114. // Regions can be highlighted by calling the Highlight() function with one or
  115. // more region IDs. This can be used to display search results, for example.
  116. //
  117. // The ScrollToHighlight() function can be used to jump to the currently
  118. // highlighted region once when the text view is drawn the next time.
  119. //
  120. // # Large Texts
  121. //
  122. // This widget is not designed for very large texts as word wrapping, color and
  123. // region tag handling, and proper Unicode handling will result in a significant
  124. // performance hit the longer your text gets. Consider using SetMaxLines() to
  125. // limit the number of lines in the text view.
  126. //
  127. // See https://github.com/rivo/tview/wiki/TextView for an example.
  128. type TextView struct {
  129. sync.Mutex
  130. *Box
  131. // The size of the text area. If set to 0, the text view will use the entire
  132. // available space.
  133. width, height int
  134. // The text buffer.
  135. buffer []string
  136. // The last bytes that have been received but are not part of the buffer yet.
  137. recentBytes []byte
  138. // The processed line index. This is nil if the buffer has changed and needs
  139. // to be re-indexed.
  140. index []*textViewIndex
  141. // The label text shown, usually when part of a form.
  142. label string
  143. // The width of the text area's label.
  144. labelWidth int
  145. // The label style.
  146. labelStyle tcell.Style
  147. // The text alignment, one of AlignLeft, AlignCenter, or AlignRight.
  148. align int
  149. // Information about visible regions as of the last call to Draw().
  150. regionInfos []*textViewRegion
  151. // Indices into the "index" slice which correspond to the first line of the
  152. // first highlight and the last line of the last highlight. This is calculated
  153. // during re-indexing. Set to -1 if there is no current highlight.
  154. fromHighlight, toHighlight int
  155. // The screen space column of the highlight in its first line. Set to -1 if
  156. // there is no current highlight.
  157. posHighlight int
  158. // A set of region IDs that are currently highlighted.
  159. highlights map[string]struct{}
  160. // The last width for which the current text view is drawn.
  161. lastWidth int
  162. // The screen width of the longest line in the index (not the buffer).
  163. longestLine int
  164. // The index of the first line shown in the text view.
  165. lineOffset int
  166. // If set to true, the text view will always remain at the end of the content.
  167. trackEnd bool
  168. // The number of characters to be skipped on each line (not used in wrap
  169. // mode).
  170. columnOffset int
  171. // The maximum number of lines kept in the line index, effectively the
  172. // latest word-wrapped lines. Ignored if 0.
  173. maxLines int
  174. // The height of the content the last time the text view was drawn.
  175. pageSize int
  176. // If set to true, the text view will keep a buffer of text which can be
  177. // navigated when the text is longer than what fits into the box.
  178. scrollable bool
  179. // If set to true, lines that are longer than the available width are wrapped
  180. // onto the next line. If set to false, any characters beyond the available
  181. // width are discarded.
  182. wrap bool
  183. // If set to true and if wrap is also true, lines are split at spaces or
  184. // after punctuation characters.
  185. wordWrap bool
  186. // The (starting) style of the text. This also defines the background color
  187. // of the main text element.
  188. textStyle tcell.Style
  189. // If set to true, the text color can be changed dynamically by piping color
  190. // strings in square brackets to the text view.
  191. dynamicColors bool
  192. // If set to true, region tags can be used to define regions.
  193. regions bool
  194. // A temporary flag which, when true, will automatically bring the current
  195. // highlight(s) into the visible screen.
  196. scrollToHighlights bool
  197. // If true, setting new highlights will be a XOR instead of an overwrite
  198. // operation.
  199. toggleHighlights bool
  200. // An optional function which is called when the content of the text view has
  201. // changed.
  202. changed func()
  203. // An optional function which is called when the user presses one of the
  204. // following keys: Escape, Enter, Tab, Backtab.
  205. done func(tcell.Key)
  206. // An optional function which is called when one or more regions were
  207. // highlighted.
  208. highlighted func(added, removed, remaining []string)
  209. // A callback function set by the Form class and called when the user leaves
  210. // this form item.
  211. finished func(tcell.Key)
  212. }
  213. // NewTextView returns a new text view.
  214. func NewTextView() *TextView {
  215. return &TextView{
  216. Box: NewBox(),
  217. labelStyle: tcell.StyleDefault.Foreground(Styles.SecondaryTextColor),
  218. highlights: make(map[string]struct{}),
  219. lineOffset: -1,
  220. scrollable: true,
  221. align: AlignLeft,
  222. wrap: true,
  223. textStyle: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.PrimaryTextColor),
  224. regions: false,
  225. dynamicColors: false,
  226. }
  227. }
  228. // SetLabel sets the text to be displayed before the text view.
  229. func (t *TextView) SetLabel(label string) *TextView {
  230. t.label = label
  231. return t
  232. }
  233. // GetLabel returns the text to be displayed before the text view.
  234. func (t *TextView) GetLabel() string {
  235. return t.label
  236. }
  237. // SetLabelWidth sets the screen width of the label. A value of 0 will cause the
  238. // primitive to use the width of the label string.
  239. func (t *TextView) SetLabelWidth(width int) *TextView {
  240. t.labelWidth = width
  241. return t
  242. }
  243. // SetSize sets the screen size of the main text element of the text view. This
  244. // element is always located next to the label which is always located in the
  245. // top left corner. If any of the values are 0 or larger than the available
  246. // space, the available space will be used.
  247. func (t *TextView) SetSize(rows, columns int) *TextView {
  248. t.width = columns
  249. t.height = rows
  250. return t
  251. }
  252. // GetFieldWidth returns this primitive's field width.
  253. func (t *TextView) GetFieldWidth() int {
  254. return t.width
  255. }
  256. // GetFieldHeight returns this primitive's field height.
  257. func (t *TextView) GetFieldHeight() int {
  258. return t.height
  259. }
  260. // SetDisabled sets whether or not the item is disabled / read-only.
  261. func (t *TextView) SetDisabled(disabled bool) FormItem {
  262. return t // Text views are always read-only.
  263. }
  264. // SetScrollable sets the flag that decides whether or not the text view is
  265. // scrollable. If true, text is kept in a buffer and can be navigated. If false,
  266. // the last line will always be visible.
  267. func (t *TextView) SetScrollable(scrollable bool) *TextView {
  268. t.scrollable = scrollable
  269. if !scrollable {
  270. t.trackEnd = true
  271. }
  272. return t
  273. }
  274. // SetWrap sets the flag that, if true, leads to lines that are longer than the
  275. // available width being wrapped onto the next line. If false, any characters
  276. // beyond the available width are not displayed.
  277. func (t *TextView) SetWrap(wrap bool) *TextView {
  278. if t.wrap != wrap {
  279. t.index = nil
  280. }
  281. t.wrap = wrap
  282. return t
  283. }
  284. // SetWordWrap sets the flag that, if true and if the "wrap" flag is also true
  285. // (see SetWrap()), wraps the line at spaces or after punctuation marks. Note
  286. // that trailing spaces will not be printed.
  287. //
  288. // This flag is ignored if the "wrap" flag is false.
  289. func (t *TextView) SetWordWrap(wrapOnWords bool) *TextView {
  290. if t.wordWrap != wrapOnWords {
  291. t.index = nil
  292. }
  293. t.wordWrap = wrapOnWords
  294. return t
  295. }
  296. // SetMaxLines sets the maximum number of lines for this text view. Lines at the
  297. // beginning of the text will be discarded when the text view is drawn, so as to
  298. // remain below this value. Broken lines via word wrapping are counted
  299. // individually.
  300. //
  301. // Note that GetText() will return the shortened text and may start with color
  302. // and/or region tags that were open at the cutoff point.
  303. //
  304. // A value of 0 (the default) will keep all lines in place.
  305. func (t *TextView) SetMaxLines(maxLines int) *TextView {
  306. t.maxLines = maxLines
  307. return t
  308. }
  309. // SetTextAlign sets the text alignment within the text view. This must be
  310. // either AlignLeft, AlignCenter, or AlignRight.
  311. func (t *TextView) SetTextAlign(align int) *TextView {
  312. if t.align != align {
  313. t.index = nil
  314. }
  315. t.align = align
  316. return t
  317. }
  318. // SetTextColor sets the initial color of the text (which can be changed
  319. // dynamically by sending color strings in square brackets to the text view if
  320. // dynamic colors are enabled).
  321. func (t *TextView) SetTextColor(color tcell.Color) *TextView {
  322. t.textStyle = t.textStyle.Foreground(color)
  323. return t
  324. }
  325. // SetBackgroundColor overrides its implementation in Box to set the background
  326. // color of this primitive. For backwards compatibility reasons, it also sets
  327. // the background color of the main text element.
  328. func (t *TextView) SetBackgroundColor(color tcell.Color) *Box {
  329. t.Box.SetBackgroundColor(color)
  330. t.textStyle = t.textStyle.Background(color)
  331. return t.Box
  332. }
  333. // SetTextStyle sets the initial style of the text (which can be changed
  334. // dynamically by sending color strings in square brackets to the text view if
  335. // dynamic colors are enabled). This style's background color also determines
  336. // the background color of the main text element (even if empty).
  337. func (t *TextView) SetTextStyle(style tcell.Style) *TextView {
  338. t.textStyle = style
  339. return t
  340. }
  341. // SetText sets the text of this text view to the provided string. Previously
  342. // contained text will be removed. As with writing to the text view io.Writer
  343. // interface directly, this does not trigger an automatic redraw but it will
  344. // trigger the "changed" callback if one is set.
  345. func (t *TextView) SetText(text string) *TextView {
  346. batch := t.BatchWriter()
  347. defer batch.Close()
  348. batch.Clear()
  349. fmt.Fprint(batch, text)
  350. return t
  351. }
  352. // GetText returns the current text of this text view. If "stripAllTags" is set
  353. // to true, any region/color tags are stripped from the text.
  354. func (t *TextView) GetText(stripAllTags bool) string {
  355. // Get the buffer.
  356. buffer := t.buffer
  357. if !stripAllTags {
  358. buffer = make([]string, len(t.buffer), len(t.buffer)+1)
  359. copy(buffer, t.buffer)
  360. buffer = append(buffer, string(t.recentBytes))
  361. }
  362. // Add newlines again.
  363. text := strings.Join(buffer, "\n")
  364. // Strip from tags if required.
  365. if stripAllTags {
  366. if t.regions {
  367. text = regionPattern.ReplaceAllString(text, "")
  368. }
  369. if t.dynamicColors {
  370. text = stripTags(text)
  371. }
  372. if t.regions && !t.dynamicColors {
  373. text = escapePattern.ReplaceAllString(text, `[$1$2]`)
  374. }
  375. }
  376. return text
  377. }
  378. // GetOriginalLineCount returns the number of lines in the original text buffer,
  379. // i.e. the number of newline characters plus one.
  380. func (t *TextView) GetOriginalLineCount() int {
  381. return len(t.buffer)
  382. }
  383. // SetDynamicColors sets the flag that allows the text color to be changed
  384. // dynamically. See class description for details.
  385. func (t *TextView) SetDynamicColors(dynamic bool) *TextView {
  386. if t.dynamicColors != dynamic {
  387. t.index = nil
  388. }
  389. t.dynamicColors = dynamic
  390. return t
  391. }
  392. // SetRegions sets the flag that allows to define regions in the text. See class
  393. // description for details.
  394. func (t *TextView) SetRegions(regions bool) *TextView {
  395. if t.regions != regions {
  396. t.index = nil
  397. }
  398. t.regions = regions
  399. return t
  400. }
  401. // SetChangedFunc sets a handler function which is called when the text of the
  402. // text view has changed. This is useful when text is written to this io.Writer
  403. // in a separate goroutine. Doing so does not automatically cause the screen to
  404. // be refreshed so you may want to use the "changed" handler to redraw the
  405. // screen.
  406. //
  407. // Note that to avoid race conditions or deadlocks, there are a few rules you
  408. // should follow:
  409. //
  410. // - You can call Application.Draw() from this handler.
  411. // - You can call TextView.HasFocus() from this handler.
  412. // - During the execution of this handler, access to any other variables from
  413. // this primitive or any other primitive must be queued using
  414. // Application.QueueUpdate().
  415. //
  416. // See package description for details on dealing with concurrency.
  417. func (t *TextView) SetChangedFunc(handler func()) *TextView {
  418. t.changed = handler
  419. return t
  420. }
  421. // SetDoneFunc sets a handler which is called when the user presses on the
  422. // following keys: Escape, Enter, Tab, Backtab. The key is passed to the
  423. // handler.
  424. func (t *TextView) SetDoneFunc(handler func(key tcell.Key)) *TextView {
  425. t.done = handler
  426. return t
  427. }
  428. // SetHighlightedFunc sets a handler which is called when the list of currently
  429. // highlighted regions change. It receives a list of region IDs which were newly
  430. // highlighted, those that are not highlighted anymore, and those that remain
  431. // highlighted.
  432. //
  433. // Note that because regions are only determined during drawing, this function
  434. // can only fire for regions that have existed during the last call to Draw().
  435. func (t *TextView) SetHighlightedFunc(handler func(added, removed, remaining []string)) *TextView {
  436. t.highlighted = handler
  437. return t
  438. }
  439. // SetFinishedFunc sets a callback invoked when the user leaves this form item.
  440. func (t *TextView) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
  441. t.finished = handler
  442. return t
  443. }
  444. // SetFormAttributes sets attributes shared by all form items.
  445. func (t *TextView) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
  446. t.labelWidth = labelWidth
  447. t.backgroundColor = bgColor
  448. t.labelStyle = t.labelStyle.Foreground(labelColor)
  449. // We ignore the field background color because this is a read-only element.
  450. t.textStyle = tcell.StyleDefault.Foreground(fieldTextColor).Background(bgColor)
  451. return t
  452. }
  453. // ScrollTo scrolls to the specified row and column (both starting with 0).
  454. func (t *TextView) ScrollTo(row, column int) *TextView {
  455. if !t.scrollable {
  456. return t
  457. }
  458. t.lineOffset = row
  459. t.columnOffset = column
  460. t.trackEnd = false
  461. return t
  462. }
  463. // ScrollToBeginning scrolls to the top left corner of the text if the text view
  464. // is scrollable.
  465. func (t *TextView) ScrollToBeginning() *TextView {
  466. if !t.scrollable {
  467. return t
  468. }
  469. t.trackEnd = false
  470. t.lineOffset = 0
  471. t.columnOffset = 0
  472. return t
  473. }
  474. // ScrollToEnd scrolls to the bottom left corner of the text if the text view
  475. // is scrollable. Adding new rows to the end of the text view will cause it to
  476. // scroll with the new data.
  477. func (t *TextView) ScrollToEnd() *TextView {
  478. if !t.scrollable {
  479. return t
  480. }
  481. t.trackEnd = true
  482. t.columnOffset = 0
  483. return t
  484. }
  485. // GetScrollOffset returns the number of rows and columns that are skipped at
  486. // the top left corner when the text view has been scrolled.
  487. func (t *TextView) GetScrollOffset() (row, column int) {
  488. return t.lineOffset, t.columnOffset
  489. }
  490. // Clear removes all text from the buffer.
  491. func (t *TextView) Clear() *TextView {
  492. t.Lock()
  493. defer t.Unlock()
  494. t.clear()
  495. return t
  496. }
  497. // clear is the internal implementaton of clear. It is used by TextViewWriter
  498. // and anywhere that we need to perform a write without locking the buffer.
  499. func (t *TextView) clear() {
  500. t.buffer = nil
  501. t.recentBytes = nil
  502. t.index = nil
  503. }
  504. // Highlight specifies which regions should be highlighted. If highlight
  505. // toggling is set to true (see SetToggleHighlights()), the highlight of the
  506. // provided regions is toggled (highlighted regions are un-highlighted and vice
  507. // versa). If toggling is set to false, the provided regions are highlighted and
  508. // all other regions will not be highlighted (you may also provide nil to turn
  509. // off all highlights).
  510. //
  511. // For more information on regions, see class description. Empty region strings
  512. // are ignored.
  513. //
  514. // Text in highlighted regions will be drawn inverted, i.e. with their
  515. // background and foreground colors swapped.
  516. func (t *TextView) Highlight(regionIDs ...string) *TextView {
  517. // Toggle highlights.
  518. if t.toggleHighlights {
  519. var newIDs []string
  520. HighlightLoop:
  521. for regionID := range t.highlights {
  522. for _, id := range regionIDs {
  523. if regionID == id {
  524. continue HighlightLoop
  525. }
  526. }
  527. newIDs = append(newIDs, regionID)
  528. }
  529. for _, regionID := range regionIDs {
  530. if _, ok := t.highlights[regionID]; !ok {
  531. newIDs = append(newIDs, regionID)
  532. }
  533. }
  534. regionIDs = newIDs
  535. } // Now we have a list of region IDs that end up being highlighted.
  536. // Determine added and removed regions.
  537. var added, removed, remaining []string
  538. if t.highlighted != nil {
  539. for _, regionID := range regionIDs {
  540. if _, ok := t.highlights[regionID]; ok {
  541. remaining = append(remaining, regionID)
  542. delete(t.highlights, regionID)
  543. } else {
  544. added = append(added, regionID)
  545. }
  546. }
  547. for regionID := range t.highlights {
  548. removed = append(removed, regionID)
  549. }
  550. }
  551. // Make new selection.
  552. t.highlights = make(map[string]struct{})
  553. for _, id := range regionIDs {
  554. if id == "" {
  555. continue
  556. }
  557. t.highlights[id] = struct{}{}
  558. }
  559. t.index = nil
  560. // Notify.
  561. if t.highlighted != nil && len(added) > 0 || len(removed) > 0 {
  562. t.highlighted(added, removed, remaining)
  563. }
  564. return t
  565. }
  566. // GetHighlights returns the IDs of all currently highlighted regions.
  567. func (t *TextView) GetHighlights() (regionIDs []string) {
  568. for id := range t.highlights {
  569. regionIDs = append(regionIDs, id)
  570. }
  571. return
  572. }
  573. // SetToggleHighlights sets a flag to determine how regions are highlighted.
  574. // When set to true, the Highlight() function (or a mouse click) will toggle the
  575. // provided/selected regions. When set to false, Highlight() (or a mouse click)
  576. // will simply highlight the provided regions.
  577. func (t *TextView) SetToggleHighlights(toggle bool) *TextView {
  578. t.toggleHighlights = toggle
  579. return t
  580. }
  581. // ScrollToHighlight will cause the visible area to be scrolled so that the
  582. // highlighted regions appear in the visible area of the text view. This
  583. // repositioning happens the next time the text view is drawn. It happens only
  584. // once so you will need to call this function repeatedly to always keep
  585. // highlighted regions in view.
  586. //
  587. // Nothing happens if there are no highlighted regions or if the text view is
  588. // not scrollable.
  589. func (t *TextView) ScrollToHighlight() *TextView {
  590. if len(t.highlights) == 0 || !t.scrollable || !t.regions {
  591. return t
  592. }
  593. t.index = nil
  594. t.scrollToHighlights = true
  595. t.trackEnd = false
  596. return t
  597. }
  598. // GetRegionText returns the text of the region with the given ID. If dynamic
  599. // colors are enabled, color tags are stripped from the text. Newlines are
  600. // always returned as '\n' runes.
  601. //
  602. // If the region does not exist or if regions are turned off, an empty string
  603. // is returned.
  604. func (t *TextView) GetRegionText(regionID string) string {
  605. if !t.regions || regionID == "" {
  606. return ""
  607. }
  608. var (
  609. buffer bytes.Buffer
  610. currentRegionID string
  611. )
  612. for _, str := range t.buffer {
  613. // Find all color tags in this line.
  614. var colorTagIndices [][]int
  615. if t.dynamicColors {
  616. colorTagIndices = colorPattern.FindAllStringIndex(str, -1)
  617. }
  618. // Find all regions in this line.
  619. var (
  620. regionIndices [][]int
  621. regions [][]string
  622. )
  623. if t.regions {
  624. regionIndices = regionPattern.FindAllStringIndex(str, -1)
  625. regions = regionPattern.FindAllStringSubmatch(str, -1)
  626. }
  627. // Analyze this line.
  628. var currentTag, currentRegion int
  629. for pos, ch := range str {
  630. // Skip any color tags.
  631. if currentTag < len(colorTagIndices) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] {
  632. tag := currentTag
  633. if pos == colorTagIndices[tag][1]-1 {
  634. currentTag++
  635. }
  636. if colorTagIndices[tag][1]-colorTagIndices[tag][0] > 2 {
  637. continue
  638. }
  639. }
  640. // Skip any regions.
  641. if currentRegion < len(regionIndices) && pos >= regionIndices[currentRegion][0] && pos < regionIndices[currentRegion][1] {
  642. if pos == regionIndices[currentRegion][1]-1 {
  643. if currentRegionID == regionID {
  644. // This is the end of the requested region. We're done.
  645. return buffer.String()
  646. }
  647. currentRegionID = regions[currentRegion][1]
  648. currentRegion++
  649. }
  650. continue
  651. }
  652. // Add this rune.
  653. if currentRegionID == regionID {
  654. buffer.WriteRune(ch)
  655. }
  656. }
  657. // Add newline.
  658. if currentRegionID == regionID {
  659. buffer.WriteRune('\n')
  660. }
  661. }
  662. return escapePattern.ReplaceAllString(buffer.String(), `[$1$2]`)
  663. }
  664. // Focus is called when this primitive receives focus.
  665. func (t *TextView) Focus(delegate func(p Primitive)) {
  666. // Implemented here with locking because this is used by layout primitives.
  667. t.Lock()
  668. defer t.Unlock()
  669. // But if we're part of a form and not scrollable, there's nothing the user
  670. // can do here so we're finished.
  671. if t.finished != nil && !t.scrollable {
  672. t.finished(-1)
  673. return
  674. }
  675. t.Box.Focus(delegate)
  676. }
  677. // HasFocus returns whether or not this primitive has focus.
  678. func (t *TextView) HasFocus() bool {
  679. // Implemented here with locking because this may be used in the "changed"
  680. // callback.
  681. t.Lock()
  682. defer t.Unlock()
  683. return t.Box.HasFocus()
  684. }
  685. // Write lets us implement the io.Writer interface. Tab characters will be
  686. // replaced with TabSize space characters. A "\n" or "\r\n" will be interpreted
  687. // as a new line.
  688. func (t *TextView) Write(p []byte) (n int, err error) {
  689. t.Lock()
  690. defer t.Unlock()
  691. return t.write(p)
  692. }
  693. // write is the internal implementation of Write. It is used by TextViewWriter
  694. // and anywhere that we need to perform a write without locking the buffer.
  695. func (t *TextView) write(p []byte) (n int, err error) {
  696. // Notify at the end.
  697. changed := t.changed
  698. if changed != nil {
  699. defer func() {
  700. // We always call the "changed" function in a separate goroutine to avoid
  701. // deadlocks.
  702. go changed()
  703. }()
  704. }
  705. // Copy data over.
  706. newBytes := append(t.recentBytes, p...)
  707. t.recentBytes = nil
  708. // If we have a trailing invalid UTF-8 byte, we'll wait.
  709. if r, _ := utf8.DecodeLastRune(p); r == utf8.RuneError {
  710. t.recentBytes = newBytes
  711. return len(p), nil
  712. }
  713. // If we have a trailing open dynamic color, exclude it.
  714. if t.dynamicColors {
  715. location := openColorRegex.FindIndex(newBytes)
  716. if location != nil {
  717. t.recentBytes = newBytes[location[0]:]
  718. newBytes = newBytes[:location[0]]
  719. }
  720. }
  721. // If we have a trailing open region, exclude it.
  722. if t.regions {
  723. location := openRegionRegex.FindIndex(newBytes)
  724. if location != nil {
  725. t.recentBytes = newBytes[location[0]:]
  726. newBytes = newBytes[:location[0]]
  727. }
  728. }
  729. // Transform the new bytes into strings.
  730. newBytes = bytes.Replace(newBytes, []byte{'\t'}, bytes.Repeat([]byte{' '}, TabSize), -1)
  731. for index, line := range newLineRegex.Split(string(newBytes), -1) {
  732. if index == 0 {
  733. if len(t.buffer) == 0 {
  734. t.buffer = []string{line}
  735. } else {
  736. t.buffer[len(t.buffer)-1] += line
  737. }
  738. } else {
  739. t.buffer = append(t.buffer, line)
  740. }
  741. }
  742. // Reset the index.
  743. t.index = nil
  744. return len(p), nil
  745. }
  746. // BatchWriter returns a new writer that can be used to write into the buffer
  747. // but without Locking/Unlocking the buffer on every write, as TextView's
  748. // Write() and Clear() functions do. The lock will be acquired once when
  749. // BatchWriter is called, and will be released when the returned writer is
  750. // closed. Example:
  751. //
  752. // tv := tview.NewTextView()
  753. // w := tv.BatchWriter()
  754. // defer w.Close()
  755. // w.Clear()
  756. // fmt.Fprintln(w, "To sit in solemn silence")
  757. // fmt.Fprintln(w, "on a dull, dark, dock")
  758. // fmt.Println(tv.GetText(false))
  759. //
  760. // Note that using the batch writer requires you to manage any issues that may
  761. // arise from concurrency yourself. See package description for details on
  762. // dealing with concurrency.
  763. func (t *TextView) BatchWriter() TextViewWriter {
  764. t.Lock()
  765. return TextViewWriter{
  766. t: t,
  767. }
  768. }
  769. // reindexBuffer re-indexes the buffer such that we can use it to easily draw
  770. // the buffer onto the screen. Each line in the index will contain a pointer
  771. // into the buffer from which on we will print text. It will also contain the
  772. // colors, attributes, and region with which the line starts.
  773. //
  774. // If maxLines is greater than 0, any extra lines will be dropped from the
  775. // buffer.
  776. func (t *TextView) reindexBuffer(width int) {
  777. if t.index != nil {
  778. return // Nothing has changed. We can still use the current index.
  779. }
  780. t.index = nil
  781. t.fromHighlight, t.toHighlight, t.posHighlight = -1, -1, -1
  782. // If there's no space, there's no index.
  783. if width < 1 {
  784. return
  785. }
  786. // Initial states.
  787. regionID := ""
  788. var (
  789. highlighted bool
  790. foregroundColor, backgroundColor, attributes string
  791. )
  792. // Go through each line in the buffer.
  793. for bufferIndex, str := range t.buffer {
  794. colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedStr, _ := decomposeString(str, t.dynamicColors, t.regions)
  795. // Split the line if required.
  796. var splitLines []string
  797. str = strippedStr
  798. if t.wrap && len(str) > 0 {
  799. for len(str) > 0 {
  800. // Truncate str to width.
  801. var splitPos, clusterWidth, lineWidth int
  802. state := -1
  803. remaining := str
  804. for splitPos == 0 || len(remaining) > 0 { // We'll extract at least one grapheme cluster.
  805. var cluster string
  806. cluster, remaining, clusterWidth, state = uniseg.FirstGraphemeClusterInString(remaining, state)
  807. lineWidth += clusterWidth
  808. if splitPos > 0 && lineWidth > width {
  809. break
  810. }
  811. splitPos += len(cluster)
  812. }
  813. extract := str[:splitPos]
  814. if t.wordWrap && len(extract) < len(str) {
  815. // Add any spaces from the next line.
  816. if spaces := spacePattern.FindStringIndex(str[len(extract):]); spaces != nil && spaces[0] == 0 {
  817. extract = str[:len(extract)+spaces[1]]
  818. }
  819. // Can we split before the mandatory end?
  820. matches := boundaryPattern.FindAllStringIndex(extract, -1)
  821. if len(matches) > 0 {
  822. // Yes. Let's split there.
  823. extract = extract[:matches[len(matches)-1][1]]
  824. }
  825. }
  826. splitLines = append(splitLines, extract)
  827. str = str[len(extract):]
  828. }
  829. } else {
  830. // No need to split the line.
  831. splitLines = []string{str}
  832. }
  833. // Create index from split lines.
  834. var originalPos, colorPos, regionPos, escapePos int
  835. for _, splitLine := range splitLines {
  836. line := &textViewIndex{
  837. Line: bufferIndex,
  838. Pos: originalPos,
  839. ForegroundColor: foregroundColor,
  840. BackgroundColor: backgroundColor,
  841. Attributes: attributes,
  842. Region: regionID,
  843. }
  844. // Shift original position with tags.
  845. lineLength := len(splitLine)
  846. remainingLength := lineLength
  847. tagEnd := originalPos
  848. totalTagLength := 0
  849. for {
  850. // Which tag comes next?
  851. nextTag := make([][3]int, 0, 3)
  852. if colorPos < len(colorTagIndices) {
  853. nextTag = append(nextTag, [3]int{colorTagIndices[colorPos][0], colorTagIndices[colorPos][1], 0}) // 0 = color tag.
  854. }
  855. if regionPos < len(regionIndices) {
  856. nextTag = append(nextTag, [3]int{regionIndices[regionPos][0], regionIndices[regionPos][1], 1}) // 1 = region tag.
  857. }
  858. if escapePos < len(escapeIndices) {
  859. nextTag = append(nextTag, [3]int{escapeIndices[escapePos][0], escapeIndices[escapePos][1], 2}) // 2 = escape tag.
  860. }
  861. minPos := -1
  862. tagIndex := -1
  863. for index, pair := range nextTag {
  864. if minPos < 0 || pair[0] < minPos {
  865. minPos = pair[0]
  866. tagIndex = index
  867. }
  868. }
  869. // Is the next tag in range?
  870. if tagIndex < 0 || minPos > tagEnd+remainingLength {
  871. break // No. We're done with this line.
  872. }
  873. // Advance.
  874. strippedTagStart := nextTag[tagIndex][0] - originalPos - totalTagLength
  875. tagEnd = nextTag[tagIndex][1]
  876. tagLength := tagEnd - nextTag[tagIndex][0]
  877. if nextTag[tagIndex][2] == 2 {
  878. tagLength = 1
  879. }
  880. totalTagLength += tagLength
  881. remainingLength = lineLength - (tagEnd - originalPos - totalTagLength)
  882. // Process the tag.
  883. switch nextTag[tagIndex][2] {
  884. case 0:
  885. // Process color tags.
  886. foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
  887. colorPos++
  888. case 1:
  889. // Process region tags.
  890. regionID = regions[regionPos][1]
  891. _, highlighted = t.highlights[regionID]
  892. // Update highlight range.
  893. if highlighted {
  894. line := len(t.index)
  895. if t.fromHighlight < 0 {
  896. t.fromHighlight, t.toHighlight = line, line
  897. t.posHighlight = uniseg.StringWidth(splitLine[:strippedTagStart])
  898. } else if line > t.toHighlight {
  899. t.toHighlight = line
  900. }
  901. }
  902. regionPos++
  903. case 2:
  904. // Process escape tags.
  905. escapePos++
  906. }
  907. }
  908. // Advance to next line.
  909. originalPos += lineLength + totalTagLength
  910. // Append this line.
  911. line.NextPos = originalPos
  912. line.Width = uniseg.StringWidth(splitLine)
  913. t.index = append(t.index, line)
  914. }
  915. // Word-wrapped lines may have trailing whitespace. Remove it.
  916. if t.wrap && t.wordWrap {
  917. for _, line := range t.index {
  918. str := t.buffer[line.Line][line.Pos:line.NextPos]
  919. spaces := spacePattern.FindAllStringIndex(str, -1)
  920. if spaces != nil && spaces[len(spaces)-1][1] == len(str) {
  921. oldNextPos := line.NextPos
  922. line.NextPos -= spaces[len(spaces)-1][1] - spaces[len(spaces)-1][0]
  923. line.Width -= uniseg.StringWidth(t.buffer[line.Line][line.NextPos:oldNextPos])
  924. }
  925. }
  926. }
  927. }
  928. // Drop lines beyond maxLines.
  929. if t.maxLines > 0 && len(t.index) > t.maxLines {
  930. removedLines := len(t.index) - t.maxLines
  931. // Adjust the index.
  932. t.index = t.index[removedLines:]
  933. if t.fromHighlight >= 0 {
  934. t.fromHighlight -= removedLines
  935. if t.fromHighlight < 0 {
  936. t.fromHighlight = 0
  937. }
  938. }
  939. if t.toHighlight >= 0 {
  940. t.toHighlight -= removedLines
  941. if t.toHighlight < 0 {
  942. t.fromHighlight, t.toHighlight, t.posHighlight = -1, -1, -1
  943. }
  944. }
  945. bufferShift := t.index[0].Line
  946. for _, line := range t.index {
  947. line.Line -= bufferShift
  948. }
  949. // Adjust the original buffer.
  950. t.buffer = t.buffer[bufferShift:]
  951. var prefix string
  952. if t.index[0].ForegroundColor != "" || t.index[0].BackgroundColor != "" || t.index[0].Attributes != "" {
  953. prefix = fmt.Sprintf("[%s:%s:%s]", t.index[0].ForegroundColor, t.index[0].BackgroundColor, t.index[0].Attributes)
  954. }
  955. if t.index[0].Region != "" {
  956. prefix += fmt.Sprintf(`["%s"]`, t.index[0].Region)
  957. }
  958. posShift := t.index[0].Pos
  959. t.buffer[0] = prefix + t.buffer[0][posShift:]
  960. t.lineOffset -= removedLines
  961. if t.lineOffset < 0 {
  962. t.lineOffset = 0
  963. }
  964. // Adjust positions of first buffer line.
  965. posShift -= len(prefix)
  966. for _, line := range t.index {
  967. if line.Line != 0 {
  968. break
  969. }
  970. line.Pos -= posShift
  971. line.NextPos -= posShift
  972. }
  973. }
  974. // Calculate longest line.
  975. t.longestLine = 0
  976. for _, line := range t.index {
  977. if line.Width > t.longestLine {
  978. t.longestLine = line.Width
  979. }
  980. }
  981. }
  982. // Draw draws this primitive onto the screen.
  983. func (t *TextView) Draw(screen tcell.Screen) {
  984. t.Box.DrawForSubclass(screen, t)
  985. t.Lock()
  986. defer t.Unlock()
  987. // Get the available size.
  988. x, y, width, height := t.GetInnerRect()
  989. t.pageSize = height
  990. // Draw label.
  991. _, labelBg, _ := t.labelStyle.Decompose()
  992. if t.labelWidth > 0 {
  993. labelWidth := t.labelWidth
  994. if labelWidth > width {
  995. labelWidth = width
  996. }
  997. printWithStyle(screen, t.label, x, y, 0, labelWidth, AlignLeft, t.labelStyle, labelBg == tcell.ColorDefault)
  998. x += labelWidth
  999. width -= labelWidth
  1000. } else {
  1001. _, drawnWidth, _, _ := printWithStyle(screen, t.label, x, y, 0, width, AlignLeft, t.labelStyle, labelBg == tcell.ColorDefault)
  1002. x += drawnWidth
  1003. width -= drawnWidth
  1004. }
  1005. // What's the space for the text element?
  1006. if t.width > 0 && t.width < width {
  1007. width = t.width
  1008. }
  1009. if t.height > 0 && t.height < height {
  1010. height = t.height
  1011. }
  1012. if width <= 0 {
  1013. return // No space left for the text area.
  1014. }
  1015. // Draw the text element if necessary.
  1016. _, bg, _ := t.textStyle.Decompose()
  1017. if bg != t.backgroundColor {
  1018. for row := 0; row < height; row++ {
  1019. for column := 0; column < width; column++ {
  1020. screen.SetContent(x+column, y+row, ' ', nil, t.textStyle)
  1021. }
  1022. }
  1023. }
  1024. // If the width has changed, we need to reindex.
  1025. if width != t.lastWidth && t.wrap {
  1026. t.index = nil
  1027. }
  1028. t.lastWidth = width
  1029. // Re-index.
  1030. t.reindexBuffer(width)
  1031. if t.regions {
  1032. t.regionInfos = nil
  1033. }
  1034. // If we don't have an index, there's nothing to draw.
  1035. if t.index == nil {
  1036. return
  1037. }
  1038. // Move to highlighted regions.
  1039. if t.regions && t.scrollToHighlights && t.fromHighlight >= 0 {
  1040. // Do we fit the entire height?
  1041. if t.toHighlight-t.fromHighlight+1 < height {
  1042. // Yes, let's center the highlights.
  1043. t.lineOffset = (t.fromHighlight + t.toHighlight - height) / 2
  1044. } else {
  1045. // No, let's move to the start of the highlights.
  1046. t.lineOffset = t.fromHighlight
  1047. }
  1048. // If the highlight is too far to the right, move it to the middle.
  1049. if t.posHighlight-t.columnOffset > 3*width/4 {
  1050. t.columnOffset = t.posHighlight - width/2
  1051. }
  1052. // If the highlight is off-screen on the left, move it on-screen.
  1053. if t.posHighlight-t.columnOffset < 0 {
  1054. t.columnOffset = t.posHighlight - width/4
  1055. }
  1056. }
  1057. t.scrollToHighlights = false
  1058. // Adjust line offset.
  1059. if t.lineOffset+height > len(t.index) {
  1060. t.trackEnd = true
  1061. }
  1062. if t.trackEnd {
  1063. t.lineOffset = len(t.index) - height
  1064. }
  1065. if t.lineOffset < 0 {
  1066. t.lineOffset = 0
  1067. }
  1068. // Adjust column offset.
  1069. if t.align == AlignLeft {
  1070. if t.columnOffset+width > t.longestLine {
  1071. t.columnOffset = t.longestLine - width
  1072. }
  1073. if t.columnOffset < 0 {
  1074. t.columnOffset = 0
  1075. }
  1076. } else if t.align == AlignRight {
  1077. if t.columnOffset-width < -t.longestLine {
  1078. t.columnOffset = width - t.longestLine
  1079. }
  1080. if t.columnOffset > 0 {
  1081. t.columnOffset = 0
  1082. }
  1083. } else { // AlignCenter.
  1084. half := (t.longestLine - width) / 2
  1085. if half > 0 {
  1086. if t.columnOffset > half {
  1087. t.columnOffset = half
  1088. }
  1089. if t.columnOffset < -half {
  1090. t.columnOffset = -half
  1091. }
  1092. } else {
  1093. t.columnOffset = 0
  1094. }
  1095. }
  1096. // Draw the buffer.
  1097. for line := t.lineOffset; line < len(t.index); line++ {
  1098. // Are we done?
  1099. if line-t.lineOffset >= height {
  1100. break
  1101. }
  1102. // Get the text for this line.
  1103. index := t.index[line]
  1104. text := t.buffer[index.Line][index.Pos:index.NextPos]
  1105. foregroundColor := index.ForegroundColor
  1106. backgroundColor := index.BackgroundColor
  1107. attributes := index.Attributes
  1108. regionID := index.Region
  1109. if t.regions {
  1110. if len(t.regionInfos) > 0 && t.regionInfos[len(t.regionInfos)-1].ID != regionID {
  1111. // End last region.
  1112. t.regionInfos[len(t.regionInfos)-1].ToX = x
  1113. t.regionInfos[len(t.regionInfos)-1].ToY = y + line - t.lineOffset
  1114. }
  1115. if regionID != "" && (len(t.regionInfos) == 0 || t.regionInfos[len(t.regionInfos)-1].ID != regionID) {
  1116. // Start a new region.
  1117. t.regionInfos = append(t.regionInfos, &textViewRegion{
  1118. ID: regionID,
  1119. FromX: x,
  1120. FromY: y + line - t.lineOffset,
  1121. ToX: -1,
  1122. ToY: -1,
  1123. })
  1124. }
  1125. }
  1126. // Process tags.
  1127. colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedText, _ := decomposeString(text, t.dynamicColors, t.regions)
  1128. // Calculate the position of the line.
  1129. var skip, posX int
  1130. if t.align == AlignLeft {
  1131. posX = -t.columnOffset
  1132. } else if t.align == AlignRight {
  1133. posX = width - index.Width - t.columnOffset
  1134. } else { // AlignCenter.
  1135. posX = (width-index.Width)/2 - t.columnOffset
  1136. }
  1137. if posX < 0 {
  1138. skip = -posX
  1139. posX = 0
  1140. }
  1141. // Print the line.
  1142. if y+line-t.lineOffset >= 0 {
  1143. var colorPos, regionPos, escapePos, tagOffset, skipped int
  1144. iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth, boundaries int) bool {
  1145. // Process tags.
  1146. for {
  1147. if colorPos < len(colorTags) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
  1148. // Get the color.
  1149. foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
  1150. tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
  1151. colorPos++
  1152. } else if regionPos < len(regionIndices) && textPos+tagOffset >= regionIndices[regionPos][0] && textPos+tagOffset < regionIndices[regionPos][1] {
  1153. // Get the region.
  1154. if regionID != "" && len(t.regionInfos) > 0 && t.regionInfos[len(t.regionInfos)-1].ID == regionID {
  1155. // End last region.
  1156. t.regionInfos[len(t.regionInfos)-1].ToX = x + posX
  1157. t.regionInfos[len(t.regionInfos)-1].ToY = y + line - t.lineOffset
  1158. }
  1159. regionID = regions[regionPos][1]
  1160. if regionID != "" {
  1161. // Start new region.
  1162. t.regionInfos = append(t.regionInfos, &textViewRegion{
  1163. ID: regionID,
  1164. FromX: x + posX,
  1165. FromY: y + line - t.lineOffset,
  1166. ToX: -1,
  1167. ToY: -1,
  1168. })
  1169. }
  1170. tagOffset += regionIndices[regionPos][1] - regionIndices[regionPos][0]
  1171. regionPos++
  1172. } else {
  1173. break
  1174. }
  1175. }
  1176. // Skip the second-to-last character of an escape tag.
  1177. if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
  1178. tagOffset++
  1179. escapePos++
  1180. }
  1181. // Mix the existing style with the new style.
  1182. style := overlayStyle(t.textStyle, foregroundColor, backgroundColor, attributes)
  1183. // Do we highlight this character?
  1184. var highlighted bool
  1185. if regionID != "" {
  1186. if _, ok := t.highlights[regionID]; ok {
  1187. highlighted = true
  1188. }
  1189. }
  1190. if highlighted {
  1191. fg, bg, _ := style.Decompose()
  1192. if bg == t.backgroundColor {
  1193. r, g, b := fg.RGB()
  1194. c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255}
  1195. _, _, li := c.Hcl()
  1196. if li < .5 {
  1197. bg = tcell.ColorWhite
  1198. } else {
  1199. bg = tcell.ColorBlack
  1200. }
  1201. }
  1202. style = style.Background(fg).Foreground(bg)
  1203. }
  1204. // Skip to the right.
  1205. if !t.wrap && skipped < skip {
  1206. skipped += screenWidth
  1207. return false
  1208. }
  1209. // Stop at the right border.
  1210. if posX+screenWidth > width {
  1211. return true
  1212. }
  1213. // Draw the character.
  1214. for offset := screenWidth - 1; offset >= 0; offset-- {
  1215. if offset == 0 {
  1216. screen.SetContent(x+posX+offset, y+line-t.lineOffset, main, comb, style)
  1217. } else {
  1218. screen.SetContent(x+posX+offset, y+line-t.lineOffset, ' ', nil, style)
  1219. }
  1220. }
  1221. // Advance.
  1222. posX += screenWidth
  1223. return false
  1224. })
  1225. }
  1226. }
  1227. // If this view is not scrollable, we'll purge the buffer of lines that have
  1228. // scrolled out of view.
  1229. if !t.scrollable && t.lineOffset > 0 {
  1230. if t.lineOffset >= len(t.index) {
  1231. t.buffer = nil
  1232. } else {
  1233. t.buffer = t.buffer[t.index[t.lineOffset].Line:]
  1234. }
  1235. t.index = nil
  1236. t.lineOffset = 0
  1237. }
  1238. }
  1239. // InputHandler returns the handler for this primitive.
  1240. func (t *TextView) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
  1241. return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
  1242. key := event.Key()
  1243. if key == tcell.KeyEscape || key == tcell.KeyEnter || key == tcell.KeyTab || key == tcell.KeyBacktab {
  1244. if t.done != nil {
  1245. t.done(key)
  1246. }
  1247. if t.finished != nil {
  1248. t.finished(key)
  1249. }
  1250. return
  1251. }
  1252. if !t.scrollable {
  1253. return
  1254. }
  1255. switch key {
  1256. case tcell.KeyRune:
  1257. switch event.Rune() {
  1258. case 'g': // Home.
  1259. t.trackEnd = false
  1260. t.lineOffset = 0
  1261. t.columnOffset = 0
  1262. case 'G': // End.
  1263. t.trackEnd = true
  1264. t.columnOffset = 0
  1265. case 'j': // Down.
  1266. t.lineOffset++
  1267. case 'k': // Up.
  1268. t.trackEnd = false
  1269. t.lineOffset--
  1270. case 'h': // Left.
  1271. t.columnOffset--
  1272. case 'l': // Right.
  1273. t.columnOffset++
  1274. }
  1275. case tcell.KeyHome:
  1276. t.trackEnd = false
  1277. t.lineOffset = 0
  1278. t.columnOffset = 0
  1279. case tcell.KeyEnd:
  1280. t.trackEnd = true
  1281. t.columnOffset = 0
  1282. case tcell.KeyUp:
  1283. t.trackEnd = false
  1284. t.lineOffset--
  1285. case tcell.KeyDown:
  1286. t.lineOffset++
  1287. case tcell.KeyLeft:
  1288. t.columnOffset--
  1289. case tcell.KeyRight:
  1290. t.columnOffset++
  1291. case tcell.KeyPgDn, tcell.KeyCtrlF:
  1292. t.lineOffset += t.pageSize
  1293. case tcell.KeyPgUp, tcell.KeyCtrlB:
  1294. t.trackEnd = false
  1295. t.lineOffset -= t.pageSize
  1296. }
  1297. })
  1298. }
  1299. // MouseHandler returns the mouse handler for this primitive.
  1300. func (t *TextView) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
  1301. return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
  1302. x, y := event.Position()
  1303. if !t.InRect(x, y) {
  1304. return false, nil
  1305. }
  1306. switch action {
  1307. case MouseLeftDown:
  1308. setFocus(t)
  1309. consumed = true
  1310. case MouseLeftClick:
  1311. if t.regions {
  1312. // Find a region to highlight.
  1313. for _, region := range t.regionInfos {
  1314. if y == region.FromY && x < region.FromX ||
  1315. y == region.ToY && x >= region.ToX ||
  1316. region.FromY >= 0 && y < region.FromY ||
  1317. region.ToY >= 0 && y > region.ToY {
  1318. continue
  1319. }
  1320. t.Highlight(region.ID)
  1321. break
  1322. }
  1323. }
  1324. consumed = true
  1325. case MouseScrollUp:
  1326. t.trackEnd = false
  1327. t.lineOffset--
  1328. consumed = true
  1329. case MouseScrollDown:
  1330. t.lineOffset++
  1331. consumed = true
  1332. }
  1333. return
  1334. })
  1335. }