2019-06-16 10:14:30 +03:00
|
|
|
|
package m
|
|
|
|
|
|
|
|
|
|
import (
|
2019-10-27 23:40:30 +03:00
|
|
|
|
"fmt"
|
2019-10-30 22:29:29 +03:00
|
|
|
|
"os"
|
2019-06-16 10:14:30 +03:00
|
|
|
|
"regexp"
|
2019-10-28 22:18:07 +03:00
|
|
|
|
"strconv"
|
2019-06-16 10:14:30 +03:00
|
|
|
|
"strings"
|
2021-04-25 21:40:38 +03:00
|
|
|
|
"unicode"
|
2019-06-16 10:14:30 +03:00
|
|
|
|
|
2020-03-25 10:55:35 +03:00
|
|
|
|
log "github.com/sirupsen/logrus"
|
2021-04-15 16:16:06 +03:00
|
|
|
|
"github.com/walles/moar/twin"
|
2019-06-16 10:14:30 +03:00
|
|
|
|
)
|
|
|
|
|
|
2019-06-17 22:39:57 +03:00
|
|
|
|
const _TabSize = 4
|
|
|
|
|
|
2021-04-25 00:15:53 +03:00
|
|
|
|
const BACKSPACE = '\b'
|
|
|
|
|
|
2021-04-15 16:16:06 +03:00
|
|
|
|
var manPageBold = twin.StyleDefault.WithAttr(twin.AttrBold)
|
|
|
|
|
var manPageUnderline = twin.StyleDefault.WithAttr(twin.AttrUnderline)
|
2019-10-30 21:52:49 +03:00
|
|
|
|
|
2021-04-14 22:41:56 +03:00
|
|
|
|
// ESC[...m: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR
|
|
|
|
|
var sgrSequencePattern = regexp.MustCompile("\x1b\\[([0-9;]*m)")
|
|
|
|
|
|
Parse lines on demand and only once
This improves line processing performance by 40%.
Fixes #36.
diff --git m/ansiTokenizer.go m/ansiTokenizer.go
index d991e23..056a227 100644
--- m/ansiTokenizer.go
+++ m/ansiTokenizer.go
@@ -23,6 +23,44 @@ type Token struct {
Style tcell.Style
}
+// A Line represents a line of text that can / will be paged
+type Line struct {
+ raw *string
+ plain *string
+ tokens []Token
+}
+
+// NewLine creates a new Line from a (potentially ANSI / man page formatted) string
+func NewLine(raw string) *Line {
+ return &Line{
+ raw: &raw,
+ plain: nil,
+ tokens: nil,
+ }
+}
+
+// Tokens returns a representation of the string split into styled tokens
+func (line *Line) Tokens() []Token {
+ line.parse()
+ return line.tokens
+}
+
+// Plain returns a plain text representation of the initial string
+func (line *Line) Plain() string {
+ line.parse()
+ return *line.plain
+}
+
+func (line *Line) parse() {
+ if line.raw == nil {
+ // Already done
+ return
+ }
+
+ line.tokens, line.plain = tokensFromString(*line.raw)
+ line.raw = nil
+}
+
// SetManPageFormatFromEnv parses LESS_TERMCAP_xx environment variables and
// adapts the moar output accordingly.
func SetManPageFormatFromEnv() {
diff --git m/pager.go m/pager.go
index 412e05b..98efa9a 100644
--- m/pager.go
+++ m/pager.go
@@ -111,7 +111,7 @@ func NewPager(r *Reader) *Pager {
}
}
-func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line string) {
+func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line *Line) {
screenWidth, _ := p.screen.Size()
prefixLength := 0
@@ -138,7 +138,7 @@ func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNum
func createScreenLine(
stringIndexAtColumnZero int,
screenColumnsCount int,
- line string,
+ line *Line,
search *regexp.Regexp,
) []Token {
var returnMe []Token
@@ -152,14 +152,14 @@ func createScreenLine(
searchHitDelta = -1
}
- tokens, plainString := tokensFromString(line)
- if stringIndexAtColumnZero >= len(tokens) {
+ if stringIndexAtColumnZero >= len(line.Tokens()) {
// Nothing (more) to display, never mind
return returnMe
}
- matchRanges := getMatchRanges(plainString, search)
- for _, token := range tokens[stringIndexAtColumnZero:] {
+ plain := line.Plain()
+ matchRanges := getMatchRanges(&plain, search)
+ for _, token := range line.Tokens()[stringIndexAtColumnZero:] {
if len(returnMe) >= screenColumnsCount {
// We are trying to add a character to the right of the screen.
// Indicate that this line continues to the right.
@@ -232,7 +232,8 @@ func (p *Pager) _AddLines(spinner string) {
// This happens when we're done
eofSpinner = "---"
}
- p._AddLine(nil, 0, screenLineNumber, _EofMarkerFormat+eofSpinner)
+ spinnerLine := NewLine(_EofMarkerFormat + eofSpinner)
+ p._AddLine(nil, 0, screenLineNumber, spinnerLine)
switch p.mode {
case _Searching:
@@ -329,8 +330,8 @@ func (p *Pager) _FindFirstHitLineOneBased(firstLineOneBased int, backwards bool)
return nil
}
- _, lineText := tokensFromString(*line)
- if p.searchPattern.MatchString(*lineText) {
+ lineText := line.Plain()
+ if p.searchPattern.MatchString(lineText) {
return &lineNumber
}
diff --git m/pager_test.go m/pager_test.go
index 65fa3c2..ce0f79b 100644
--- m/pager_test.go
+++ m/pager_test.go
@@ -265,13 +265,15 @@ func assertTokenRangesEqual(t *testing.T, actual []Token, expected []Token) {
}
func TestCreateScreenLineBase(t *testing.T) {
- line := createScreenLine(0, 3, "", nil)
- assert.Assert(t, len(line) == 0)
+ line := NewLine("")
+ screenLine := createScreenLine(0, 3, line, nil)
+ assert.Assert(t, len(screenLine) == 0)
}
func TestCreateScreenLineOverflowRight(t *testing.T) {
- line := createScreenLine(0, 3, "012345", nil)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("012345")
+ screenLine := createScreenLine(0, 3, line, nil)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('0', tcell.StyleDefault),
createExpectedCell('1', tcell.StyleDefault),
createExpectedCell('>', tcell.StyleDefault.Reverse(true)),
@@ -279,8 +281,9 @@ func TestCreateScreenLineOverflowRight(t *testing.T) {
}
func TestCreateScreenLineUnderflowLeft(t *testing.T) {
- line := createScreenLine(1, 3, "012", nil)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("012")
+ screenLine := createScreenLine(1, 3, line, nil)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('1', tcell.StyleDefault),
createExpectedCell('2', tcell.StyleDefault),
@@ -293,8 +296,9 @@ func TestCreateScreenLineSearchHit(t *testing.T) {
panic(err)
}
- line := createScreenLine(0, 3, "abc", pattern)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("abc")
+ screenLine := createScreenLine(0, 3, line, pattern)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('a', tcell.StyleDefault),
createExpectedCell('b', tcell.StyleDefault.Reverse(true)),
createExpectedCell('c', tcell.StyleDefault),
@@ -307,8 +311,9 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) {
panic(err)
}
- line := createScreenLine(0, 3, "åäö", pattern)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("åäö")
+ screenLine := createScreenLine(0, 3, line, pattern)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
createExpectedCell('ö', tcell.StyleDefault),
@@ -318,9 +323,10 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) {
func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) {
pattern := regexp.MustCompile("ä")
- line := createScreenLine(1, 4, "ååäö", pattern)
+ line := NewLine("ååäö")
+ screenLine := createScreenLine(1, 4, line, pattern)
- assertTokenRangesEqual(t, line, []Token{
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
@@ -331,9 +337,10 @@ func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) {
func TestCreateScreenLineScrolled2Utf8SearchHit(t *testing.T) {
pattern := regexp.MustCompile("ä")
- line := createScreenLine(2, 4, "åååäö", pattern)
+ line := NewLine("åååäö")
+ screenLine := createScreenLine(2, 4, line, pattern)
- assertTokenRangesEqual(t, line, []Token{
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
diff --git m/reader.go m/reader.go
index 418c4c5..d47b710 100644
--- m/reader.go
+++ m/reader.go
@@ -29,7 +29,7 @@ import (
//
// This package provides query methods for the struct, no peeking!!
type Reader struct {
- lines []string
+ lines []*Line
name *string
lock *sync.Mutex
err error
@@ -41,7 +41,7 @@ type Reader struct {
// Lines contains a number of lines from the reader, plus metadata
type Lines struct {
- lines []string
+ lines []*Line
// One-based line number of the first line returned
firstLineOneBased int
@@ -136,7 +136,7 @@ func readStream(stream io.Reader, reader *Reader, fromFilter *exec.Cmd) {
}
reader.lock.Lock()
- reader.lines = append(reader.lines, string(completeLine))
+ reader.lines = append(reader.lines, NewLine(string(completeLine)))
reader.lock.Unlock()
// This is how to do a non-blocking write to a channel:
@@ -172,7 +172,7 @@ func NewReaderFromStream(name string, reader io.Reader) *Reader {
// If fromFilter is not nil this method will wait() for it,
// and effectively takes over ownership for it.
func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader {
- var lines []string
+ var lines []*Line
var lock = &sync.Mutex{}
done := make(chan bool, 1)
@@ -201,9 +201,11 @@ func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader {
// Moar in the bottom left corner of the screen.
func NewReaderFromText(name string, text string) *Reader {
noExternalNewlines := strings.Trim(text, "\n")
- lines := []string{}
+ lines := []*Line{}
if len(noExternalNewlines) > 0 {
- lines = strings.Split(noExternalNewlines, "\n")
+ for _, line := range strings.Split(noExternalNewlines, "\n") {
+ lines = append(lines, NewLine(line))
+ }
}
done := make(chan bool, 1)
done <- true
@@ -380,7 +382,7 @@ func (r *Reader) GetLineCount() int {
}
// GetLine gets a line. If the requested line number is out of bounds, nil is returned.
-func (r *Reader) GetLine(lineNumberOneBased int) *string {
+func (r *Reader) GetLine(lineNumberOneBased int) *Line {
r.lock.Lock()
defer r.lock.Unlock()
@@ -390,7 +392,7 @@ func (r *Reader) GetLine(lineNumberOneBased int) *string {
if lineNumberOneBased > len(r.lines) {
return nil
}
- return &r.lines[lineNumberOneBased-1]
+ return r.lines[lineNumberOneBased-1]
}
// GetLines gets the indicated lines from the input
diff --git m/reader_test.go m/reader_test.go
index 2ba7326..0e2aed2 100644
--- m/reader_test.go
+++ m/reader_test.go
@@ -158,8 +158,8 @@ func TestGetLongLine(t *testing.T) {
assert.Equal(t, len(lines.lines), 1)
line := lines.lines[0]
- assert.Assert(t, strings.HasPrefix(line, "1 2 3 4"), "<%s>", line)
- assert.Assert(t, strings.HasSuffix(line, "0123456789"), line)
+ assert.Assert(t, strings.HasPrefix(line.Plain(), "1 2 3 4"), "<%s>", line)
+ assert.Assert(t, strings.HasSuffix(line.Plain(), "0123456789"), line)
stat, err := os.Stat(file)
if err != nil {
@@ -168,7 +168,7 @@ func TestGetLongLine(t *testing.T) {
fileSize := stat.Size()
// The "+1" is because the Reader strips off the ending linefeed
- assert.Equal(t, len(line)+1, int(fileSize))
+ assert.Equal(t, len(line.Plain())+1, int(fileSize))
}
func getReaderWithLineCount(totalLines int) *Reader {
@@ -219,7 +219,7 @@ func testCompressedFile(t *testing.T, filename string) {
panic(err)
}
- assert.Equal(t, reader.GetLines(1, 5).lines[0], "This is a compressed file", "%s", filename)
+ assert.Equal(t, reader.GetLines(1, 5).lines[0].Plain(), "This is a compressed file", "%s", filename)
}
func TestCompressedFiles(t *testing.T) {
Change-Id: Id8671001ec7c1038e2df0b87a45d346a1f1dd663
2021-01-11 12:42:34 +03:00
|
|
|
|
// A Line represents a line of text that can / will be paged
|
|
|
|
|
type Line struct {
|
2021-04-15 16:16:06 +03:00
|
|
|
|
raw *string
|
|
|
|
|
plain *string
|
Parse lines on demand and only once
This improves line processing performance by 40%.
Fixes #36.
diff --git m/ansiTokenizer.go m/ansiTokenizer.go
index d991e23..056a227 100644
--- m/ansiTokenizer.go
+++ m/ansiTokenizer.go
@@ -23,6 +23,44 @@ type Token struct {
Style tcell.Style
}
+// A Line represents a line of text that can / will be paged
+type Line struct {
+ raw *string
+ plain *string
+ tokens []Token
+}
+
+// NewLine creates a new Line from a (potentially ANSI / man page formatted) string
+func NewLine(raw string) *Line {
+ return &Line{
+ raw: &raw,
+ plain: nil,
+ tokens: nil,
+ }
+}
+
+// Tokens returns a representation of the string split into styled tokens
+func (line *Line) Tokens() []Token {
+ line.parse()
+ return line.tokens
+}
+
+// Plain returns a plain text representation of the initial string
+func (line *Line) Plain() string {
+ line.parse()
+ return *line.plain
+}
+
+func (line *Line) parse() {
+ if line.raw == nil {
+ // Already done
+ return
+ }
+
+ line.tokens, line.plain = tokensFromString(*line.raw)
+ line.raw = nil
+}
+
// SetManPageFormatFromEnv parses LESS_TERMCAP_xx environment variables and
// adapts the moar output accordingly.
func SetManPageFormatFromEnv() {
diff --git m/pager.go m/pager.go
index 412e05b..98efa9a 100644
--- m/pager.go
+++ m/pager.go
@@ -111,7 +111,7 @@ func NewPager(r *Reader) *Pager {
}
}
-func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line string) {
+func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line *Line) {
screenWidth, _ := p.screen.Size()
prefixLength := 0
@@ -138,7 +138,7 @@ func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNum
func createScreenLine(
stringIndexAtColumnZero int,
screenColumnsCount int,
- line string,
+ line *Line,
search *regexp.Regexp,
) []Token {
var returnMe []Token
@@ -152,14 +152,14 @@ func createScreenLine(
searchHitDelta = -1
}
- tokens, plainString := tokensFromString(line)
- if stringIndexAtColumnZero >= len(tokens) {
+ if stringIndexAtColumnZero >= len(line.Tokens()) {
// Nothing (more) to display, never mind
return returnMe
}
- matchRanges := getMatchRanges(plainString, search)
- for _, token := range tokens[stringIndexAtColumnZero:] {
+ plain := line.Plain()
+ matchRanges := getMatchRanges(&plain, search)
+ for _, token := range line.Tokens()[stringIndexAtColumnZero:] {
if len(returnMe) >= screenColumnsCount {
// We are trying to add a character to the right of the screen.
// Indicate that this line continues to the right.
@@ -232,7 +232,8 @@ func (p *Pager) _AddLines(spinner string) {
// This happens when we're done
eofSpinner = "---"
}
- p._AddLine(nil, 0, screenLineNumber, _EofMarkerFormat+eofSpinner)
+ spinnerLine := NewLine(_EofMarkerFormat + eofSpinner)
+ p._AddLine(nil, 0, screenLineNumber, spinnerLine)
switch p.mode {
case _Searching:
@@ -329,8 +330,8 @@ func (p *Pager) _FindFirstHitLineOneBased(firstLineOneBased int, backwards bool)
return nil
}
- _, lineText := tokensFromString(*line)
- if p.searchPattern.MatchString(*lineText) {
+ lineText := line.Plain()
+ if p.searchPattern.MatchString(lineText) {
return &lineNumber
}
diff --git m/pager_test.go m/pager_test.go
index 65fa3c2..ce0f79b 100644
--- m/pager_test.go
+++ m/pager_test.go
@@ -265,13 +265,15 @@ func assertTokenRangesEqual(t *testing.T, actual []Token, expected []Token) {
}
func TestCreateScreenLineBase(t *testing.T) {
- line := createScreenLine(0, 3, "", nil)
- assert.Assert(t, len(line) == 0)
+ line := NewLine("")
+ screenLine := createScreenLine(0, 3, line, nil)
+ assert.Assert(t, len(screenLine) == 0)
}
func TestCreateScreenLineOverflowRight(t *testing.T) {
- line := createScreenLine(0, 3, "012345", nil)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("012345")
+ screenLine := createScreenLine(0, 3, line, nil)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('0', tcell.StyleDefault),
createExpectedCell('1', tcell.StyleDefault),
createExpectedCell('>', tcell.StyleDefault.Reverse(true)),
@@ -279,8 +281,9 @@ func TestCreateScreenLineOverflowRight(t *testing.T) {
}
func TestCreateScreenLineUnderflowLeft(t *testing.T) {
- line := createScreenLine(1, 3, "012", nil)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("012")
+ screenLine := createScreenLine(1, 3, line, nil)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('1', tcell.StyleDefault),
createExpectedCell('2', tcell.StyleDefault),
@@ -293,8 +296,9 @@ func TestCreateScreenLineSearchHit(t *testing.T) {
panic(err)
}
- line := createScreenLine(0, 3, "abc", pattern)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("abc")
+ screenLine := createScreenLine(0, 3, line, pattern)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('a', tcell.StyleDefault),
createExpectedCell('b', tcell.StyleDefault.Reverse(true)),
createExpectedCell('c', tcell.StyleDefault),
@@ -307,8 +311,9 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) {
panic(err)
}
- line := createScreenLine(0, 3, "åäö", pattern)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("åäö")
+ screenLine := createScreenLine(0, 3, line, pattern)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
createExpectedCell('ö', tcell.StyleDefault),
@@ -318,9 +323,10 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) {
func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) {
pattern := regexp.MustCompile("ä")
- line := createScreenLine(1, 4, "ååäö", pattern)
+ line := NewLine("ååäö")
+ screenLine := createScreenLine(1, 4, line, pattern)
- assertTokenRangesEqual(t, line, []Token{
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
@@ -331,9 +337,10 @@ func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) {
func TestCreateScreenLineScrolled2Utf8SearchHit(t *testing.T) {
pattern := regexp.MustCompile("ä")
- line := createScreenLine(2, 4, "åååäö", pattern)
+ line := NewLine("åååäö")
+ screenLine := createScreenLine(2, 4, line, pattern)
- assertTokenRangesEqual(t, line, []Token{
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
diff --git m/reader.go m/reader.go
index 418c4c5..d47b710 100644
--- m/reader.go
+++ m/reader.go
@@ -29,7 +29,7 @@ import (
//
// This package provides query methods for the struct, no peeking!!
type Reader struct {
- lines []string
+ lines []*Line
name *string
lock *sync.Mutex
err error
@@ -41,7 +41,7 @@ type Reader struct {
// Lines contains a number of lines from the reader, plus metadata
type Lines struct {
- lines []string
+ lines []*Line
// One-based line number of the first line returned
firstLineOneBased int
@@ -136,7 +136,7 @@ func readStream(stream io.Reader, reader *Reader, fromFilter *exec.Cmd) {
}
reader.lock.Lock()
- reader.lines = append(reader.lines, string(completeLine))
+ reader.lines = append(reader.lines, NewLine(string(completeLine)))
reader.lock.Unlock()
// This is how to do a non-blocking write to a channel:
@@ -172,7 +172,7 @@ func NewReaderFromStream(name string, reader io.Reader) *Reader {
// If fromFilter is not nil this method will wait() for it,
// and effectively takes over ownership for it.
func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader {
- var lines []string
+ var lines []*Line
var lock = &sync.Mutex{}
done := make(chan bool, 1)
@@ -201,9 +201,11 @@ func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader {
// Moar in the bottom left corner of the screen.
func NewReaderFromText(name string, text string) *Reader {
noExternalNewlines := strings.Trim(text, "\n")
- lines := []string{}
+ lines := []*Line{}
if len(noExternalNewlines) > 0 {
- lines = strings.Split(noExternalNewlines, "\n")
+ for _, line := range strings.Split(noExternalNewlines, "\n") {
+ lines = append(lines, NewLine(line))
+ }
}
done := make(chan bool, 1)
done <- true
@@ -380,7 +382,7 @@ func (r *Reader) GetLineCount() int {
}
// GetLine gets a line. If the requested line number is out of bounds, nil is returned.
-func (r *Reader) GetLine(lineNumberOneBased int) *string {
+func (r *Reader) GetLine(lineNumberOneBased int) *Line {
r.lock.Lock()
defer r.lock.Unlock()
@@ -390,7 +392,7 @@ func (r *Reader) GetLine(lineNumberOneBased int) *string {
if lineNumberOneBased > len(r.lines) {
return nil
}
- return &r.lines[lineNumberOneBased-1]
+ return r.lines[lineNumberOneBased-1]
}
// GetLines gets the indicated lines from the input
diff --git m/reader_test.go m/reader_test.go
index 2ba7326..0e2aed2 100644
--- m/reader_test.go
+++ m/reader_test.go
@@ -158,8 +158,8 @@ func TestGetLongLine(t *testing.T) {
assert.Equal(t, len(lines.lines), 1)
line := lines.lines[0]
- assert.Assert(t, strings.HasPrefix(line, "1 2 3 4"), "<%s>", line)
- assert.Assert(t, strings.HasSuffix(line, "0123456789"), line)
+ assert.Assert(t, strings.HasPrefix(line.Plain(), "1 2 3 4"), "<%s>", line)
+ assert.Assert(t, strings.HasSuffix(line.Plain(), "0123456789"), line)
stat, err := os.Stat(file)
if err != nil {
@@ -168,7 +168,7 @@ func TestGetLongLine(t *testing.T) {
fileSize := stat.Size()
// The "+1" is because the Reader strips off the ending linefeed
- assert.Equal(t, len(line)+1, int(fileSize))
+ assert.Equal(t, len(line.Plain())+1, int(fileSize))
}
func getReaderWithLineCount(totalLines int) *Reader {
@@ -219,7 +219,7 @@ func testCompressedFile(t *testing.T, filename string) {
panic(err)
}
- assert.Equal(t, reader.GetLines(1, 5).lines[0], "This is a compressed file", "%s", filename)
+ assert.Equal(t, reader.GetLines(1, 5).lines[0].Plain(), "This is a compressed file", "%s", filename)
}
func TestCompressedFiles(t *testing.T) {
Change-Id: Id8671001ec7c1038e2df0b87a45d346a1f1dd663
2021-01-11 12:42:34 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewLine creates a new Line from a (potentially ANSI / man page formatted) string
|
|
|
|
|
func NewLine(raw string) *Line {
|
|
|
|
|
return &Line{
|
2021-04-15 16:16:06 +03:00
|
|
|
|
raw: &raw,
|
|
|
|
|
plain: nil,
|
Parse lines on demand and only once
This improves line processing performance by 40%.
Fixes #36.
diff --git m/ansiTokenizer.go m/ansiTokenizer.go
index d991e23..056a227 100644
--- m/ansiTokenizer.go
+++ m/ansiTokenizer.go
@@ -23,6 +23,44 @@ type Token struct {
Style tcell.Style
}
+// A Line represents a line of text that can / will be paged
+type Line struct {
+ raw *string
+ plain *string
+ tokens []Token
+}
+
+// NewLine creates a new Line from a (potentially ANSI / man page formatted) string
+func NewLine(raw string) *Line {
+ return &Line{
+ raw: &raw,
+ plain: nil,
+ tokens: nil,
+ }
+}
+
+// Tokens returns a representation of the string split into styled tokens
+func (line *Line) Tokens() []Token {
+ line.parse()
+ return line.tokens
+}
+
+// Plain returns a plain text representation of the initial string
+func (line *Line) Plain() string {
+ line.parse()
+ return *line.plain
+}
+
+func (line *Line) parse() {
+ if line.raw == nil {
+ // Already done
+ return
+ }
+
+ line.tokens, line.plain = tokensFromString(*line.raw)
+ line.raw = nil
+}
+
// SetManPageFormatFromEnv parses LESS_TERMCAP_xx environment variables and
// adapts the moar output accordingly.
func SetManPageFormatFromEnv() {
diff --git m/pager.go m/pager.go
index 412e05b..98efa9a 100644
--- m/pager.go
+++ m/pager.go
@@ -111,7 +111,7 @@ func NewPager(r *Reader) *Pager {
}
}
-func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line string) {
+func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line *Line) {
screenWidth, _ := p.screen.Size()
prefixLength := 0
@@ -138,7 +138,7 @@ func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNum
func createScreenLine(
stringIndexAtColumnZero int,
screenColumnsCount int,
- line string,
+ line *Line,
search *regexp.Regexp,
) []Token {
var returnMe []Token
@@ -152,14 +152,14 @@ func createScreenLine(
searchHitDelta = -1
}
- tokens, plainString := tokensFromString(line)
- if stringIndexAtColumnZero >= len(tokens) {
+ if stringIndexAtColumnZero >= len(line.Tokens()) {
// Nothing (more) to display, never mind
return returnMe
}
- matchRanges := getMatchRanges(plainString, search)
- for _, token := range tokens[stringIndexAtColumnZero:] {
+ plain := line.Plain()
+ matchRanges := getMatchRanges(&plain, search)
+ for _, token := range line.Tokens()[stringIndexAtColumnZero:] {
if len(returnMe) >= screenColumnsCount {
// We are trying to add a character to the right of the screen.
// Indicate that this line continues to the right.
@@ -232,7 +232,8 @@ func (p *Pager) _AddLines(spinner string) {
// This happens when we're done
eofSpinner = "---"
}
- p._AddLine(nil, 0, screenLineNumber, _EofMarkerFormat+eofSpinner)
+ spinnerLine := NewLine(_EofMarkerFormat + eofSpinner)
+ p._AddLine(nil, 0, screenLineNumber, spinnerLine)
switch p.mode {
case _Searching:
@@ -329,8 +330,8 @@ func (p *Pager) _FindFirstHitLineOneBased(firstLineOneBased int, backwards bool)
return nil
}
- _, lineText := tokensFromString(*line)
- if p.searchPattern.MatchString(*lineText) {
+ lineText := line.Plain()
+ if p.searchPattern.MatchString(lineText) {
return &lineNumber
}
diff --git m/pager_test.go m/pager_test.go
index 65fa3c2..ce0f79b 100644
--- m/pager_test.go
+++ m/pager_test.go
@@ -265,13 +265,15 @@ func assertTokenRangesEqual(t *testing.T, actual []Token, expected []Token) {
}
func TestCreateScreenLineBase(t *testing.T) {
- line := createScreenLine(0, 3, "", nil)
- assert.Assert(t, len(line) == 0)
+ line := NewLine("")
+ screenLine := createScreenLine(0, 3, line, nil)
+ assert.Assert(t, len(screenLine) == 0)
}
func TestCreateScreenLineOverflowRight(t *testing.T) {
- line := createScreenLine(0, 3, "012345", nil)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("012345")
+ screenLine := createScreenLine(0, 3, line, nil)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('0', tcell.StyleDefault),
createExpectedCell('1', tcell.StyleDefault),
createExpectedCell('>', tcell.StyleDefault.Reverse(true)),
@@ -279,8 +281,9 @@ func TestCreateScreenLineOverflowRight(t *testing.T) {
}
func TestCreateScreenLineUnderflowLeft(t *testing.T) {
- line := createScreenLine(1, 3, "012", nil)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("012")
+ screenLine := createScreenLine(1, 3, line, nil)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('1', tcell.StyleDefault),
createExpectedCell('2', tcell.StyleDefault),
@@ -293,8 +296,9 @@ func TestCreateScreenLineSearchHit(t *testing.T) {
panic(err)
}
- line := createScreenLine(0, 3, "abc", pattern)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("abc")
+ screenLine := createScreenLine(0, 3, line, pattern)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('a', tcell.StyleDefault),
createExpectedCell('b', tcell.StyleDefault.Reverse(true)),
createExpectedCell('c', tcell.StyleDefault),
@@ -307,8 +311,9 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) {
panic(err)
}
- line := createScreenLine(0, 3, "åäö", pattern)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("åäö")
+ screenLine := createScreenLine(0, 3, line, pattern)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
createExpectedCell('ö', tcell.StyleDefault),
@@ -318,9 +323,10 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) {
func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) {
pattern := regexp.MustCompile("ä")
- line := createScreenLine(1, 4, "ååäö", pattern)
+ line := NewLine("ååäö")
+ screenLine := createScreenLine(1, 4, line, pattern)
- assertTokenRangesEqual(t, line, []Token{
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
@@ -331,9 +337,10 @@ func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) {
func TestCreateScreenLineScrolled2Utf8SearchHit(t *testing.T) {
pattern := regexp.MustCompile("ä")
- line := createScreenLine(2, 4, "åååäö", pattern)
+ line := NewLine("åååäö")
+ screenLine := createScreenLine(2, 4, line, pattern)
- assertTokenRangesEqual(t, line, []Token{
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
diff --git m/reader.go m/reader.go
index 418c4c5..d47b710 100644
--- m/reader.go
+++ m/reader.go
@@ -29,7 +29,7 @@ import (
//
// This package provides query methods for the struct, no peeking!!
type Reader struct {
- lines []string
+ lines []*Line
name *string
lock *sync.Mutex
err error
@@ -41,7 +41,7 @@ type Reader struct {
// Lines contains a number of lines from the reader, plus metadata
type Lines struct {
- lines []string
+ lines []*Line
// One-based line number of the first line returned
firstLineOneBased int
@@ -136,7 +136,7 @@ func readStream(stream io.Reader, reader *Reader, fromFilter *exec.Cmd) {
}
reader.lock.Lock()
- reader.lines = append(reader.lines, string(completeLine))
+ reader.lines = append(reader.lines, NewLine(string(completeLine)))
reader.lock.Unlock()
// This is how to do a non-blocking write to a channel:
@@ -172,7 +172,7 @@ func NewReaderFromStream(name string, reader io.Reader) *Reader {
// If fromFilter is not nil this method will wait() for it,
// and effectively takes over ownership for it.
func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader {
- var lines []string
+ var lines []*Line
var lock = &sync.Mutex{}
done := make(chan bool, 1)
@@ -201,9 +201,11 @@ func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader {
// Moar in the bottom left corner of the screen.
func NewReaderFromText(name string, text string) *Reader {
noExternalNewlines := strings.Trim(text, "\n")
- lines := []string{}
+ lines := []*Line{}
if len(noExternalNewlines) > 0 {
- lines = strings.Split(noExternalNewlines, "\n")
+ for _, line := range strings.Split(noExternalNewlines, "\n") {
+ lines = append(lines, NewLine(line))
+ }
}
done := make(chan bool, 1)
done <- true
@@ -380,7 +382,7 @@ func (r *Reader) GetLineCount() int {
}
// GetLine gets a line. If the requested line number is out of bounds, nil is returned.
-func (r *Reader) GetLine(lineNumberOneBased int) *string {
+func (r *Reader) GetLine(lineNumberOneBased int) *Line {
r.lock.Lock()
defer r.lock.Unlock()
@@ -390,7 +392,7 @@ func (r *Reader) GetLine(lineNumberOneBased int) *string {
if lineNumberOneBased > len(r.lines) {
return nil
}
- return &r.lines[lineNumberOneBased-1]
+ return r.lines[lineNumberOneBased-1]
}
// GetLines gets the indicated lines from the input
diff --git m/reader_test.go m/reader_test.go
index 2ba7326..0e2aed2 100644
--- m/reader_test.go
+++ m/reader_test.go
@@ -158,8 +158,8 @@ func TestGetLongLine(t *testing.T) {
assert.Equal(t, len(lines.lines), 1)
line := lines.lines[0]
- assert.Assert(t, strings.HasPrefix(line, "1 2 3 4"), "<%s>", line)
- assert.Assert(t, strings.HasSuffix(line, "0123456789"), line)
+ assert.Assert(t, strings.HasPrefix(line.Plain(), "1 2 3 4"), "<%s>", line)
+ assert.Assert(t, strings.HasSuffix(line.Plain(), "0123456789"), line)
stat, err := os.Stat(file)
if err != nil {
@@ -168,7 +168,7 @@ func TestGetLongLine(t *testing.T) {
fileSize := stat.Size()
// The "+1" is because the Reader strips off the ending linefeed
- assert.Equal(t, len(line)+1, int(fileSize))
+ assert.Equal(t, len(line.Plain())+1, int(fileSize))
}
func getReaderWithLineCount(totalLines int) *Reader {
@@ -219,7 +219,7 @@ func testCompressedFile(t *testing.T, filename string) {
panic(err)
}
- assert.Equal(t, reader.GetLines(1, 5).lines[0], "This is a compressed file", "%s", filename)
+ assert.Equal(t, reader.GetLines(1, 5).lines[0].Plain(), "This is a compressed file", "%s", filename)
}
func TestCompressedFiles(t *testing.T) {
Change-Id: Id8671001ec7c1038e2df0b87a45d346a1f1dd663
2021-01-11 12:42:34 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tokens returns a representation of the string split into styled tokens
|
2021-04-15 16:16:06 +03:00
|
|
|
|
func (line *Line) Tokens() []twin.Cell {
|
2021-04-24 18:20:09 +03:00
|
|
|
|
return cellsFromString(*line.raw)
|
Parse lines on demand and only once
This improves line processing performance by 40%.
Fixes #36.
diff --git m/ansiTokenizer.go m/ansiTokenizer.go
index d991e23..056a227 100644
--- m/ansiTokenizer.go
+++ m/ansiTokenizer.go
@@ -23,6 +23,44 @@ type Token struct {
Style tcell.Style
}
+// A Line represents a line of text that can / will be paged
+type Line struct {
+ raw *string
+ plain *string
+ tokens []Token
+}
+
+// NewLine creates a new Line from a (potentially ANSI / man page formatted) string
+func NewLine(raw string) *Line {
+ return &Line{
+ raw: &raw,
+ plain: nil,
+ tokens: nil,
+ }
+}
+
+// Tokens returns a representation of the string split into styled tokens
+func (line *Line) Tokens() []Token {
+ line.parse()
+ return line.tokens
+}
+
+// Plain returns a plain text representation of the initial string
+func (line *Line) Plain() string {
+ line.parse()
+ return *line.plain
+}
+
+func (line *Line) parse() {
+ if line.raw == nil {
+ // Already done
+ return
+ }
+
+ line.tokens, line.plain = tokensFromString(*line.raw)
+ line.raw = nil
+}
+
// SetManPageFormatFromEnv parses LESS_TERMCAP_xx environment variables and
// adapts the moar output accordingly.
func SetManPageFormatFromEnv() {
diff --git m/pager.go m/pager.go
index 412e05b..98efa9a 100644
--- m/pager.go
+++ m/pager.go
@@ -111,7 +111,7 @@ func NewPager(r *Reader) *Pager {
}
}
-func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line string) {
+func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line *Line) {
screenWidth, _ := p.screen.Size()
prefixLength := 0
@@ -138,7 +138,7 @@ func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNum
func createScreenLine(
stringIndexAtColumnZero int,
screenColumnsCount int,
- line string,
+ line *Line,
search *regexp.Regexp,
) []Token {
var returnMe []Token
@@ -152,14 +152,14 @@ func createScreenLine(
searchHitDelta = -1
}
- tokens, plainString := tokensFromString(line)
- if stringIndexAtColumnZero >= len(tokens) {
+ if stringIndexAtColumnZero >= len(line.Tokens()) {
// Nothing (more) to display, never mind
return returnMe
}
- matchRanges := getMatchRanges(plainString, search)
- for _, token := range tokens[stringIndexAtColumnZero:] {
+ plain := line.Plain()
+ matchRanges := getMatchRanges(&plain, search)
+ for _, token := range line.Tokens()[stringIndexAtColumnZero:] {
if len(returnMe) >= screenColumnsCount {
// We are trying to add a character to the right of the screen.
// Indicate that this line continues to the right.
@@ -232,7 +232,8 @@ func (p *Pager) _AddLines(spinner string) {
// This happens when we're done
eofSpinner = "---"
}
- p._AddLine(nil, 0, screenLineNumber, _EofMarkerFormat+eofSpinner)
+ spinnerLine := NewLine(_EofMarkerFormat + eofSpinner)
+ p._AddLine(nil, 0, screenLineNumber, spinnerLine)
switch p.mode {
case _Searching:
@@ -329,8 +330,8 @@ func (p *Pager) _FindFirstHitLineOneBased(firstLineOneBased int, backwards bool)
return nil
}
- _, lineText := tokensFromString(*line)
- if p.searchPattern.MatchString(*lineText) {
+ lineText := line.Plain()
+ if p.searchPattern.MatchString(lineText) {
return &lineNumber
}
diff --git m/pager_test.go m/pager_test.go
index 65fa3c2..ce0f79b 100644
--- m/pager_test.go
+++ m/pager_test.go
@@ -265,13 +265,15 @@ func assertTokenRangesEqual(t *testing.T, actual []Token, expected []Token) {
}
func TestCreateScreenLineBase(t *testing.T) {
- line := createScreenLine(0, 3, "", nil)
- assert.Assert(t, len(line) == 0)
+ line := NewLine("")
+ screenLine := createScreenLine(0, 3, line, nil)
+ assert.Assert(t, len(screenLine) == 0)
}
func TestCreateScreenLineOverflowRight(t *testing.T) {
- line := createScreenLine(0, 3, "012345", nil)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("012345")
+ screenLine := createScreenLine(0, 3, line, nil)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('0', tcell.StyleDefault),
createExpectedCell('1', tcell.StyleDefault),
createExpectedCell('>', tcell.StyleDefault.Reverse(true)),
@@ -279,8 +281,9 @@ func TestCreateScreenLineOverflowRight(t *testing.T) {
}
func TestCreateScreenLineUnderflowLeft(t *testing.T) {
- line := createScreenLine(1, 3, "012", nil)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("012")
+ screenLine := createScreenLine(1, 3, line, nil)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('1', tcell.StyleDefault),
createExpectedCell('2', tcell.StyleDefault),
@@ -293,8 +296,9 @@ func TestCreateScreenLineSearchHit(t *testing.T) {
panic(err)
}
- line := createScreenLine(0, 3, "abc", pattern)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("abc")
+ screenLine := createScreenLine(0, 3, line, pattern)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('a', tcell.StyleDefault),
createExpectedCell('b', tcell.StyleDefault.Reverse(true)),
createExpectedCell('c', tcell.StyleDefault),
@@ -307,8 +311,9 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) {
panic(err)
}
- line := createScreenLine(0, 3, "åäö", pattern)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("åäö")
+ screenLine := createScreenLine(0, 3, line, pattern)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
createExpectedCell('ö', tcell.StyleDefault),
@@ -318,9 +323,10 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) {
func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) {
pattern := regexp.MustCompile("ä")
- line := createScreenLine(1, 4, "ååäö", pattern)
+ line := NewLine("ååäö")
+ screenLine := createScreenLine(1, 4, line, pattern)
- assertTokenRangesEqual(t, line, []Token{
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
@@ -331,9 +337,10 @@ func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) {
func TestCreateScreenLineScrolled2Utf8SearchHit(t *testing.T) {
pattern := regexp.MustCompile("ä")
- line := createScreenLine(2, 4, "åååäö", pattern)
+ line := NewLine("åååäö")
+ screenLine := createScreenLine(2, 4, line, pattern)
- assertTokenRangesEqual(t, line, []Token{
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
diff --git m/reader.go m/reader.go
index 418c4c5..d47b710 100644
--- m/reader.go
+++ m/reader.go
@@ -29,7 +29,7 @@ import (
//
// This package provides query methods for the struct, no peeking!!
type Reader struct {
- lines []string
+ lines []*Line
name *string
lock *sync.Mutex
err error
@@ -41,7 +41,7 @@ type Reader struct {
// Lines contains a number of lines from the reader, plus metadata
type Lines struct {
- lines []string
+ lines []*Line
// One-based line number of the first line returned
firstLineOneBased int
@@ -136,7 +136,7 @@ func readStream(stream io.Reader, reader *Reader, fromFilter *exec.Cmd) {
}
reader.lock.Lock()
- reader.lines = append(reader.lines, string(completeLine))
+ reader.lines = append(reader.lines, NewLine(string(completeLine)))
reader.lock.Unlock()
// This is how to do a non-blocking write to a channel:
@@ -172,7 +172,7 @@ func NewReaderFromStream(name string, reader io.Reader) *Reader {
// If fromFilter is not nil this method will wait() for it,
// and effectively takes over ownership for it.
func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader {
- var lines []string
+ var lines []*Line
var lock = &sync.Mutex{}
done := make(chan bool, 1)
@@ -201,9 +201,11 @@ func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader {
// Moar in the bottom left corner of the screen.
func NewReaderFromText(name string, text string) *Reader {
noExternalNewlines := strings.Trim(text, "\n")
- lines := []string{}
+ lines := []*Line{}
if len(noExternalNewlines) > 0 {
- lines = strings.Split(noExternalNewlines, "\n")
+ for _, line := range strings.Split(noExternalNewlines, "\n") {
+ lines = append(lines, NewLine(line))
+ }
}
done := make(chan bool, 1)
done <- true
@@ -380,7 +382,7 @@ func (r *Reader) GetLineCount() int {
}
// GetLine gets a line. If the requested line number is out of bounds, nil is returned.
-func (r *Reader) GetLine(lineNumberOneBased int) *string {
+func (r *Reader) GetLine(lineNumberOneBased int) *Line {
r.lock.Lock()
defer r.lock.Unlock()
@@ -390,7 +392,7 @@ func (r *Reader) GetLine(lineNumberOneBased int) *string {
if lineNumberOneBased > len(r.lines) {
return nil
}
- return &r.lines[lineNumberOneBased-1]
+ return r.lines[lineNumberOneBased-1]
}
// GetLines gets the indicated lines from the input
diff --git m/reader_test.go m/reader_test.go
index 2ba7326..0e2aed2 100644
--- m/reader_test.go
+++ m/reader_test.go
@@ -158,8 +158,8 @@ func TestGetLongLine(t *testing.T) {
assert.Equal(t, len(lines.lines), 1)
line := lines.lines[0]
- assert.Assert(t, strings.HasPrefix(line, "1 2 3 4"), "<%s>", line)
- assert.Assert(t, strings.HasSuffix(line, "0123456789"), line)
+ assert.Assert(t, strings.HasPrefix(line.Plain(), "1 2 3 4"), "<%s>", line)
+ assert.Assert(t, strings.HasSuffix(line.Plain(), "0123456789"), line)
stat, err := os.Stat(file)
if err != nil {
@@ -168,7 +168,7 @@ func TestGetLongLine(t *testing.T) {
fileSize := stat.Size()
// The "+1" is because the Reader strips off the ending linefeed
- assert.Equal(t, len(line)+1, int(fileSize))
+ assert.Equal(t, len(line.Plain())+1, int(fileSize))
}
func getReaderWithLineCount(totalLines int) *Reader {
@@ -219,7 +219,7 @@ func testCompressedFile(t *testing.T, filename string) {
panic(err)
}
- assert.Equal(t, reader.GetLines(1, 5).lines[0], "This is a compressed file", "%s", filename)
+ assert.Equal(t, reader.GetLines(1, 5).lines[0].Plain(), "This is a compressed file", "%s", filename)
}
func TestCompressedFiles(t *testing.T) {
Change-Id: Id8671001ec7c1038e2df0b87a45d346a1f1dd663
2021-01-11 12:42:34 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Plain returns a plain text representation of the initial string
|
|
|
|
|
func (line *Line) Plain() string {
|
2021-04-24 18:20:09 +03:00
|
|
|
|
if line.plain == nil {
|
|
|
|
|
plain := withoutFormatting(*line.raw)
|
|
|
|
|
line.plain = &plain
|
Parse lines on demand and only once
This improves line processing performance by 40%.
Fixes #36.
diff --git m/ansiTokenizer.go m/ansiTokenizer.go
index d991e23..056a227 100644
--- m/ansiTokenizer.go
+++ m/ansiTokenizer.go
@@ -23,6 +23,44 @@ type Token struct {
Style tcell.Style
}
+// A Line represents a line of text that can / will be paged
+type Line struct {
+ raw *string
+ plain *string
+ tokens []Token
+}
+
+// NewLine creates a new Line from a (potentially ANSI / man page formatted) string
+func NewLine(raw string) *Line {
+ return &Line{
+ raw: &raw,
+ plain: nil,
+ tokens: nil,
+ }
+}
+
+// Tokens returns a representation of the string split into styled tokens
+func (line *Line) Tokens() []Token {
+ line.parse()
+ return line.tokens
+}
+
+// Plain returns a plain text representation of the initial string
+func (line *Line) Plain() string {
+ line.parse()
+ return *line.plain
+}
+
+func (line *Line) parse() {
+ if line.raw == nil {
+ // Already done
+ return
+ }
+
+ line.tokens, line.plain = tokensFromString(*line.raw)
+ line.raw = nil
+}
+
// SetManPageFormatFromEnv parses LESS_TERMCAP_xx environment variables and
// adapts the moar output accordingly.
func SetManPageFormatFromEnv() {
diff --git m/pager.go m/pager.go
index 412e05b..98efa9a 100644
--- m/pager.go
+++ m/pager.go
@@ -111,7 +111,7 @@ func NewPager(r *Reader) *Pager {
}
}
-func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line string) {
+func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line *Line) {
screenWidth, _ := p.screen.Size()
prefixLength := 0
@@ -138,7 +138,7 @@ func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNum
func createScreenLine(
stringIndexAtColumnZero int,
screenColumnsCount int,
- line string,
+ line *Line,
search *regexp.Regexp,
) []Token {
var returnMe []Token
@@ -152,14 +152,14 @@ func createScreenLine(
searchHitDelta = -1
}
- tokens, plainString := tokensFromString(line)
- if stringIndexAtColumnZero >= len(tokens) {
+ if stringIndexAtColumnZero >= len(line.Tokens()) {
// Nothing (more) to display, never mind
return returnMe
}
- matchRanges := getMatchRanges(plainString, search)
- for _, token := range tokens[stringIndexAtColumnZero:] {
+ plain := line.Plain()
+ matchRanges := getMatchRanges(&plain, search)
+ for _, token := range line.Tokens()[stringIndexAtColumnZero:] {
if len(returnMe) >= screenColumnsCount {
// We are trying to add a character to the right of the screen.
// Indicate that this line continues to the right.
@@ -232,7 +232,8 @@ func (p *Pager) _AddLines(spinner string) {
// This happens when we're done
eofSpinner = "---"
}
- p._AddLine(nil, 0, screenLineNumber, _EofMarkerFormat+eofSpinner)
+ spinnerLine := NewLine(_EofMarkerFormat + eofSpinner)
+ p._AddLine(nil, 0, screenLineNumber, spinnerLine)
switch p.mode {
case _Searching:
@@ -329,8 +330,8 @@ func (p *Pager) _FindFirstHitLineOneBased(firstLineOneBased int, backwards bool)
return nil
}
- _, lineText := tokensFromString(*line)
- if p.searchPattern.MatchString(*lineText) {
+ lineText := line.Plain()
+ if p.searchPattern.MatchString(lineText) {
return &lineNumber
}
diff --git m/pager_test.go m/pager_test.go
index 65fa3c2..ce0f79b 100644
--- m/pager_test.go
+++ m/pager_test.go
@@ -265,13 +265,15 @@ func assertTokenRangesEqual(t *testing.T, actual []Token, expected []Token) {
}
func TestCreateScreenLineBase(t *testing.T) {
- line := createScreenLine(0, 3, "", nil)
- assert.Assert(t, len(line) == 0)
+ line := NewLine("")
+ screenLine := createScreenLine(0, 3, line, nil)
+ assert.Assert(t, len(screenLine) == 0)
}
func TestCreateScreenLineOverflowRight(t *testing.T) {
- line := createScreenLine(0, 3, "012345", nil)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("012345")
+ screenLine := createScreenLine(0, 3, line, nil)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('0', tcell.StyleDefault),
createExpectedCell('1', tcell.StyleDefault),
createExpectedCell('>', tcell.StyleDefault.Reverse(true)),
@@ -279,8 +281,9 @@ func TestCreateScreenLineOverflowRight(t *testing.T) {
}
func TestCreateScreenLineUnderflowLeft(t *testing.T) {
- line := createScreenLine(1, 3, "012", nil)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("012")
+ screenLine := createScreenLine(1, 3, line, nil)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('1', tcell.StyleDefault),
createExpectedCell('2', tcell.StyleDefault),
@@ -293,8 +296,9 @@ func TestCreateScreenLineSearchHit(t *testing.T) {
panic(err)
}
- line := createScreenLine(0, 3, "abc", pattern)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("abc")
+ screenLine := createScreenLine(0, 3, line, pattern)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('a', tcell.StyleDefault),
createExpectedCell('b', tcell.StyleDefault.Reverse(true)),
createExpectedCell('c', tcell.StyleDefault),
@@ -307,8 +311,9 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) {
panic(err)
}
- line := createScreenLine(0, 3, "åäö", pattern)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("åäö")
+ screenLine := createScreenLine(0, 3, line, pattern)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
createExpectedCell('ö', tcell.StyleDefault),
@@ -318,9 +323,10 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) {
func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) {
pattern := regexp.MustCompile("ä")
- line := createScreenLine(1, 4, "ååäö", pattern)
+ line := NewLine("ååäö")
+ screenLine := createScreenLine(1, 4, line, pattern)
- assertTokenRangesEqual(t, line, []Token{
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
@@ -331,9 +337,10 @@ func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) {
func TestCreateScreenLineScrolled2Utf8SearchHit(t *testing.T) {
pattern := regexp.MustCompile("ä")
- line := createScreenLine(2, 4, "åååäö", pattern)
+ line := NewLine("åååäö")
+ screenLine := createScreenLine(2, 4, line, pattern)
- assertTokenRangesEqual(t, line, []Token{
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
diff --git m/reader.go m/reader.go
index 418c4c5..d47b710 100644
--- m/reader.go
+++ m/reader.go
@@ -29,7 +29,7 @@ import (
//
// This package provides query methods for the struct, no peeking!!
type Reader struct {
- lines []string
+ lines []*Line
name *string
lock *sync.Mutex
err error
@@ -41,7 +41,7 @@ type Reader struct {
// Lines contains a number of lines from the reader, plus metadata
type Lines struct {
- lines []string
+ lines []*Line
// One-based line number of the first line returned
firstLineOneBased int
@@ -136,7 +136,7 @@ func readStream(stream io.Reader, reader *Reader, fromFilter *exec.Cmd) {
}
reader.lock.Lock()
- reader.lines = append(reader.lines, string(completeLine))
+ reader.lines = append(reader.lines, NewLine(string(completeLine)))
reader.lock.Unlock()
// This is how to do a non-blocking write to a channel:
@@ -172,7 +172,7 @@ func NewReaderFromStream(name string, reader io.Reader) *Reader {
// If fromFilter is not nil this method will wait() for it,
// and effectively takes over ownership for it.
func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader {
- var lines []string
+ var lines []*Line
var lock = &sync.Mutex{}
done := make(chan bool, 1)
@@ -201,9 +201,11 @@ func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader {
// Moar in the bottom left corner of the screen.
func NewReaderFromText(name string, text string) *Reader {
noExternalNewlines := strings.Trim(text, "\n")
- lines := []string{}
+ lines := []*Line{}
if len(noExternalNewlines) > 0 {
- lines = strings.Split(noExternalNewlines, "\n")
+ for _, line := range strings.Split(noExternalNewlines, "\n") {
+ lines = append(lines, NewLine(line))
+ }
}
done := make(chan bool, 1)
done <- true
@@ -380,7 +382,7 @@ func (r *Reader) GetLineCount() int {
}
// GetLine gets a line. If the requested line number is out of bounds, nil is returned.
-func (r *Reader) GetLine(lineNumberOneBased int) *string {
+func (r *Reader) GetLine(lineNumberOneBased int) *Line {
r.lock.Lock()
defer r.lock.Unlock()
@@ -390,7 +392,7 @@ func (r *Reader) GetLine(lineNumberOneBased int) *string {
if lineNumberOneBased > len(r.lines) {
return nil
}
- return &r.lines[lineNumberOneBased-1]
+ return r.lines[lineNumberOneBased-1]
}
// GetLines gets the indicated lines from the input
diff --git m/reader_test.go m/reader_test.go
index 2ba7326..0e2aed2 100644
--- m/reader_test.go
+++ m/reader_test.go
@@ -158,8 +158,8 @@ func TestGetLongLine(t *testing.T) {
assert.Equal(t, len(lines.lines), 1)
line := lines.lines[0]
- assert.Assert(t, strings.HasPrefix(line, "1 2 3 4"), "<%s>", line)
- assert.Assert(t, strings.HasSuffix(line, "0123456789"), line)
+ assert.Assert(t, strings.HasPrefix(line.Plain(), "1 2 3 4"), "<%s>", line)
+ assert.Assert(t, strings.HasSuffix(line.Plain(), "0123456789"), line)
stat, err := os.Stat(file)
if err != nil {
@@ -168,7 +168,7 @@ func TestGetLongLine(t *testing.T) {
fileSize := stat.Size()
// The "+1" is because the Reader strips off the ending linefeed
- assert.Equal(t, len(line)+1, int(fileSize))
+ assert.Equal(t, len(line.Plain())+1, int(fileSize))
}
func getReaderWithLineCount(totalLines int) *Reader {
@@ -219,7 +219,7 @@ func testCompressedFile(t *testing.T, filename string) {
panic(err)
}
- assert.Equal(t, reader.GetLines(1, 5).lines[0], "This is a compressed file", "%s", filename)
+ assert.Equal(t, reader.GetLines(1, 5).lines[0].Plain(), "This is a compressed file", "%s", filename)
}
func TestCompressedFiles(t *testing.T) {
Change-Id: Id8671001ec7c1038e2df0b87a45d346a1f1dd663
2021-01-11 12:42:34 +03:00
|
|
|
|
}
|
2021-04-24 18:20:09 +03:00
|
|
|
|
return *line.plain
|
Parse lines on demand and only once
This improves line processing performance by 40%.
Fixes #36.
diff --git m/ansiTokenizer.go m/ansiTokenizer.go
index d991e23..056a227 100644
--- m/ansiTokenizer.go
+++ m/ansiTokenizer.go
@@ -23,6 +23,44 @@ type Token struct {
Style tcell.Style
}
+// A Line represents a line of text that can / will be paged
+type Line struct {
+ raw *string
+ plain *string
+ tokens []Token
+}
+
+// NewLine creates a new Line from a (potentially ANSI / man page formatted) string
+func NewLine(raw string) *Line {
+ return &Line{
+ raw: &raw,
+ plain: nil,
+ tokens: nil,
+ }
+}
+
+// Tokens returns a representation of the string split into styled tokens
+func (line *Line) Tokens() []Token {
+ line.parse()
+ return line.tokens
+}
+
+// Plain returns a plain text representation of the initial string
+func (line *Line) Plain() string {
+ line.parse()
+ return *line.plain
+}
+
+func (line *Line) parse() {
+ if line.raw == nil {
+ // Already done
+ return
+ }
+
+ line.tokens, line.plain = tokensFromString(*line.raw)
+ line.raw = nil
+}
+
// SetManPageFormatFromEnv parses LESS_TERMCAP_xx environment variables and
// adapts the moar output accordingly.
func SetManPageFormatFromEnv() {
diff --git m/pager.go m/pager.go
index 412e05b..98efa9a 100644
--- m/pager.go
+++ m/pager.go
@@ -111,7 +111,7 @@ func NewPager(r *Reader) *Pager {
}
}
-func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line string) {
+func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line *Line) {
screenWidth, _ := p.screen.Size()
prefixLength := 0
@@ -138,7 +138,7 @@ func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNum
func createScreenLine(
stringIndexAtColumnZero int,
screenColumnsCount int,
- line string,
+ line *Line,
search *regexp.Regexp,
) []Token {
var returnMe []Token
@@ -152,14 +152,14 @@ func createScreenLine(
searchHitDelta = -1
}
- tokens, plainString := tokensFromString(line)
- if stringIndexAtColumnZero >= len(tokens) {
+ if stringIndexAtColumnZero >= len(line.Tokens()) {
// Nothing (more) to display, never mind
return returnMe
}
- matchRanges := getMatchRanges(plainString, search)
- for _, token := range tokens[stringIndexAtColumnZero:] {
+ plain := line.Plain()
+ matchRanges := getMatchRanges(&plain, search)
+ for _, token := range line.Tokens()[stringIndexAtColumnZero:] {
if len(returnMe) >= screenColumnsCount {
// We are trying to add a character to the right of the screen.
// Indicate that this line continues to the right.
@@ -232,7 +232,8 @@ func (p *Pager) _AddLines(spinner string) {
// This happens when we're done
eofSpinner = "---"
}
- p._AddLine(nil, 0, screenLineNumber, _EofMarkerFormat+eofSpinner)
+ spinnerLine := NewLine(_EofMarkerFormat + eofSpinner)
+ p._AddLine(nil, 0, screenLineNumber, spinnerLine)
switch p.mode {
case _Searching:
@@ -329,8 +330,8 @@ func (p *Pager) _FindFirstHitLineOneBased(firstLineOneBased int, backwards bool)
return nil
}
- _, lineText := tokensFromString(*line)
- if p.searchPattern.MatchString(*lineText) {
+ lineText := line.Plain()
+ if p.searchPattern.MatchString(lineText) {
return &lineNumber
}
diff --git m/pager_test.go m/pager_test.go
index 65fa3c2..ce0f79b 100644
--- m/pager_test.go
+++ m/pager_test.go
@@ -265,13 +265,15 @@ func assertTokenRangesEqual(t *testing.T, actual []Token, expected []Token) {
}
func TestCreateScreenLineBase(t *testing.T) {
- line := createScreenLine(0, 3, "", nil)
- assert.Assert(t, len(line) == 0)
+ line := NewLine("")
+ screenLine := createScreenLine(0, 3, line, nil)
+ assert.Assert(t, len(screenLine) == 0)
}
func TestCreateScreenLineOverflowRight(t *testing.T) {
- line := createScreenLine(0, 3, "012345", nil)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("012345")
+ screenLine := createScreenLine(0, 3, line, nil)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('0', tcell.StyleDefault),
createExpectedCell('1', tcell.StyleDefault),
createExpectedCell('>', tcell.StyleDefault.Reverse(true)),
@@ -279,8 +281,9 @@ func TestCreateScreenLineOverflowRight(t *testing.T) {
}
func TestCreateScreenLineUnderflowLeft(t *testing.T) {
- line := createScreenLine(1, 3, "012", nil)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("012")
+ screenLine := createScreenLine(1, 3, line, nil)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('1', tcell.StyleDefault),
createExpectedCell('2', tcell.StyleDefault),
@@ -293,8 +296,9 @@ func TestCreateScreenLineSearchHit(t *testing.T) {
panic(err)
}
- line := createScreenLine(0, 3, "abc", pattern)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("abc")
+ screenLine := createScreenLine(0, 3, line, pattern)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('a', tcell.StyleDefault),
createExpectedCell('b', tcell.StyleDefault.Reverse(true)),
createExpectedCell('c', tcell.StyleDefault),
@@ -307,8 +311,9 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) {
panic(err)
}
- line := createScreenLine(0, 3, "åäö", pattern)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("åäö")
+ screenLine := createScreenLine(0, 3, line, pattern)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
createExpectedCell('ö', tcell.StyleDefault),
@@ -318,9 +323,10 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) {
func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) {
pattern := regexp.MustCompile("ä")
- line := createScreenLine(1, 4, "ååäö", pattern)
+ line := NewLine("ååäö")
+ screenLine := createScreenLine(1, 4, line, pattern)
- assertTokenRangesEqual(t, line, []Token{
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
@@ -331,9 +337,10 @@ func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) {
func TestCreateScreenLineScrolled2Utf8SearchHit(t *testing.T) {
pattern := regexp.MustCompile("ä")
- line := createScreenLine(2, 4, "åååäö", pattern)
+ line := NewLine("åååäö")
+ screenLine := createScreenLine(2, 4, line, pattern)
- assertTokenRangesEqual(t, line, []Token{
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
diff --git m/reader.go m/reader.go
index 418c4c5..d47b710 100644
--- m/reader.go
+++ m/reader.go
@@ -29,7 +29,7 @@ import (
//
// This package provides query methods for the struct, no peeking!!
type Reader struct {
- lines []string
+ lines []*Line
name *string
lock *sync.Mutex
err error
@@ -41,7 +41,7 @@ type Reader struct {
// Lines contains a number of lines from the reader, plus metadata
type Lines struct {
- lines []string
+ lines []*Line
// One-based line number of the first line returned
firstLineOneBased int
@@ -136,7 +136,7 @@ func readStream(stream io.Reader, reader *Reader, fromFilter *exec.Cmd) {
}
reader.lock.Lock()
- reader.lines = append(reader.lines, string(completeLine))
+ reader.lines = append(reader.lines, NewLine(string(completeLine)))
reader.lock.Unlock()
// This is how to do a non-blocking write to a channel:
@@ -172,7 +172,7 @@ func NewReaderFromStream(name string, reader io.Reader) *Reader {
// If fromFilter is not nil this method will wait() for it,
// and effectively takes over ownership for it.
func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader {
- var lines []string
+ var lines []*Line
var lock = &sync.Mutex{}
done := make(chan bool, 1)
@@ -201,9 +201,11 @@ func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader {
// Moar in the bottom left corner of the screen.
func NewReaderFromText(name string, text string) *Reader {
noExternalNewlines := strings.Trim(text, "\n")
- lines := []string{}
+ lines := []*Line{}
if len(noExternalNewlines) > 0 {
- lines = strings.Split(noExternalNewlines, "\n")
+ for _, line := range strings.Split(noExternalNewlines, "\n") {
+ lines = append(lines, NewLine(line))
+ }
}
done := make(chan bool, 1)
done <- true
@@ -380,7 +382,7 @@ func (r *Reader) GetLineCount() int {
}
// GetLine gets a line. If the requested line number is out of bounds, nil is returned.
-func (r *Reader) GetLine(lineNumberOneBased int) *string {
+func (r *Reader) GetLine(lineNumberOneBased int) *Line {
r.lock.Lock()
defer r.lock.Unlock()
@@ -390,7 +392,7 @@ func (r *Reader) GetLine(lineNumberOneBased int) *string {
if lineNumberOneBased > len(r.lines) {
return nil
}
- return &r.lines[lineNumberOneBased-1]
+ return r.lines[lineNumberOneBased-1]
}
// GetLines gets the indicated lines from the input
diff --git m/reader_test.go m/reader_test.go
index 2ba7326..0e2aed2 100644
--- m/reader_test.go
+++ m/reader_test.go
@@ -158,8 +158,8 @@ func TestGetLongLine(t *testing.T) {
assert.Equal(t, len(lines.lines), 1)
line := lines.lines[0]
- assert.Assert(t, strings.HasPrefix(line, "1 2 3 4"), "<%s>", line)
- assert.Assert(t, strings.HasSuffix(line, "0123456789"), line)
+ assert.Assert(t, strings.HasPrefix(line.Plain(), "1 2 3 4"), "<%s>", line)
+ assert.Assert(t, strings.HasSuffix(line.Plain(), "0123456789"), line)
stat, err := os.Stat(file)
if err != nil {
@@ -168,7 +168,7 @@ func TestGetLongLine(t *testing.T) {
fileSize := stat.Size()
// The "+1" is because the Reader strips off the ending linefeed
- assert.Equal(t, len(line)+1, int(fileSize))
+ assert.Equal(t, len(line.Plain())+1, int(fileSize))
}
func getReaderWithLineCount(totalLines int) *Reader {
@@ -219,7 +219,7 @@ func testCompressedFile(t *testing.T, filename string) {
panic(err)
}
- assert.Equal(t, reader.GetLines(1, 5).lines[0], "This is a compressed file", "%s", filename)
+ assert.Equal(t, reader.GetLines(1, 5).lines[0].Plain(), "This is a compressed file", "%s", filename)
}
func TestCompressedFiles(t *testing.T) {
Change-Id: Id8671001ec7c1038e2df0b87a45d346a1f1dd663
2021-01-11 12:42:34 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-10-30 22:29:29 +03:00
|
|
|
|
// SetManPageFormatFromEnv parses LESS_TERMCAP_xx environment variables and
|
|
|
|
|
// adapts the moar output accordingly.
|
2019-11-27 20:43:46 +03:00
|
|
|
|
func SetManPageFormatFromEnv() {
|
2019-10-30 22:29:29 +03:00
|
|
|
|
// Requested here: https://github.com/walles/moar/issues/14
|
|
|
|
|
|
|
|
|
|
lessTermcapMd := os.Getenv("LESS_TERMCAP_md")
|
|
|
|
|
if lessTermcapMd != "" {
|
2020-12-29 19:08:54 +03:00
|
|
|
|
manPageBold = termcapToStyle(lessTermcapMd)
|
2019-10-30 22:29:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lessTermcapUs := os.Getenv("LESS_TERMCAP_us")
|
|
|
|
|
if lessTermcapUs != "" {
|
2020-12-29 19:08:54 +03:00
|
|
|
|
manPageUnderline = termcapToStyle(lessTermcapUs)
|
2019-10-30 22:29:29 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-05 10:32:06 +03:00
|
|
|
|
// Used from tests
|
2020-12-29 19:08:54 +03:00
|
|
|
|
func resetManPageFormatForTesting() {
|
2021-04-15 16:16:06 +03:00
|
|
|
|
manPageBold = twin.StyleDefault.WithAttr(twin.AttrBold)
|
|
|
|
|
manPageUnderline = twin.StyleDefault.WithAttr(twin.AttrUnderline)
|
2019-11-05 10:32:06 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-04-15 16:16:06 +03:00
|
|
|
|
func termcapToStyle(termcap string) twin.Style {
|
2019-10-30 22:29:29 +03:00
|
|
|
|
// Add a character to be sure we have one to take the format from
|
2021-04-24 18:20:09 +03:00
|
|
|
|
cells := cellsFromString(termcap + "x")
|
2021-04-15 16:16:06 +03:00
|
|
|
|
return cells[len(cells)-1].Style
|
2019-10-30 22:29:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-04-24 18:20:09 +03:00
|
|
|
|
func withoutFormatting(s string) string {
|
2021-04-25 00:15:53 +03:00
|
|
|
|
stripped := make([]rune, 0, len(s))
|
2021-04-24 18:20:09 +03:00
|
|
|
|
for _, styledString := range styledStringsFromString(s) {
|
2021-04-24 21:49:58 +03:00
|
|
|
|
for _, char := range styledString.String {
|
2021-04-25 00:15:53 +03:00
|
|
|
|
switch char {
|
|
|
|
|
case '\t':
|
|
|
|
|
// Expand the TAB character
|
|
|
|
|
for {
|
|
|
|
|
stripped = append(stripped, ' ')
|
|
|
|
|
if (len(stripped))%_TabSize == 0 {
|
|
|
|
|
// We arrived at the next tab stop
|
|
|
|
|
break
|
|
|
|
|
}
|
2021-04-24 21:49:58 +03:00
|
|
|
|
}
|
2021-04-25 00:15:53 +03:00
|
|
|
|
case BACKSPACE:
|
|
|
|
|
if len(stripped) == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
stripped = stripped[0 : len(stripped)-1]
|
|
|
|
|
default:
|
2021-04-25 21:40:38 +03:00
|
|
|
|
if !unicode.IsPrint(char) {
|
|
|
|
|
stripped = append(stripped, '?')
|
|
|
|
|
continue
|
|
|
|
|
}
|
2021-04-25 00:15:53 +03:00
|
|
|
|
stripped = append(stripped, char)
|
2021-04-24 21:49:58 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
2021-04-24 18:20:09 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-04-25 00:15:53 +03:00
|
|
|
|
return string(stripped)
|
2021-04-24 18:20:09 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Turn a (formatted) string into a series of screen cells
|
|
|
|
|
func cellsFromString(s string) []twin.Cell {
|
|
|
|
|
var cells []twin.Cell
|
2019-06-16 10:14:30 +03:00
|
|
|
|
|
2021-04-15 16:16:06 +03:00
|
|
|
|
// Specs: https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit
|
|
|
|
|
styleBrokenUtf8 := twin.StyleDefault.Background(twin.NewColor16(1)).Foreground(twin.NewColor16(7))
|
2019-06-18 20:08:35 +03:00
|
|
|
|
|
2020-12-29 19:08:54 +03:00
|
|
|
|
for _, styledString := range styledStringsFromString(s) {
|
|
|
|
|
for _, token := range tokensFromStyledString(styledString) {
|
2019-06-27 22:39:46 +03:00
|
|
|
|
switch token.Rune {
|
2019-06-18 20:08:35 +03:00
|
|
|
|
|
|
|
|
|
case '\x09': // TAB
|
2019-06-17 22:39:57 +03:00
|
|
|
|
for {
|
2021-04-24 18:20:09 +03:00
|
|
|
|
cells = append(cells, twin.Cell{
|
2019-06-17 22:39:57 +03:00
|
|
|
|
Rune: ' ',
|
|
|
|
|
Style: styledString.Style,
|
|
|
|
|
})
|
|
|
|
|
|
2021-04-24 18:20:09 +03:00
|
|
|
|
if (len(cells))%_TabSize == 0 {
|
2019-06-17 22:39:57 +03:00
|
|
|
|
// We arrived at the next tab stop
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-18 20:08:35 +03:00
|
|
|
|
|
|
|
|
|
case '<27>': // Go's broken-UTF8 marker
|
2021-04-24 18:20:09 +03:00
|
|
|
|
cells = append(cells, twin.Cell{
|
2019-06-18 20:08:35 +03:00
|
|
|
|
Rune: '?',
|
|
|
|
|
Style: styleBrokenUtf8,
|
|
|
|
|
})
|
|
|
|
|
|
2021-04-26 00:35:47 +03:00
|
|
|
|
case BACKSPACE:
|
2021-04-24 18:20:09 +03:00
|
|
|
|
cells = append(cells, twin.Cell{
|
2019-06-27 22:39:46 +03:00
|
|
|
|
Rune: '<',
|
|
|
|
|
Style: styleBrokenUtf8,
|
2019-06-17 22:39:57 +03:00
|
|
|
|
})
|
2019-06-27 22:39:46 +03:00
|
|
|
|
|
|
|
|
|
default:
|
2021-04-26 00:35:47 +03:00
|
|
|
|
if !unicode.IsPrint(token.Rune) {
|
|
|
|
|
cells = append(cells, twin.Cell{
|
|
|
|
|
Rune: '?',
|
|
|
|
|
Style: styleBrokenUtf8,
|
|
|
|
|
})
|
|
|
|
|
continue
|
|
|
|
|
}
|
2021-04-24 18:20:09 +03:00
|
|
|
|
cells = append(cells, token)
|
2019-06-27 22:39:46 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-24 18:20:09 +03:00
|
|
|
|
return cells
|
2019-06-27 22:39:46 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-27 23:20:50 +03:00
|
|
|
|
// Consume 'x<x', where '<' is backspace and the result is a bold 'x'
|
2021-04-15 16:16:06 +03:00
|
|
|
|
func consumeBold(runes []rune, index int) (int, *twin.Cell) {
|
2019-11-27 22:59:27 +03:00
|
|
|
|
if index+2 >= len(runes) {
|
|
|
|
|
// Not enough runes left for a bold
|
|
|
|
|
return index, nil
|
|
|
|
|
}
|
2019-06-27 22:39:46 +03:00
|
|
|
|
|
2021-04-25 00:15:53 +03:00
|
|
|
|
if runes[index+1] != BACKSPACE {
|
2019-11-27 22:59:27 +03:00
|
|
|
|
// No backspace in the middle, never mind
|
|
|
|
|
return index, nil
|
|
|
|
|
}
|
2019-06-27 22:39:46 +03:00
|
|
|
|
|
2019-11-27 22:59:27 +03:00
|
|
|
|
if runes[index] != runes[index+2] {
|
|
|
|
|
// First and last rune not the same, never mind
|
|
|
|
|
return index, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We have a match!
|
2021-04-15 16:16:06 +03:00
|
|
|
|
return index + 3, &twin.Cell{
|
2019-11-27 22:59:27 +03:00
|
|
|
|
Rune: runes[index],
|
|
|
|
|
Style: manPageBold,
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-27 22:39:46 +03:00
|
|
|
|
|
2019-11-27 23:20:50 +03:00
|
|
|
|
// Consume '_<x', where '<' is backspace and the result is an underlined 'x'
|
2021-04-15 16:16:06 +03:00
|
|
|
|
func consumeUnderline(runes []rune, index int) (int, *twin.Cell) {
|
2019-11-27 22:59:27 +03:00
|
|
|
|
if index+2 >= len(runes) {
|
|
|
|
|
// Not enough runes left for a underline
|
|
|
|
|
return index, nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-25 00:15:53 +03:00
|
|
|
|
if runes[index+1] != BACKSPACE {
|
2019-11-27 22:59:27 +03:00
|
|
|
|
// No backspace in the middle, never mind
|
|
|
|
|
return index, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if runes[index] != '_' {
|
|
|
|
|
// No underline, never mind
|
|
|
|
|
return index, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We have a match!
|
2021-04-15 16:16:06 +03:00
|
|
|
|
return index + 3, &twin.Cell{
|
2019-11-27 22:59:27 +03:00
|
|
|
|
Rune: runes[index+2],
|
|
|
|
|
Style: manPageUnderline,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-31 10:36:53 +03:00
|
|
|
|
// Consume '+<+<o<o' / '+<o', where '<' is backspace and the result is a unicode bullet.
|
2019-11-27 23:20:50 +03:00
|
|
|
|
//
|
|
|
|
|
// Used on man pages, try "man printf" on macOS for one example.
|
2021-04-15 16:16:06 +03:00
|
|
|
|
func consumeBullet(runes []rune, index int) (int, *twin.Cell) {
|
2021-04-14 22:41:56 +03:00
|
|
|
|
patterns := [][]byte{[]byte("+\bo"), []byte("+\b+\bo\bo")}
|
2020-03-31 10:36:53 +03:00
|
|
|
|
for _, pattern := range patterns {
|
|
|
|
|
if index+len(pattern) > len(runes) {
|
|
|
|
|
// Not enough runes left for a bullet
|
|
|
|
|
continue
|
|
|
|
|
}
|
2019-11-27 23:20:50 +03:00
|
|
|
|
|
2020-03-31 10:36:53 +03:00
|
|
|
|
mismatch := false
|
2021-04-14 22:41:56 +03:00
|
|
|
|
for delta, patternByte := range pattern {
|
|
|
|
|
if patternByte != byte(runes[index+delta]) {
|
2020-03-31 10:36:53 +03:00
|
|
|
|
// Bullet pattern mismatch, never mind
|
|
|
|
|
mismatch = true
|
2021-04-14 22:41:56 +03:00
|
|
|
|
break
|
2020-03-31 10:36:53 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if mismatch {
|
|
|
|
|
continue
|
2019-11-27 23:20:50 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-31 10:36:53 +03:00
|
|
|
|
// We have a match!
|
2021-04-15 16:16:06 +03:00
|
|
|
|
return index + len(pattern), &twin.Cell{
|
2020-03-31 10:36:53 +03:00
|
|
|
|
Rune: '•', // Unicode bullet point
|
2021-04-15 16:16:06 +03:00
|
|
|
|
Style: twin.StyleDefault,
|
2020-03-31 10:36:53 +03:00
|
|
|
|
}
|
2019-11-27 23:20:50 +03:00
|
|
|
|
}
|
2020-03-31 10:36:53 +03:00
|
|
|
|
|
|
|
|
|
return index, nil
|
2019-11-27 23:20:50 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-04-15 16:16:06 +03:00
|
|
|
|
func tokensFromStyledString(styledString _StyledString) []twin.Cell {
|
2019-11-27 22:59:27 +03:00
|
|
|
|
runes := []rune(styledString.String)
|
2021-04-15 16:16:06 +03:00
|
|
|
|
tokens := make([]twin.Cell, 0, len(runes))
|
2019-11-27 22:59:27 +03:00
|
|
|
|
|
|
|
|
|
for index := 0; index < len(runes); index++ {
|
2020-12-29 19:08:54 +03:00
|
|
|
|
nextIndex, token := consumeBullet(runes, index)
|
2019-11-27 23:20:50 +03:00
|
|
|
|
if nextIndex != index {
|
|
|
|
|
tokens = append(tokens, *token)
|
|
|
|
|
index = nextIndex - 1
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-29 19:08:54 +03:00
|
|
|
|
nextIndex, token = consumeBold(runes, index)
|
2019-11-27 22:59:27 +03:00
|
|
|
|
if nextIndex != index {
|
|
|
|
|
tokens = append(tokens, *token)
|
|
|
|
|
index = nextIndex - 1
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-29 19:08:54 +03:00
|
|
|
|
nextIndex, token = consumeUnderline(runes, index)
|
2019-11-27 22:59:27 +03:00
|
|
|
|
if nextIndex != index {
|
|
|
|
|
tokens = append(tokens, *token)
|
|
|
|
|
index = nextIndex - 1
|
|
|
|
|
continue
|
2019-06-16 10:14:30 +03:00
|
|
|
|
}
|
2019-06-27 22:39:46 +03:00
|
|
|
|
|
2021-04-15 16:16:06 +03:00
|
|
|
|
tokens = append(tokens, twin.Cell{
|
2019-11-27 22:59:27 +03:00
|
|
|
|
Rune: runes[index],
|
2019-06-27 22:39:46 +03:00
|
|
|
|
Style: styledString.Style,
|
|
|
|
|
})
|
2019-06-16 10:14:30 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tokens
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type _StyledString struct {
|
|
|
|
|
String string
|
2021-04-15 16:16:06 +03:00
|
|
|
|
Style twin.Style
|
2019-06-16 10:14:30 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-12-29 19:08:54 +03:00
|
|
|
|
func styledStringsFromString(s string) []_StyledString {
|
2019-06-16 10:14:30 +03:00
|
|
|
|
// This function was inspired by the
|
|
|
|
|
// https://golang.org/pkg/regexp/#Regexp.Split source code
|
|
|
|
|
|
2021-04-14 22:41:56 +03:00
|
|
|
|
matches := sgrSequencePattern.FindAllStringIndex(s, -1)
|
2019-06-16 10:14:30 +03:00
|
|
|
|
styledStrings := make([]_StyledString, 0, len(matches)+1)
|
|
|
|
|
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style := twin.StyleDefault
|
2019-06-16 10:14:30 +03:00
|
|
|
|
|
|
|
|
|
beg := 0
|
|
|
|
|
end := 0
|
|
|
|
|
for _, match := range matches {
|
|
|
|
|
end = match[0]
|
|
|
|
|
|
2019-06-16 10:23:25 +03:00
|
|
|
|
if end > beg {
|
2019-06-27 22:39:46 +03:00
|
|
|
|
// Found non-zero length string
|
2019-06-16 10:14:30 +03:00
|
|
|
|
styledStrings = append(styledStrings, _StyledString{
|
|
|
|
|
String: s[beg:end],
|
|
|
|
|
Style: style,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
matchedPart := s[match[0]:match[1]]
|
2020-12-29 19:08:54 +03:00
|
|
|
|
style = updateStyle(style, matchedPart)
|
2019-06-16 10:14:30 +03:00
|
|
|
|
|
|
|
|
|
beg = match[1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if end != len(s) {
|
|
|
|
|
styledStrings = append(styledStrings, _StyledString{
|
|
|
|
|
String: s[beg:],
|
|
|
|
|
Style: style,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return styledStrings
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-29 19:08:54 +03:00
|
|
|
|
// updateStyle parses a string of the form "ESC[33m" into changes to style
|
2021-04-15 16:16:06 +03:00
|
|
|
|
func updateStyle(style twin.Style, escapeSequence string) twin.Style {
|
2019-10-27 11:15:16 +03:00
|
|
|
|
numbers := strings.Split(escapeSequence[2:len(escapeSequence)-1], ";")
|
|
|
|
|
index := 0
|
|
|
|
|
for index < len(numbers) {
|
|
|
|
|
number := numbers[index]
|
|
|
|
|
index++
|
2019-10-28 23:54:57 +03:00
|
|
|
|
switch strings.TrimLeft(number, "0") {
|
|
|
|
|
case "":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = twin.StyleDefault
|
2019-06-16 21:57:03 +03:00
|
|
|
|
|
2019-06-16 21:58:19 +03:00
|
|
|
|
case "1":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.WithAttr(twin.AttrBold)
|
2019-06-16 21:58:19 +03:00
|
|
|
|
|
2020-10-29 17:48:56 +03:00
|
|
|
|
case "2":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.WithAttr(twin.AttrDim)
|
2020-10-29 17:48:56 +03:00
|
|
|
|
|
2020-04-13 17:43:56 +03:00
|
|
|
|
case "3":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.WithAttr(twin.AttrItalic)
|
2020-04-13 17:43:56 +03:00
|
|
|
|
|
Support SGR 4, underline
diff --git m/ansiTokenizer.go m/ansiTokenizer.go
index 365b76a..7161bc6 100644
--- m/ansiTokenizer.go
+++ m/ansiTokenizer.go
@@ -179,6 +179,9 @@ func _UpdateStyle(logger *log.Logger, style tcell.Style, escapeSequence string)
case "1":
style = style.Bold(true)
+ case "4":
+ style = style.Underline(true)
+
case "7":
style = style.Reverse(true)
@@ -226,7 +229,7 @@ func _UpdateStyle(logger *log.Logger, style tcell.Style, escapeSequence string)
style = style.Background(tcell.ColorDefault)
default:
- logger.Printf("Unrecognized ANSI SGI code <%s>", number)
+ logger.Printf("Unrecognized ANSI SGR code <%s>", number)
}
}
Change-Id: I0527cbaff8b53cf25c99876789d69412af5ca118
2019-09-25 14:41:50 +03:00
|
|
|
|
case "4":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.WithAttr(twin.AttrUnderline)
|
Support SGR 4, underline
diff --git m/ansiTokenizer.go m/ansiTokenizer.go
index 365b76a..7161bc6 100644
--- m/ansiTokenizer.go
+++ m/ansiTokenizer.go
@@ -179,6 +179,9 @@ func _UpdateStyle(logger *log.Logger, style tcell.Style, escapeSequence string)
case "1":
style = style.Bold(true)
+ case "4":
+ style = style.Underline(true)
+
case "7":
style = style.Reverse(true)
@@ -226,7 +229,7 @@ func _UpdateStyle(logger *log.Logger, style tcell.Style, escapeSequence string)
style = style.Background(tcell.ColorDefault)
default:
- logger.Printf("Unrecognized ANSI SGI code <%s>", number)
+ logger.Printf("Unrecognized ANSI SGR code <%s>", number)
}
}
Change-Id: I0527cbaff8b53cf25c99876789d69412af5ca118
2019-09-25 14:41:50 +03:00
|
|
|
|
|
2019-06-16 22:39:27 +03:00
|
|
|
|
case "7":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.WithAttr(twin.AttrReverse)
|
2019-06-16 22:39:27 +03:00
|
|
|
|
|
2020-10-29 17:48:56 +03:00
|
|
|
|
case "22":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.WithoutAttr(twin.AttrBold).WithoutAttr(twin.AttrDim)
|
2020-10-29 17:48:56 +03:00
|
|
|
|
|
2020-04-13 17:43:56 +03:00
|
|
|
|
case "23":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.WithoutAttr(twin.AttrItalic)
|
2020-04-13 17:43:56 +03:00
|
|
|
|
|
2020-03-25 10:55:35 +03:00
|
|
|
|
case "24":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.WithoutAttr(twin.AttrUnderline)
|
2020-03-25 10:55:35 +03:00
|
|
|
|
|
2019-06-16 22:39:27 +03:00
|
|
|
|
case "27":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.WithoutAttr(twin.AttrReverse)
|
2019-06-16 22:39:27 +03:00
|
|
|
|
|
2020-12-06 23:37:00 +03:00
|
|
|
|
// Foreground colors, https://pkg.go.dev/github.com/gdamore/tcell#Color
|
2019-06-16 10:14:30 +03:00
|
|
|
|
case "30":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.NewColor16(0))
|
2019-06-16 10:14:30 +03:00
|
|
|
|
case "31":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.NewColor16(1))
|
2019-06-16 10:14:30 +03:00
|
|
|
|
case "32":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.NewColor16(2))
|
2019-06-16 10:14:30 +03:00
|
|
|
|
case "33":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.NewColor16(3))
|
2019-06-16 10:14:30 +03:00
|
|
|
|
case "34":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.NewColor16(4))
|
2019-06-16 10:14:30 +03:00
|
|
|
|
case "35":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.NewColor16(5))
|
2019-06-16 10:14:30 +03:00
|
|
|
|
case "36":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.NewColor16(6))
|
2019-06-16 10:14:30 +03:00
|
|
|
|
case "37":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.NewColor16(7))
|
2019-10-27 11:15:16 +03:00
|
|
|
|
case "38":
|
|
|
|
|
var err error = nil
|
2021-04-15 16:16:06 +03:00
|
|
|
|
var color *twin.Color
|
2019-10-27 23:40:30 +03:00
|
|
|
|
index, color, err = consumeCompositeColor(numbers, index-1)
|
2019-10-27 11:15:16 +03:00
|
|
|
|
if err != nil {
|
2019-12-06 21:36:31 +03:00
|
|
|
|
log.Warnf("Foreground: %s", err.Error())
|
2019-10-27 11:15:16 +03:00
|
|
|
|
return style
|
|
|
|
|
}
|
|
|
|
|
style = style.Foreground(*color)
|
2019-07-15 14:34:42 +03:00
|
|
|
|
case "39":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.ColorDefault)
|
2019-06-16 21:57:03 +03:00
|
|
|
|
|
2021-04-15 16:16:06 +03:00
|
|
|
|
// Background colors, see https://pkg.go.dev/github.com/gdamore/Color
|
2019-06-16 21:57:03 +03:00
|
|
|
|
case "40":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Background(twin.NewColor16(0))
|
2019-06-16 21:57:03 +03:00
|
|
|
|
case "41":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Background(twin.NewColor16(1))
|
2019-06-16 21:57:03 +03:00
|
|
|
|
case "42":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Background(twin.NewColor16(2))
|
2019-06-16 21:57:03 +03:00
|
|
|
|
case "43":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Background(twin.NewColor16(3))
|
2019-06-16 21:57:03 +03:00
|
|
|
|
case "44":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Background(twin.NewColor16(4))
|
2019-06-16 21:57:03 +03:00
|
|
|
|
case "45":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Background(twin.NewColor16(5))
|
2019-06-16 21:57:03 +03:00
|
|
|
|
case "46":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Background(twin.NewColor16(6))
|
2019-06-16 21:57:03 +03:00
|
|
|
|
case "47":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Background(twin.NewColor16(7))
|
2019-10-27 11:15:16 +03:00
|
|
|
|
case "48":
|
|
|
|
|
var err error = nil
|
2021-04-15 16:16:06 +03:00
|
|
|
|
var color *twin.Color
|
2019-10-27 23:40:30 +03:00
|
|
|
|
index, color, err = consumeCompositeColor(numbers, index-1)
|
2019-10-27 11:15:16 +03:00
|
|
|
|
if err != nil {
|
2019-12-06 21:36:31 +03:00
|
|
|
|
log.Warnf("Background: %s", err.Error())
|
2019-10-27 11:15:16 +03:00
|
|
|
|
return style
|
|
|
|
|
}
|
|
|
|
|
style = style.Background(*color)
|
2019-07-15 14:34:42 +03:00
|
|
|
|
case "49":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Background(twin.ColorDefault)
|
2019-06-16 21:57:03 +03:00
|
|
|
|
|
2021-04-15 16:16:06 +03:00
|
|
|
|
// Bright foreground colors: see https://pkg.go.dev/github.com/gdamore/Color
|
2020-04-22 22:37:52 +03:00
|
|
|
|
//
|
|
|
|
|
// After testing vs less and cat on iTerm2 3.3.9 / macOS Catalina
|
2020-04-23 07:26:09 +03:00
|
|
|
|
// 10.15.4 that's how they seem to handle this, tested with:
|
|
|
|
|
// * TERM=xterm-256color
|
|
|
|
|
// * TERM=screen-256color
|
2020-04-22 22:37:52 +03:00
|
|
|
|
case "90":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.NewColor16(8))
|
2020-04-22 22:37:52 +03:00
|
|
|
|
case "91":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.NewColor16(9))
|
2020-04-22 22:37:52 +03:00
|
|
|
|
case "92":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.NewColor16(10))
|
2020-04-22 22:37:52 +03:00
|
|
|
|
case "93":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.NewColor16(11))
|
2020-04-22 22:37:52 +03:00
|
|
|
|
case "94":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.NewColor16(12))
|
2020-04-22 22:37:52 +03:00
|
|
|
|
case "95":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.NewColor16(13))
|
2020-04-22 22:37:52 +03:00
|
|
|
|
case "96":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.NewColor16(14))
|
2020-04-22 22:37:52 +03:00
|
|
|
|
case "97":
|
2021-04-15 16:16:06 +03:00
|
|
|
|
style = style.Foreground(twin.NewColor16(15))
|
2020-04-22 22:37:52 +03:00
|
|
|
|
|
2019-06-16 21:57:03 +03:00
|
|
|
|
default:
|
2019-12-06 21:36:31 +03:00
|
|
|
|
log.Warnf("Unrecognized ANSI SGR code <%s>", number)
|
2019-06-16 10:14:30 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return style
|
|
|
|
|
}
|
2019-10-27 11:15:16 +03:00
|
|
|
|
|
2019-10-27 23:40:30 +03:00
|
|
|
|
// numbers is a list of numbers from a ANSI SGR string
|
2019-10-27 11:15:16 +03:00
|
|
|
|
// index points to either 38 or 48 in that string
|
|
|
|
|
//
|
|
|
|
|
// This method will return:
|
|
|
|
|
// * The first index in the string that this function did not consume
|
|
|
|
|
// * A color value that can be applied to a style
|
2021-04-15 16:16:06 +03:00
|
|
|
|
func consumeCompositeColor(numbers []string, index int) (int, *twin.Color, error) {
|
2019-10-28 22:09:08 +03:00
|
|
|
|
baseIndex := index
|
2019-10-27 23:40:30 +03:00
|
|
|
|
if numbers[index] != "38" && numbers[index] != "48" {
|
|
|
|
|
err := fmt.Errorf(
|
2021-04-19 21:29:05 +03:00
|
|
|
|
"unknown start of color sequence <%s>, expected 38 (foreground) or 48 (background): <CSI %sm>",
|
2019-10-27 23:40:30 +03:00
|
|
|
|
numbers[index],
|
2019-10-28 22:09:08 +03:00
|
|
|
|
strings.Join(numbers[baseIndex:], ";"))
|
|
|
|
|
return -1, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
index++
|
|
|
|
|
if index >= len(numbers) {
|
|
|
|
|
err := fmt.Errorf(
|
2021-04-19 21:29:05 +03:00
|
|
|
|
"incomplete color sequence: <CSI %sm>",
|
2019-10-28 22:09:08 +03:00
|
|
|
|
strings.Join(numbers[baseIndex:], ";"))
|
2019-10-27 23:40:30 +03:00
|
|
|
|
return -1, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-28 22:18:07 +03:00
|
|
|
|
if numbers[index] == "5" {
|
2019-10-28 22:30:04 +03:00
|
|
|
|
// Handle 8 bit color
|
2019-10-28 22:18:07 +03:00
|
|
|
|
index++
|
|
|
|
|
if index >= len(numbers) {
|
|
|
|
|
err := fmt.Errorf(
|
2021-04-19 21:29:05 +03:00
|
|
|
|
"incomplete 8 bit color sequence: <CSI %sm>",
|
2019-10-28 22:18:07 +03:00
|
|
|
|
strings.Join(numbers[baseIndex:], ";"))
|
|
|
|
|
return -1, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
colorNumber, err := strconv.Atoi(numbers[index])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return -1, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-15 16:16:06 +03:00
|
|
|
|
colorValue := twin.NewColor256(uint8(colorNumber))
|
2019-10-28 22:18:07 +03:00
|
|
|
|
return index + 1, &colorValue, nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-28 22:21:39 +03:00
|
|
|
|
if numbers[index] == "2" {
|
2019-10-28 22:30:04 +03:00
|
|
|
|
// Handle 24 bit color
|
|
|
|
|
rIndex := index + 1
|
|
|
|
|
gIndex := index + 2
|
|
|
|
|
bIndex := index + 3
|
|
|
|
|
if bIndex >= len(numbers) {
|
|
|
|
|
err := fmt.Errorf(
|
2021-04-19 21:29:05 +03:00
|
|
|
|
"incomplete 24 bit color sequence, expected N8;2;R;G;Bm: <CSI %sm>",
|
2019-10-28 22:30:04 +03:00
|
|
|
|
strings.Join(numbers[baseIndex:], ";"))
|
|
|
|
|
return -1, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rValueX, err := strconv.ParseInt(numbers[rIndex], 10, 32)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return -1, nil, err
|
|
|
|
|
}
|
2021-04-15 16:16:06 +03:00
|
|
|
|
rValue := uint8(rValueX)
|
2019-10-28 22:30:04 +03:00
|
|
|
|
|
|
|
|
|
gValueX, err := strconv.Atoi(numbers[gIndex])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return -1, nil, err
|
|
|
|
|
}
|
2021-04-15 16:16:06 +03:00
|
|
|
|
gValue := uint8(gValueX)
|
2019-10-28 22:30:04 +03:00
|
|
|
|
|
|
|
|
|
bValueX, err := strconv.Atoi(numbers[bIndex])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return -1, nil, err
|
|
|
|
|
}
|
2021-04-15 16:16:06 +03:00
|
|
|
|
bValue := uint8(bValueX)
|
2019-10-28 22:30:04 +03:00
|
|
|
|
|
2021-04-15 16:16:06 +03:00
|
|
|
|
colorValue := twin.NewColor24Bit(rValue, gValue, bValue)
|
2019-10-28 22:30:04 +03:00
|
|
|
|
return bIndex + 1, &colorValue, nil
|
2019-10-28 22:21:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := fmt.Errorf(
|
2021-04-19 21:29:05 +03:00
|
|
|
|
"unknown color type <%s>, expected 5 (8 bit color) or 2 (24 bit color): <CSI %sm>",
|
2019-10-28 22:21:39 +03:00
|
|
|
|
numbers[index],
|
|
|
|
|
strings.Join(numbers[baseIndex:], ";"))
|
|
|
|
|
return -1, nil, err
|
2019-10-27 11:15:16 +03:00
|
|
|
|
}
|