1
1
mirror of https://github.com/walles/moar.git synced 2024-09-11 12:15:43 +03:00

Use our new tcell replacement

This commit is contained in:
Johan Walles 2021-04-15 15:16:06 +02:00
parent 44a064c024
commit 90e374601d
10 changed files with 314 additions and 339 deletions

View File

@ -1,6 +1,6 @@
#!/bin/bash
VERSION="$(git describe --dirty)"
VERSION="$(git describe --tags --dirty --always)"
BINARY="moar"
if [ -n "$GOOS$GOARCH" ] ; then

8
go.mod
View File

@ -1,12 +1,12 @@
module github.com/walles/moar
go 1.12
go 1.16
require (
github.com/alecthomas/chroma v0.8.2
github.com/gdamore/tcell/v2 v2.0.0
github.com/google/go-cmp v0.3.0 // indirect
github.com/google/go-cmp v0.5.5 // indirect
github.com/sirupsen/logrus v1.4.2
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
gotest.tools v2.2.0+incompatible
)

10
go.sum
View File

@ -13,10 +13,10 @@ github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.0.0 h1:GRWG8aLfWAlekj9Q6W29bVvkHENc6hp79XOqG4AWDOs=
github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -65,9 +65,15 @@ golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HX
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201029080932-201ba4db2418 h1:HlFl4V6pEMziuLXyRkm5BIYq1y1GAbb02pRlWvI54OM=
golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=

View File

