diff --git a/README.md b/README.md index 32f9228..60058a1 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ Installing And now you can just invoke `moar` from the prompt! +Try `moar --help` to see options. + If a binary for your platform is not available, please [file a ticket](https://github.com/walles/moar/releases) or contact . @@ -155,9 +157,7 @@ TODO * Start at a certain line if run as "moar.rb file.txt:42" -* Redefine 'g' without any prefix to prompt for which line to go - to. This definition makes more sense to me than having to prefix 'g' - to jump. +* Define 'g' to prompt for a line number to go to. * Handle search hits to the right of the right screen edge. Searching forwards should move first right, then to the left edge and @@ -171,6 +171,11 @@ TODO * Retain the search string when pressing / to search a second time. +* [Word wrap text rather than character wrap it](m/linewrapper.go). + +* Arrow keys up / down while in wrapped mode should scroll by screen line, not + by input file line. + Done ---- diff --git a/m/linewrapper.go b/m/linewrapper.go new file mode 100644 index 0000000..978cb2d --- /dev/null +++ b/m/linewrapper.go @@ -0,0 +1,23 @@ +package m + +import "github.com/walles/moar/twin" + +func wrapLine(width int, line []twin.Cell) [][]twin.Cell { + if len(line) == 0 { + return [][]twin.Cell{{}} + } + + wrapped := make([][]twin.Cell, 0, len(line)/width) + for len(line) > width { + firstPart := line[:width] + wrapped = append(wrapped, firstPart) + + line = line[width:] + } + + if len(line) > 0 { + wrapped = append(wrapped, line) + } + + return wrapped +} diff --git a/m/linewrapper_test.go b/m/linewrapper_test.go new file mode 100644 index 0000000..a49d98b --- /dev/null +++ b/m/linewrapper_test.go @@ -0,0 +1,47 @@ +package m + +import ( + "reflect" + "testing" + + "github.com/walles/moar/twin" + "gotest.tools/assert" +) + +func tokenize(input string) []twin.Cell { + line := NewLine(input) + return line.HighlightedTokens(nil) +} + +func TestEnoughRoomNoWrapping(t *testing.T) { + toWrap := tokenize("This is a test") + wrapped := wrapLine(20, toWrap) + assert.Assert(t, reflect.DeepEqual(wrapped, [][]twin.Cell{toWrap})) +} + +func TestWrapEmpty(t *testing.T) { + empty := tokenize("") + wrapped := wrapLine(20, empty) + assert.Assert(t, reflect.DeepEqual(wrapped, [][]twin.Cell{empty})) +} + +func TestWordLongerThanLine(t *testing.T) { + toWrap := tokenize("intermediary") + wrapped := wrapLine(6, toWrap) + assert.Assert(t, reflect.DeepEqual(wrapped, [][]twin.Cell{ + tokenize("interm"), + tokenize("ediary"), + })) +} + +// FIXME: Test word wrapping + +// FIXME: Test wrapping with multiple consecutive spaces + +// FIXME: Test wrapping on single dashes + +// FIXME: Test wrapping with double dashes (not sure what we should do with those) + +// FIXME: Test wrapping formatted strings, is there formatting that should affect the wrapping + +// FIXME: Test wrapping with trailing whitespace diff --git a/m/pager.go b/m/pager.go index 46e84ca..bf1dc46 100644 --- a/m/pager.go +++ b/m/pager.go @@ -11,8 +11,6 @@ import ( "github.com/walles/moar/twin" ) -// FIXME: Profile the pager while searching through a large file - type _PagerMode int const ( @@ -48,6 +46,8 @@ type Pager struct { // NewPager shows lines by default, this field can hide them ShowLineNumbers bool + WrapLongLines bool + // If true, pager will clear the screen on return. If false, pager will // clear the last line, and show the cursor. DeInit bool @@ -64,9 +64,10 @@ const _EofMarkerFormat = "\x1b[7m" // Reverse video var _HelpReader = NewReaderFromText("Help", ` Welcome to Moar, the nice pager! -Quitting --------- +Miscellaneous +------------- * Press 'q' or ESC to quit +* Press 'w' to toggle wrapping of long lines Moving around ------------- @@ -116,7 +117,7 @@ func NewPager(r *Reader) *Pager { } } -func (p *Pager) _AddLine(fileLineNumber *int, numberPrefixLength int, screenLineNumber int, cells []twin.Cell) { +func (p *Pager) addLine(fileLineNumber *int, numberPrefixLength int, screenLineNumber int, cells []twin.Cell) { screenWidth, _ := p.screen.Size() lineNumberString := "" @@ -128,8 +129,6 @@ func (p *Pager) _AddLine(fileLineNumber *int, numberPrefixLength int, screenLine "lineNumberString <%s> longer than numberPrefixLength %d", lineNumberString, numberPrefixLength)) } - } else { - numberPrefixLength = 0 } for column, digit := range lineNumberString { @@ -196,7 +195,7 @@ func (p *Pager) _AddSearchFooter() { } func (p *Pager) _AddLines(spinner string) { - _, height := p.screen.Size() + width, height := p.screen.Size() wantedLineCount := height - 1 lines := p.reader.GetLines(p.firstLineOneBased, wantedLineCount) @@ -224,10 +223,37 @@ func (p *Pager) _AddLines(spinner string) { } screenLineNumber := 0 - for i, line := range lines.lines { - lineNumber := p.firstLineOneBased + i - p._AddLine(&lineNumber, numberPrefixLength, screenLineNumber, line.HighlightedTokens(p.searchPattern)) - screenLineNumber++ + screenFull := false + for lineIndex, line := range lines.lines { + lineNumber := p.firstLineOneBased + lineIndex + + highlighted := line.HighlightedTokens(p.searchPattern) + var wrapped [][]twin.Cell + if p.WrapLongLines { + wrapped = wrapLine(width-numberPrefixLength, highlighted) + } else { + // All on one line + wrapped = [][]twin.Cell{highlighted} + } + + for wrapIndex, linePart := range wrapped { + visibleLineNumber := &lineNumber + if wrapIndex > 0 { + visibleLineNumber = nil + } + p.addLine(visibleLineNumber, numberPrefixLength, screenLineNumber, linePart) + screenLineNumber++ + + if screenLineNumber >= height-1 { + // We have shown all the lines that can fit on the screen + screenFull = true + break + } + } + + if screenFull { + break + } } eofSpinner := spinner @@ -236,7 +262,7 @@ func (p *Pager) _AddLines(spinner string) { eofSpinner = "---" } spinnerLine := cellsFromString(_EofMarkerFormat + eofSpinner) - p._AddLine(nil, 0, screenLineNumber, spinnerLine) + p.addLine(nil, 0, screenLineNumber, spinnerLine) switch p.mode { case _Searching: @@ -661,6 +687,9 @@ func (p *Pager) _OnRune(char rune) { case 'p', 'N': p._ScrollToPreviousSearchHit() + case 'w': + p.WrapLongLines = !p.WrapLongLines + default: log.Debugf("Unhandled rune keypress '%s'", string(char)) } diff --git a/m/reader.go b/m/reader.go index 5935e50..467db72 100644 --- a/m/reader.go +++ b/m/reader.go @@ -454,12 +454,14 @@ func (r *Reader) _CreateStatusUnlocked(firstLineOneBased int, lastLineOneBased i return prefix + "" } + if len(r.lines) == 1 { + return prefix + "1 line 100%" + } + percent := int(100 * float64(lastLineOneBased) / float64(len(r.lines))) - return fmt.Sprintf("%s%s-%s/%s %d%%", + return fmt.Sprintf("%s%s lines %d%%", prefix, - formatNumber(uint(firstLineOneBased)), - formatNumber(uint(lastLineOneBased)), formatNumber(uint(len(r.lines))), percent) } diff --git a/m/reader_test.go b/m/reader_test.go index c85c353..c18762c 100644 --- a/m/reader_test.go +++ b/m/reader_test.go @@ -259,12 +259,12 @@ func testStatusText(t *testing.T, fromLine int, toLine int, totalLines int, expe } func TestStatusText(t *testing.T) { - testStatusText(t, 1, 10, 20, "1-10/20 50%") - testStatusText(t, 1, 5, 5, "1-5/5 100%") - testStatusText(t, 998, 999, 1000, "998-999/1_000 99%") + testStatusText(t, 1, 10, 20, "20 lines 50%") + testStatusText(t, 1, 5, 5, "5 lines 100%") + testStatusText(t, 998, 999, 1000, "1_000 lines 99%") testStatusText(t, 0, 0, 0, "") - testStatusText(t, 1, 1, 1, "1-1/1 100%") + testStatusText(t, 1, 1, 1, "1 line 100%") // Test with filename testMe, err := NewReaderFromFilename(getSamplesDir()+"/empty", *styles.Native, formatters.TTY16m) diff --git a/moar.go b/moar.go index 88cd012..5906266 100644 --- a/moar.go +++ b/moar.go @@ -136,6 +136,7 @@ func main() { printVersion := flagSet.Bool("version", false, "Prints the moar version number") debug := flagSet.Bool("debug", false, "Print debug logs after exiting") trace := flagSet.Bool("trace", false, "Print trace logs after exiting") + wrap := flagSet.Bool("wrap", false, "Wrap long lines") styleOption := flagSet.String("style", "native", "Highlighting style from https://xyproto.github.io/splash/docs/longer/all.html") colorsOption := flagSet.String("colors", "16M", "Highlighting palette size: 8, 16, 256, 16M") @@ -235,7 +236,7 @@ func main() { if stdinIsRedirected { // Display input pipe contents reader := m.NewReaderFromStream("", os.Stdin) - startPaging(reader) + startPaging(reader, *wrap) return } @@ -245,10 +246,10 @@ func main() { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) os.Exit(1) } - startPaging(reader) + startPaging(reader, *wrap) } -func startPaging(reader *m.Reader) { +func startPaging(reader *m.Reader, wrapLongLines bool) { screen, e := twin.NewScreen() if e != nil { panic(e) @@ -276,5 +277,7 @@ func startPaging(reader *m.Reader) { }() log.SetOutput(&loglines) - m.NewPager(reader).StartPaging(screen) + pager := m.NewPager(reader) + pager.WrapLongLines = wrapLongLines + pager.StartPaging(screen) } diff --git a/screenshot.png b/screenshot.png index 465d75c..9bbfff3 100644 Binary files a/screenshot.png and b/screenshot.png differ