1
1
mirror of https://github.com/walles/moar.git synced 2024-11-11 00:27:04 +03:00
Commit Graph

285 Commits

Author SHA1 Message Date
Johan Walles
9e0d48e866 Fix arrow keys handling 2021-04-17 22:29:45 +02:00
Johan Walles
9b86fc5736 Windows: Color output, character input
Arrow keys don't work.
2021-04-17 22:29:45 +02:00
Johan Walles
9f9dc37594 Remove a superfluous : 2021-04-17 22:29:45 +02:00
Johan Walles
e5ec2ce19a Open CONIN$ the usual way 2021-04-17 22:29:45 +02:00
Johan Walles
e72baff02e Try getting hold of the console on Windows 2021-04-17 22:29:45 +02:00
Johan Walles
90e374601d Use our new tcell replacement 2021-04-17 22:29:41 +02:00
Johan Walles
44a064c024 Write tcell replacement
Fixes #37 and fixes #34.
2021-04-17 22:24:40 +02:00
Johan Walles
26754cde93
Merge pull request #41 from 89z/master
m: make Page a method
2021-04-17 20:47:38 +02:00
Steven Penny
fed03bf413 Update Readme example 2021-04-17 13:00:59 -05:00
Steven Penny
8b9e7f104b Merge branch 'master' of https://github.com/walles/moar 2021-04-17 12:28:06 -05:00
Johan Walles
8884096935
Merge pull request #43 from walles/walles/windows-ci
Run CI on Windows
2021-04-15 15:01:32 +02:00
Johan Walles
e88aa3f43e Skip xz compressed file in one more place 2021-04-15 14:57:56 +02:00
Johan Walles
9b86ae7b19 Fix long-line test
On Windows the sample file gets cloned with Windows newlines, breaking
the file-size assumption.

And having a constant here makes the test simpler and better anyway,
Windows or not.
2021-04-15 14:51:51 +02:00
Johan Walles
e502b68bc9 Only test xz decompression if xz binary is available 2021-04-15 13:24:24 +02:00
Johan Walles
4bba75645b Use our own empty file rather than /dev/null
Needed for unit testing on Windows.
2021-04-15 13:20:00 +02:00
Johan Walles
ae6f4ae810 Run CI on Windows as well 2021-04-15 13:12:32 +02:00
Johan Walles
ee299fa02c Improve search performance 2021-04-14 21:41:56 +02:00
Steven Penny
f3dd0389e5 m: make Page a method
Page is a great function, but it is limited, because it makes too many choices
for the user. The global DeInit was a good start, but really that shouldve always
been a Pager field.

Move DeInit inside Pager, and set default to true as before in the NewPager
function. Then, users can call Page, but still have control over DeInit, as well
as Line Numbers, and not have to worry about creating a Screen. Example code:

    r := m.NewReaderFromText("moar", strings.Repeat("moar\n", 99))
    p := m.NewPager(r)
    p.DeInit, p.ShowLineNumbers = false, false
    p.Page()
2021-04-11 21:55:03 -05:00
Johan Walles
94041129b8
Merge pull request #40 from 89z/master
Expose ShowLineNumbers
2021-04-11 08:03:57 +02:00
Steven Penny
9f09e4491c ShowLineNumbers: complete implementation 2021-04-10 00:16:34 -05:00
Steven Penny
cb4f3af742 Expose ShowLineNumbers
Currently the library has no way to configure line numbers, so they are just
forced on for all programs. Leave NewPager default as is, but expose option so
numbers can be turned off if wanted.
2021-04-10 00:12:21 -05:00
Johan Walles
4692fb00f9 Improve log timestamp precision
Because I want to know how long certain sub-second things take.
2021-04-06 16:22:40 +02:00
Johan Walles
019021c50a
Merge pull request #39 from 89z/master
Add DeInit variable
2021-04-06 08:11:17 +02:00
Johan Walles
a11e2d5d64
Add a reference to the special shutdown sequence 2021-04-06 08:09:20 +02:00
Steven Penny
17bc66a8e3 Add DeInit variable
This allows users to control whether the screen is cleared on return. Default is
to clear the screen. Example use:

    r := m.NewReaderFromText("dir", strings.Repeat("north\n", 50))
    m.DeInit = false
    m.Page(r)

Fixes #33
2021-04-05 16:30:33 -05:00
Johan Walles
5343d1f627 Always make room for 3 line number digits
Fixes #38
2021-01-12 21:53:09 +01: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
db4acb57ef Make binaries somewhat smaller 2021-01-10 15:50:55 +01:00
Johan Walles
b12114ef56 Merge branch 'walles/chroma'
This removes our dependency on an externally installed highlight.
2021-01-10 15:27:14 +01:00
Johan Walles
0bca4a42a1 No we shouldn't
That's done by the pager, and basenaming here breaks testing.
2021-01-10 15:26:14 +01:00
Johan Walles
4715bff950 Update test to match Chroma's highlighting 2021-01-10 15:16:22 +01:00
Johan Walles
fc24862ac9 Don't highlighting text files
Costs performance, breaks tests, no obvious upside.
2021-01-10 15:00:53 +01:00
Johan Walles
722464d2b0 Fix a failing test 2021-01-10 14:36:59 +01:00
Johan Walles
2b12953600 Fix a broken test 2021-01-10 08:42:34 +01:00
Johan Walles
54935615ed Switch highlighting engine to Chroma
Which is linked into here, so no need for users to install any external
tools.

Chroma home page: https://github.com/alecthomas/chroma
2021-01-09 17:18:10 +01:00
Johan Walles
570d780bc2 NewReaderFromStream(): Make name mandatory
This makes the API somewhat simpler to use. To support not providing any
name we still allow the empty string as the name, and document that
thoroughly.
2020-12-30 19:00:03 +01:00
Johan Walles
f0dead13ad Move historical releases into their own directory
Because of <https://github.com/microsoft/vscode/issues/38878> vscode
doesn't handle having old releases in the top dir. This change moves
them away.
2020-12-30 18:43:25 +01:00
Johan Walles
ccf2af496e Merge branch 'walles/api-cleanup'
This creates a public API that has received at least some thought,
unlike the previous one which mostly happened by accident.
2020-12-30 15:19:47 +01:00
Johan Walles
0ea058db7c Tune embedding example 2020-12-30 15:19:19 +01:00
Johan Walles
2b18347022 Make a lot fewer functions public 2020-12-29 22:57:44 +01:00
Johan Walles
f069409725 Make the paging function accept a Reader
This should cover all use cases with just one method.
2020-12-29 17:22:18 +01:00
Johan Walles
7f7b0107d5 Make the correct reader constructors public 2020-12-29 17:19:56 +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
d7e0e7e8ff Add embedding instructions
Improves #31 with both documentation and a simpler API.
2020-12-28 19:27:25 +01:00
Johan Walles
17ac7e8e62 Mark string readers complete from the start
Related to #31.

Before this change, string readers always rendered as in-progress.

With this change in place, they instead always render as complete.
2020-12-28 18:44:28 +01:00
Johan Walles
eace381664 Merge branch 'walles/bump-tcell-to-v2'
In theory this should fix #21, but I don't know how to validate.
2020-12-06 21:38:01 +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
3e4591fd90 Fix install instructions
Fixes #30.
2020-11-18 16:27:58 +01:00