@ -8,44 +8,37 @@ import (
"strings"
log "github.com/sirupsen/logrus"
"github.com/gdamore/tcell/v2"
"github.com/walles/moar/twin"
)
const _TabSize = 4
var manPageBold = tcell.StyleDefault.Bold(true)
var manPageUnderline = tcell.StyleDefault.Underline(true)
var manPageBold = twin.StyleDefault.WithAttr(twin.AttrBold)
var manPageUnderline = twin.StyleDefault.WithAttr(twin.AttrUnderline)
// ESC[...m: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR
var sgrSequencePattern = regexp.MustCompile("\x1b\\[([0-9;]*m)")
// Token is a rune with a style to be written to a cell on screen
type Token struct {
Rune rune
Style tcell.Style
}
// A Line represents a line of text that can / will be paged
type Line struct {
raw *string
plain *string
tokens []Token
raw *string
plain *string
cells []twin.Cell
}
// NewLine creates a new Line from a (potentially ANSI / man page formatted) string
func NewLine(raw string) *Line {
return &Line{
raw: &raw,
plain: nil,
tokens: nil,
raw: &raw,
plain: nil,
cells: nil,
}
}
// Tokens returns a representation of the string split into styled tokens
func (line *Line) Tokens() []Token {
func (line *Line) Tokens() []twin.Cell {
line.parse()
return line.tokens
return line.cells
}
// Plain returns a plain text representation of the initial string
@ -60,7 +53,7 @@ func (line *Line) parse() {
return
}
line.tokens, line.plain = tokensFromString(*line.raw)
line.cells, line.plain = cellsFromString(*line.raw)
line.raw = nil
}
@ -82,22 +75,23 @@ func SetManPageFormatFromEnv() {
// Used from tests
func resetManPageFormatForTesting() {
manPageBold = tcell.StyleDefault.Bold(true)
manPageUnderline = tcell.StyleDefault.Underline(true)
manPageBold = twin.StyleDefault.WithAttr(twin.AttrBold)
manPageUnderline = twin.StyleDefault.WithAttr(twin.AttrUnderline)
}
func termcapToStyle(termcap string) tcell.Style {
func termcapToStyle(termcap string) twin.Style {
// Add a character to be sure we have one to take the format from
tokens, _ := tokensFromString(termcap + "x")
return tokens[len(tokens)-1].Style
cells, _ := cellsFromString(termcap + "x")
return cells[len(cells)-1].Style
}
// tokensFromString turns a (formatted) string into a series of tokens,
// cellsFromString turns a (formatted) string into a series of screen cells,
// and an unformatted string
func tokensFromString(s string) ([]Token, *string) {
var tokens []Token
func cellsFromString(s string) ([]twin.Cell, *string) {
var tokens []twin.Cell
styleBrokenUtf8 := tcell.StyleDefault.Background(tcell.ColorSilver).Foreground(tcell.ColorMaroon)
// Specs: https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit
styleBrokenUtf8 := twin.StyleDefault.Background(twin.NewColor16(1)).Foreground(twin.NewColor16(7))
for _, styledString := range styledStringsFromString(s) {
for _, token := range tokensFromStyledString(styledString) {
@ -105,7 +99,7 @@ func tokensFromString(s string) ([]Token, *string) {
case '\x09': // TAB
for {
tokens = append(tokens, Token{
tokens = append(tokens, twin.Cell{
Rune: ' ',
Style: styledString.Style,
})
@ -117,13 +111,13 @@ func tokensFromString(s string) ([]Token, *string) {
}
case '<27>': // Go's broken-UTF8 marker
tokens = append(tokens, Token{
tokens = append(tokens, twin.Cell{
Rune: '?',
Style: styleBrokenUtf8,
})
case '\x08': // Backspace
tokens = append(tokens, Token{
tokens = append(tokens, twin.Cell{
Rune: '<',
Style: styleBrokenUtf8,
})
@ -144,7 +138,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, *twin.Cell) {
if index+2 >= len(runes) {
// Not enough runes left for a bold
return index, nil
@ -161,14 +155,14 @@ func consumeBold(runes []rune, index int) (int, *Token) {
}
// We have a match!
return index + 3, &Token{
return index + 3, &twin.Cell{
Rune: runes[index],
Style: manPageBold,
}
}
// 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, *twin.Cell) {
if index+2 >= len(runes) {
// Not enough runes left for a underline
return index, nil
@ -185,7 +179,7 @@ func consumeUnderline(runes []rune, index int) (int, *Token) {
}
// We have a match!
return index + 3, &Token{
return index + 3, &twin.Cell{
Rune: runes[index+2],
Style: manPageUnderline,
}
@ -194,7 +188,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, *twin.Cell) {
patterns := [][]byte{[]byte("+\bo"), []byte("+\b+\bo\bo")}
for _, pattern := range patterns {
if index+len(pattern) > len(runes) {
@ -215,18 +209,18 @@ func consumeBullet(runes []rune, index int) (int, *Token) {
}
// We have a match!
return index + len(pattern), &Token{
return index + len(pattern), &twin.Cell{
Rune: '•', // Unicode bullet point
Style: tcell.StyleDefault,
Style: twin.StyleDefault,
}
}
return index, nil
}
func tokensFromStyledString(styledString _StyledString) []Token {
func tokensFromStyledString(styledString _StyledString) []twin.Cell {
runes := []rune(styledString.String)
tokens := make([]Token, 0, len(runes))
tokens := make([]twin.Cell, 0, len(runes))
for index := 0; index < len(runes); index++ {
nextIndex, token := consumeBullet(runes, index)
@ -250,7 +244,7 @@ func tokensFromStyledString(styledString _StyledString) []Token {
continue
}
tokens = append(tokens, Token{
tokens = append(tokens, twin.Cell{
Rune: runes[index],
Style: styledString.Style,
})
@ -261,7 +255,7 @@ func tokensFromStyledString(styledString _StyledString) []Token {
type _StyledString struct {
String string
Style tcell.Style
Style twin.Style
}
func styledStringsFromString(s string) []_StyledString {
@ -271,7 +265,7 @@ func styledStringsFromString(s string) []_StyledString {
matches := sgrSequencePattern.FindAllStringIndex(s, -1)
styledStrings := make([]_StyledString, 0, len(matches)+1)
style := tcell.StyleDefault
style := twin.StyleDefault
beg := 0
end := 0
@ -303,7 +297,7 @@ func styledStringsFromString(s string) []_StyledString {
}
// updateStyle parses a string of the form "ESC[33m" into changes to style
func updateStyle(style tcell.Style, escapeSequence string) tcell.Style {
func updateStyle(style twin.Style, escapeSequence string) twin.Style {
numbers := strings.Split(escapeSequence[2:len(escapeSequence)-1], ";")
index := 0
for index < len(numbers) {
@ -311,55 +305,55 @@ func updateStyle(style tcell.Style, escapeSequence string) tcell.Style {
index++
switch strings.TrimLeft(number, "0") {
case "":
style = tcell.StyleDefault
style = twin.StyleDefault
case "1":
style = style.Bold(true)
style = style.WithAttr(twin.AttrBold)
case "2":
style = style.Dim(true)
style = style.WithAttr(twin.AttrDim)
case "3":
style = style.Italic(true)
style = style.WithAttr(twin.AttrItalic)
case "4":
style = style.Underline(true)
style = style.WithAttr(twin.AttrUnderline)
case "7":
style = style.Reverse(true)
style = style.WithAttr(twin.AttrReverse)
case "22":
style = style.Bold(false).Dim(false)
style = style.WithoutAttr(twin.AttrBold).WithoutAttr(twin.AttrDim)
case "23":
style = style.Italic(false)
style = style.WithoutAttr(twin.AttrItalic)
case "24":
style = style.Underline(false)
style = style.WithoutAttr(twin.AttrUnderline)
case "27":
style = style.Reverse(false)
style = style.WithoutAttr(twin.AttrReverse)
// Foreground colors, https://pkg.go.dev/github.com/gdamore/tcell#Color
case "30":
style = style.Foreground(tcell.ColorBlack)
style = style.Foreground(twin.NewColor16(0))
case "31":
style = style.Foreground(tcell.ColorMaroon)
style = style.Foreground(twin.NewColor16(1))
case "32":
style = style.Foreground(tcell.ColorGreen)
style = style.Foreground(twin.NewColor16(2))
case "33":
style = style.Foreground(tcell.ColorOlive)
style = style.Foreground(twin.NewColor16(3))
case "34":
style = style.Foreground(tcell.ColorNavy)
style = style.Foreground(twin.NewColor16(4))
case "35":
style = style.Foreground(tcell.ColorPurple)
style = style.Foreground(twin.NewColor16(5))
case "36":
style = style.Foreground(tcell.ColorTeal)
style = style.Foreground(twin.NewColor16(6))
case "37":
style = style.Foreground(tcell.ColorSilver)
style = style.Foreground(twin.NewColor16(7))
case "38":
var err error = nil
var color *tcell.Color
var color *twin.Color
index, color, err = consumeCompositeColor(numbers, index-1)
if err != nil {
log.Warnf("Foreground: %s", err.Error())
@ -367,28 +361,28 @@ func updateStyle(style tcell.Style, escapeSequence string) tcell.Style {
}
style = style.Foreground(*color)
case "39":
style = style.Foreground(tcell.ColorDefault)
style = style.Foreground(twin.ColorDefault)
// Background colors, see https://pkg.go.dev/github.com/gdamore/tcell#Color
// Background colors, see https://pkg.go.dev/github.com/gdamore/Color
case "40":
style = style.Background(tcell.ColorBlack)
style = style.Background(twin.NewColor16(0))
case "41":
style = style.Background(tcell.ColorMaroon)
style = style.Background(twin.NewColor16(1))
case "42":
style = style.Background(tcell.ColorGreen)
style = style.Background(twin.NewColor16(2))
case "43":
style = style.Background(tcell.ColorOlive)
style = style.Background(twin.NewColor16(3))
case "44":
style = style.Background(tcell.ColorNavy)
style = style.Background(twin.NewColor16(4))
case "45":
style = style.Background(tcell.ColorPurple)
style = style.Background(twin.NewColor16(5))
case "46":
style = style.Background(tcell.ColorTeal)
style = style.Background(twin.NewColor16(6))
case "47":
style = style.Background(tcell.ColorSilver)
style = style.Background(twin.NewColor16(7))
case "48":
var err error = nil
var color *tcell.Color
var color *twin.Color
index, color, err = consumeCompositeColor(numbers, index-1)
if err != nil {
log.Warnf("Background: %s", err.Error())
@ -396,30 +390,30 @@ func updateStyle(style tcell.Style, escapeSequence string) tcell.Style {
}
style = style.Background(*color)
case "49":
style = style.Background(tcell.ColorDefault)
style = style.Background(twin.ColorDefault)
// Bright foreground colors: see https://pkg.go.dev/github.com/gdamore/tcell#Color
// Bright foreground colors: see https://pkg.go.dev/github.com/gdamore/Color
//
// After testing vs less and cat on iTerm2 3.3.9 / macOS Catalina
// 10.15.4 that's how they seem to handle this, tested with:
// * TERM=xterm-256color
// * TERM=screen-256color
case "90":
style = style.Foreground(tcell.ColorGray)
style = style.Foreground(twin.NewColor16(8))
case "91":
style = style.Foreground(tcell.ColorRed)
style = style.Foreground(twin.NewColor16(9))
case "92":
style = style.Foreground(tcell.ColorLime)
style = style.Foreground(twin.NewColor16(10))
case "93":
style = style.Foreground(tcell.ColorYellow)
style = style.Foreground(twin.NewColor16(11))
case "94":
style = style.Foreground(tcell.ColorBlue)
style = style.Foreground(twin.NewColor16(12))
case "95":
style = style.Foreground(tcell.ColorFuchsia)
style = style.Foreground(twin.NewColor16(13))
case "96":
style = style.Foreground(tcell.ColorAqua)
style = style.Foreground(twin.NewColor16(14))
case "97":
style = style.Foreground(tcell.ColorWhite)
style = style.Foreground(twin.NewColor16(15))
default:
log.Warnf("Unrecognized ANSI SGR code <%s>", number)
@ -435,7 +429,7 @@ func updateStyle(style tcell.Style, escapeSequence string) tcell.Style {
// This method will return:
// * The first index in the string that this function did not consume
// * A color value that can be applied to a style
func consumeCompositeColor(numbers []string, index int) (int, *tcell.Color, error) {
func consumeCompositeColor(numbers []string, index int) (int, *twin.Color, error) {
baseIndex := index
if numbers[index] != "38" && numbers[index] != "48" {
err := fmt.Errorf(
@ -468,8 +462,7 @@ func consumeCompositeColor(numbers []string, index int) (int, *tcell.Color, erro
return -1, nil, err
}
// Workaround for https://github.com/gdamore/tcell/issues/404
colorValue := tcell.Color(int64(tcell.Color16) - 16 + int64(colorNumber))
colorValue := twin.NewColor256(uint8(colorNumber))
return index + 1, &colorValue, nil
}
@ -489,21 +482,21 @@ func consumeCompositeColor(numbers []string, index int) (int, *tcell.Color, erro
if err != nil {
return -1, nil, err
}
rValue := int32(rValueX)
rValue := uint8(rValueX)
gValueX, err := strconv.Atoi(numbers[gIndex])
if err != nil {
return -1, nil, err
}
gValue := int32(gValueX)
gValue := uint8(gValueX)
bValueX, err := strconv.Atoi(numbers[bIndex])
if err != nil {
return -1, nil, err
}
bValue := int32(bValueX)
bValue := uint8(bValueX)
colorValue := tcell.NewRGBColor(rValue, gValue, bValue)
colorValue := twin.NewColor24Bit(rValue, gValue, bValue)
return bIndex + 1, &colorValue, nil
}

View File

@ -9,9 +9,8 @@ import (
log "github.com/sirupsen/logrus"
"github.com/walles/moar/twin"
"gotest.tools/assert"
"github.com/gdamore/tcell/v2"
)
// Verify that we can tokenize all lines in ../sample-files/*
@ -34,7 +33,7 @@ func TestTokenize(t *testing.T) {
var loglines strings.Builder
log.SetOutput(&loglines)
tokens, plainString := tokensFromString(line)
tokens, plainString := cellsFromString(line)
if len(tokens) != utf8.RuneCountInString(*plainString) {
t.Errorf("%s:%d: len(tokens)=%d, len(plainString)=%d for: <%s>",
fileName, lineNumber,
@ -51,43 +50,43 @@ func TestTokenize(t *testing.T) {
}
func TestUnderline(t *testing.T) {
tokens, _ := tokensFromString("a\x1b[4mb\x1b[24mc")
tokens, _ := cellsFromString("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)})
assert.Equal(t, tokens[2], Token{Rune: 'c', Style: tcell.StyleDefault})
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.Cell{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrUnderline)})
assert.Equal(t, tokens[2], twin.Cell{Rune: 'c', Style: twin.StyleDefault})
}
func TestManPages(t *testing.T) {
// Bold
tokens, _ := tokensFromString("ab\bbc")
tokens, _ := cellsFromString("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})
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.Cell{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrBold)})
assert.Equal(t, tokens[2], twin.Cell{Rune: 'c', Style: twin.StyleDefault})
// Underline
tokens, _ = tokensFromString("a_\bbc")
tokens, _ = cellsFromString("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)})
assert.Equal(t, tokens[2], Token{Rune: 'c', Style: tcell.StyleDefault})
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.Cell{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrUnderline)})
assert.Equal(t, tokens[2], twin.Cell{Rune: 'c', Style: twin.StyleDefault})
// 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, _ = cellsFromString("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})
assert.Equal(t, tokens[2], Token{Rune: 'b', Style: tcell.StyleDefault})
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.Cell{Rune: '•', Style: twin.StyleDefault})
assert.Equal(t, tokens[2], twin.Cell{Rune: 'b', Style: twin.StyleDefault})
// 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, _ = cellsFromString("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})
assert.Equal(t, tokens[2], Token{Rune: 'b', Style: tcell.StyleDefault})
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.Cell{Rune: '•', Style: twin.StyleDefault})
assert.Equal(t, tokens[2], twin.Cell{Rune: 'b', Style: twin.StyleDefault})
}
func TestConsumeCompositeColorHappy(t *testing.T) {
@ -96,13 +95,13 @@ func TestConsumeCompositeColorHappy(t *testing.T) {
newIndex, color, err := consumeCompositeColor([]string{"38", "5", "74"}, 0)
assert.NilError(t, err)
assert.Equal(t, newIndex, 3)
assert.Equal(t, *color, tcell.Color74)
assert.Equal(t, *color, twin.NewColor256(74))
// 24 bit color
newIndex, color, err = consumeCompositeColor([]string{"38", "2", "10", "20", "30"}, 0)
assert.NilError(t, err)
assert.Equal(t, newIndex, 5)
assert.Equal(t, *color, tcell.NewRGBColor(10, 20, 30))
assert.Equal(t, *color, twin.NewColor24Bit(10, 20, 30))
}
func TestConsumeCompositeColorHappyMidSequence(t *testing.T) {
@ -111,13 +110,13 @@ func TestConsumeCompositeColorHappyMidSequence(t *testing.T) {
newIndex, color, err := consumeCompositeColor([]string{"whatever", "38", "5", "74"}, 1)
assert.NilError(t, err)
assert.Equal(t, newIndex, 4)
assert.Equal(t, *color, tcell.Color74)
assert.Equal(t, *color, twin.NewColor256(74))
// 24 bit color
newIndex, color, err = consumeCompositeColor([]string{"whatever", "38", "2", "10", "20", "30"}, 1)
assert.NilError(t, err)
assert.Equal(t, newIndex, 6)
assert.Equal(t, *color, tcell.NewRGBColor(10, 20, 30))
assert.Equal(t, *color, twin.NewColor24Bit(10, 20, 30))
}
func TestConsumeCompositeColorBadPrefix(t *testing.T) {
@ -179,6 +178,6 @@ func TestConsumeCompositeColorIncomplete24Bit(t *testing.T) {
}
func TestUpdateStyle(t *testing.T) {
numberColored := updateStyle(tcell.StyleDefault, "\x1b[33m")
assert.Equal(t, numberColored, tcell.StyleDefault.Foreground(tcell.ColorOlive))
numberColored := updateStyle(twin.StyleDefault, "\x1b[33m")
assert.Equal(t, numberColored, twin.StyleDefault.Foreground(twin.NewColor16(3)))
}

View File

@ -1,24 +1,26 @@
package m
import "github.com/gdamore/tcell/v2"
import "github.com/walles/moar/twin"
// Page displays text in a pager.
func (p *Pager) Page() error {
screen, e := tcell.NewScreen()
screen, e := twin.NewScreen()
if e != nil {
// Screen setup failed
return e
}
defer func() {
if p.DeInit {
screen.Fini()
screen.Close()
return
}
// See: https://github.com/walles/moar/pull/39
// FIXME: Consider moving this logic into the twin package.
w, h := screen.Size()
screen.ShowCursor(0, h - 1)
screen.ShowCursorAt(0, h-1)
for x := 0; x < w; x++ {
screen.SetContent(x, h - 1, ' ', nil, tcell.StyleDefault)
screen.SetCell(x, h-1, twin.NewCell(' ', twin.StyleDefault))
}
screen.Show()
}()

View File

@ -2,7 +2,6 @@ package m
import (
"fmt"
"os"
"regexp"
"strconv"
"time"
@ -10,8 +9,7 @@ import (
"unicode/utf8"
log "github.com/sirupsen/logrus"
"github.com/gdamore/tcell/v2"
"github.com/walles/moar/twin"
)
// FIXME: Profile the pager while searching through a large file
@ -24,13 +22,19 @@ const (
_NotFound _PagerMode = 2
)
type eventSpinnerUpdate struct {
spinner string
}
type eventMoreLinesAvailable struct{}
// Styling of line numbers
var _NumberStyle = tcell.StyleDefault.Dim(true)
var _NumberStyle = twin.StyleDefault.WithAttr(twin.AttrDim)
// Pager is the main on-screen pager
type Pager struct {
reader *Reader
screen tcell.Screen
screen twin.Screen
quit bool
firstLineOneBased int
leftColumnZeroBased int
@ -132,12 +136,12 @@ func (p *Pager) _AddLine(fileLineNumber *int, numberPrefixLength int, screenLine
break
}
p.screen.SetContent(column, screenLineNumber, digit, nil, _NumberStyle)
p.screen.SetCell(column, screenLineNumber, twin.NewCell(digit, _NumberStyle))
}
tokens := createScreenLine(p.leftColumnZeroBased, screenWidth-numberPrefixLength, line, p.searchPattern)
for column, token := range tokens {
p.screen.SetContent(column+numberPrefixLength, screenLineNumber, token.Rune, nil, token.Style)
p.screen.SetCell(column+numberPrefixLength, screenLineNumber, token)
}
}
@ -146,14 +150,14 @@ func createScreenLine(
screenColumnsCount int,
line *Line,
search *regexp.Regexp,
) []Token {
var returnMe []Token
) []twin.Cell {
var returnMe []twin.Cell
searchHitDelta := 0
if stringIndexAtColumnZero > 0 {
// Indicate that it's possible to scroll left
returnMe = append(returnMe, Token{
returnMe = append(returnMe, twin.Cell{
Rune: '<',
Style: tcell.StyleDefault.Reverse(true),
Style: twin.StyleDefault.WithAttr(twin.AttrReverse),
})
searchHitDelta = -1
}
@ -169,9 +173,9 @@ func createScreenLine(
if len(returnMe) >= screenColumnsCount {
// We are trying to add a character to the right of the screen.
// Indicate that this line continues to the right.
returnMe[len(returnMe)-1] = Token{
returnMe[len(returnMe)-1] = twin.Cell{
Rune: '>',
Style: tcell.StyleDefault.Reverse(true),
Style: twin.StyleDefault.WithAttr(twin.AttrReverse),
}
break
}
@ -179,10 +183,10 @@ func createScreenLine(
style := token.Style
if matchRanges.InRange(len(returnMe) + stringIndexAtColumnZero + searchHitDelta) {
// Search hits in reverse video
style = style.Reverse(true)
style = style.WithAttr(twin.AttrReverse)
}
returnMe = append(returnMe, Token{
returnMe = append(returnMe, twin.Cell{
Rune: token.Rune,
Style: style,
})
@ -196,12 +200,12 @@ func (p *Pager) _AddSearchFooter() {
pos := 0
for _, token := range "Search: " + p.searchString {
p.screen.SetContent(pos, height-1, token, nil, tcell.StyleDefault)
p.screen.SetCell(pos, height-1, twin.NewCell(token, twin.StyleDefault))
pos++
}
// Add a cursor
p.screen.SetContent(pos, height-1, ' ', nil, tcell.StyleDefault.Reverse(true))
p.screen.SetCell(pos, height-1, twin.NewCell(' ', twin.StyleDefault.WithAttr(twin.AttrReverse)))
}
func (p *Pager) _AddLines(spinner string) {
@ -270,14 +274,14 @@ func (p *Pager) _SetFooter(footer string) {
width, height := p.screen.Size()
pos := 0
footerStyle := tcell.StyleDefault.Reverse(true)
footerStyle := twin.StyleDefault.WithAttr(twin.AttrReverse)
for _, token := range footer {
p.screen.SetContent(pos, height-1, token, nil, footerStyle)
p.screen.SetCell(pos, height-1, twin.NewCell(token, footerStyle))
pos++
}
for ; pos < width; pos++ {
p.screen.SetContent(pos, height-1, ' ', nil, footerStyle)
p.screen.SetCell(pos, height-1, twin.NewCell(' ', footerStyle))
}
}
@ -484,12 +488,12 @@ func removeLastChar(s string) string {
return s[:len(s)-size]
}
func (p *Pager) _OnSearchKey(key tcell.Key) {
func (p *Pager) _OnSearchKey(key twin.KeyCode) {
switch key {
case tcell.KeyEscape, tcell.KeyEnter:
case twin.KeyEscape, twin.KeyEnter:
p.mode = _Viewing
case tcell.KeyBackspace, tcell.KeyDEL:
case twin.KeyBackspace, twin.KeyDelete:
if len(p.searchString) == 0 {
return
}
@ -497,22 +501,22 @@ func (p *Pager) _OnSearchKey(key tcell.Key) {
p.searchString = removeLastChar(p.searchString)
p._UpdateSearchPattern()
case tcell.KeyUp:
case twin.KeyUp:
// Clipping is done in _AddLines()
p.firstLineOneBased--
p.mode = _Viewing
case tcell.KeyDown:
case twin.KeyDown:
// Clipping is done in _AddLines()
p.firstLineOneBased++
p.mode = _Viewing
case tcell.KeyPgUp:
case twin.KeyPgUp:
_, height := p.screen.Size()
p.firstLineOneBased -= (height - 1)
p.mode = _Viewing
case tcell.KeyPgDn:
case twin.KeyPgDown:
_, height := p.screen.Size()
p.firstLineOneBased += (height - 1)
p.mode = _Viewing
@ -541,16 +545,9 @@ func (p *Pager) _MoveRight(delta int) {
}
}
func (p *Pager) _OnKey(key tcell.Key) {
if key == tcell.KeyCtrlL {
// This is useful when we're piping in from something writing to both
// stdout and stderr.
p.screen.Sync()
return
}
func (p *Pager) _OnKey(keyCode twin.KeyCode) {
if p.mode == _Searching {
p._OnSearchKey(key)
p._OnSearchKey(keyCode)
return
}
if p.mode != _Viewing && p.mode != _NotFound {
@ -560,40 +557,40 @@ func (p *Pager) _OnKey(key tcell.Key) {
// Reset the not-found marker on non-search keypresses
p.mode = _Viewing
switch key {
case tcell.KeyEscape:
switch keyCode {
case twin.KeyEscape:
p.Quit()
case tcell.KeyUp:
case twin.KeyUp:
// Clipping is done in _AddLines()
p.firstLineOneBased--
case tcell.KeyDown, tcell.KeyEnter:
case twin.KeyDown, twin.KeyEnter:
// Clipping is done in _AddLines()
p.firstLineOneBased++
case tcell.KeyRight:
case twin.KeyRight:
p._MoveRight(16)
case tcell.KeyLeft:
case twin.KeyLeft:
p._MoveRight(-16)
case tcell.KeyHome:
case twin.KeyHome:
p.firstLineOneBased = 1
case tcell.KeyEnd:
case twin.KeyEnd:
p.firstLineOneBased = p.reader.GetLineCount()
case tcell.KeyPgDn:
case twin.KeyPgDown:
_, height := p.screen.Size()
p.firstLineOneBased += (height - 1)
case tcell.KeyPgUp:
case twin.KeyPgUp:
_, height := p.screen.Size()
p.firstLineOneBased -= (height - 1)
default:
log.Debugf("Unhandled key event %v", key)
log.Debugf("Unhandled key event %v", keyCode)
}
}
@ -683,27 +680,18 @@ func (p *Pager) _OnRune(char rune) {
}
// StartPaging brings up the pager on screen
func (p *Pager) StartPaging(screen tcell.Screen) {
// We want to match the terminal theme, see screen.Init() source code
os.Setenv("TCELL_TRUECOLOR", "disable")
func (p *Pager) StartPaging(screen twin.Screen) {
SetManPageFormatFromEnv()
if e := screen.Init(); e != nil {
fmt.Fprintf(os.Stderr, "%v\n", e)
os.Exit(1)
}
p.screen = screen
screen.EnableMouse()
screen.Show()
p._Redraw("")
go func() {
for {
// Wait for new lines to appear
// Wait for new lines to appear...
<-p.reader.moreLinesAdded
screen.PostEvent(tcell.NewEventInterrupt(nil))
// ... and notify the main loop so it can show them:
screen.Events() <- eventMoreLinesAvailable{}
// Delay updates a bit so that we don't waste time refreshing
// the screen too often.
@ -716,6 +704,7 @@ func (p *Pager) StartPaging(screen tcell.Screen) {
}()
go func() {
// Spin the spinner as long as contents is still loading
done := false
spinnerFrames := [...]string{"/.\\", "-o-", "\\O/", "| |"}
spinnerIndex := 0
@ -732,7 +721,7 @@ func (p *Pager) StartPaging(screen tcell.Screen) {
break
}
screen.PostEvent(tcell.NewEventInterrupt(spinnerFrames[spinnerIndex]))
screen.Events() <- eventSpinnerUpdate{spinnerFrames[spinnerIndex]}
spinnerIndex++
if spinnerIndex >= len(spinnerFrames) {
spinnerIndex = 0
@ -742,59 +731,55 @@ func (p *Pager) StartPaging(screen tcell.Screen) {
}
// Empty our spinner, loading done!
screen.PostEvent(tcell.NewEventInterrupt(""))
screen.Events() <- eventSpinnerUpdate{""}
}()
// Main loop
spinner := ""
for !p.quit {
ev := screen.PollEvent()
switch ev := ev.(type) {
case *tcell.EventKey:
if ev.Key() == tcell.KeyRune {
p._OnRune(ev.Rune())
} else {
p._OnKey(ev.Key())
}
if len(screen.Events()) == 0 {
// Nothing more to process for now, redraw the screen!
p._Redraw(spinner)
}
case *tcell.EventMouse:
switch ev.Buttons() {
case tcell.WheelUp:
event := <-screen.Events()
switch event := event.(type) {
case twin.EventKeyCode:
p._OnKey(event.KeyCode())
case twin.EventRune:
p._OnRune(event.Rune())
case twin.EventMouse:
switch event.Buttons() {
case twin.MouseWheelUp:
// Clipping is done in _AddLines()
p.firstLineOneBased--
case tcell.WheelDown:
case twin.MouseWheelDown:
// Clipping is done in _AddLines()
p.firstLineOneBased++
case tcell.WheelRight:
p._MoveRight(16)
case tcell.WheelLeft:
case twin.MouseWheelLeft:
p._MoveRight(-16)
case twin.MouseWheelRight:
p._MoveRight(16)
}
case *tcell.EventResize:
case twin.EventResize:
// We'll be implicitly redrawn just by taking another lap in the loop
case *tcell.EventInterrupt:
// This means we got more lines, look for NewEventInterrupt higher up
// in this file. Doing nothing here is fine, the refresh happens after
// this switch statement.
data := ev.Data()
if data != nil {
// From: https://yourbasic.org/golang/interface-to-string/
spinner = fmt.Sprintf("%v", data)
}
case eventMoreLinesAvailable:
// Doing nothing here is fine; screen will be refreshed on the next
// iteration of the main loop.
case eventSpinnerUpdate:
spinner = event.spinner
default:
log.Warnf("Unhandled event type: %v", ev)
log.Warnf("Unhandled event type: %v", event)
}
// FIXME: If more events are ready, skip this redraw, that
// should speed up mouse wheel scrolling
p._Redraw(spinner)
}
if p.reader.err != nil {

View File

@ -7,7 +7,7 @@ import (
"strings"
"testing"
"github.com/gdamore/tcell/v2"
"github.com/walles/moar/twin"
"gotest.tools/assert"
)
@ -17,33 +17,24 @@ func TestUnicodeRendering(t *testing.T) {
panic(err)
}
var answers = []Token{
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault),
createExpectedCell('ö', tcell.StyleDefault),
var answers = []twin.Cell{
twin.NewCell('å', twin.StyleDefault),
twin.NewCell('ä', twin.StyleDefault),
twin.NewCell('ö', twin.StyleDefault),
}
contents := startPaging(t, reader)
contents := startPaging(t, reader).GetRow(0)
for pos, expected := range answers {
expected.LogDifference(t, contents[pos])
logDifference(t, expected, contents[pos])
}
}
func (expected Token) LogDifference(t *testing.T, actual tcell.SimCell) {
if actual.Runes[0] == expected.Rune && actual.Style == expected.Style {
func logDifference(t *testing.T, expected twin.Cell, actual twin.Cell) {
if actual.Rune == expected.Rune && actual.Style == expected.Style {
return
}
t.Errorf("Expected '%s'/0x%x, got '%s'/0x%x",
string(expected.Rune), expected.Style,
string(actual.Runes[0]), actual.Style)
}
func createExpectedCell(Rune rune, Style tcell.Style) Token {
return Token{
Rune: Rune,
Style: Style,
}
t.Errorf("Expected %v, got %v", expected, actual)
}
func TestFgColorRendering(t *testing.T) {
@ -53,21 +44,21 @@ func TestFgColorRendering(t *testing.T) {
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),
var answers = []twin.Cell{
twin.NewCell('a', twin.StyleDefault.Foreground(twin.NewColor16(0))),
twin.NewCell('b', twin.StyleDefault.Foreground(twin.NewColor16(1))),
twin.NewCell('c', twin.StyleDefault.Foreground(twin.NewColor16(2))),
twin.NewCell('d', twin.StyleDefault.Foreground(twin.NewColor16(3))),
twin.NewCell('e', twin.StyleDefault.Foreground(twin.NewColor16(4))),
twin.NewCell('f', twin.StyleDefault.Foreground(twin.NewColor16(5))),
twin.NewCell('g', twin.StyleDefault.Foreground(twin.NewColor16(6))),
twin.NewCell('h', twin.StyleDefault.Foreground(twin.NewColor16(7))),
twin.NewCell('i', twin.StyleDefault),
}
contents := startPaging(t, reader)
contents := startPaging(t, reader).GetRow(0)
for pos, expected := range answers {
expected.LogDifference(t, contents[pos])
logDifference(t, expected, contents[pos])
}
}
@ -78,37 +69,37 @@ func TestBrokenUtf8(t *testing.T) {
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),
var answers = []twin.Cell{
twin.NewCell('a', twin.StyleDefault),
twin.NewCell('b', twin.StyleDefault),
twin.NewCell('c', twin.StyleDefault),
twin.NewCell('?', twin.StyleDefault.Foreground(twin.NewColor16(7)).Background(twin.NewColor16(1))),
twin.NewCell('d', twin.StyleDefault),
twin.NewCell('e', twin.StyleDefault),
twin.NewCell('f', twin.StyleDefault),
}
contents := startPaging(t, reader)
contents := startPaging(t, reader).GetRow(0)
for pos, expected := range answers {
expected.LogDifference(t, contents[pos])
logDifference(t, expected, contents[pos])
}
}
func startPaging(t *testing.T, reader *Reader) []tcell.SimCell {
screen := tcell.NewSimulationScreen("UTF-8")
func startPaging(t *testing.T, reader *Reader) *twin.FakeScreen {
screen := twin.NewFakeScreen(20, 10)
pager := NewPager(reader)
pager.ShowLineNumbers = false
// Tell our Pager to quit immediately
pager.Quit()
var loglines strings.Builder
// Except for just quitting, this also associates our FakeScreen with the Pager
pager.StartPaging(screen)
contents, _, _ := screen.GetContents()
if len(loglines.String()) > 0 {
t.Logf("%s", loglines.String())
}
// This makes sure at least one frame gets rendered
pager._Redraw("")
return contents
return screen
}
// assertIndexOfFirstX verifies the (zero-based) index of the first 'x'
@ -118,9 +109,9 @@ func assertIndexOfFirstX(t *testing.T, s string, expectedIndex int) {
panic(err)
}
contents := startPaging(t, reader)
contents := startPaging(t, reader).GetRow(0)
for pos, cell := range contents {
if cell.Runes[0] != 'x' {
if cell.Rune != 'x' {
continue
}
@ -171,27 +162,27 @@ func TestCodeHighlighting(t *testing.T) {
panic(err)
}
packageKeywordStyle := tcell.StyleDefault.Bold(true).Foreground(tcell.NewHexColor(0x6AB825))
packageNameStyle := tcell.StyleDefault.Foreground(tcell.NewHexColor(0xD0D0D0))
var answers = []Token{
createExpectedCell('p', packageKeywordStyle),
createExpectedCell('a', packageKeywordStyle),
createExpectedCell('c', packageKeywordStyle),
createExpectedCell('k', packageKeywordStyle),
createExpectedCell('a', packageKeywordStyle),
createExpectedCell('g', packageKeywordStyle),
createExpectedCell('e', packageKeywordStyle),
createExpectedCell(' ', packageNameStyle),
createExpectedCell('m', packageNameStyle),
packageKeywordStyle := twin.StyleDefault.WithAttr(twin.AttrBold).Foreground(twin.NewColorHex(0x6AB825))
packageNameStyle := twin.StyleDefault.Foreground(twin.NewColorHex(0xD0D0D0))
var answers = []twin.Cell{
twin.NewCell('p', packageKeywordStyle),
twin.NewCell('a', packageKeywordStyle),
twin.NewCell('c', packageKeywordStyle),
twin.NewCell('k', packageKeywordStyle),
twin.NewCell('a', packageKeywordStyle),
twin.NewCell('g', packageKeywordStyle),
twin.NewCell('e', packageKeywordStyle),
twin.NewCell(' ', packageNameStyle),
twin.NewCell('m', packageNameStyle),
}
contents := startPaging(t, reader)
contents := startPaging(t, reader).GetRow(0)
for pos, expected := range answers {
expected.LogDifference(t, contents[pos])
logDifference(t, expected, contents[pos])
}
}
func testManPageFormatting(t *testing.T, input string, expected Token) {
func testManPageFormatting(t *testing.T, input string, expected twin.Cell) {
reader := NewReaderFromStream("", strings.NewReader(input))
if err := reader._Wait(); err != nil {
panic(err)
@ -203,17 +194,17 @@ func testManPageFormatting(t *testing.T, input string, expected Token) {
os.Setenv("LESS_TERMCAP_us", "")
resetManPageFormatForTesting()
contents := startPaging(t, reader)
expected.LogDifference(t, contents[0])
assert.Equal(t, contents[1].Runes[0], ' ')
contents := startPaging(t, reader).GetRow(0)
logDifference(t, expected, contents[0])
assert.Equal(t, contents[1].Rune, ' ')
}
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", twin.NewCell('N', twin.StyleDefault.WithAttr(twin.AttrBold)))
testManPageFormatting(t, "_\x08x", twin.NewCell('x', twin.StyleDefault.WithAttr(twin.AttrUnderline)))
// Corner cases
testManPageFormatting(t, "\x08", createExpectedCell('<', tcell.StyleDefault.Foreground(tcell.ColorMaroon).Background(tcell.ColorSilver)))
testManPageFormatting(t, "\x08", twin.NewCell('<', twin.StyleDefault.Foreground(twin.NewColor16(7)).Background(twin.NewColor16(1))))
// FIXME: Test two consecutive backspaces
@ -240,7 +231,7 @@ func TestToPattern(t *testing.T) {
assert.Assert(t, toPattern(")g").MatchString(")g"))
}
func assertTokenRangesEqual(t *testing.T, actual []Token, expected []Token) {
func assertTokenRangesEqual(t *testing.T, actual []twin.Cell, expected []twin.Cell) {
if len(actual) != len(expected) {
t.Errorf("String lengths mismatch; expected %d but got %d",
len(expected), len(actual))
@ -257,10 +248,7 @@ func assertTokenRangesEqual(t *testing.T, actual []Token, expected []Token) {
continue
}
t.Errorf("At (0-based) position %d: Expected '%s'/0x%x, got '%s'/0x%x",
pos,
string(expectedToken.Rune), expectedToken.Style,
string(actualToken.Rune), actualToken.Style)
t.Errorf("At (0-based) position %d: Expected %v, got %v", pos, expectedToken, actualToken)
}
}
@ -273,20 +261,20 @@ func TestCreateScreenLineBase(t *testing.T) {
func TestCreateScreenLineOverflowRight(t *testing.T) {
line := NewLine("012345")
screenLine := createScreenLine(0, 3, line, nil)
assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('0', tcell.StyleDefault),
createExpectedCell('1', tcell.StyleDefault),
createExpectedCell('>', tcell.StyleDefault.Reverse(true)),
assertTokenRangesEqual(t, screenLine, []twin.Cell{
twin.NewCell('0', twin.StyleDefault),
twin.NewCell('1', twin.StyleDefault),
twin.NewCell('>', twin.StyleDefault.WithAttr(twin.AttrReverse)),
})
}
func TestCreateScreenLineUnderflowLeft(t *testing.T) {
line := NewLine("012")
screenLine := createScreenLine(1, 3, line, nil)
assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('1', tcell.StyleDefault),
createExpectedCell('2', tcell.StyleDefault),
assertTokenRangesEqual(t, screenLine, []twin.Cell{
twin.NewCell('<', twin.StyleDefault.WithAttr(twin.AttrReverse)),
twin.NewCell('1', twin.StyleDefault),
twin.NewCell('2', twin.StyleDefault),
})
}
@ -298,10 +286,10 @@ func TestCreateScreenLineSearchHit(t *testing.T) {
line := NewLine("abc")
screenLine := createScreenLine(0, 3, line, pattern)
assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('a', tcell.StyleDefault),
createExpectedCell('b', tcell.StyleDefault.Reverse(true)),
createExpectedCell('c', tcell.StyleDefault),
assertTokenRangesEqual(t, screenLine, []twin.Cell{
twin.NewCell('a', twin.StyleDefault),
twin.NewCell('b', twin.StyleDefault.WithAttr(twin.AttrReverse)),
twin.NewCell('c', twin.StyleDefault),
})
}
@ -313,10 +301,10 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) {
line := NewLine("åäö")
screenLine := createScreenLine(0, 3, line, pattern)
assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
createExpectedCell('ö', tcell.StyleDefault),
assertTokenRangesEqual(t, screenLine, []twin.Cell{
twin.NewCell('å', twin.StyleDefault),
twin.NewCell('ä', twin.StyleDefault.WithAttr(twin.AttrReverse)),
twin.NewCell('ö', twin.StyleDefault),
})
}
@ -326,11 +314,11 @@ func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) {
line := NewLine("ååäö")
screenLine := createScreenLine(1, 4, line, pattern)
assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
createExpectedCell('ö', tcell.StyleDefault),
assertTokenRangesEqual(t, screenLine, []twin.Cell{
twin.NewCell('<', twin.StyleDefault.WithAttr(twin.AttrReverse)),
twin.NewCell('å', twin.StyleDefault),
twin.NewCell('ä', twin.StyleDefault.WithAttr(twin.AttrReverse)),
twin.NewCell('ö', twin.StyleDefault),
})
}
@ -340,11 +328,11 @@ func TestCreateScreenLineScrolled2Utf8SearchHit(t *testing.T) {
line := NewLine("åååäö")
screenLine := createScreenLine(2, 4, line, pattern)
assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
createExpectedCell('ö', tcell.StyleDefault),
assertTokenRangesEqual(t, screenLine, []twin.Cell{
twin.NewCell('<', twin.StyleDefault.WithAttr(twin.AttrReverse)),
twin.NewCell('å', twin.StyleDefault),
twin.NewCell('ä', twin.StyleDefault.WithAttr(twin.AttrReverse)),
twin.NewCell('ö', twin.StyleDefault),
})
}

18
moar.go
View File

@ -11,11 +11,10 @@ import (
"time"
log "github.com/sirupsen/logrus"
"golang.org/x/term"
"github.com/gdamore/tcell/v2"
"github.com/walles/moar/m"
"golang.org/x/crypto/ssh/terminal"
"github.com/walles/moar/twin"
)
var versionString = "Should be set when building, please use build.sh to build"
@ -86,6 +85,7 @@ func main() {
}
printVersion := flag.Bool("version", false, "Prints the moar version number")
debug := flag.Bool("debug", false, "Print debug logs after exiting")
trace := flag.Bool("trace", false, "Print trace logs after exiting")
// FIXME: Support --no-highlight
@ -96,7 +96,9 @@ func main() {
}
log.SetLevel(log.InfoLevel)
if *debug {
if *trace {
log.SetLevel(log.TraceLevel)
} else if *debug {
log.SetLevel(log.DebugLevel)
}
@ -104,8 +106,8 @@ func main() {
TimestampFormat: time.RFC3339Nano,
})
stdinIsRedirected := !terminal.IsTerminal(int(os.Stdin.Fd()))
stdoutIsRedirected := !terminal.IsTerminal(int(os.Stdout.Fd()))
stdinIsRedirected := !term.IsTerminal(int(os.Stdin.Fd()))
stdoutIsRedirected := !term.IsTerminal(int(os.Stdout.Fd()))
if stdinIsRedirected && stdoutIsRedirected {
io.Copy(os.Stdout, os.Stdin)
os.Exit(0)
@ -150,7 +152,7 @@ func main() {
}
func startPaging(reader *m.Reader) {
screen, e := tcell.NewScreen()
screen, e := twin.NewScreen()
if e != nil {
panic(e)
}
@ -158,7 +160,7 @@ func startPaging(reader *m.Reader) {
var loglines strings.Builder
defer func() {
// Restore screen...
screen.Fini()
screen.Close()
// ... before printing panic() output, otherwise the output will have
// broken linefeeds and be hard to follow.

View File

@ -42,7 +42,7 @@ fi
echo Test --version...
./moar --version > /dev/null # Should exit with code 0
diff -u <(./moar --version) <(git describe --tags --dirty)
diff -u <(./moar --version) <(git describe --tags --dirty --always)
# FIXME: On unknown command line options, test that help text goes to stderr