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

299 Commits

Author SHA1 Message Date
Johan Walles
ccfe3d3b20 Run "go vet" as part of testing 2021-04-19 15:29:28 +02:00
Johan Walles
d122901ca8 Add gomft -s flag
Adds code simplifications. Recommended online somewhere.
2021-04-19 15:20:33 +02:00
Johan Walles
1e211974e1 Merge branch 'walles/enforce-formatting' 2021-04-19 08:45:29 +02:00
Johan Walles
40c7a473c9 Run all tests in the source tree
Changed from "run all tests in github.com/walles/moar/m"
2021-04-19 08:41:09 +02:00
Johan Walles
ede1a8cbb3 Verify code formatting in ./test.sh 2021-04-19 08:41:09 +02:00
Johan Walles
0795329ac5 Format sources on build, but not in CI 2021-04-18 21:59:38 +02:00
Johan Walles
44dc59d365 Better error message on paging to non-tty 2021-04-18 18:37:55 +02:00
Johan Walles
60da229cab Merge branch 'walles/drop-tcell'
* Fixes #37
* Fixes #34
* Replaces #44
* Makes window resizing smoother.

Before this change, the main loop was roughly:
* Await one single user input event
* Handle single user input event
* Redraw screen

With this change in place, the main loop is:
* Await one single user input event (like before)
* Handle single user input event (like before)
* If there are no more events, redraw screen (new!)

The difference is huge if you do a fling scroll on a trackpad. Before,
scrolling was slow at constant speed, and the scrolling kept on at that
slow constant speed way longer than what it should have. Now, scrolling
feels just like it should.
2021-04-18 16:10:14 +02:00
Johan Walles
39ab1fc93d Don't panic on no-piped-stdin on Windows 2021-04-18 13:33:13 +02:00
Johan Walles
2f0df183c7 Do "go build" on Windows, not just "go test" 2021-04-18 12:18:36 +02:00
Johan Walles
1b292fa050 Improve messaging 2021-04-18 07:45:41 +02:00
Johan Walles
4e8c49ea8d Warn about getting piped data on Windows 2021-04-18 07:36:13 +02:00
Johan Walles
1e46089cff Fix build errors on Unix 2021-04-17 22:29:45 +02:00
Johan Walles
ff9380fe15 Restore console settings when done 2021-04-17 22:29:45 +02:00
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