1
1
mirror of https://github.com/walles/moar.git synced 2024-09-22 01:18:48 +03:00
Commit Graph

44 Commits

Author SHA1 Message Date
Johan Walles
d5827bbc99 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 10:42:34 +01:00
Johan Walles
2b18347022 Make a lot fewer functions public 2020-12-29 22:57:44 +01:00
Johan Walles
1ac14b1c23 Fix private-functions naming
It's start-with-lowercase, not start-with-underscore.
2020-12-29 17:08:54 +01:00
Johan Walles
08db9e9cd6 Stop creating our own low-numbered colors
Because it didn't work.
2020-12-06 21:37:00 +01:00
Johan Walles
ff268ae481 Fix one failing test, add test case for another 2020-12-06 16:22:24 +01:00
Johan Walles
2583747590 Bump tcell to v2
Should fix 21.
2020-12-06 14:47:10 +01:00
Johan Walles
7ee8844a1f Add support for ANSI SGR codes 2 and 22
Fixes #27
2020-10-29 15:48:56 +01:00
Johan Walles
8b0bc846fc Brighten the bright colors
This makes moar match less and cat with TERM=screen-256color.
2020-04-23 06:28:55 +02:00
Johan Walles
a9ee2fae9d Support SGR codes 90-97, bright colors
We just treat them as their plain counterparts.

After testing vs less and cat on iTerm2 3.3.9 / macOS Catalina 10.15.4
that's how they seem to handle this. Counter examples welcome.

Fixes #24
2020-04-22 21:39:58 +02:00
Johan Walles
46881e4519 Add support for italics 2020-04-13 16:43:56 +02:00
Johan Walles
a174d86180 Support both kinds of bullet point patterns 2020-03-31 09:36:53 +02:00
Johan Walles
97906da4b0 Fix bullet pattern 2020-03-31 09:24:29 +02:00
Johan Walles
70a21f097f Enable a disabled-by-mistake test
Having had a linter find this for me earlier would have been nice.
2020-03-25 21:42:18 +01:00
Johan Walles
3c7d017138 Implement SGI 24 "Underline off"
Ref: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters

Fixes #23
2020-03-25 08:55:35 +01:00
Johan Walles
ad767671eb Condition some log messages on --debug flag
With this change, to get some log messages (about unsupported keys
mostly), you have to pass --debug on the command line.

Fixes #20
2019-12-06 19:36:31 +01:00
Johan Walles
212ff0efa7 Fix rendering of man page bullet points 2019-11-27 21:20:50 +01:00
Johan Walles
e7ae84c1aa Rewrite man page formatting
To make it easier to extend.
2019-11-27 20:59:27 +01:00
Johan Walles
1fa52b9bc7 Use a global logger
This saves us from a lot of passing-a-logger-around and makes the code
generally easier to read and to work with.
2019-11-27 18:43:46 +01:00
Johan Walles
a438439c11 Improve long-lines performance
diff --git m/ansiTokenizer.go m/ansiTokenizer.go
index d5a5272..601b939 100644
--- m/ansiTokenizer.go
+++ m/ansiTokenizer.go
@@ -92,10 +92,12 @@ func TokensFromString(logger *log.Logger, s string) ([]Token, *string) {
 		}
 	}

-	plainString := ""
+	var stringBuilder strings.Builder
+	stringBuilder.Grow(len(tokens))
 	for _, token := range tokens {
-		plainString += string(token.Rune)
+		stringBuilder.WriteRune(token.Rune)
 	}
+	plainString := stringBuilder.String()
 	return tokens, &plainString
 }

Change-Id: Ieb2c3aa9f1daa18ab2280f499025994b561266e1
2019-11-19 17:28:58 +01:00
Johan Walles
8e08e729c4 Fix environment depending test failure 2019-11-05 08:32:06 +01:00
Johan Walles
6334be7dcc Adapt to LESS_TERMCAP_md and LESS_TERMCAP_us
Fixes #14
2019-10-30 20:29:29 +01:00
Johan Walles
a7cba8bb16 Move man page formats into variables 2019-10-30 20:13:28 +01:00
Johan Walles
7405293e25 Add sample file for color handling
From #14
2019-10-28 21:54:57 +01:00
Johan Walles
1dda4b1a87 Handle 24 bit color 2019-10-28 20:30:04 +01:00
Johan Walles
94774e121a Report final error message 2019-10-28 20:21:39 +01:00
Johan Walles
8b9e85958c Handle 8 bit colors 2019-10-28 20:18:07 +01:00
Johan Walles
f973ed5c46 Report incomplete color sequence 2019-10-28 20:09:08 +01:00
Johan Walles
7b935e7c49 Sanity check input 2019-10-27 21:40:30 +01:00
Johan Walles
b73810dc14 WIP: Initial tests for 8/24 bit color 2019-10-27 09:15:16 +01:00
Johan Walles
c756bd0288 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 13:41:50 +02:00
Johan Walles
1103ebf5bc Add tokenizer tests 2019-07-15 13:34:42 +02:00
Johan Walles
f97e50fe2c Add two comments 2019-07-08 08:58:53 +02:00
Johan Walles
f16d818715 Have the tokenizer provide an unformatted string as well 2019-06-30 15:06:33 +02:00
Johan Walles
e78f452557 Add support for man page rendering 2019-06-27 21:39:46 +02:00
Johan Walles
c33fd4eb47 Add missing SGI code
00 is produced by highlight.
2019-06-18 20:12:35 +02:00
Johan Walles
5cc47df367 Render broken UTF8 as red-on-white question marks
The default broken UTF8 marker in Go doesn't render well in my terminal.
2019-06-18 19:08:35 +02:00
Johan Walles
9619c020e4 Add support for rendering TABs 2019-06-17 21:39:57 +02:00
Johan Walles
e4006b2c78 Support inverse video 2019-06-16 21:39:27 +02:00
Johan Walles
aa1cd1da5f Actually match moar and less output colors 2019-06-16 21:26:04 +02:00
Johan Walles
eecd9120cd Look more like Ruby moar and less 2019-06-16 21:12:08 +02:00
Johan Walles
75760fb7fa Support rendering bold text 2019-06-16 20:58:19 +02:00
Johan Walles
4717624c89 Handle background color changes 2019-06-16 20:57:03 +02:00
Johan Walles
abe8f37eaf Fix an off-by-one error 2019-06-16 09:23:25 +02:00
Johan Walles
d7dacd4dae WIP: Implement ANSI foreground color setting
Doesn't work, but the parts are there. Go fix.
2019-06-16 09:14:30 +02:00