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