package cviewimport ()var (// TabSize is the number of spaces with which a tab character will be replaced.TabSize = 4)var ( openColorRegex = regexp.MustCompile(`\[([a-zA-Z]*|#[0-9a-zA-Z]*)$`) openRegionRegex = regexp.MustCompile(`\["[a-zA-Z0-9_,;: \-\.]*"?$`))// textViewIndex contains information about each line displayed in the text// view.type textViewIndex struct { Line int// The index into the "buffer" variable. Pos int// The index into the "buffer" line ([]byte position). NextPos int// The (byte) index of the next character in this buffer line. Width int// The screen width of this line. ForegroundColor string// The starting foreground color ("" = don't change, "-" = reset). BackgroundColor string// The starting background color ("" = don't change, "-" = reset). Attributes string// The starting attributes ("" = don't change, "-" = reset). Region []byte// The starting region ID.}// textViewRegion contains information about a region.type textViewRegion struct {// The region ID. ID []byte// The starting and end screen position of the region as determined the last // time Draw() was called. A negative value indicates out-of-rect positions. FromX, FromY, ToX, ToY int}// TextView is a box which displays text. It implements the io.Writer interface// so you can stream text to it. This does not trigger a redraw automatically// but if a handler is installed via SetChangedFunc(), you can cause it to be// redrawn. (See SetChangedFunc() for more details.)//// # Navigation//// If the text view is scrollable (the default), text is kept in a buffer which// may be larger than the screen and can be navigated similarly to Vim://// - h, left arrow: Move left.// - l, right arrow: Move right.// - j, down arrow: Move down.// - k, up arrow: Move up.// - g, home: Move to the top.// - G, end: Move to the bottom.// - Ctrl-F, page down: Move down by one page.// - Ctrl-B, page up: Move up by one page.//// If the text is not scrollable, any text above the top visible line is// discarded.//// Use SetInputCapture() to override or modify keyboard input.//// # Colors//// If dynamic colors are enabled via SetDynamicColors(), text color can be// changed dynamically by embedding color strings in square brackets. This works// the same way as anywhere else. Please see the package documentation for more// information.//// # Regions and Highlights//// If regions are enabled via SetRegions(), you can define text regions within// the text and assign region IDs to them. Text regions start with region tags.// Region tags are square brackets that contain a region ID in double quotes,// for example://// We define a ["rg"]region[""] here.//// A text region ends with the next region tag. Tags with no region ID ([""])// don't start new regions. They can therefore be used to mark the end of a// region. Region IDs must satisfy the following regular expression://// [a-zA-Z0-9_,;: \-\.]+//// Regions can be highlighted by calling the Highlight() function with one or// more region IDs. This can be used to display search results, for example.//// The ScrollToHighlight() function can be used to jump to the currently// highlighted region once when the text view is drawn the next time.typeTextViewstruct { *Box// The text buffer. buffer [][]byte// The last bytes that have been received but are not part of the buffer yet. recentBytes []byte// The last width and height of the text view. lastWidth, lastHeight int// The processed line index. This is nil if the buffer has changed and needs // to be re-indexed. index []*textViewIndex// The width of the text view buffer index. indexWidth int// If set to true, the buffer will be reindexed each time it is modified. reindex bool// The horizontal text alignment, one of AlignLeft, AlignCenter, or AlignRight. align int// The vertical text alignment, one of AlignTop, AlignMiddle, or AlignBottom. valign VerticalAlignment// Information about visible regions as of the last call to Draw(). regionInfos []*textViewRegion// Indices into the "index" slice which correspond to the first line of the // first highlight and the last line of the last highlight. This is calculated // during re-indexing. Set to -1 if there is no current highlight. fromHighlight, toHighlight int// The screen space column of the highlight in its first line. Set to -1 if // there is no current highlight. posHighlight int// A set of region IDs that are currently highlighted. highlights map[string]struct{}// The screen width of the longest line in the index (not the buffer). longestLine int// The index of the first line shown in the text view. lineOffset int// The maximum number of newlines the text view will hold (0 = unlimited). maxLines int// If set to true, the text view will always remain at the end of the content. trackEnd bool// The number of characters to be skipped on each line (not in wrap mode). columnOffset int// The height of the content the last time the text view was drawn. pageSize int// If set to true, the text view will keep a buffer of text which can be // navigated when the text is longer than what fits into the box. scrollable bool// Visibility of the scroll bar. scrollBarVisibility ScrollBarVisibility// The scroll bar color. scrollBarColor tcell.Color// If set to true, lines that are longer than the available width are wrapped // onto the next line. If set to false, any characters beyond the available // width are discarded. wrap bool// The maximum line width when wrapping (0 = use TextView width). wrapWidth int// If set to true and if wrap is also true, lines are split at spaces or // after punctuation characters. wordWrap bool// The (starting) color of the text. textColor tcell.Color// The foreground color of highlighted text. highlightForeground tcell.Color// The background color of highlighted text. highlightBackground tcell.Color// If set to true, the text color can be changed dynamically by piping color // strings in square brackets to the text view. dynamicColors bool// If set to true, region tags can be used to define regions. regions bool// A temporary flag which, when true, will automatically bring the current // highlight(s) into the visible screen. scrollToHighlights bool// If true, setting new highlights will be a XOR instead of an overwrite // operation. toggleHighlights bool// An optional function which is called when the content of the text view has // changed. changed func()// An optional function which is called when the user presses one of the // following keys: Escape, Enter, Tab, Backtab. done func(tcell.Key)// An optional function which is called when one or more regions were // highlighted. highlighted func(added, removed, remaining []string)sync.RWMutex clicked func(regionId string)}// NewTextView returns a new text view.func () *TextView {return &TextView{Box: NewBox(),highlights: make(map[string]struct{}),lineOffset: -1,reindex: true,scrollable: true,scrollBarVisibility: ScrollBarAuto,scrollBarColor: Styles.ScrollBarColor,align: AlignLeft,valign: AlignTop,wrap: true,textColor: Styles.PrimaryTextColor,highlightForeground: Styles.PrimitiveBackgroundColor,highlightBackground: Styles.PrimaryTextColor, }}// SetScrollable sets the flag that decides whether or not the text view is// scrollable. If true, text is kept in a buffer and can be navigated. If false,// the last line will always be visible.func ( *TextView) ( bool) { .Lock()defer .Unlock() .scrollable = if ! { .trackEnd = true }}// SetScrollBarVisibility specifies the display of the scroll bar.func ( *TextView) ( ScrollBarVisibility) { .Lock()defer .Unlock() .scrollBarVisibility = }// SetScrollBarColor sets the color of the scroll bar.func ( *TextView) ( tcell.Color) { .Lock()defer .Unlock() .scrollBarColor = }// SetWrap sets the flag that, if true, leads to lines that are longer than the// available width being wrapped onto the next line. If false, any characters// beyond the available width are not displayed.func ( *TextView) ( bool) { .Lock()defer .Unlock()if .wrap != { .index = nil } .wrap = }// SetWordWrap sets the flag that, if true and if the "wrap" flag is also true// (see SetWrap()), wraps the line at spaces or after punctuation marks. Note// that trailing spaces will not be printed.//// This flag is ignored if the "wrap" flag is false.func ( *TextView) ( bool) { .Lock()defer .Unlock()if .wordWrap != { .index = nil } .wordWrap = }// SetTextAlign sets the horizontal alignment of the text. This must be either// AlignLeft, AlignCenter, or AlignRight.func ( *TextView) ( int) { .Lock()defer .Unlock()if .align != { .index = nil } .align = }// SetVerticalAlign sets the vertical alignment of the text. This must be// either AlignTop, AlignMiddle, or AlignBottom.func ( *TextView) ( VerticalAlignment) { .Lock()defer .Unlock()if .valign != { .index = nil } .valign = }// SetTextColor sets the initial color of the text (which can be changed// dynamically by sending color strings in square brackets to the text view if// dynamic colors are enabled).func ( *TextView) ( tcell.Color) { .Lock()defer .Unlock() .textColor = }// SetHighlightForegroundColor sets the foreground color of highlighted text.func ( *TextView) ( tcell.Color) { .Lock()defer .Unlock() .highlightForeground = }// SetHighlightBackgroundColor sets the foreground color of highlighted text.func ( *TextView) ( tcell.Color) { .Lock()defer .Unlock() .highlightBackground = }// SetBytes sets the text of this text view to the provided byte slice.// Previously contained text will be removed.func ( *TextView) ( []byte) { .Lock()defer .Unlock() .clear() .write()}// SetText sets the text of this text view to the provided string. Previously// contained text will be removed.func ( *TextView) ( string) { .SetBytes([]byte())}// GetBytes returns the current text of this text view. If "stripTags" is set// to true, any region/color tags are stripped from the text.func ( *TextView) ( bool) []byte { .RLock()defer .RUnlock()if ! {iflen(.recentBytes) > 0 {returnbytes.Join(append(.buffer, .recentBytes), []byte("\n")) }returnbytes.Join(.buffer, []byte("\n")) } := bytes.Join(.buffer, []byte("\n"))returnStripTags(, .dynamicColors, .regions)}// GetText returns the current text of this text view. If "stripTags" is set// to true, any region/color tags are stripped from the text.func ( *TextView) ( bool) string {returnstring(.GetBytes())}// GetBufferSize returns the number of lines and the length of the longest line// in the text buffer. The screen size of the widget is available via GetRect.func ( *TextView) () ( int, int) { .RLock()defer .RUnlock()returnlen(.buffer), .longestLine}// SetDynamicColors sets the flag that allows the text color to be changed// dynamically. See class description for details.func ( *TextView) ( bool) { .Lock()defer .Unlock()if .dynamicColors != { .index = nil } .dynamicColors = }// SetRegions sets the flag that allows to define regions in the text. See class// description for details.func ( *TextView) ( bool) { .Lock()defer .Unlock()if .regions != { .index = nil } .regions = }// SetChangedFunc sets a handler function which is called when the text of the// text view has changed. This is useful when text is written to this io.Writer// in a separate goroutine. Doing so does not automatically cause the screen to// be refreshed so you may want to use the "changed" handler to redraw the// screen.//// Note that to avoid race conditions or deadlocks, there are a few rules you// should follow://// - You can call Application.Draw() from this handler.// - You can call TextView.HasFocus() from this handler.// - During the execution of this handler, access to any other variables from// this primitive or any other primitive should be queued using// Application.QueueUpdate().//// See package description for details on dealing with concurrency.func ( *TextView) ( func()) { .Lock()defer .Unlock() .changed = }// SetDoneFunc sets a handler which is called when the user presses on the// following keys: Escape, Enter, Tab, Backtab. The key is passed to the// handler.func ( *TextView) ( func( tcell.Key)) { .Lock()defer .Unlock() .done = }// SetHighlightedFunc sets a handler which is called when the list of currently// highlighted regions change. It receives a list of region IDs which were newly// highlighted, those that are not highlighted anymore, and those that remain// highlighted.//// Note that because regions are only determined during drawing, this function// can only fire for regions that have existed during the last call to Draw().func ( *TextView) ( func(, , []string)) { .highlighted = }// SetClickedFunc Handler to run when a region is clicked.func ( *TextView) ( func( string)) { .clicked = }func ( *TextView) () {if .maxLines <= 0 {return } := len(.buffer)if > .maxLines { .buffer = .buffer[-.maxLines:] }}// SetMaxLines sets the maximum number of newlines the text view will hold// before discarding older data from the buffer.func ( *TextView) ( int) { .maxLines = .clipBuffer()}// ScrollTo scrolls to the specified row and column (both starting with 0).func ( *TextView) (, int) { .Lock()defer .Unlock()if !.scrollable {return } .lineOffset = .columnOffset = .trackEnd = false}// ScrollToBeginning scrolls to the top left corner of the text if the text view// is scrollable.func ( *TextView) () { .Lock()defer .Unlock()if !.scrollable {return } .trackEnd = false .lineOffset = 0 .columnOffset = 0}// ScrollToEnd scrolls to the bottom left corner of the text if the text view// is scrollable. Adding new rows to the end of the text view will cause it to// scroll with the new data.func ( *TextView) () { .Lock()defer .Unlock()if !.scrollable {return } .trackEnd = true .columnOffset = 0}// GetScrollOffset returns the number of rows and columns that are skipped at// the top left corner when the text view has been scrolled.func ( *TextView) () (, int) { .RLock()defer .RUnlock()return .lineOffset, .columnOffset}// Clear removes all text from the buffer.func ( *TextView) () { .Lock()defer .Unlock() .clear()}func ( *TextView) () { .buffer = nil .recentBytes = nilif .reindex { .index = nil }}// Highlight specifies which regions should be highlighted. If highlight// toggling is set to true (see SetToggleHighlights()), the highlight of the// provided regions is toggled (highlighted regions are un-highlighted and vice// versa). If toggling is set to false, the provided regions are highlighted and// all other regions will not be highlighted (you may also provide nil to turn// off all highlights).//// For more information on regions, see class description. Empty region strings// are ignored.//// Text in highlighted regions will be drawn inverted, i.e. with their// background and foreground colors swapped.func ( *TextView) ( ...string) { .Lock()// Toggle highlights.if .toggleHighlights {var []string :for := range .highlights {for , := range {if == {continue } } = append(, ) }for , := range {if , := .highlights[]; ! { = append(, ) } } = } // Now we have a list of region IDs that end up being highlighted.// Determine added and removed regions.var , , []stringif .highlighted != nil {for , := range {if , := .highlights[]; { = append(, )delete(.highlights, ) } else { = append(, ) } }for := range .highlights { = append(, ) } }// Make new selection. .highlights = make(map[string]struct{})for , := range {if == "" {continue } .highlights[] = struct{}{} } .index = nil// Notify.if .highlighted != nil && (len() > 0 || len() > 0) { .Unlock() .highlighted(, , ) } else { .Unlock() }}// GetHighlights returns the IDs of all currently highlighted regions.func ( *TextView) () ( []string) { .RLock()defer .RUnlock()for := range .highlights { = append(, ) }return}// SetToggleHighlights sets a flag to determine how regions are highlighted.// When set to true, the Highlight() function will toggle the// provided/selected regions. When set to false, Highlight()// will simply highlight the provided regions.func ( *TextView) ( bool) { .toggleHighlights = }// ScrollToHighlight will cause the visible area to be scrolled so that the// highlighted regions appear in the visible area of the text view. This// repositioning happens the next time the text view is drawn. It happens only// once so you will need to call this function repeatedly to always keep// highlighted regions in view.//// Nothing happens if there are no highlighted regions or if the text view is// not scrollable.func ( *TextView) () { .Lock()defer .Unlock()iflen(.highlights) == 0 || !.scrollable || !.regions {return } .index = nil .scrollToHighlights = true .trackEnd = false}// GetRegionText returns the text of the region with the given ID. If dynamic// colors are enabled, color tags are stripped from the text. Newlines are// always returned as '\n' runes.//// If the region does not exist or if regions are turned off, an empty string// is returned.func ( *TextView) ( string) string { .RLock()defer .RUnlock()if !.regions || len() == 0 {return"" }var (bytes.Bufferstring )for , := range .buffer {// Find all color tags in this line.var [][]intif .dynamicColors { = colorPattern.FindAllIndex(, -1) }// Find all regions in this line.var ( [][]int [][][]byte )if .regions { = regionPattern.FindAllIndex(, -1) = regionPattern.FindAllSubmatch(, -1) }// Analyze this line.var , intfor , := range {// Skip any color tags.if < len() && >= [][0] && < [][1] {if == [][1]-1 { ++if == len() {continue } }if [][1]-[][0] > 2 {continue } }// Skip any regions.if < len() && >= [][0] && < [][1] {if == [][1]-1 {if == {// This is the end of the requested region. We're done.return .String() } = string([][1]) ++ }continue }// Add this rune.if == { .WriteByte() } }// Add newline.if == { .WriteRune('\n') } }returnescapePattern.ReplaceAllString(.String(), `[$1$2]`)}// Focus is called when this primitive receives focus.func ( *TextView) ( func( Primitive)) { .Lock()defer .Unlock()// Implemented here with locking because this is used by layout primitives. .hasFocus = true}// HasFocus returns whether or not this primitive has focus.func ( *TextView) () bool { .RLock()defer .RUnlock()// Implemented here with locking because this may be used in the "changed" // callback.return .hasFocus}// Write lets us implement the io.Writer interface. Tab characters will be// replaced with TabSize space characters. A "\n" or "\r\n" will be interpreted// as a new line.func ( *TextView) ( []byte) ( int, error) { .Lock() := .changedif != nil {// Notify at the end.defer () }defer .Unlock()return .write()}func ( *TextView) ( []byte) ( int, error) {// Copy data over. := append(.recentBytes, ...) .recentBytes = nil// If we have a trailing invalid UTF-8 byte, we'll wait.if , := utf8.DecodeLastRune(); == utf8.RuneError { .recentBytes = returnlen(), nil }// If we have a trailing open dynamic color, exclude it.if .dynamicColors { := openColorRegex.FindIndex()if != nil { .recentBytes = [[0]:] = [:[0]] } }// If we have a trailing open region, exclude it.if .regions { := openRegionRegex.FindIndex()if != nil { .recentBytes = [[0]:] = [:[0]] } }// Transform the new bytes into strings. = bytes.Replace(, []byte{'\t'}, bytes.Repeat([]byte{' '}, TabSize), -1)for , := rangebytes.Split(, []byte("\n")) {if == 0 {iflen(.buffer) == 0 { .buffer = [][]byte{} } else { .buffer[len(.buffer)-1] = append(.buffer[len(.buffer)-1], ...) } } else { .buffer = append(.buffer, ) } } .clipBuffer()// Reset the index.if .reindex { .index = nil }returnlen(), nil}// SetWrapWidth set the maximum width of lines when wrapping is enabled.// When set to 0 the width of the TextView is used.func ( *TextView) ( int) { .Lock()defer .Unlock() .wrapWidth = }// SetReindexBuffer set a flag controlling whether the buffer is reindexed when// it is modified. This improves the performance of TextViews whose contents// always have line-breaks in the same location. This must be called after the// buffer has been indexed.func ( *TextView) ( bool) { .Lock()defer .Unlock() .reindex = if { .index = nil }}// reindexBuffer re-indexes the buffer such that we can use it to easily draw// the buffer onto the screen. Each line in the index will contain a pointer// into the buffer from which on we will print text. It will also contain the// color with which the line starts.func ( *TextView) ( int) {if .index != nil && (!.wrap || == .indexWidth) {return// Nothing has changed. We can still use the current index. } .index = nil .indexWidth = .fromHighlight, .toHighlight, .posHighlight = -1, -1, -1// If there's no space, there's no index.if < 1 {return }if .wrapWidth > 0 && .wrapWidth < { = .wrapWidth }// Initial states.var []bytevar (bool , , string )// Go through each line in the buffer.for , := range .buffer { , , , , , , := decomposeText(, .dynamicColors, .regions)// Split the line if required.var []string := string()if .wrap && len() > 0 {forlen() > 0 { := runewidth.Truncate(, , "")iflen() == 0 {// We'll extract at least one grapheme cluster. := uniseg.NewGraphemes() .Next() , := .Positions() = [:] }if .wordWrap && len() < len() {// Add any spaces from the next line.if := spacePattern.FindStringIndex([len():]); != nil && [0] == 0 { = [:len()+[1]] }// Can we split before the mandatory end? := boundaryPattern.FindAllStringIndex(, -1)iflen() > 0 {// Yes. Let's split there. = [:[len()-1][1]] } } = append(, ) = [len():] } } else {// No need to split the line. = []string{} }// Create index from split lines.var , , , intfor , := range { := &textViewIndex{Line: ,Pos: ,ForegroundColor: ,BackgroundColor: ,Attributes: ,Region: , }// Shift original position with tags. := len() := := := 0for {// Which tag comes next? := make([][3]int, 0, 3)if < len() { = append(, [3]int{[][0], [][1], 0}) // 0 = color tag. }if < len() { = append(, [3]int{[][0], [][1], 1}) // 1 = region tag. }if < len() { = append(, [3]int{[][0], [][1], 2}) // 2 = escape tag. } := -1 := -1for , := range {if < 0 || [0] < { = [0] = } }// Is the next tag in range?if < 0 || > + {break// No. We're done with this line. }// Advance. := [][0] - - = [][1] := - [][0]if [][2] == 2 { = 1 } += = - ( - - )// Process the tag.switch [][2] {case0:// Process color tags. , , = styleFromTag(, , , []) ++case1:// Process region tags. = [][1] _, = .highlights[string()]// Update highlight range.if { := len(.index)if .fromHighlight < 0 { .fromHighlight, .toHighlight = , .posHighlight = runewidth.StringWidth([:]) } elseif > .toHighlight { .toHighlight = } } ++case2:// Process escape tags. ++ } }// Advance to next line. += + // Append this line. .NextPos = .Width = runewidth.StringWidth() .index = append(.index, ) }// Word-wrapped lines may have trailing whitespace. Remove it.if .wrap && .wordWrap {for , := range .index { := .buffer[.Line][.Pos:.NextPos] := bytes.TrimRightFunc(, unicode.IsSpace)iflen() != len() { := .NextPos .NextPos -= len() - len() .Width -= runewidth.StringWidth(string(.buffer[.Line][.NextPos:])) } } } }// Calculate longest line. .longestLine = 0for , := range .index {if .Width > .longestLine { .longestLine = .Width } }}// Draw draws this primitive onto the screen.func ( *TextView) ( tcell.Screen) {if !.GetVisible() {return } .Box.Draw() .Lock()defer .Unlock()// Get the available size. , , , := .GetInnerRect()if == 0 {return } .pageSize = if .index == nil || != .lastWidth || != .lastHeight { .reindexBuffer() } .lastWidth, .lastHeight = , := .scrollBarVisibility == ScrollBarAlways || (.scrollBarVisibility == ScrollBarAuto && len(.index) > )if { -- // Subtract space for scroll bar. } .reindexBuffer()if .regions { .regionInfos = nil }// Draw scroll bar last.deferfunc() {if ! {return } := len(.index) := int(float64(len(.index)) * (float64(.lineOffset) / float64(len(.index)-)))// Render cursor at the bottom when tracking endif .trackEnd && <= { = + 1 = }for := 0; < ; ++ {RenderScrollBar(, .scrollBarVisibility, +, +, , , , , .hasFocus, .scrollBarColor) } }()// If we don't have an index, there's nothing to draw.if .index == nil {return }// Move to highlighted regions.if .regions && .scrollToHighlights && .fromHighlight >= 0 {// Do we fit the entire height?if .toHighlight-.fromHighlight+1 < {// Yes, let's center the highlights. .lineOffset = (.fromHighlight + .toHighlight - ) / 2 } else {// No, let's move to the start of the highlights. .lineOffset = .fromHighlight }// If the highlight is too far to the right, move it to the middle.if .posHighlight-.columnOffset > 3*/4 { .columnOffset = .posHighlight - /2 }// If the highlight is offscreen on the left, move it onscreen.if .posHighlight-.columnOffset < 0 { .columnOffset = .posHighlight - /4 } } .scrollToHighlights = false// Adjust line offset.if .lineOffset+ > len(.index) { .trackEnd = true }if .trackEnd { .lineOffset = len(.index) - }if .lineOffset < 0 { .lineOffset = 0 }// Adjust column offset.if .align == AlignLeft {if .columnOffset+ > .longestLine { .columnOffset = .longestLine - }if .columnOffset < 0 { .columnOffset = 0 } } elseif .align == AlignRight {if .columnOffset- < -.longestLine { .columnOffset = - .longestLine }if .columnOffset > 0 { .columnOffset = 0 } } else { // AlignCenter. := (.longestLine - ) / 2if > 0 {if .columnOffset > { .columnOffset = }if .columnOffset < - { .columnOffset = - } } else { .columnOffset = 0 } }// Calculate offset to apply vertical alignment := 0iflen(.index) < {if .valign == AlignMiddle { = ( - len(.index)) / 2 } elseif .valign == AlignBottom { = - len(.index) } }// Draw the buffer. := tcell.StyleDefault.Foreground(.textColor).Background(.backgroundColor)for := .lineOffset; < len(.index); ++ {// Are we done?if -.lineOffset >= {break }// Get the text for this line. := .index[] := .buffer[.Line][.Pos:.NextPos] := .ForegroundColor := .BackgroundColor := .Attributes := .Regionif .regions {iflen(.regionInfos) > 0 && !bytes.Equal(.regionInfos[len(.regionInfos)-1].ID, ) {// End last region. .regionInfos[len(.regionInfos)-1].ToX = .regionInfos[len(.regionInfos)-1].ToY = + - .lineOffset }iflen() > 0 && (len(.regionInfos) == 0 || !bytes.Equal(.regionInfos[len(.regionInfos)-1].ID, )) {// Start a new region. .regionInfos = append(.regionInfos, &textViewRegion{ID: ,FromX: ,FromY: + - .lineOffset,ToX: -1,ToY: -1, }) } }// Process tags. , , , , , , := decomposeText(, .dynamicColors, .regions)// Calculate the position of the line.var , intif .align == AlignLeft { = -.columnOffset } elseif .align == AlignRight { = - .Width - .columnOffset } else { // AlignCenter. = (-.Width)/2 - .columnOffset }if < 0 { = - = 0 } := + - .lineOffset + // Print the line.if >= 0 {var , , , , intiterateString(string(), func( rune, []rune, , , , int) bool {// Process tags.for {if < len() && + >= [][0] && + < [][1] {// Get the color. , , = styleFromTag(, , , []) += [][1] - [][0] ++ } elseif < len() && + >= [][0] && + < [][1] {// Get the region.iflen() > 0 && len(.regionInfos) > 0 && bytes.Equal(.regionInfos[len(.regionInfos)-1].ID, ) {// End last region. .regionInfos[len(.regionInfos)-1].ToX = + .regionInfos[len(.regionInfos)-1].ToY = + - .lineOffset } = [][1]iflen() > 0 {// Start new region. .regionInfos = append(.regionInfos, &textViewRegion{ID: ,FromX: + ,FromY: + - .lineOffset,ToX: -1,ToY: -1, }) } += [][1] - [][0] ++ } else {break } }// Skip the second-to-last character of an escape tag.if < len() && + == [][1]-2 { ++ ++ }// Mix the existing style with the new style. , , , := .GetContent(+, ) , , := .Decompose() := overlayStyle(, , , , )// Do we highlight this character?varbooliflen() > 0 {if , := .highlights[string()]; { = true } }if { := .highlightForeground := .highlightBackgroundif == tcell.ColorDefault { = Styles.PrimaryTextColorif == tcell.ColorDefault { = tcell.ColorWhite.TrueColor() } }if == tcell.ColorDefault { , , := .RGB() := colorful.Color{R: float64() / 255, G: float64() / 255, B: float64() / 255} , , := .Hcl()if < .5 { = tcell.ColorWhite.TrueColor() } else { = tcell.ColorBlack.TrueColor() } } = .Foreground().Background() }// Skip to the right.if !.wrap && < { += returnfalse }// Stop at the right border.if + > {returntrue }// Draw the character.for := - 1; >= 0; -- {if == 0 { .SetContent(++, , , , ) } else { .SetContent(++, , ' ', nil, ) } }// Advance. += returnfalse }) } }// If this view is not scrollable, we'll purge the buffer of lines that have // scrolled out of view.if !.scrollable && .lineOffset > 0 {if .lineOffset >= len(.index) { .buffer = nil } else { .buffer = .buffer[.index[.lineOffset].Line:] } .index = nil .lineOffset = 0 }}// InputHandler returns the handler for this primitive.func ( *TextView) () func( *tcell.EventKey, func( Primitive)) {return .WrapInputHandler(func( *tcell.EventKey, func( Primitive)) { := .Key()ifHitShortcut(, Keys.Cancel, Keys.Select, Keys.Select2, Keys.MovePreviousField, Keys.MoveNextField) {if .done != nil { .done() }return } .Lock()defer .Unlock()if !.scrollable {return }ifHitShortcut(, Keys.MoveFirst, Keys.MoveFirst2) { .trackEnd = false .lineOffset = 0 .columnOffset = 0 } elseifHitShortcut(, Keys.MoveLast, Keys.MoveLast2) { .trackEnd = true .columnOffset = 0 } elseifHitShortcut(, Keys.MoveUp, Keys.MoveUp2) { .trackEnd = false .lineOffset-- } elseifHitShortcut(, Keys.MoveDown, Keys.MoveDown2) { .lineOffset++ } elseifHitShortcut(, Keys.MoveLeft, Keys.MoveLeft2) { .columnOffset-- } elseifHitShortcut(, Keys.MoveRight, Keys.MoveRight2) { .columnOffset++ } elseifHitShortcut(, Keys.MovePreviousPage) { .trackEnd = false .lineOffset -= .pageSize } elseifHitShortcut(, Keys.MoveNextPage) { .lineOffset += .pageSize } })}// MouseHandler returns the mouse handler for this primitive.func ( *TextView) () func( MouseAction, *tcell.EventMouse, func( Primitive)) ( bool, Primitive) {return .WrapMouseHandler(func( MouseAction, *tcell.EventMouse, func( Primitive)) ( bool, Primitive) { , := .Position()if !.InRect(, ) {returnfalse, nil }switch {caseMouseLeftClick:if .regions {// Find a region to highlight.for , := range .regionInfos {if == .FromY && < .FromX || == .ToY && >= .ToX || .FromY >= 0 && < .FromY || .ToY >= 0 && > .ToY {continue }if .clicked != nil { .clicked(string(.ID)) }break } } = true ()caseMouseScrollUp:if .scrollable { .trackEnd = false .lineOffset-- = true }caseMouseScrollDown:if .scrollable { .lineOffset++ = true } }return })}
The pages are generated with Goldsv0.8.2. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.