1
1
mirror of https://github.com/walles/moar.git synced 2024-11-27 01:05:23 +03:00
moar/m/pager.go

799 lines
18 KiB
Go
Raw Normal View History

package m
import (
"fmt"
"os"
2019-06-29 19:29:37 +03:00
"regexp"
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 13:24:01 +03:00
"strconv"
"time"
"unicode"
"unicode/utf8"
log "github.com/sirupsen/logrus"
2020-10-30 10:19:13 +03:00
"github.com/gdamore/tcell/v2"
)
// FIXME: Profile the pager while searching through a large file
2019-07-06 08:45:07 +03:00
type _PagerMode int
const (
_Viewing _PagerMode = 0
_Searching _PagerMode = 1
_NotFound _PagerMode = 2
)
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 13:12:23 +03:00
// Styling of line numbers
var _NumberStyle = tcell.StyleDefault.Dim(true)
// Pager is the main on-screen pager
2019-10-25 20:24:37 +03:00
type Pager struct {
reader *Reader
2019-07-06 14:33:41 +03:00
screen tcell.Screen
quit bool
firstLineOneBased int
leftColumnZeroBased int
2019-07-06 08:45:07 +03:00
mode _PagerMode
2019-06-29 19:29:37 +03:00
searchString string
searchPattern *regexp.Regexp
isShowingHelp bool
preHelpState *_PreHelpState
// NewPager shows lines by default, this field can hide them
ShowLineNumbers bool
}
type _PreHelpState struct {
reader *Reader
firstLineOneBased int
leftColumnZeroBased int
}
2019-10-16 07:09:21 +03:00
const _EofMarkerFormat = "\x1b[7m" // Reverse video
var _HelpReader = NewReaderFromText("Help", `
2019-07-26 20:15:24 +03:00
Welcome to Moar, the nice pager!
Quitting
--------
* Press 'q' or ESC to quit
Miscellaneous
-------------
* Press CTRL-l to refresh screen if it has become garbled
2019-07-26 20:15:24 +03:00
Moving around
-------------
* Arrow keys
* 'h', 'l' for left and right (as in vim)
* Left / right can be used to hide / show line numbers
2019-07-26 20:15:24 +03:00
* PageUp / 'b' and PageDown / 'f'
* Half page 'u'p / 'd'own
* Home and End for start / end of the document
* < to go to the start of the document
* > to go to the end of the document
* RETURN moves down one line
* SPACE moves down a page
Searching
---------
* Type / to start searching, then type what you want to find
* Type RETURN to stop searching
* Find next by typing 'n' (for "next")
* Find previous by typing SHIFT-N or 'p' (for "previous")
* Search is case sensitive if it contains any UPPER CASE CHARACTERS
* Search is interpreted as a regexp if it is a valid one
Reporting bugs
--------------
File issues at https://github.com/walles/moar/issues, or post
questions to johan.walles@gmail.com.
Installing Moar as your default pager
-------------------------------------
Put the following line in your .bashrc or .bash_profile:
export PAGER=/usr/local/bin/moar.rb
Source Code
-----------
Available at https://github.com/walles/moar/.
`)
// NewPager creates a new Pager
2019-10-25 20:24:37 +03:00
func NewPager(r *Reader) *Pager {
return &Pager{
reader: r,
2019-06-15 10:23:53 +03:00
quit: false,
firstLineOneBased: 1,
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 13:38:41 +03:00
showLineNumbers: true,
}
}
func (p *Pager) _AddLine(fileLineNumber *int, numberPrefixLength int, screenLineNumber int, line *Line) {
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 13:12:23 +03:00
screenWidth, _ := p.screen.Size()
lineNumberString := ""
if numberPrefixLength > 0 && fileLineNumber != nil {
lineNumberString = fmt.Sprintf("%*d ", numberPrefixLength-1, *fileLineNumber)
} else {
numberPrefixLength = 0
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 13:12:23 +03:00
}
for column, digit := range lineNumberString {
if column >= numberPrefixLength {
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 13:12:23 +03:00
break
}
p.screen.SetContent(column, screenLineNumber, digit, nil, _NumberStyle)
}
tokens := createScreenLine(p.leftColumnZeroBased, screenWidth-numberPrefixLength, line, p.searchPattern)
for column, token := range tokens {
p.screen.SetContent(column+numberPrefixLength, screenLineNumber, token.Rune, nil, token.Style)
}
}
func createScreenLine(
stringIndexAtColumnZero int,
screenColumnsCount int,
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 12:42:34 +03:00
line *Line,
search *regexp.Regexp,
) []Token {
var returnMe []Token
searchHitDelta := 0
if stringIndexAtColumnZero > 0 {
// Indicate that it's possible to scroll left
returnMe = append(returnMe, Token{
Rune: '<',
Style: tcell.StyleDefault.Reverse(true),
})
searchHitDelta = -1
2019-07-06 14:33:41 +03:00
}
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 12:42:34 +03:00
if stringIndexAtColumnZero >= len(line.Tokens()) {
// Nothing (more) to display, never mind
return returnMe
2019-07-06 14:33:41 +03:00
}
2019-06-29 19:29:37 +03:00
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 12:42:34 +03:00
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.
returnMe[len(returnMe)-1] = Token{
Rune: '>',
Style: tcell.StyleDefault.Reverse(true),
}
break
}
2019-06-29 19:29:37 +03:00
style := token.Style
if matchRanges.InRange(len(returnMe) + stringIndexAtColumnZero + searchHitDelta) {
// Search hits in reverse video
2019-06-29 19:29:37 +03:00
style = style.Reverse(true)
}
returnMe = append(returnMe, Token{
Rune: token.Rune,
Style: style,
})
2019-06-12 08:07:13 +03:00
}
return returnMe
2019-06-12 08:07:13 +03:00
}
2019-10-25 20:24:37 +03:00
func (p *Pager) _AddSearchFooter() {
_, height := p.screen.Size()
pos := 0
for _, token := range "Search: " + p.searchString {
p.screen.SetContent(pos, height-1, token, nil, tcell.StyleDefault)
pos++
}
2019-06-29 12:20:48 +03:00
// Add a cursor
p.screen.SetContent(pos, height-1, ' ', nil, tcell.StyleDefault.Reverse(true))
}
func (p *Pager) _AddLines(spinner string) {
2019-07-06 08:45:07 +03:00
_, height := p.screen.Size()
wantedLineCount := height - 1
2019-06-11 19:52:38 +03:00
lines := p.reader.GetLines(p.firstLineOneBased, wantedLineCount)
2019-06-12 08:07:13 +03:00
// If we're asking for past-the-end lines, the Reader will clip for us,
// and we should adapt to that. Otherwise if you scroll 100 lines past
// the end, you'll then have to scroll 100 lines up again before the
// display starts scrolling visibly.
p.firstLineOneBased = lines.firstLineOneBased
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 13:24:01 +03:00
// Count the length of the last line number
//
// Offsets figured out through trial-and-error...
lastLineOneBased := lines.firstLineOneBased + len(lines.lines) - 1
numberPrefixLength := len(strconv.Itoa(lastLineOneBased)) + 1
if numberPrefixLength < 4 {
// 4 = space for 3 digits followed by one whitespace
//
// https://github.com/walles/moar/issues/38
numberPrefixLength = 4
}
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 13:24:01 +03:00
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 13:38:41 +03:00
if !p.showLineNumbers {
numberPrefixLength = 0
}
screenLineNumber := 0
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 13:12:23 +03:00
for i, line := range lines.lines {
lineNumber := p.firstLineOneBased + i
p._AddLine(&lineNumber, numberPrefixLength, screenLineNumber, line)
screenLineNumber++
2019-06-11 19:52:38 +03:00
}
2019-06-14 08:08:20 +03:00
2019-10-16 07:09:21 +03:00
eofSpinner := spinner
if eofSpinner == "" {
// This happens when we're done
eofSpinner = "---"
}
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 12:42:34 +03:00
spinnerLine := NewLine(_EofMarkerFormat + eofSpinner)
p._AddLine(nil, 0, screenLineNumber, spinnerLine)
2019-07-06 08:45:07 +03:00
switch p.mode {
case _Searching:
p._AddSearchFooter()
2019-07-06 08:45:07 +03:00
case _NotFound:
p._SetFooter("Not found: " + p.searchString)
case _Viewing:
helpText := "Press ESC / q to exit, '/' to search, '?' for help"
if p.isShowingHelp {
helpText = "Press ESC / q to exit help, '/' to search"
}
p._SetFooter(lines.statusText + spinner + " " + helpText)
2019-07-06 08:45:07 +03:00
default:
2019-07-06 08:54:33 +03:00
panic(fmt.Sprint("Unsupported pager mode: ", p.mode))
}
2019-07-06 08:45:07 +03:00
}
2019-10-25 20:24:37 +03:00
func (p *Pager) _SetFooter(footer string) {
2019-07-06 08:45:07 +03:00
width, height := p.screen.Size()
pos := 0
footerStyle := tcell.StyleDefault.Reverse(true)
2019-07-06 08:45:07 +03:00
for _, token := range footer {
p.screen.SetContent(pos, height-1, token, nil, footerStyle)
pos++
2019-06-14 08:08:20 +03:00
}
for ; pos < width; pos++ {
p.screen.SetContent(pos, height-1, ' ', nil, footerStyle)
2019-06-14 08:08:20 +03:00
}
2019-06-12 08:07:13 +03:00
}
func (p *Pager) _Redraw(spinner string) {
2019-06-11 22:09:57 +03:00
p.screen.Clear()
2019-06-11 19:52:38 +03:00
p._AddLines(spinner)
2019-06-11 19:52:38 +03:00
p.screen.Show()
2019-06-11 19:52:38 +03:00
}
2019-10-25 20:24:37 +03:00
// Quit leaves the help screen or quits the pager
func (p *Pager) Quit() {
if !p.isShowingHelp {
p.quit = true
return
}
// Reset help
p.isShowingHelp = false
p.reader = p.preHelpState.reader
p.firstLineOneBased = p.preHelpState.firstLineOneBased
p.leftColumnZeroBased = p.preHelpState.leftColumnZeroBased
p.preHelpState = nil
2019-06-11 22:21:12 +03:00
}
func (p *Pager) _ScrollToSearchHits() {
2019-06-30 23:16:04 +03:00
if p.searchPattern == nil {
// This is not a search
return
}
firstHitLine := p._FindFirstHitLineOneBased(p.firstLineOneBased, false)
2019-07-06 08:45:07 +03:00
if firstHitLine == nil {
// No match, give up
return
}
if *firstHitLine <= p._GetLastVisibleLineOneBased() {
// Already on-screen, never mind
return
}
p.firstLineOneBased = *firstHitLine
}
2019-10-25 20:24:37 +03:00
func (p *Pager) _GetLastVisibleLineOneBased() int {
2019-06-30 23:16:04 +03:00
firstVisibleLineOneBased := p.firstLineOneBased
_, windowHeight := p.screen.Size()
// If first line is 1 and window is 2 high, and one line is the status
// line, the last line will be 1 + 2 - 2 = 1
2019-07-06 08:45:07 +03:00
return firstVisibleLineOneBased + windowHeight - 2
}
func (p *Pager) _FindFirstHitLineOneBased(firstLineOneBased int, backwards bool) *int {
2019-07-06 08:45:07 +03:00
lineNumber := firstLineOneBased
2019-06-30 23:16:04 +03:00
for {
line := p.reader.GetLine(lineNumber)
if line == nil {
// No match, give up
2019-07-06 08:45:07 +03:00
return nil
2019-06-30 23:16:04 +03:00
}
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 12:42:34 +03:00
lineText := line.Plain()
if p.searchPattern.MatchString(lineText) {
2019-07-06 08:45:07 +03:00
return &lineNumber
2019-06-30 23:16:04 +03:00
}
2019-07-06 11:31:40 +03:00
if backwards {
lineNumber--
} else {
lineNumber++
}
2019-06-30 23:16:04 +03:00
}
}
func (p *Pager) _ScrollToNextSearchHit() {
2019-07-06 08:45:07 +03:00
if p.searchPattern == nil {
// Nothing to search for, never mind
return
}
2019-07-06 11:31:40 +03:00
if p.reader.GetLineCount() == 0 {
// Nothing to search in, never mind
return
}
2019-07-06 08:45:07 +03:00
var firstSearchLineOneBased int
switch p.mode {
case _Viewing:
// Start searching on the first line below the bottom of the screen
firstSearchLineOneBased = p._GetLastVisibleLineOneBased() + 1
case _NotFound:
// Restart searching from the top
p.mode = _Viewing
firstSearchLineOneBased = 1
default:
2019-07-06 08:54:33 +03:00
panic(fmt.Sprint("Unknown search mode when finding next: ", p.mode))
2019-07-06 08:45:07 +03:00
}
firstHitLine := p._FindFirstHitLineOneBased(firstSearchLineOneBased, false)
2019-07-06 11:31:40 +03:00
if firstHitLine == nil {
p.mode = _NotFound
return
}
p.firstLineOneBased = *firstHitLine
}
func (p *Pager) _ScrollToPreviousSearchHit() {
2019-07-06 11:31:40 +03:00
if p.searchPattern == nil {
// Nothing to search for, never mind
return
}
if p.reader.GetLineCount() == 0 {
// Nothing to search in, never mind
return
}
var firstSearchLineOneBased int
switch p.mode {
case _Viewing:
// Start searching on the first line above the top of the screen
firstSearchLineOneBased = p.firstLineOneBased - 1
case _NotFound:
// Restart searching from the bottom
p.mode = _Viewing
firstSearchLineOneBased = p.reader.GetLineCount()
default:
panic(fmt.Sprint("Unknown search mode when finding previous: ", p.mode))
}
firstHitLine := p._FindFirstHitLineOneBased(firstSearchLineOneBased, true)
2019-07-06 08:45:07 +03:00
if firstHitLine == nil {
p.mode = _NotFound
return
}
p.firstLineOneBased = *firstHitLine
}
func (p *Pager) _UpdateSearchPattern() {
2020-12-30 00:57:44 +03:00
p.searchPattern = toPattern(p.searchString)
p._ScrollToSearchHits()
// FIXME: If the user is typing, indicate to user if we didn't find anything
}
2019-06-29 19:29:37 +03:00
2020-12-30 00:57:44 +03:00
// toPattern compiles a search string into a pattern.
//
// If the string contains only lower-case letter the pattern will be case insensitive.
//
// If the string is empty the pattern will be nil.
//
// If the string does not compile into a regexp the pattern will match the string verbatim
2020-12-30 00:57:44 +03:00
func toPattern(compileMe string) *regexp.Regexp {
if len(compileMe) == 0 {
return nil
}
hasUppercase := false
for _, char := range compileMe {
if unicode.IsUpper(char) {
hasUppercase = true
}
}
// Smart case; be case insensitive unless there are upper case chars
// in the search string
prefix := "(?i)"
if hasUppercase {
prefix = ""
}
pattern, err := regexp.Compile(prefix + compileMe)
2019-06-29 19:29:37 +03:00
if err == nil {
// Search string is a regexp
return pattern
2019-06-29 19:29:37 +03:00
}
pattern, err = regexp.Compile(prefix + regexp.QuoteMeta(compileMe))
2019-06-29 19:29:37 +03:00
if err == nil {
// Pattern matching the string exactly
return pattern
2019-06-29 19:29:37 +03:00
}
// Unable to create a match-string-verbatim pattern
panic(err)
}
// From: https://stackoverflow.com/a/57005674/473672
func removeLastChar(s string) string {
r, size := utf8.DecodeLastRuneInString(s)
if r == utf8.RuneError && (size == 0 || size == 1) {
size = 0
}
return s[:len(s)-size]
}
func (p *Pager) _OnSearchKey(key tcell.Key) {
switch key {
case tcell.KeyEscape, tcell.KeyEnter:
2019-07-06 08:45:07 +03:00
p.mode = _Viewing
2019-06-29 12:20:48 +03:00
case tcell.KeyBackspace, tcell.KeyDEL:
if len(p.searchString) == 0 {
return
}
p.searchString = removeLastChar(p.searchString)
p._UpdateSearchPattern()
2019-06-29 12:20:48 +03:00
2019-08-04 08:40:26 +03:00
case tcell.KeyUp:
// Clipping is done in _AddLines()
p.firstLineOneBased--
p.mode = _Viewing
case tcell.KeyDown:
// Clipping is done in _AddLines()
p.firstLineOneBased++
p.mode = _Viewing
case tcell.KeyPgUp:
_, height := p.screen.Size()
p.firstLineOneBased -= (height - 1)
p.mode = _Viewing
case tcell.KeyPgDn:
_, height := p.screen.Size()
p.firstLineOneBased += (height - 1)
p.mode = _Viewing
default:
log.Debugf("Unhandled search key event %v", key)
}
}
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 13:38:41 +03:00
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
}
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 13:38:41 +03:00
result := p.leftColumnZeroBased + delta
if result < 0 {
p.leftColumnZeroBased = 0
} else {
p.leftColumnZeroBased = result
}
}
func (p *Pager) _OnKey(key tcell.Key) {
if key == tcell.KeyCtrlL {
// This is useful when we're piping in from something writing to both
// stdout and stderr.
p.screen.Sync()
return
}
2019-07-06 08:45:07 +03:00
if p.mode == _Searching {
p._OnSearchKey(key)
return
}
2019-07-06 08:54:33 +03:00
if p.mode != _Viewing && p.mode != _NotFound {
panic(fmt.Sprint("Unhandled mode: ", p.mode))
}
2019-07-06 08:45:07 +03:00
// Reset the not-found marker on non-search keypresses
p.mode = _Viewing
switch key {
2019-06-13 16:56:06 +03:00
case tcell.KeyEscape:
p.Quit()
case tcell.KeyUp:
// Clipping is done in _AddLines()
p.firstLineOneBased--
2019-06-13 16:56:06 +03:00
case tcell.KeyDown, tcell.KeyEnter:
// Clipping is done in _AddLines()
p.firstLineOneBased++
2019-06-13 07:14:41 +03:00
2019-07-06 14:33:41 +03:00
case tcell.KeyRight:
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 13:38:41 +03:00
p._MoveRight(16)
2019-07-06 14:33:41 +03:00
case tcell.KeyLeft:
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 13:38:41 +03:00
p._MoveRight(-16)
2019-07-06 08:45:07 +03:00
2019-06-13 07:14:41 +03:00
case tcell.KeyHome:
p.firstLineOneBased = 1
case tcell.KeyEnd:
p.firstLineOneBased = p.reader.GetLineCount()
2019-06-13 07:14:41 +03:00
2019-06-13 07:21:43 +03:00
case tcell.KeyPgDn:
_, height := p.screen.Size()
p.firstLineOneBased += (height - 1)
case tcell.KeyPgUp:
_, height := p.screen.Size()
p.firstLineOneBased -= (height - 1)
2019-06-16 11:02:19 +03:00
default:
log.Debugf("Unhandled key event %v", key)
}
}
func (p *Pager) _OnSearchRune(char rune) {
2019-06-29 12:20:48 +03:00
p.searchString = p.searchString + string(char)
p._UpdateSearchPattern()
}
func (p *Pager) _OnRune(char rune) {
2019-07-06 08:45:07 +03:00
if p.mode == _Searching {
p._OnSearchRune(char)
return
}
2019-07-06 08:54:33 +03:00
if p.mode != _Viewing && p.mode != _NotFound {
panic(fmt.Sprint("Unhandled mode: ", p.mode))
}
2019-06-11 22:32:24 +03:00
switch char {
case 'q':
p.Quit()
2019-06-13 07:21:43 +03:00
case '?':
if !p.isShowingHelp {
p.preHelpState = &_PreHelpState{
reader: p.reader,
firstLineOneBased: p.firstLineOneBased,
leftColumnZeroBased: p.leftColumnZeroBased,
}
p.reader = _HelpReader
p.firstLineOneBased = 1
p.leftColumnZeroBased = 0
p.isShowingHelp = true
}
2019-06-13 16:56:06 +03:00
case 'k', 'y':
// Clipping is done in _AddLines()
p.firstLineOneBased--
case 'j', 'e':
// Clipping is done in _AddLines()
p.firstLineOneBased++
case 'l':
// vim right
p._MoveRight(16)
case 'h':
// vim left
p._MoveRight(-16)
2019-06-13 07:14:41 +03:00
case '<', 'g':
p.firstLineOneBased = 1
2019-06-13 07:21:43 +03:00
2019-06-13 07:14:41 +03:00
case '>', 'G':
p.firstLineOneBased = p.reader.GetLineCount()
2019-06-13 07:21:43 +03:00
2019-06-13 16:56:06 +03:00
case 'f', ' ':
2019-06-13 07:21:43 +03:00
_, height := p.screen.Size()
p.firstLineOneBased += (height - 1)
case 'b':
_, height := p.screen.Size()
p.firstLineOneBased -= (height - 1)
2019-07-26 20:15:24 +03:00
case 'u':
_, height := p.screen.Size()
p.firstLineOneBased -= (height / 2)
case 'd':
_, height := p.screen.Size()
p.firstLineOneBased += (height / 2)
case '/':
2019-07-06 08:45:07 +03:00
p.mode = _Searching
2019-06-30 23:15:27 +03:00
p.searchString = ""
p.searchPattern = nil
2019-07-06 08:45:07 +03:00
case 'n':
p._ScrollToNextSearchHit()
2019-07-06 08:45:07 +03:00
2019-07-06 11:31:40 +03:00
case 'p', 'N':
p._ScrollToPreviousSearchHit()
2019-07-06 08:45:07 +03:00
2019-06-16 11:02:19 +03:00
default:
log.Debugf("Unhandled rune keypress '%s'", string(char))
2019-06-11 22:32:24 +03:00
}
}
// StartPaging brings up the pager on screen
func (p *Pager) StartPaging(screen tcell.Screen) {
// We want to match the terminal theme, see screen.Init() source code
os.Setenv("TCELL_TRUECOLOR", "disable")
SetManPageFormatFromEnv()
if e := screen.Init(); e != nil {
fmt.Fprintf(os.Stderr, "%v\n", e)
os.Exit(1)
}
p.screen = screen
screen.EnableMouse()
screen.Show()
p._Redraw("")
go func() {
for {
// Wait for new lines to appear
<-p.reader.moreLinesAdded
screen.PostEvent(tcell.NewEventInterrupt(nil))
// Delay updates a bit so that we don't waste time refreshing
// the screen too often.
//
// Note that the delay is *after* reacting, this way single-line
// updates are reacted to immediately, and the first output line
// read will appear on screen without delay.
time.Sleep(200 * time.Millisecond)
}
}()
go func() {
done := false
2019-10-30 00:05:42 +03:00
spinnerFrames := [...]string{"/.\\", "-o-", "\\O/", "| |"}
spinnerIndex := 0
for {
// Break this loop on the reader.done signal...
select {
case <-p.reader.done:
done = true
default:
// This default case makes this an async read
}
if done {
break
}
screen.PostEvent(tcell.NewEventInterrupt(spinnerFrames[spinnerIndex]))
spinnerIndex++
if spinnerIndex >= len(spinnerFrames) {
spinnerIndex = 0
}
time.Sleep(200 * time.Millisecond)
}
// Empty our spinner, loading done!
screen.PostEvent(tcell.NewEventInterrupt(""))
}()
// Main loop
spinner := ""
2019-06-15 10:23:53 +03:00
for !p.quit {
ev := screen.PollEvent()
switch ev := ev.(type) {
case *tcell.EventKey:
if ev.Key() == tcell.KeyRune {
p._OnRune(ev.Rune())
2019-06-15 10:23:53 +03:00
} else {
p._OnKey(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.WheelRight:
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 13:38:41 +03:00
p._MoveRight(16)
case tcell.WheelLeft:
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 13:38:41 +03:00
p._MoveRight(-16)
}
2019-06-15 10:23:53 +03:00
case *tcell.EventResize:
// We'll be implicitly redrawn just by taking another lap in the loop
2019-06-16 11:02:19 +03:00
case *tcell.EventInterrupt:
// This means we got more lines, look for NewEventInterrupt higher up
// in this file. Doing nothing here is fine, the refresh happens after
// this switch statement.
data := ev.Data()
if data != nil {
// From: https://yourbasic.org/golang/interface-to-string/
spinner = fmt.Sprintf("%v", data)
}
2019-06-16 11:02:19 +03:00
default:
log.Warnf("Unhandled event type: %v", ev)
}
2019-06-22 00:24:53 +03:00
// FIXME: If more events are ready, skip this redraw, that
// should speed up mouse wheel scrolling
p._Redraw(spinner)
2019-06-15 10:23:53 +03:00
}
2019-07-11 19:34:10 +03:00
if p.reader.err != nil {
log.Warnf("Reader reported an error: %s", p.reader.err.Error())
}
}