1
1
mirror of https://github.com/walles/moar.git synced 2024-11-30 12:42:26 +03:00
Commit Graph

101 Commits

Author SHA1 Message Date
Johan Walles
b96c88c4d1 Support retaining screen contents after exit
Fixes #76.
2021-11-09 18:56:02 +01:00
Johan Walles
8b242b42bc Fix rendering of empty input
Before this change, we rendered empty input as one empty line.

With this change in place, we now put the EOF marker at the top, where
it belongs if there's nothing to show.
2021-05-31 19:08:19 +02:00
Johan Walles
f7bad91000 Move search pattern into ScreenLines struct 2021-05-30 15:33:28 +02:00
Johan Walles
002d4e40af Modify input lines rendering API
Make the rendering function return both the lines and the clipped
first-line-number value.
2021-05-30 11:36:07 +02:00
Johan Walles
5ff126946a Move pager lines rendering into its own file 2021-05-29 14:38:41 +02:00
Johan Walles
ecae4811de Test that scolling to the last line works
It doesn't when we're in word wrapping mode.

Not sure how to fix this, but it is broken.
2021-05-27 06:48:51 +02:00
Johan Walles
4efa280389 Make wrapping optional
And controllable from within by pressing 'w'.

NOTE: Wrapping should:
    * default to off
    * be controllable from the command line
    * documented
	* not mean we get the displayed line numbers wrong in the status line
2021-05-22 15:41:44 +02:00
Johan Walles
dcf8bbf9a7 Always wrap
NOTE: Wrapping should:
* be optional
* documented
* default to off
* be controllable from the command line
* be possible to toggle by pressing 'w'
2021-05-22 15:41:44 +02:00
Johan Walles
dc070cad75 Left pad line numbers
Fixes a regression, this is what it used to look like and should have
looked like all the time.
2021-05-22 15:39:39 +02:00
Johan Walles
f1061202ba Extract search highlighting into its own Line method 2021-05-22 11:25:44 +02:00
Johan Walles
7e3a73f053 Three-group line numbers
Both the numbers in the left column of the file display, and the line
numbers shown in the page footer.

Fixes #6.
2021-05-21 19:38:10 +02:00
Johan Walles
2b1e87b8ac Log received events on trace level
If you run moar with -trace you'll get all handled events listed after
moar finishes running.

The point would be to be able to debug mishandled keys as reported
in #55.

Also, remove the screen refresh timings from the trace logs. We get lots
of them, and I don't think they provide value any more.
2021-05-13 19:12:44 +02:00
Johan Walles
e3e8be0b8b CTRL-L isn't a thing any more
Since replacing tcell with twin, we now redraw the whole screen every
time, and CTRL-L doesn't do anything.
2021-05-08 15:05:12 +02:00
Johan Walles
3f4113e7bf Log trace events on user input 2021-04-29 23:04:12 +02:00
Johan Walles
90e374601d Use our new tcell replacement 2021-04-17 22:29:41 +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
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
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
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
2583747590 Bump tcell to v2
Should fix 21.
2020-12-06 14:47:10 +01:00
Johan Walles
64b2fd4b73 "h" and "l" for moving left and right
vim inspired.

Fixes #25.
2020-09-05 20:31:21 +02: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
64015a0f90 CTRL-l to refresh screen
Useful if the screen has become garbled, this can happen when piping in
from something writing to both stdout and stderr.
2019-11-28 18:14:09 +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
69c007eab4 Make search find formatted text
Fixes #19
2019-11-26 21:20:09 +01:00
Johan Walles
2df60227cf Tell user about Reader errors
diff --git m/pager.go m/pager.go
index 51a007a..070e39d 100644
--- m/pager.go
+++ m/pager.go
@@ -763,5 +763,7 @@ func (p *Pager) StartPaging(logger *log.Logger, screen tcell.Screen) {
 		p._Redraw(logger, spinner)
 	}

-	// FIXME: Log p.reader.err if it's non-nil
+	if p.reader.err != nil {
+		logger.Printf("Reader reported an error: %s", p.reader.err.Error())
+	}
 }

