mirror of
https://github.com/walles/moar.git
synced 2024-11-13 11:14:30 +03:00
Merge branch 'walles/api-cleanup'
This creates a public API that has received at least some thought, unlike the previous one which mostly happened by accident.
This commit is contained in:
commit
ccf2af496e
17
README.md
17
README.md
@ -75,24 +75,35 @@ you can send questions to <johan.walles@gmail.com>.
|
||||
Embedding
|
||||
---------
|
||||
|
||||
Here's how to embed `moar` in your app:
|
||||
Here's one way to embed `moar` in your app:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/walles/moar/m"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := m.PageString("Months", "March, April\nMay")
|
||||
buf := bytes.Buffer{}
|
||||
fmt.Fprintln(&buf, "March")
|
||||
fmt.Fprintln(&buf, "April")
|
||||
|
||||
name := "Months"
|
||||
err := m.Page(m.NewReaderFromStream(&name, &buf))
|
||||
if err != nil {
|
||||
// Handle pager problems
|
||||
// Handle paging problems
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `m.Page()` parameter can also be initialized using `NewReaderFromText()` or
|
||||
`NewReaderFromFilename()`.
|
||||
|
||||
Developing
|
||||
----------
|
||||
|
||||
|
@ -30,36 +30,36 @@ func SetManPageFormatFromEnv() {
|
||||
|
||||
lessTermcapMd := os.Getenv("LESS_TERMCAP_md")
|
||||
if lessTermcapMd != "" {
|
||||
manPageBold = _TermcapToStyle(lessTermcapMd)
|
||||
manPageBold = termcapToStyle(lessTermcapMd)
|
||||
}
|
||||
|
||||
lessTermcapUs := os.Getenv("LESS_TERMCAP_us")
|
||||
if lessTermcapUs != "" {
|
||||
manPageUnderline = _TermcapToStyle(lessTermcapUs)
|
||||
manPageUnderline = termcapToStyle(lessTermcapUs)
|
||||
}
|
||||
}
|
||||
|
||||
// Used from tests
|
||||
func _ResetManPageFormatForTesting() {
|
||||
func resetManPageFormatForTesting() {
|
||||
manPageBold = tcell.StyleDefault.Bold(true)
|
||||
manPageUnderline = tcell.StyleDefault.Underline(true)
|
||||
}
|
||||
|
||||
func _TermcapToStyle(termcap string) tcell.Style {
|
||||
func termcapToStyle(termcap string) tcell.Style {
|
||||
// Add a character to be sure we have one to take the format from
|
||||
tokens, _ := TokensFromString(termcap + "x")
|
||||
tokens, _ := tokensFromString(termcap + "x")
|
||||
return tokens[len(tokens)-1].Style
|
||||
}
|
||||
|
||||
// TokensFromString turns a (formatted) string into a series of tokens,
|
||||
// tokensFromString turns a (formatted) string into a series of tokens,
|
||||
// and an unformatted string
|
||||
func TokensFromString(s string) ([]Token, *string) {
|
||||
func tokensFromString(s string) ([]Token, *string) {
|
||||
var tokens []Token
|
||||
|
||||
styleBrokenUtf8 := tcell.StyleDefault.Background(tcell.ColorSilver).Foreground(tcell.ColorMaroon)
|
||||
|
||||
for _, styledString := range _StyledStringsFromString(s) {
|
||||
for _, token := range _TokensFromStyledString(styledString) {
|
||||
for _, styledString := range styledStringsFromString(s) {
|
||||
for _, token := range tokensFromStyledString(styledString) {
|
||||
switch token.Rune {
|
||||
|
||||
case '\x09': // TAB
|
||||
@ -103,7 +103,7 @@ func TokensFromString(s string) ([]Token, *string) {
|
||||
}
|
||||
|
||||
// Consume 'x<x', where '<' is backspace and the result is a bold 'x'
|
||||
func _ConsumeBold(runes []rune, index int) (int, *Token) {
|
||||
func consumeBold(runes []rune, index int) (int, *Token) {
|
||||
if index+2 >= len(runes) {
|
||||
// Not enough runes left for a bold
|
||||
return index, nil
|
||||
@ -127,7 +127,7 @@ func _ConsumeBold(runes []rune, index int) (int, *Token) {
|
||||
}
|
||||
|
||||
// Consume '_<x', where '<' is backspace and the result is an underlined 'x'
|
||||
func _ConsumeUnderline(runes []rune, index int) (int, *Token) {
|
||||
func consumeUnderline(runes []rune, index int) (int, *Token) {
|
||||
if index+2 >= len(runes) {
|
||||
// Not enough runes left for a underline
|
||||
return index, nil
|
||||
@ -153,7 +153,7 @@ func _ConsumeUnderline(runes []rune, index int) (int, *Token) {
|
||||
// Consume '+<+<o<o' / '+<o', where '<' is backspace and the result is a unicode bullet.
|
||||
//
|
||||
// Used on man pages, try "man printf" on macOS for one example.
|
||||
func _ConsumeBullet(runes []rune, index int) (int, *Token) {
|
||||
func consumeBullet(runes []rune, index int) (int, *Token) {
|
||||
patterns := []string{"+\bo", "+\b+\bo\bo"}
|
||||
for _, pattern := range patterns {
|
||||
if index+len(pattern) > len(runes) {
|
||||
@ -182,26 +182,26 @@ func _ConsumeBullet(runes []rune, index int) (int, *Token) {
|
||||
return index, nil
|
||||
}
|
||||
|
||||
func _TokensFromStyledString(styledString _StyledString) []Token {
|
||||
func tokensFromStyledString(styledString _StyledString) []Token {
|
||||
runes := []rune(styledString.String)
|
||||
tokens := make([]Token, 0, len(runes))
|
||||
|
||||
for index := 0; index < len(runes); index++ {
|
||||
nextIndex, token := _ConsumeBullet(runes, index)
|
||||
nextIndex, token := consumeBullet(runes, index)
|
||||
if nextIndex != index {
|
||||
tokens = append(tokens, *token)
|
||||
index = nextIndex - 1
|
||||
continue
|
||||
}
|
||||
|
||||
nextIndex, token = _ConsumeBold(runes, index)
|
||||
nextIndex, token = consumeBold(runes, index)
|
||||
if nextIndex != index {
|
||||
tokens = append(tokens, *token)
|
||||
index = nextIndex - 1
|
||||
continue
|
||||
}
|
||||
|
||||
nextIndex, token = _ConsumeUnderline(runes, index)
|
||||
nextIndex, token = consumeUnderline(runes, index)
|
||||
if nextIndex != index {
|
||||
tokens = append(tokens, *token)
|
||||
index = nextIndex - 1
|
||||
@ -222,7 +222,7 @@ type _StyledString struct {
|
||||
Style tcell.Style
|
||||
}
|
||||
|
||||
func _StyledStringsFromString(s string) []_StyledString {
|
||||
func styledStringsFromString(s string) []_StyledString {
|
||||
// This function was inspired by the
|
||||
// https://golang.org/pkg/regexp/#Regexp.Split source code
|
||||
|
||||
@ -247,7 +247,7 @@ func _StyledStringsFromString(s string) []_StyledString {
|
||||
}
|
||||
|
||||
matchedPart := s[match[0]:match[1]]
|
||||
style = _UpdateStyle(style, matchedPart)
|
||||
style = updateStyle(style, matchedPart)
|
||||
|
||||
beg = match[1]
|
||||
}
|
||||
@ -262,8 +262,8 @@ func _StyledStringsFromString(s string) []_StyledString {
|
||||
return styledStrings
|
||||
}
|
||||
|
||||
// _UpdateStyle parses a string of the form "ESC[33m" into changes to style
|
||||
func _UpdateStyle(style tcell.Style, escapeSequence string) tcell.Style {
|
||||
// updateStyle parses a string of the form "ESC[33m" into changes to style
|
||||
func updateStyle(style tcell.Style, escapeSequence string) tcell.Style {
|
||||
numbers := strings.Split(escapeSequence[2:len(escapeSequence)-1], ";")
|
||||
index := 0
|
||||
for index < len(numbers) {
|
||||
|
@ -17,7 +17,7 @@ import (
|
||||
// Verify that we can tokenize all lines in ../sample-files/*
|
||||
// without logging any errors
|
||||
func TestTokenize(t *testing.T) {
|
||||
for _, fileName := range _GetTestFiles() {
|
||||
for _, fileName := range getTestFiles() {
|
||||
file, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
t.Errorf("Error opening file <%s>: %s", fileName, err.Error())
|
||||
@ -34,7 +34,7 @@ func TestTokenize(t *testing.T) {
|
||||
var loglines strings.Builder
|
||||
log.SetOutput(&loglines)
|
||||
|
||||
tokens, plainString := TokensFromString(line)
|
||||
tokens, plainString := tokensFromString(line)
|
||||
if len(tokens) != utf8.RuneCountInString(*plainString) {
|
||||
t.Errorf("%s:%d: len(tokens)=%d, len(plainString)=%d for: <%s>",
|
||||
fileName, lineNumber,
|
||||
@ -51,7 +51,7 @@ func TestTokenize(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUnderline(t *testing.T) {
|
||||
tokens, _ := TokensFromString("a\x1b[4mb\x1b[24mc")
|
||||
tokens, _ := tokensFromString("a\x1b[4mb\x1b[24mc")
|
||||
assert.Equal(t, len(tokens), 3)
|
||||
assert.Equal(t, tokens[0], Token{Rune: 'a', Style: tcell.StyleDefault})
|
||||
assert.Equal(t, tokens[1], Token{Rune: 'b', Style: tcell.StyleDefault.Underline(true)})
|
||||
@ -60,14 +60,14 @@ func TestUnderline(t *testing.T) {
|
||||
|
||||
func TestManPages(t *testing.T) {
|
||||
// Bold
|
||||
tokens, _ := TokensFromString("ab\bbc")
|
||||
tokens, _ := tokensFromString("ab\bbc")
|
||||
assert.Equal(t, len(tokens), 3)
|
||||
assert.Equal(t, tokens[0], Token{Rune: 'a', Style: tcell.StyleDefault})
|
||||
assert.Equal(t, tokens[1], Token{Rune: 'b', Style: tcell.StyleDefault.Bold(true)})
|
||||
assert.Equal(t, tokens[2], Token{Rune: 'c', Style: tcell.StyleDefault})
|
||||
|
||||
// Underline
|
||||
tokens, _ = TokensFromString("a_\bbc")
|
||||
tokens, _ = tokensFromString("a_\bbc")
|
||||
assert.Equal(t, len(tokens), 3)
|
||||
assert.Equal(t, tokens[0], Token{Rune: 'a', Style: tcell.StyleDefault})
|
||||
assert.Equal(t, tokens[1], Token{Rune: 'b', Style: tcell.StyleDefault.Underline(true)})
|
||||
@ -75,7 +75,7 @@ func TestManPages(t *testing.T) {
|
||||
|
||||
// Bullet point 1, taken from doing this on my macOS system:
|
||||
// env PAGER="hexdump -C" man printf | moar
|
||||
tokens, _ = TokensFromString("a+\b+\bo\bob")
|
||||
tokens, _ = tokensFromString("a+\b+\bo\bob")
|
||||
assert.Equal(t, len(tokens), 3)
|
||||
assert.Equal(t, tokens[0], Token{Rune: 'a', Style: tcell.StyleDefault})
|
||||
assert.Equal(t, tokens[1], Token{Rune: '•', Style: tcell.StyleDefault})
|
||||
@ -83,7 +83,7 @@ func TestManPages(t *testing.T) {
|
||||
|
||||
// Bullet point 2, taken from doing this using the "fish" shell on my macOS system:
|
||||
// man printf | hexdump -C | moar
|
||||
tokens, _ = TokensFromString("a+\bob")
|
||||
tokens, _ = tokensFromString("a+\bob")
|
||||
assert.Equal(t, len(tokens), 3)
|
||||
assert.Equal(t, tokens[0], Token{Rune: 'a', Style: tcell.StyleDefault})
|
||||
assert.Equal(t, tokens[1], Token{Rune: '•', Style: tcell.StyleDefault})
|
||||
@ -179,6 +179,6 @@ func TestConsumeCompositeColorIncomplete24Bit(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdateStyle(t *testing.T) {
|
||||
numberColored := _UpdateStyle(tcell.StyleDefault, "\x1b[33m")
|
||||
numberColored := updateStyle(tcell.StyleDefault, "\x1b[33m")
|
||||
assert.Equal(t, numberColored, tcell.StyleDefault.Foreground(tcell.ColorOlive))
|
||||
}
|
||||
|
24
m/embed-api.go
Normal file
24
m/embed-api.go
Normal file
@ -0,0 +1,24 @@
|
||||
package m
|
||||
|
||||
import "github.com/gdamore/tcell/v2"
|
||||
|
||||
// Page displays text in a pager.
|
||||
//
|
||||
// The reader parameter can be constructed using one of:
|
||||
// * NewReaderFromFilename()
|
||||
// * NewReaderFromText()
|
||||
// * NewReaderFromStream()
|
||||
//
|
||||
// Or your could roll your own Reader based on the source code for any of those
|
||||
// constructors.
|
||||
func Page(reader *Reader) error {
|
||||
screen, e := tcell.NewScreen()
|
||||
if e != nil {
|
||||
// Screen setup failed
|
||||
return e
|
||||
}
|
||||
defer screen.Fini()
|
||||
|
||||
NewPager(reader).StartPaging(screen)
|
||||
return nil
|
||||
}
|
@ -7,19 +7,19 @@ type MatchRanges struct {
|
||||
Matches [][2]int
|
||||
}
|
||||
|
||||
// GetMatchRanges locates one or more regexp matches in a string
|
||||
func GetMatchRanges(String *string, Pattern *regexp.Regexp) *MatchRanges {
|
||||
// getMatchRanges locates one or more regexp matches in a string
|
||||
func getMatchRanges(String *string, Pattern *regexp.Regexp) *MatchRanges {
|
||||
if Pattern == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &MatchRanges{
|
||||
Matches: _ToRunePositions(Pattern.FindAllStringIndex(*String, -1), String),
|
||||
Matches: toRunePositions(Pattern.FindAllStringIndex(*String, -1), String),
|
||||
}
|
||||
}
|
||||
|
||||
// Convert byte indices to rune indices
|
||||
func _ToRunePositions(byteIndices [][]int, matchedString *string) [][2]int {
|
||||
func toRunePositions(byteIndices [][]int, matchedString *string) [][2]int {
|
||||
// FIXME: Will this function need to handle overlapping ranges?
|
||||
|
||||
var returnMe [][2]int
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
var _TestString = "mamma"
|
||||
|
||||
func TestGetMatchRanges(t *testing.T) {
|
||||
matchRanges := GetMatchRanges(&_TestString, regexp.MustCompile("m+"))
|
||||
matchRanges := getMatchRanges(&_TestString, regexp.MustCompile("m+"))
|
||||
assert.Equal(t, len(matchRanges.Matches), 2) // Two matches
|
||||
|
||||
assert.DeepEqual(t, matchRanges.Matches[0][0], 0) // First match starts at 0
|
||||
@ -22,14 +22,14 @@ func TestGetMatchRanges(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetMatchRangesNilPattern(t *testing.T) {
|
||||
matchRanges := GetMatchRanges(&_TestString, nil)
|
||||
matchRanges := getMatchRanges(&_TestString, nil)
|
||||
assert.Assert(t, matchRanges == nil)
|
||||
assert.Assert(t, !matchRanges.InRange(0))
|
||||
}
|
||||
|
||||
func TestInRange(t *testing.T) {
|
||||
// Should match the one in TestGetMatchRanges()
|
||||
matchRanges := GetMatchRanges(&_TestString, regexp.MustCompile("m+"))
|
||||
matchRanges := getMatchRanges(&_TestString, regexp.MustCompile("m+"))
|
||||
|
||||
assert.Assert(t, !matchRanges.InRange(-1)) // Before start
|
||||
assert.Assert(t, matchRanges.InRange(0)) // m
|
||||
@ -43,7 +43,7 @@ func TestInRange(t *testing.T) {
|
||||
func TestUtf8(t *testing.T) {
|
||||
// This test verifies that the match ranges are by rune rather than by byte
|
||||
unicodes := "-ä-ä-"
|
||||
matchRanges := GetMatchRanges(&unicodes, regexp.MustCompile("ä"))
|
||||
matchRanges := getMatchRanges(&unicodes, regexp.MustCompile("ä"))
|
||||
|
||||
assert.Assert(t, !matchRanges.InRange(0)) // -
|
||||
assert.Assert(t, matchRanges.InRange(1)) // ä
|
||||
@ -55,7 +55,7 @@ func TestUtf8(t *testing.T) {
|
||||
func TestNoMatch(t *testing.T) {
|
||||
// This test verifies that the match ranges are by rune rather than by byte
|
||||
unicodes := "gris"
|
||||
matchRanges := GetMatchRanges(&unicodes, regexp.MustCompile("apa"))
|
||||
matchRanges := getMatchRanges(&unicodes, regexp.MustCompile("apa"))
|
||||
|
||||
assert.Assert(t, !matchRanges.InRange(0))
|
||||
assert.Assert(t, !matchRanges.InRange(1))
|
||||
@ -67,7 +67,7 @@ func TestNoMatch(t *testing.T) {
|
||||
func TestEndMatch(t *testing.T) {
|
||||
// This test verifies that the match ranges are by rune rather than by byte
|
||||
unicodes := "-ä"
|
||||
matchRanges := GetMatchRanges(&unicodes, regexp.MustCompile("ä"))
|
||||
matchRanges := getMatchRanges(&unicodes, regexp.MustCompile("ä"))
|
||||
|
||||
assert.Assert(t, !matchRanges.InRange(0)) // -
|
||||
assert.Assert(t, matchRanges.InRange(1)) // ä
|
||||
|
16
m/pager.go
16
m/pager.go
@ -129,13 +129,13 @@ func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNum
|
||||
p.screen.SetContent(column, screenLineNumber, digit, nil, _NumberStyle)
|
||||
}
|
||||
|
||||
tokens := _CreateScreenLine(p.leftColumnZeroBased, screenWidth-prefixLength, line, p.searchPattern)
|
||||
tokens := createScreenLine(p.leftColumnZeroBased, screenWidth-prefixLength, line, p.searchPattern)
|
||||
for column, token := range tokens {
|
||||
p.screen.SetContent(column+prefixLength, screenLineNumber, token.Rune, nil, token.Style)
|
||||
}
|
||||
}
|
||||
|
||||
func _CreateScreenLine(
|
||||
func createScreenLine(
|
||||
stringIndexAtColumnZero int,
|
||||
screenColumnsCount int,
|
||||
line string,
|
||||
@ -152,13 +152,13 @@ func _CreateScreenLine(
|
||||
searchHitDelta = -1
|
||||
}
|
||||
|
||||
tokens, plainString := TokensFromString(line)
|
||||
tokens, plainString := tokensFromString(line)
|
||||
if stringIndexAtColumnZero >= len(tokens) {
|
||||
// Nothing (more) to display, never mind
|
||||
return returnMe
|
||||
}
|
||||
|
||||
matchRanges := GetMatchRanges(plainString, search)
|
||||
matchRanges := getMatchRanges(plainString, search)
|
||||
for _, token := range tokens[stringIndexAtColumnZero:] {
|
||||
if len(returnMe) >= screenColumnsCount {
|
||||
// We are trying to add a character to the right of the screen.
|
||||
@ -329,7 +329,7 @@ func (p *Pager) _FindFirstHitLineOneBased(firstLineOneBased int, backwards bool)
|
||||
return nil
|
||||
}
|
||||
|
||||
_, lineText := TokensFromString(*line)
|
||||
_, lineText := tokensFromString(*line)
|
||||
if p.searchPattern.MatchString(*lineText) {
|
||||
return &lineNumber
|
||||
}
|
||||
@ -413,21 +413,21 @@ func (p *Pager) _ScrollToPreviousSearchHit() {
|
||||
}
|
||||
|
||||
func (p *Pager) _UpdateSearchPattern() {
|
||||
p.searchPattern = ToPattern(p.searchString)
|
||||
p.searchPattern = toPattern(p.searchString)
|
||||
|
||||
p._ScrollToSearchHits()
|
||||
|
||||
// FIXME: If the user is typing, indicate to user if we didn't find anything
|
||||
}
|
||||
|
||||
// ToPattern compiles a search string into a pattern.
|
||||
// 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
|
||||
func ToPattern(compileMe string) *regexp.Regexp {
|
||||
func toPattern(compileMe string) *regexp.Regexp {
|
||||
if len(compileMe) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
205
m/pager_test.go
205
m/pager_test.go
@ -12,18 +12,18 @@ import (
|
||||
)
|
||||
|
||||
func TestUnicodeRendering(t *testing.T) {
|
||||
reader := NewReaderFromStream(strings.NewReader("åäö"), nil)
|
||||
reader := NewReaderFromStream(nil, strings.NewReader("åäö"))
|
||||
if err := reader._Wait(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var answers = []Token{
|
||||
_CreateExpectedCell('å', tcell.StyleDefault),
|
||||
_CreateExpectedCell('ä', tcell.StyleDefault),
|
||||
_CreateExpectedCell('ö', tcell.StyleDefault),
|
||||
createExpectedCell('å', tcell.StyleDefault),
|
||||
createExpectedCell('ä', tcell.StyleDefault),
|
||||
createExpectedCell('ö', tcell.StyleDefault),
|
||||
}
|
||||
|
||||
contents := _StartPaging(t, reader)
|
||||
contents := startPaging(t, reader)
|
||||
for pos, expected := range answers {
|
||||
expected.LogDifference(t, contents[pos])
|
||||
}
|
||||
@ -39,7 +39,7 @@ func (expected Token) LogDifference(t *testing.T, actual tcell.SimCell) {
|
||||
string(actual.Runes[0]), actual.Style)
|
||||
}
|
||||
|
||||
func _CreateExpectedCell(Rune rune, Style tcell.Style) Token {
|
||||
func createExpectedCell(Rune rune, Style tcell.Style) Token {
|
||||
return Token{
|
||||
Rune: Rune,
|
||||
Style: Style,
|
||||
@ -47,25 +47,25 @@ func _CreateExpectedCell(Rune rune, Style tcell.Style) Token {
|
||||
}
|
||||
|
||||
func TestFgColorRendering(t *testing.T) {
|
||||
reader := NewReaderFromStream(strings.NewReader(
|
||||
"\x1b[30ma\x1b[31mb\x1b[32mc\x1b[33md\x1b[34me\x1b[35mf\x1b[36mg\x1b[37mh\x1b[0mi"), nil)
|
||||
reader := NewReaderFromStream(nil, strings.NewReader(
|
||||
"\x1b[30ma\x1b[31mb\x1b[32mc\x1b[33md\x1b[34me\x1b[35mf\x1b[36mg\x1b[37mh\x1b[0mi"))
|
||||
if err := reader._Wait(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var answers = []Token{
|
||||
_CreateExpectedCell('a', tcell.StyleDefault.Foreground(tcell.ColorBlack)),
|
||||
_CreateExpectedCell('b', tcell.StyleDefault.Foreground(tcell.ColorMaroon)),
|
||||
_CreateExpectedCell('c', tcell.StyleDefault.Foreground(tcell.ColorGreen)),
|
||||
_CreateExpectedCell('d', tcell.StyleDefault.Foreground(tcell.ColorOlive)),
|
||||
_CreateExpectedCell('e', tcell.StyleDefault.Foreground(tcell.ColorNavy)),
|
||||
_CreateExpectedCell('f', tcell.StyleDefault.Foreground(tcell.ColorPurple)),
|
||||
_CreateExpectedCell('g', tcell.StyleDefault.Foreground(tcell.ColorTeal)),
|
||||
_CreateExpectedCell('h', tcell.StyleDefault.Foreground(tcell.ColorSilver)),
|
||||
_CreateExpectedCell('i', tcell.StyleDefault),
|
||||
createExpectedCell('a', tcell.StyleDefault.Foreground(tcell.ColorBlack)),
|
||||
createExpectedCell('b', tcell.StyleDefault.Foreground(tcell.ColorMaroon)),
|
||||
createExpectedCell('c', tcell.StyleDefault.Foreground(tcell.ColorGreen)),
|
||||
createExpectedCell('d', tcell.StyleDefault.Foreground(tcell.ColorOlive)),
|
||||
createExpectedCell('e', tcell.StyleDefault.Foreground(tcell.ColorNavy)),
|
||||
createExpectedCell('f', tcell.StyleDefault.Foreground(tcell.ColorPurple)),
|
||||
createExpectedCell('g', tcell.StyleDefault.Foreground(tcell.ColorTeal)),
|
||||
createExpectedCell('h', tcell.StyleDefault.Foreground(tcell.ColorSilver)),
|
||||
createExpectedCell('i', tcell.StyleDefault),
|
||||
}
|
||||
|
||||
contents := _StartPaging(t, reader)
|
||||
contents := startPaging(t, reader)
|
||||
for pos, expected := range answers {
|
||||
expected.LogDifference(t, contents[pos])
|
||||
}
|
||||
@ -73,29 +73,28 @@ func TestFgColorRendering(t *testing.T) {
|
||||
|
||||
func TestBrokenUtf8(t *testing.T) {
|
||||
// The broken UTF8 character in the middle is based on "©" = 0xc2a9
|
||||
reader := NewReaderFromStream(strings.NewReader(
|
||||
"abc\xc2def"), nil)
|
||||
reader := NewReaderFromStream(nil, strings.NewReader("abc\xc2def"))
|
||||
if err := reader._Wait(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var answers = []Token{
|
||||
_CreateExpectedCell('a', tcell.StyleDefault),
|
||||
_CreateExpectedCell('b', tcell.StyleDefault),
|
||||
_CreateExpectedCell('c', tcell.StyleDefault),
|
||||
_CreateExpectedCell('?', tcell.StyleDefault.Foreground(tcell.ColorMaroon).Background(tcell.ColorSilver)),
|
||||
_CreateExpectedCell('d', tcell.StyleDefault),
|
||||
_CreateExpectedCell('e', tcell.StyleDefault),
|
||||
_CreateExpectedCell('f', tcell.StyleDefault),
|
||||
createExpectedCell('a', tcell.StyleDefault),
|
||||
createExpectedCell('b', tcell.StyleDefault),
|
||||
createExpectedCell('c', tcell.StyleDefault),
|
||||
createExpectedCell('?', tcell.StyleDefault.Foreground(tcell.ColorMaroon).Background(tcell.ColorSilver)),
|
||||
createExpectedCell('d', tcell.StyleDefault),
|
||||
createExpectedCell('e', tcell.StyleDefault),
|
||||
createExpectedCell('f', tcell.StyleDefault),
|
||||
}
|
||||
|
||||
contents := _StartPaging(t, reader)
|
||||
contents := startPaging(t, reader)
|
||||
for pos, expected := range answers {
|
||||
expected.LogDifference(t, contents[pos])
|
||||
}
|
||||
}
|
||||
|
||||
func _StartPaging(t *testing.T, reader *Reader) []tcell.SimCell {
|
||||
func startPaging(t *testing.T, reader *Reader) []tcell.SimCell {
|
||||
screen := tcell.NewSimulationScreen("UTF-8")
|
||||
pager := NewPager(reader)
|
||||
pager.showLineNumbers = false
|
||||
@ -112,14 +111,14 @@ func _StartPaging(t *testing.T, reader *Reader) []tcell.SimCell {
|
||||
return contents
|
||||
}
|
||||
|
||||
// _AssertIndexOfFirstX verifies the (zero-based) index of the first 'x'
|
||||
func _AssertIndexOfFirstX(t *testing.T, s string, expectedIndex int) {
|
||||
reader := NewReaderFromStream(strings.NewReader(s), nil)
|
||||
// assertIndexOfFirstX verifies the (zero-based) index of the first 'x'
|
||||
func assertIndexOfFirstX(t *testing.T, s string, expectedIndex int) {
|
||||
reader := NewReaderFromStream(nil, strings.NewReader(s))
|
||||
if err := reader._Wait(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
contents := _StartPaging(t, reader)
|
||||
contents := startPaging(t, reader)
|
||||
for pos, cell := range contents {
|
||||
if cell.Runes[0] != 'x' {
|
||||
continue
|
||||
@ -139,22 +138,22 @@ func _AssertIndexOfFirstX(t *testing.T, s string, expectedIndex int) {
|
||||
}
|
||||
|
||||
func TestTabHandling(t *testing.T) {
|
||||
_AssertIndexOfFirstX(t, "x", 0)
|
||||
assertIndexOfFirstX(t, "x", 0)
|
||||
|
||||
_AssertIndexOfFirstX(t, "\x09x", 4)
|
||||
_AssertIndexOfFirstX(t, "\x09\x09x", 8)
|
||||
assertIndexOfFirstX(t, "\x09x", 4)
|
||||
assertIndexOfFirstX(t, "\x09\x09x", 8)
|
||||
|
||||
_AssertIndexOfFirstX(t, "J\x09x", 4)
|
||||
_AssertIndexOfFirstX(t, "Jo\x09x", 4)
|
||||
_AssertIndexOfFirstX(t, "Joh\x09x", 4)
|
||||
_AssertIndexOfFirstX(t, "Joha\x09x", 8)
|
||||
_AssertIndexOfFirstX(t, "Johan\x09x", 8)
|
||||
assertIndexOfFirstX(t, "J\x09x", 4)
|
||||
assertIndexOfFirstX(t, "Jo\x09x", 4)
|
||||
assertIndexOfFirstX(t, "Joh\x09x", 4)
|
||||
assertIndexOfFirstX(t, "Joha\x09x", 8)
|
||||
assertIndexOfFirstX(t, "Johan\x09x", 8)
|
||||
|
||||
_AssertIndexOfFirstX(t, "\x09J\x09x", 8)
|
||||
_AssertIndexOfFirstX(t, "\x09Jo\x09x", 8)
|
||||
_AssertIndexOfFirstX(t, "\x09Joh\x09x", 8)
|
||||
_AssertIndexOfFirstX(t, "\x09Joha\x09x", 12)
|
||||
_AssertIndexOfFirstX(t, "\x09Johan\x09x", 12)
|
||||
assertIndexOfFirstX(t, "\x09J\x09x", 8)
|
||||
assertIndexOfFirstX(t, "\x09Jo\x09x", 8)
|
||||
assertIndexOfFirstX(t, "\x09Joh\x09x", 8)
|
||||
assertIndexOfFirstX(t, "\x09Joha\x09x", 12)
|
||||
assertIndexOfFirstX(t, "\x09Johan\x09x", 12)
|
||||
}
|
||||
|
||||
// This test assumes highlight is installed:
|
||||
@ -175,25 +174,25 @@ func TestCodeHighlighting(t *testing.T) {
|
||||
}
|
||||
|
||||
var answers = []Token{
|
||||
_CreateExpectedCell('p', tcell.StyleDefault.Foreground(tcell.ColorOlive)),
|
||||
_CreateExpectedCell('a', tcell.StyleDefault.Foreground(tcell.ColorOlive)),
|
||||
_CreateExpectedCell('c', tcell.StyleDefault.Foreground(tcell.ColorOlive)),
|
||||
_CreateExpectedCell('k', tcell.StyleDefault.Foreground(tcell.ColorOlive)),
|
||||
_CreateExpectedCell('a', tcell.StyleDefault.Foreground(tcell.ColorOlive)),
|
||||
_CreateExpectedCell('g', tcell.StyleDefault.Foreground(tcell.ColorOlive)),
|
||||
_CreateExpectedCell('e', tcell.StyleDefault.Foreground(tcell.ColorOlive)),
|
||||
_CreateExpectedCell(' ', tcell.StyleDefault),
|
||||
_CreateExpectedCell('m', tcell.StyleDefault),
|
||||
createExpectedCell('p', tcell.StyleDefault.Foreground(tcell.ColorOlive)),
|
||||
createExpectedCell('a', tcell.StyleDefault.Foreground(tcell.ColorOlive)),
|
||||
createExpectedCell('c', tcell.StyleDefault.Foreground(tcell.ColorOlive)),
|
||||
createExpectedCell('k', tcell.StyleDefault.Foreground(tcell.ColorOlive)),
|
||||
createExpectedCell('a', tcell.StyleDefault.Foreground(tcell.ColorOlive)),
|
||||
createExpectedCell('g', tcell.StyleDefault.Foreground(tcell.ColorOlive)),
|
||||
createExpectedCell('e', tcell.StyleDefault.Foreground(tcell.ColorOlive)),
|
||||
createExpectedCell(' ', tcell.StyleDefault),
|
||||
createExpectedCell('m', tcell.StyleDefault),
|
||||
}
|
||||
|
||||
contents := _StartPaging(t, reader)
|
||||
contents := startPaging(t, reader)
|
||||
for pos, expected := range answers {
|
||||
expected.LogDifference(t, contents[pos])
|
||||
}
|
||||
}
|
||||
|
||||
func _TestManPageFormatting(t *testing.T, input string, expected Token) {
|
||||
reader := NewReaderFromStream(strings.NewReader(input), nil)
|
||||
func testManPageFormatting(t *testing.T, input string, expected Token) {
|
||||
reader := NewReaderFromStream(nil, strings.NewReader(input))
|
||||
if err := reader._Wait(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -202,19 +201,19 @@ func _TestManPageFormatting(t *testing.T, input string, expected Token) {
|
||||
// environment variables are set when the tests are run.
|
||||
os.Setenv("LESS_TERMCAP_md", "")
|
||||
os.Setenv("LESS_TERMCAP_us", "")
|
||||
_ResetManPageFormatForTesting()
|
||||
resetManPageFormatForTesting()
|
||||
|
||||
contents := _StartPaging(t, reader)
|
||||
contents := startPaging(t, reader)
|
||||
expected.LogDifference(t, contents[0])
|
||||
assert.Equal(t, contents[1].Runes[0], ' ')
|
||||
}
|
||||
|
||||
func TestManPageFormatting(t *testing.T) {
|
||||
_TestManPageFormatting(t, "N\x08N", _CreateExpectedCell('N', tcell.StyleDefault.Bold(true)))
|
||||
_TestManPageFormatting(t, "_\x08x", _CreateExpectedCell('x', tcell.StyleDefault.Underline(true)))
|
||||
testManPageFormatting(t, "N\x08N", createExpectedCell('N', tcell.StyleDefault.Bold(true)))
|
||||
testManPageFormatting(t, "_\x08x", createExpectedCell('x', tcell.StyleDefault.Underline(true)))
|
||||
|
||||
// Corner cases
|
||||
_TestManPageFormatting(t, "\x08", _CreateExpectedCell('<', tcell.StyleDefault.Foreground(tcell.ColorMaroon).Background(tcell.ColorSilver)))
|
||||
testManPageFormatting(t, "\x08", createExpectedCell('<', tcell.StyleDefault.Foreground(tcell.ColorMaroon).Background(tcell.ColorSilver)))
|
||||
|
||||
// FIXME: Test two consecutive backspaces
|
||||
|
||||
@ -222,23 +221,23 @@ func TestManPageFormatting(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestToPattern(t *testing.T) {
|
||||
assert.Assert(t, ToPattern("") == nil)
|
||||
assert.Assert(t, toPattern("") == nil)
|
||||
|
||||
// Test regexp matching
|
||||
assert.Assert(t, ToPattern("G.*S").MatchString("GRIIIS"))
|
||||
assert.Assert(t, !ToPattern("G.*S").MatchString("gRIIIS"))
|
||||
assert.Assert(t, toPattern("G.*S").MatchString("GRIIIS"))
|
||||
assert.Assert(t, !toPattern("G.*S").MatchString("gRIIIS"))
|
||||
|
||||
// Test case insensitive regexp matching
|
||||
assert.Assert(t, ToPattern("g.*s").MatchString("GRIIIS"))
|
||||
assert.Assert(t, ToPattern("g.*s").MatchString("gRIIIS"))
|
||||
assert.Assert(t, toPattern("g.*s").MatchString("GRIIIS"))
|
||||
assert.Assert(t, toPattern("g.*s").MatchString("gRIIIS"))
|
||||
|
||||
// Test non-regexp matching
|
||||
assert.Assert(t, ToPattern(")G").MatchString(")G"))
|
||||
assert.Assert(t, !ToPattern(")G").MatchString(")g"))
|
||||
assert.Assert(t, toPattern(")G").MatchString(")G"))
|
||||
assert.Assert(t, !toPattern(")G").MatchString(")g"))
|
||||
|
||||
// Test case insensitive non-regexp matching
|
||||
assert.Assert(t, ToPattern(")g").MatchString(")G"))
|
||||
assert.Assert(t, ToPattern(")g").MatchString(")g"))
|
||||
assert.Assert(t, toPattern(")g").MatchString(")G"))
|
||||
assert.Assert(t, toPattern(")g").MatchString(")g"))
|
||||
}
|
||||
|
||||
func assertTokenRangesEqual(t *testing.T, actual []Token, expected []Token) {
|
||||
@ -266,25 +265,25 @@ func assertTokenRangesEqual(t *testing.T, actual []Token, expected []Token) {
|
||||
}
|
||||
|
||||
func TestCreateScreenLineBase(t *testing.T) {
|
||||
line := _CreateScreenLine(0, 3, "", nil)
|
||||
line := createScreenLine(0, 3, "", nil)
|
||||
assert.Assert(t, len(line) == 0)
|
||||
}
|
||||
|
||||
func TestCreateScreenLineOverflowRight(t *testing.T) {
|
||||
line := _CreateScreenLine(0, 3, "012345", nil)
|
||||
line := createScreenLine(0, 3, "012345", nil)
|
||||
assertTokenRangesEqual(t, line, []Token{
|
||||
_CreateExpectedCell('0', tcell.StyleDefault),
|
||||
_CreateExpectedCell('1', tcell.StyleDefault),
|
||||
_CreateExpectedCell('>', tcell.StyleDefault.Reverse(true)),
|
||||
createExpectedCell('0', tcell.StyleDefault),
|
||||
createExpectedCell('1', tcell.StyleDefault),
|
||||
createExpectedCell('>', tcell.StyleDefault.Reverse(true)),
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateScreenLineUnderflowLeft(t *testing.T) {
|
||||
line := _CreateScreenLine(1, 3, "012", nil)
|
||||
line := createScreenLine(1, 3, "012", nil)
|
||||
assertTokenRangesEqual(t, line, []Token{
|
||||
_CreateExpectedCell('<', tcell.StyleDefault.Reverse(true)),
|
||||
_CreateExpectedCell('1', tcell.StyleDefault),
|
||||
_CreateExpectedCell('2', tcell.StyleDefault),
|
||||
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
|
||||
createExpectedCell('1', tcell.StyleDefault),
|
||||
createExpectedCell('2', tcell.StyleDefault),
|
||||
})
|
||||
}
|
||||
|
||||
@ -294,11 +293,11 @@ func TestCreateScreenLineSearchHit(t *testing.T) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
line := _CreateScreenLine(0, 3, "abc", pattern)
|
||||
line := createScreenLine(0, 3, "abc", pattern)
|
||||
assertTokenRangesEqual(t, line, []Token{
|
||||
_CreateExpectedCell('a', tcell.StyleDefault),
|
||||
_CreateExpectedCell('b', tcell.StyleDefault.Reverse(true)),
|
||||
_CreateExpectedCell('c', tcell.StyleDefault),
|
||||
createExpectedCell('a', tcell.StyleDefault),
|
||||
createExpectedCell('b', tcell.StyleDefault.Reverse(true)),
|
||||
createExpectedCell('c', tcell.StyleDefault),
|
||||
})
|
||||
}
|
||||
|
||||
@ -308,48 +307,48 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
line := _CreateScreenLine(0, 3, "åäö", pattern)
|
||||
line := createScreenLine(0, 3, "åäö", pattern)
|
||||
assertTokenRangesEqual(t, line, []Token{
|
||||
_CreateExpectedCell('å', tcell.StyleDefault),
|
||||
_CreateExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
|
||||
_CreateExpectedCell('ö', tcell.StyleDefault),
|
||||
createExpectedCell('å', tcell.StyleDefault),
|
||||
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
|
||||
createExpectedCell('ö', tcell.StyleDefault),
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) {
|
||||
pattern := regexp.MustCompile("ä")
|
||||
|
||||
line := _CreateScreenLine(1, 4, "ååäö", pattern)
|
||||
line := createScreenLine(1, 4, "ååäö", pattern)
|
||||
|
||||
assertTokenRangesEqual(t, line, []Token{
|
||||
_CreateExpectedCell('<', tcell.StyleDefault.Reverse(true)),
|
||||
_CreateExpectedCell('å', tcell.StyleDefault),
|
||||
_CreateExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
|
||||
_CreateExpectedCell('ö', tcell.StyleDefault),
|
||||
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
|
||||
createExpectedCell('å', tcell.StyleDefault),
|
||||
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
|
||||
createExpectedCell('ö', tcell.StyleDefault),
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateScreenLineScrolled2Utf8SearchHit(t *testing.T) {
|
||||
pattern := regexp.MustCompile("ä")
|
||||
|
||||
line := _CreateScreenLine(2, 4, "åååäö", pattern)
|
||||
line := createScreenLine(2, 4, "åååäö", pattern)
|
||||
|
||||
assertTokenRangesEqual(t, line, []Token{
|
||||
_CreateExpectedCell('<', tcell.StyleDefault.Reverse(true)),
|
||||
_CreateExpectedCell('å', tcell.StyleDefault),
|
||||
_CreateExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
|
||||
_CreateExpectedCell('ö', tcell.StyleDefault),
|
||||
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
|
||||
createExpectedCell('å', tcell.StyleDefault),
|
||||
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
|
||||
createExpectedCell('ö', tcell.StyleDefault),
|
||||
})
|
||||
}
|
||||
|
||||
func TestFindFirstLineOneBasedSimple(t *testing.T) {
|
||||
reader := NewReaderFromStream(strings.NewReader("AB"), nil)
|
||||
reader := NewReaderFromStream(nil, strings.NewReader("AB"))
|
||||
pager := NewPager(reader)
|
||||
|
||||
// Wait for reader to finish reading
|
||||
<-reader.done
|
||||
|
||||
pager.searchPattern = ToPattern("AB")
|
||||
pager.searchPattern = toPattern("AB")
|
||||
|
||||
hitLine := pager._FindFirstHitLineOneBased(1, false)
|
||||
assert.Check(t, hitLine != nil)
|
||||
@ -357,13 +356,13 @@ func TestFindFirstLineOneBasedSimple(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFindFirstLineOneBasedAnsi(t *testing.T) {
|
||||
reader := NewReaderFromStream(strings.NewReader("A\x1b[30mB"), nil)
|
||||
reader := NewReaderFromStream(nil, strings.NewReader("A\x1b[30mB"))
|
||||
pager := NewPager(reader)
|
||||
|
||||
// Wait for reader to finish reading
|
||||
<-reader.done
|
||||
|
||||
pager.searchPattern = ToPattern("AB")
|
||||
pager.searchPattern = toPattern("AB")
|
||||
|
||||
hitLine := pager._FindFirstHitLineOneBased(1, false)
|
||||
assert.Check(t, hitLine != nil)
|
||||
|
61
m/reader.go
61
m/reader.go
@ -20,11 +20,10 @@ import (
|
||||
|
||||
// Reader reads a file into an array of strings.
|
||||
//
|
||||
// When this thing grows up it's going to do the reading in the
|
||||
// background, and it will return parts of the read data upon
|
||||
// request.
|
||||
// It does the reading in the background, and it returns parts of the read data
|
||||
// upon request.
|
||||
//
|
||||
// This package should provide query methods for the struct, no peeking!!
|
||||
// This package provides query methods for the struct, no peeking!!
|
||||
type Reader struct {
|
||||
lines []string
|
||||
name *string
|
||||
@ -47,7 +46,7 @@ type Lines struct {
|
||||
statusText string
|
||||
}
|
||||
|
||||
func _ReadStream(stream io.Reader, reader *Reader, fromFilter *exec.Cmd) {
|
||||
func readStream(stream io.Reader, reader *Reader, fromFilter *exec.Cmd) {
|
||||
// FIXME: Close the stream when done reading it?
|
||||
|
||||
defer func() {
|
||||
@ -147,10 +146,20 @@ func _ReadStream(stream io.Reader, reader *Reader, fromFilter *exec.Cmd) {
|
||||
}
|
||||
|
||||
// NewReaderFromStream creates a new stream reader
|
||||
func NewReaderFromStream(name *string, reader io.Reader) *Reader {
|
||||
mReader := newReaderFromStream(reader, nil)
|
||||
mReader.lock.Lock()
|
||||
mReader.name = name
|
||||
mReader.lock.Unlock()
|
||||
|
||||
return mReader
|
||||
}
|
||||
|
||||
// newReaderFromStream creates a new stream 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 {
|
||||
func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader {
|
||||
var lines []string
|
||||
var lock = &sync.Mutex{}
|
||||
done := make(chan bool, 1)
|
||||
@ -169,12 +178,15 @@ func NewReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader {
|
||||
|
||||
// FIXME: Make sure that if we panic somewhere inside of this goroutine,
|
||||
// the main program terminates and prints our panic stack trace.
|
||||
go _ReadStream(reader, &returnMe, fromFilter)
|
||||
go readStream(reader, &returnMe, fromFilter)
|
||||
|
||||
return &returnMe
|
||||
}
|
||||
|
||||
// NewReaderFromText creates a Reader from a block of text
|
||||
// NewReaderFromText creates a Reader from a block of text.
|
||||
//
|
||||
// First parameter is the name of this Reader. This name will be displayed by
|
||||
// Moar in the bottom left corner of the screen.
|
||||
func NewReaderFromText(name string, text string) *Reader {
|
||||
noExternalNewlines := strings.Trim(text, "\n")
|
||||
done := make(chan bool, 1)
|
||||
@ -197,8 +209,8 @@ func (r *Reader) _Wait() error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
// NewReaderFromCommand creates a new reader by running a file through a filter
|
||||
func NewReaderFromCommand(filename string, filterCommand ...string) (*Reader, error) {
|
||||
// newReaderFromCommand creates a new reader by running a file through a filter
|
||||
func newReaderFromCommand(filename string, filterCommand ...string) (*Reader, error) {
|
||||
filterWithFilename := append(filterCommand, filename)
|
||||
filter := exec.Command(filterWithFilename[0], filterWithFilename[1:]...)
|
||||
|
||||
@ -219,7 +231,7 @@ func NewReaderFromCommand(filename string, filterCommand ...string) (*Reader, er
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader := NewReaderFromStream(filterOut, filter)
|
||||
reader := newReaderFromStream(filterOut, filter)
|
||||
reader.lock.Lock()
|
||||
reader.name = &filename
|
||||
reader._stderr = filterErr
|
||||
@ -227,7 +239,7 @@ func NewReaderFromCommand(filename string, filterCommand ...string) (*Reader, er
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func _CanHighlight(filename string) bool {
|
||||
func canHighlight(filename string) bool {
|
||||
extension := filepath.Ext(filename)
|
||||
if len(extension) <= 1 {
|
||||
// No extension or a single "."
|
||||
@ -275,7 +287,7 @@ func _CanHighlight(filename string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func _TryOpen(filename string) error {
|
||||
func tryOpen(filename string) error {
|
||||
// Try opening the file
|
||||
tryMe, err := os.Open(filename)
|
||||
if err != nil {
|
||||
@ -295,27 +307,31 @@ func _TryOpen(filename string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// NewReaderFromFilename creates a new file reader
|
||||
// NewReaderFromFilename creates a new file reader.
|
||||
//
|
||||
// The Reader will try to uncompress various compressed file format, and also
|
||||
// apply highlighting to the file using highlight:
|
||||
// http://www.andre-simon.de/doku/highlight/en/highlight.php
|
||||
func NewReaderFromFilename(filename string) (*Reader, error) {
|
||||
fileError := _TryOpen(filename)
|
||||
fileError := tryOpen(filename)
|
||||
if fileError != nil {
|
||||
return nil, fileError
|
||||
}
|
||||
|
||||
if strings.HasSuffix(filename, ".gz") {
|
||||
return NewReaderFromCommand(filename, "gzip", "-d", "-c")
|
||||
return newReaderFromCommand(filename, "gzip", "-d", "-c")
|
||||
}
|
||||
if strings.HasSuffix(filename, ".bz2") {
|
||||
return NewReaderFromCommand(filename, "bzip2", "-d", "-c")
|
||||
return newReaderFromCommand(filename, "bzip2", "-d", "-c")
|
||||
}
|
||||
if strings.HasSuffix(filename, ".xz") {
|
||||
return NewReaderFromCommand(filename, "xz", "-d", "-c")
|
||||
return newReaderFromCommand(filename, "xz", "-d", "-c")
|
||||
}
|
||||
|
||||
// Highlight input file using highlight:
|
||||
// http://www.andre-simon.de/doku/highlight/en/highlight.php
|
||||
if _CanHighlight(filename) {
|
||||
highlighted, err := NewReaderFromCommand(filename, "highlight", "--out-format=esc", "-i")
|
||||
if canHighlight(filename) {
|
||||
highlighted, err := newReaderFromCommand(filename, "highlight", "--out-format=esc", "-i")
|
||||
if err == nil {
|
||||
return highlighted, err
|
||||
}
|
||||
@ -326,10 +342,7 @@ func NewReaderFromFilename(filename string) (*Reader, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader := NewReaderFromStream(stream, nil)
|
||||
reader.lock.Lock()
|
||||
reader.name = &filename
|
||||
reader.lock.Unlock()
|
||||
reader := NewReaderFromStream(&filename, stream)
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func _TestGetLineCount(t *testing.T, reader *Reader) {
|
||||
func testGetLineCount(t *testing.T, reader *Reader) {
|
||||
if strings.Contains(*reader.name, "compressed") {
|
||||
// We are no good at counting lines of compressed files, never mind
|
||||
return
|
||||
@ -43,7 +43,7 @@ func _TestGetLineCount(t *testing.T, reader *Reader) {
|
||||
}
|
||||
}
|
||||
|
||||
func _TestGetLines(t *testing.T, reader *Reader) {
|
||||
func testGetLines(t *testing.T, reader *Reader) {
|
||||
t.Logf("Testing file: %s...", *reader.name)
|
||||
|
||||
lines := reader.GetLines(1, 10)
|
||||
@ -102,7 +102,7 @@ func _TestGetLines(t *testing.T, reader *Reader) {
|
||||
}
|
||||
}
|
||||
|
||||
func _GetSamplesDir() string {
|
||||
func getSamplesDir() string {
|
||||
// From: https://coderwall.com/p/_fmbug/go-get-path-to-current-file
|
||||
_, filename, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
@ -112,8 +112,8 @@ func _GetSamplesDir() string {
|
||||
return path.Join(path.Dir(filename), "../sample-files")
|
||||
}
|
||||
|
||||
func _GetTestFiles() []string {
|
||||
files, err := ioutil.ReadDir(_GetSamplesDir())
|
||||
func getTestFiles() []string {
|
||||
files, err := ioutil.ReadDir(getSamplesDir())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -127,7 +127,7 @@ func _GetTestFiles() []string {
|
||||
}
|
||||
|
||||
func TestGetLines(t *testing.T) {
|
||||
for _, file := range _GetTestFiles() {
|
||||
for _, file := range getTestFiles() {
|
||||
reader, err := NewReaderFromFilename(file)
|
||||
if err != nil {
|
||||
t.Errorf("Error opening file <%s>: %s", file, err.Error())
|
||||
@ -138,8 +138,8 @@ func TestGetLines(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
_TestGetLines(t, reader)
|
||||
_TestGetLineCount(t, reader)
|
||||
testGetLines(t, reader)
|
||||
testGetLineCount(t, reader)
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,8 +171,8 @@ func TestGetLongLine(t *testing.T) {
|
||||
assert.Equal(t, len(line)+1, int(fileSize))
|
||||
}
|
||||
|
||||
func _GetReaderWithLineCount(totalLines int) *Reader {
|
||||
reader := NewReaderFromStream(strings.NewReader(strings.Repeat("x\n", totalLines)), nil)
|
||||
func getReaderWithLineCount(totalLines int) *Reader {
|
||||
reader := NewReaderFromStream(nil, strings.NewReader(strings.Repeat("x\n", totalLines)))
|
||||
if err := reader._Wait(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -180,20 +180,20 @@ func _GetReaderWithLineCount(totalLines int) *Reader {
|
||||
return reader
|
||||
}
|
||||
|
||||
func _TestStatusText(t *testing.T, fromLine int, toLine int, totalLines int, expected string) {
|
||||
testMe := _GetReaderWithLineCount(totalLines)
|
||||
func testStatusText(t *testing.T, fromLine int, toLine int, totalLines int, expected string) {
|
||||
testMe := getReaderWithLineCount(totalLines)
|
||||
linesRequested := toLine - fromLine + 1
|
||||
statusText := testMe.GetLines(fromLine, linesRequested).statusText
|
||||
assert.Equal(t, statusText, expected)
|
||||
}
|
||||
|
||||
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/1000 99%")
|
||||
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/1000 99%")
|
||||
|
||||
_TestStatusText(t, 0, 0, 0, "<empty>")
|
||||
_TestStatusText(t, 1, 1, 1, "1-1/1 100%")
|
||||
testStatusText(t, 0, 0, 0, "<empty>")
|
||||
testStatusText(t, 1, 1, 1, "1-1/1 100%")
|
||||
|
||||
// Test with filename
|
||||
testMe, err := NewReaderFromFilename("/dev/null")
|
||||
@ -208,8 +208,8 @@ func TestStatusText(t *testing.T) {
|
||||
assert.Equal(t, statusText, "null: <empty>")
|
||||
}
|
||||
|
||||
func _TestCompressedFile(t *testing.T, filename string) {
|
||||
filenameWithPath := _GetSamplesDir() + "/" + filename
|
||||
func testCompressedFile(t *testing.T, filename string) {
|
||||
filenameWithPath := getSamplesDir() + "/" + filename
|
||||
reader, e := NewReaderFromFilename(filenameWithPath)
|
||||
if e != nil {
|
||||
t.Errorf("Error opening file <%s>: %s", filenameWithPath, e.Error())
|
||||
@ -223,9 +223,9 @@ func _TestCompressedFile(t *testing.T, filename string) {
|
||||
}
|
||||
|
||||
func TestCompressedFiles(t *testing.T) {
|
||||
_TestCompressedFile(t, "compressed.txt.gz")
|
||||
_TestCompressedFile(t, "compressed.txt.bz2")
|
||||
_TestCompressedFile(t, "compressed.txt.xz")
|
||||
testCompressedFile(t, "compressed.txt.gz")
|
||||
testCompressedFile(t, "compressed.txt.bz2")
|
||||
testCompressedFile(t, "compressed.txt.xz")
|
||||
}
|
||||
|
||||
func TestFilterNotInstalled(t *testing.T) {
|
||||
@ -244,7 +244,7 @@ func TestFilterFileNotFound(t *testing.T) {
|
||||
// What happens if the filter cannot read its input file?
|
||||
NonExistentPath := "/does-not-exist"
|
||||
|
||||
reader, err := NewReaderFromCommand(NonExistentPath, "cat")
|
||||
reader, err := newReaderFromCommand(NonExistentPath, "cat")
|
||||
|
||||
// Creating should be fine, it's waiting for it to finish that should fail.
|
||||
// Feel free to re-evaluate in the future.
|
||||
|
22
m/util.go
22
m/util.go
@ -1,22 +0,0 @@
|
||||
package m
|
||||
|
||||
import "github.com/gdamore/tcell/v2"
|
||||
|
||||
// PageString displays a multi-line text in a pager.
|
||||
//
|
||||
// name - Will be displayed in the bottom left corner of the pager window
|
||||
//
|
||||
// text - This is the (potentially long) multi line text that will be displayed
|
||||
func PageString(name string, text string) error {
|
||||
reader := NewReaderFromText(name, text)
|
||||
|
||||
screen, e := tcell.NewScreen()
|
||||
if e != nil {
|
||||
// Screen setup failed
|
||||
return e
|
||||
}
|
||||
defer screen.Fini()
|
||||
|
||||
NewPager(reader).StartPaging(screen)
|
||||
return nil
|
||||
}
|
22
moar.go
22
moar.go
@ -20,7 +20,7 @@ import (
|
||||
|
||||
var versionString = "Should be set when building, please use build.sh to build"
|
||||
|
||||
func _PrintUsage(output io.Writer) {
|
||||
func printUsage(output io.Writer) {
|
||||
// This controls where PrintDefaults() prints, see below
|
||||
flag.CommandLine.SetOutput(output)
|
||||
|
||||
@ -59,8 +59,8 @@ func _PrintUsage(output io.Writer) {
|
||||
}
|
||||
}
|
||||
|
||||
// PrintProblemsHeader prints bug reporting information to stderr
|
||||
func PrintProblemsHeader() {
|
||||
// printProblemsHeader prints bug reporting information to stderr
|
||||
func printProblemsHeader() {
|
||||
fmt.Fprintln(os.Stderr, "Please post the following report at <https://github.com/walles/moar/issues>,")
|
||||
fmt.Fprintln(os.Stderr, "or e-mail it to johan.walles@gmail.com.")
|
||||
fmt.Fprintln(os.Stderr)
|
||||
@ -85,12 +85,12 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
PrintProblemsHeader()
|
||||
printProblemsHeader()
|
||||
panic(err)
|
||||
}()
|
||||
|
||||
flag.Usage = func() {
|
||||
_PrintUsage(os.Stdout)
|
||||
printUsage(os.Stdout)
|
||||
}
|
||||
printVersion := flag.Bool("version", false, "Prints the moar version number")
|
||||
debug := flag.Bool("debug", false, "Print debug logs after exiting")
|
||||
@ -117,15 +117,15 @@ func main() {
|
||||
|
||||
if stdinIsRedirected && !stdoutIsRedirected {
|
||||
// Display input pipe contents
|
||||
reader := m.NewReaderFromStream(os.Stdin, nil)
|
||||
_StartPaging(reader)
|
||||
reader := m.NewReaderFromStream(nil, os.Stdin)
|
||||
startPaging(reader)
|
||||
return
|
||||
}
|
||||
|
||||
if len(flag.Args()) != 1 {
|
||||
fmt.Fprintln(os.Stderr, "ERROR: Expected exactly one filename, got: ", flag.Args())
|
||||
fmt.Fprintln(os.Stderr)
|
||||
_PrintUsage(os.Stderr)
|
||||
printUsage(os.Stderr)
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
@ -150,10 +150,10 @@ func main() {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
_StartPaging(reader)
|
||||
startPaging(reader)
|
||||
}
|
||||
|
||||
func _StartPaging(reader *m.Reader) {
|
||||
func startPaging(reader *m.Reader) {
|
||||
screen, e := tcell.NewScreen()
|
||||
if e != nil {
|
||||
panic(e)
|
||||
@ -171,7 +171,7 @@ func _StartPaging(reader *m.Reader) {
|
||||
}
|
||||
|
||||
if len(loglines.String()) > 0 {
|
||||
PrintProblemsHeader()
|
||||
printProblemsHeader()
|
||||
|
||||
// FIXME: Don't print duplicate log messages more than once,
|
||||
// maybe invent our own logger for this?
|
||||
|
2
test.sh
2
test.sh
@ -2,7 +2,7 @@
|
||||
|
||||
set -e -o pipefail
|
||||
|
||||
echo Test that we only pass tcell.Color constants to these methods, not numbers
|
||||
# Test that we only pass tcell.Color constants to these methods, not numbers
|
||||
grep -En 'Foreground\([1-9]' ./*.go ./*/*.go && exit 1
|
||||
grep -En 'Background\([1-9]' ./*.go ./*/*.go && exit 1
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user