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