mirror of
https://github.com/walles/moar.git
synced 2024-09-21 00:49:59 +03:00
commit
eea4afad18
11
README.md
11
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
|
||||
<johan.walles@gmail.com>.
|
||||
@ -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
|
||||
----
|
||||
|
||||
|
23
m/linewrapper.go
Normal file
23
m/linewrapper.go
Normal file
@ -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
|
||||
}
|
47
m/linewrapper_test.go
Normal file
47
m/linewrapper_test.go
Normal file
@ -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
|
53
m/pager.go
53
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))
|
||||
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))
|
||||
}
|
||||
|
@ -454,12 +454,14 @@ func (r *Reader) _CreateStatusUnlocked(firstLineOneBased int, lastLineOneBased i
|
||||
return prefix + "<empty>"
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -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, "<empty>")
|
||||
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)
|
||||
|
11
moar.go
11
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)
|
||||
}
|
||||
|
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
Before Width: | Height: | Size: 338 KiB After Width: | Height: | Size: 311 KiB |
Loading…
Reference in New Issue
Block a user