gemini-browser

A text-based gemini browser
git clone git://git.laack.co/gemini-browser.git
Log | Files | Refs | README

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 }