1
1
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:
Johan Walles 2020-12-30 15:19:47 +01:00
commit ccf2af496e
13 changed files with 258 additions and 233 deletions

View File

@ -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
----------

View File

@ -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) {

View File

@ -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
View 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
}

View File

@ -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

View File

@ -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)) // ä

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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.

View File

@ -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
View File

@ -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?

View File

@ -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