1
1
mirror of https://github.com/walles/moar.git synced 2024-09-19 07:58:00 +03:00
Commit Graph

64 Commits

Author SHA1 Message Date
Johan Walles
f1061202ba Extract search highlighting into its own Line method 2021-05-22 11:25:44 +02:00
Johan Walles
78ab63273a Move some test-only code into *_test.go files 2021-05-08 10:32:04 +02:00
Johan Walles
7a66c633ca Lower memory usage by 10%
When loading a 577MB file.
2021-05-02 19:28:51 +02:00
Johan Walles
e63871ae6a Make BenchmarkPlainTextSearch() 15+% faster
By shortcutting styledStringsFromString() on strings without ESC
characters.
2021-04-26 20:06:08 +02:00
Johan Walles
ef612a5620 Make BenchmarkPlainTextSearch() 50+% faster
By using a strings.Builder rather than roll-our-own to build a plain
string.
2021-04-26 08:55:42 +02:00
Johan Walles
860dcd22b4 Make BenchmarkPlainTextSearch() 30% faster
By taking a cheap shortcut on strings without backspace characters.
2021-04-26 06:53:30 +02:00
Johan Walles
f5273fc4b3 Make BenchmarkPlainTextSearch() 20% faster
Using shortcuts on lines without backspace characters.
2021-04-26 06:40:30 +02:00
Johan Walles
7520c9fc8a Make BenchmarkPlainTextSearch() 12% faster
By optimizing plainification for all-ASCII strings.
2021-04-26 00:24:21 +02:00
Johan Walles
a5f43e3d2f Do plaintext like we do cells
This loses a lot of the performance improvement originally intended, but
this is still 30% faster than before.

Good step in the right direction.
2021-04-25 23:50:14 +02:00
Johan Walles
d6d428f0ef Plaintext broken UTF8 as ?
Just like we render it, but without the coloring.
2021-04-25 23:38:59 +02:00
Johan Walles
17d8a0372a Render unprintable characters as highlighted '?' 2021-04-25 23:35:47 +02:00
Johan Walles
e7995bc40f Use ? to mark unprintable chars in plain text rendering 2021-04-25 20:40:38 +02:00
Johan Walles
5e2a63dc36 Handle backspace while plaintexting
More tests pass now, still not all though.
2021-04-24 23:15:53 +02:00
Johan Walles
66175f02aa Initial tab expansion
Tests still fail, but not as many.
2021-04-24 20:49:58 +02:00
Johan Walles
2c20fc31fe Special case stripping string formatting
Stripping string formatting is on the hot path while searching. This
change makes BenchmarkPlainTextSearch() over 7x faster.

But it also has problems with tab expansion so some tests fail, let's
see how we should handle that.
2021-04-24 17:20:09 +02:00
Johan Walles
c8a8cb4517 Improve memory usage for large files
Test:
* Open a 35MB text file
* Search it for some string it doesn't contain

Memory usage before this change: 2.3GB
Memory usage after this change:  0.5GB

The trick is to only provide cells from Lines on demand, since we only
need a few of them when scrolling.

And not storing all of those cells is where the gain in memory usage
comes from.
2021-04-24 16:27:54 +02:00
Steven Penny
374378b979 DeInit: make output copy friendly
With DeInit turned off, the current output cannot be usefully copied, as no
newlines are emitted. Close the screen in all cases now, but if DeInit is
disabled, reprint the current buffer with newlines after the screen is closed.

Fixes #51
2021-04-23 19:24:05 -05:00
Johan Walles
f8e658f0e3 Fix staticcheck reported errors 2021-04-20 06:46:14 +02:00
Johan Walles
90e374601d Use our new tcell replacement 2021-04-17 22:29:41 +02:00
Johan Walles
ee299fa02c Improve search performance 2021-04-14 21:41:56 +02:00
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