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