Change-Id: Ia848d1daced793c39ebd4518fa144627a2598ecd
2019-11-19 15:18:40 +01:00
Johan Walles
2e931b400c Scroll left / right to show / hide line numbers
diff --git m/pager.go m/pager.go
index 0e22e83..51a007a 100644
--- m/pager.go
+++ m/pager.go
@@ -504,6 +504,16 @@ func (p *Pager) _OnSearchKey(logger *log.Logger, key tcell.Key) {
 }

 func (p *Pager) _MoveRight(delta int) {
+	if p.showLineNumbers && delta > 0 {
+		p.showLineNumbers = false
+		return
+	}
+
+	if p.leftColumnZeroBased == 0 && delta < 0 {
+		p.showLineNumbers = true
+		return
+	}
+
 	result := p.leftColumnZeroBased + delta
 	if result < 0 {
 		p.leftColumnZeroBased = 0

Change-Id: I69040ac8da06e50b866fdb6b678a0ae54fe0a84f
2019-11-19 11:41:56 +01:00
Johan Walles
7de5025257 Extract sideways scrolling into its own method
diff --git m/pager.go m/pager.go
index ec81976..0e22e83 100644
--- m/pager.go
+++ m/pager.go
@@ -41,7 +41,7 @@ type Pager struct {
 	isShowingHelp bool
 	preHelpState  *_PreHelpState

-	lineNumbersWanted bool
+	showLineNumbers bool
 }

 type _PreHelpState struct {
@@ -101,7 +101,7 @@ func NewPager(r *Reader) *Pager {
 		reader:            r,
 		quit:              false,
 		firstLineOneBased: 1,
-		lineNumbersWanted: true,
+		showLineNumbers:   true,
 	}
 }

@@ -211,7 +211,7 @@ func (p *Pager) _AddLines(logger *log.Logger, spinner string) {
 	lastLineOneBased := lines.firstLineOneBased + len(lines.lines) - 1
 	maxPrefixLength := len(strconv.Itoa(lastLineOneBased)) + 1

-	if !p.lineNumbersWanted {
+	if !p.showLineNumbers {
 		maxPrefixLength = 0
 	}

@@ -503,6 +503,15 @@ func (p *Pager) _OnSearchKey(logger *log.Logger, key tcell.Key) {
 	}
 }

+func (p *Pager) _MoveRight(delta int) {
+	result := p.leftColumnZeroBased + delta
+	if result < 0 {
+		p.leftColumnZeroBased = 0
+	} else {
+		p.leftColumnZeroBased = result
+	}
+}
+
 func (p *Pager) _OnKey(logger *log.Logger, key tcell.Key) {
 	if p.mode == _Searching {
 		p._OnSearchKey(logger, key)
@@ -528,13 +537,10 @@ func (p *Pager) _OnKey(logger *log.Logger, key tcell.Key) {
 		p.firstLineOneBased++

 	case tcell.KeyRight:
-		p.leftColumnZeroBased += 16
+		p._MoveRight(16)

 	case tcell.KeyLeft:
-		p.leftColumnZeroBased -= 16
-		if p.leftColumnZeroBased < 0 {
-			p.leftColumnZeroBased = 0
-		}
+		p._MoveRight(-16)

 	case tcell.KeyHome:
 		p.firstLineOneBased = 1
@@ -718,13 +724,10 @@ func (p *Pager) StartPaging(logger *log.Logger, screen tcell.Screen) {
 				p.firstLineOneBased++

 			case tcell.WheelRight:
-				p.leftColumnZeroBased += 16
+				p._MoveRight(16)

 			case tcell.WheelLeft:
-				p.leftColumnZeroBased -= 16
-				if p.leftColumnZeroBased < 0 {
-					p.leftColumnZeroBased = 0
-				}
+				p._MoveRight(-16)
 			}

 		case *tcell.EventResize:

Change-Id: I5925876d42ec3cd7b8486bb96eb47b81c6855032
2019-11-19 11:38:41 +01:00
Johan Walles
47b34c3351 Make line number configurable through code
diff --git m/pager.go m/pager.go
index 2133057..ec81976 100644
--- m/pager.go
+++ m/pager.go
@@ -40,6 +40,8 @@ type Pager struct {

 	isShowingHelp bool
 	preHelpState  *_PreHelpState
+
+	lineNumbersWanted bool
 }

 type _PreHelpState struct {
@@ -60,6 +62,7 @@ Quitting
 Moving around
 -------------
 * Arrow keys
+* Left / right can be used to hide / show line numbers
 * PageUp / 'b' and PageDown / 'f'
 * Half page 'u'p / 'd'own
 * Home and End for start / end of the document
@@ -98,6 +101,7 @@ func NewPager(r *Reader) *Pager {
 		reader:            r,
 		quit:              false,
 		firstLineOneBased: 1,
+		lineNumbersWanted: true,
 	}
 }

@@ -106,7 +110,7 @@ func (p *Pager) _AddLine(logger *log.Logger, fileLineNumber *int, maxPrefixLengt

 	prefixLength := 0
 	lineNumberString := ""
-	if fileLineNumber != nil {
+	if maxPrefixLength > 0 && fileLineNumber != nil {
 		prefixLength = maxPrefixLength
 		lineNumberString = fmt.Sprintf("%*d ", prefixLength-1, *fileLineNumber)
 	}
@@ -207,6 +211,10 @@ func (p *Pager) _AddLines(logger *log.Logger, spinner string) {
 	lastLineOneBased := lines.firstLineOneBased + len(lines.lines) - 1
 	maxPrefixLength := len(strconv.Itoa(lastLineOneBased)) + 1

+	if !p.lineNumbersWanted {
+		maxPrefixLength = 0
+	}
+
 	screenLineNumber := 0
 	for i, line := range lines.lines {
 		lineNumber := p.firstLineOneBased + i

Change-Id: I79cd0d46e8590cfdcd9e0ee942eeb0098676d25d
2019-11-19 11:28:28 +01:00
Johan Walles
a14f756d99 Adaptive width of the line numbers column
diff --git m/pager.go m/pager.go
index 0c7c3a2..2133057 100644
--- m/pager.go
+++ m/pager.go
@@ -5,6 +5,7 @@ import (
 	"log"
 	"os"
 	"regexp"
+	"strconv"
 	"time"
 	"unicode"
 	"unicode/utf8"
@@ -100,13 +101,13 @@ func NewPager(r *Reader) *Pager {
 	}
 }

-func (p *Pager) _AddLine(logger *log.Logger, fileLineNumber *int, screenLineNumber int, line string) {
+func (p *Pager) _AddLine(logger *log.Logger, fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line string) {
 	screenWidth, _ := p.screen.Size()

 	prefixLength := 0
 	lineNumberString := ""
 	if fileLineNumber != nil {
-		prefixLength = 3
+		prefixLength = maxPrefixLength
 		lineNumberString = fmt.Sprintf("%*d ", prefixLength-1, *fileLineNumber)
 	}

@@ -200,10 +201,16 @@ func (p *Pager) _AddLines(logger *log.Logger, spinner string) {
 	// display starts scrolling visibly.
 	p.firstLineOneBased = lines.firstLineOneBased

+	// Count the length of the last line number
+	//
+	// Offsets figured out through trial-and-error...
+	lastLineOneBased := lines.firstLineOneBased + len(lines.lines) - 1
+	maxPrefixLength := len(strconv.Itoa(lastLineOneBased)) + 1
+
 	screenLineNumber := 0
 	for i, line := range lines.lines {
 		lineNumber := p.firstLineOneBased + i
-		p._AddLine(logger, &lineNumber, screenLineNumber, line)
+		p._AddLine(logger, &lineNumber, maxPrefixLength, screenLineNumber, line)
 		screenLineNumber++
 	}

@@ -212,7 +219,7 @@ func (p *Pager) _AddLines(logger *log.Logger, spinner string) {
 		// This happens when we're done
 		eofSpinner = "---"
 	}
-	p._AddLine(logger, nil, screenLineNumber, _EofMarkerFormat+eofSpinner)
+	p._AddLine(logger, nil, 0, screenLineNumber, _EofMarkerFormat+eofSpinner)

 	switch p.mode {
 	case _Searching:

Change-Id: I7ab67a61048557fd11cd9a044dbae5c13264f492
2019-11-19 11:24:01 +01:00
Johan Walles
fd286d4004 Mandatory line numbers, badly formatted
diff --git m/pager.go m/pager.go
index 2c2736b..0c7c3a2 100644
--- m/pager.go
+++ m/pager.go
@@ -22,6 +22,9 @@ const (
 	_NotFound  _PagerMode = 2
 )

+// Styling of line numbers
+var _NumberStyle = tcell.StyleDefault.Dim(true)
+
 // Pager is the main on-screen pager
 type Pager struct {
 	reader              *Reader
@@ -97,17 +100,32 @@ func NewPager(r *Reader) *Pager {
 	}
 }

-func (p *Pager) _AddLine(logger *log.Logger, lineNumber int, line string) {
-	width, _ := p.screen.Size()
-	tokens := _CreateScreenLine(logger, lineNumber, p.leftColumnZeroBased, width, line, p.searchPattern)
+func (p *Pager) _AddLine(logger *log.Logger, fileLineNumber *int, screenLineNumber int, line string) {
+	screenWidth, _ := p.screen.Size()
+
+	prefixLength := 0
+	lineNumberString := ""
+	if fileLineNumber != nil {
+		prefixLength = 3
+		lineNumberString = fmt.Sprintf("%*d ", prefixLength-1, *fileLineNumber)
+	}
+
+	for column, digit := range lineNumberString {
+		if column >= prefixLength {
+			break
+		}
+
+		p.screen.SetContent(column, screenLineNumber, digit, nil, _NumberStyle)
+	}
+
+	tokens := _CreateScreenLine(logger, p.leftColumnZeroBased, screenWidth-prefixLength, line, p.searchPattern)
 	for column, token := range tokens {
-		p.screen.SetContent(column, lineNumber, token.Rune, nil, token.Style)
+		p.screen.SetContent(column+prefixLength, screenLineNumber, token.Rune, nil, token.Style)
 	}
 }

 func _CreateScreenLine(
 	logger *log.Logger,
-	lineNumber int,
 	stringIndexAtColumnZero int,
 	screenColumnsCount int,
 	line string,
@@ -183,8 +201,9 @@ func (p *Pager) _AddLines(logger *log.Logger, spinner string) {
 	p.firstLineOneBased = lines.firstLineOneBased

 	screenLineNumber := 0
-	for _, line := range lines.lines {
-		p._AddLine(logger, screenLineNumber, line)
+	for i, line := range lines.lines {
+		lineNumber := p.firstLineOneBased + i
+		p._AddLine(logger, &lineNumber, screenLineNumber, line)
 		screenLineNumber++
 	}

@@ -193,7 +212,7 @@ func (p *Pager) _AddLines(logger *log.Logger, spinner string) {
 		// This happens when we're done
 		eofSpinner = "---"
 	}
-	p._AddLine(logger, screenLineNumber, _EofMarkerFormat+eofSpinner)
+	p._AddLine(logger, nil, screenLineNumber, _EofMarkerFormat+eofSpinner)

 	switch p.mode {
 	case _Searching:

Change-Id: I2cafedb3e8a87c88564982f42819b16e911c6a1b
2019-11-19 11:12:23 +01:00
Johan Walles
8d37e9cfe8 Sideways mouse wheel scrolling
diff --git m/pager.go m/pager.go
index 16e91e7..2c2736b 100644
--- m/pager.go
+++ m/pager.go
@@ -682,6 +682,15 @@ func (p *Pager) StartPaging(logger *log.Logger, screen tcell.Screen) {
 			case tcell.WheelDown:
 				// Clipping is done in _AddLines()
 				p.firstLineOneBased++
+
+			case tcell.WheelRight:
+				p.leftColumnZeroBased += 16
+
+			case tcell.WheelLeft:
+				p.leftColumnZeroBased -= 16
+				if p.leftColumnZeroBased < 0 {
+					p.leftColumnZeroBased = 0
+				}
 			}

 		case *tcell.EventResize:

Change-Id: Ibe36c3d88392dda1f4d6e7be182cf5ea5d168703
2019-11-14 07:46:54 +01:00
Johan Walles
7fcf99b006 Handle mouse events
Fixes #16
diff --git m/pager.go m/pager.go
index b7cce09..16e91e7 100644
--- m/pager.go
+++ m/pager.go
@@ -611,6 +611,7 @@ func (p *Pager) StartPaging(logger *log.Logger, screen tcell.Screen) {
 	}

 	p.screen = screen
+	screen.EnableMouse()
 	screen.Show()
 	p._Redraw(logger, "")

@@ -672,6 +673,17 @@ func (p *Pager) StartPaging(logger *log.Logger, screen tcell.Screen) {
 				p._OnKey(logger, ev.Key())
 			}

+		case *tcell.EventMouse:
+			switch ev.Buttons() {
+			case tcell.WheelUp:
+				// Clipping is done in _AddLines()
+				p.firstLineOneBased--
+
+			case tcell.WheelDown:
+				// Clipping is done in _AddLines()
+				p.firstLineOneBased++
+			}
+
 		case *tcell.EventResize:
 			// We'll be implicitly redrawn just by taking another lap in the loop

Change-Id: I3c9971077de9a720b90d6d91960f7f33a3a089e3
2019-11-14 07:45:15 +01:00
Johan Walles
b0cf8bc4c8 Fix a crash on 32 bit systems
Fixes #18
diff --git m/pager.go m/pager.go
index 178c828..b7cce09 100644
--- m/pager.go
+++ m/pager.go
@@ -3,7 +3,6 @@ package m
 import (
 	"fmt"
 	"log"
-	"math"
 	"os"
 	"regexp"
 	"time"
@@ -507,7 +506,7 @@ func (p *Pager) _OnKey(logger *log.Logger, key tcell.Key) {
 		p.firstLineOneBased = 1

 	case tcell.KeyEnd:
-		p.firstLineOneBased = math.MaxInt32
+		p.firstLineOneBased = p.reader.GetLineCount()

 	case tcell.KeyPgDn:
 		_, height := p.screen.Size()
@@ -565,7 +564,7 @@ func (p *Pager) _OnRune(logger *log.Logger, char rune) {
 		p.firstLineOneBased = 1

 	case '>', 'G':
-		p.firstLineOneBased = math.MaxInt32
+		p.firstLineOneBased = p.reader.GetLineCount()

 	case 'f', ' ':
 		_, height := p.screen.Size()

Change-Id: I17712d93322703ae3fa5b54a5a07169ddd1357c4
2019-11-13 16:17:26 +01:00
Johan Walles
fb3b6abbe7 Pass the tests
But crash the app.

Do:
* ./moar.sh sample-files/long-and-wide.txt
* Search (/) for "monkey"
2019-11-06 20:30:59 +01:00
Johan Walles
ab0e62123b WIP: Line rendering tests
Implement search hits tests as well.
2019-11-05 21:09:45 +01:00
Johan Walles
742401ec82 Fix crash removing unicode char from search string
Fixes #12
2019-11-02 12:19:04 +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
ad0f174651 Make loading spinner more obvious 2019-10-29 22:05:42 +01:00
Johan Walles
7a4e5a36bc Fix a warning 2019-10-25 19:24:37 +02:00
Johan Walles
d3e3dc75a9 Animate the EOF marker 2019-10-16 06:09:21 +02:00
Johan Walles
b13c904c51 Add rolling ... to status bar while loading 2019-10-16 06:01:49 +02:00
Johan Walles
2b5828275c Exit search on scroll keypresses 2019-08-04 07:40:26 +02:00
Johan Walles
19f477b8e7 Add "---" as an EOF-marker for short files 2019-08-04 07:15:35 +02:00
Johan Walles
5bb74e4f10 Fix search highlighting
After scrolling right.
2019-08-03 23:21:40 +02:00
Johan Walles
df9920c11c Add scroll-right markers
For lines extending to the right of the screen width.
2019-08-03 20:14:22 +02:00
Johan Walles
193805abc9 Re-plan future development 2019-07-26 19:40:40 +02:00