...

Source file src/github.com/rivo/tview/textview.go

Documentation: github.com/rivo/tview

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

View as plain text