mirror of
https://github.com/walles/moar.git
synced 2024-11-22 03:14:56 +03:00
Start telling screen cells from styled runes
One rune can cover multiple screen cells.
This commit is contained in:
parent
bac6d0d80e
commit
e0e9ee6610
18
m/line.go
18
m/line.go
@ -24,15 +24,15 @@ func NewLine(raw string) Line {
|
|||||||
|
|
||||||
// Returns a representation of the string split into styled tokens. Any regexp
|
// Returns a representation of the string split into styled tokens. Any regexp
|
||||||
// matches are highlighted. A nil regexp means no highlighting.
|
// matches are highlighted. A nil regexp means no highlighting.
|
||||||
func (line *Line) HighlightedTokens(linePrefix string, search *regexp.Regexp, lineNumber *linenumbers.LineNumber) textstyles.CellsWithTrailer {
|
func (line *Line) HighlightedTokens(linePrefix string, search *regexp.Regexp, lineNumber *linenumbers.LineNumber) textstyles.StyledRunesWithTrailer {
|
||||||
plain := line.Plain(lineNumber)
|
plain := line.Plain(lineNumber)
|
||||||
matchRanges := getMatchRanges(&plain, search)
|
matchRanges := getMatchRanges(&plain, search)
|
||||||
|
|
||||||
fromString := textstyles.CellsFromString(linePrefix, line.raw, lineNumber)
|
fromString := textstyles.StyledRunesFromString(linePrefix, line.raw, lineNumber)
|
||||||
returnCells := make([]twin.Cell, 0, len(fromString.Cells))
|
returnRunes := make([]twin.StyledRune, 0, len(fromString.StyledRunes))
|
||||||
for _, token := range fromString.Cells {
|
for _, token := range fromString.StyledRunes {
|
||||||
style := token.Style
|
style := token.Style
|
||||||
if matchRanges.InRange(len(returnCells)) {
|
if matchRanges.InRange(len(returnRunes)) {
|
||||||
if standoutStyle != nil {
|
if standoutStyle != nil {
|
||||||
style = *standoutStyle
|
style = *standoutStyle
|
||||||
} else {
|
} else {
|
||||||
@ -42,15 +42,15 @@ func (line *Line) HighlightedTokens(linePrefix string, search *regexp.Regexp, li
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
returnCells = append(returnCells, twin.Cell{
|
returnRunes = append(returnRunes, twin.StyledRune{
|
||||||
Rune: token.Rune,
|
Rune: token.Rune,
|
||||||
Style: style,
|
Style: style,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return textstyles.CellsWithTrailer{
|
return textstyles.StyledRunesWithTrailer{
|
||||||
Cells: returnCells,
|
StyledRunes: returnRunes,
|
||||||
Trailer: fromString.Trailer,
|
Trailer: fromString.Trailer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,8 +25,8 @@ func TestHighlightedTokensWithManPageHeading(t *testing.T) {
|
|||||||
line := NewLine(manPageHeading)
|
line := NewLine(manPageHeading)
|
||||||
highlighted := line.HighlightedTokens(prefix, nil, nil)
|
highlighted := line.HighlightedTokens(prefix, nil, nil)
|
||||||
|
|
||||||
assert.Equal(t, len(highlighted.Cells), len(headingText))
|
assert.Equal(t, len(highlighted.StyledRunes), len(headingText))
|
||||||
for i, cell := range highlighted.Cells {
|
for i, cell := range highlighted.StyledRunes {
|
||||||
assert.Equal(t, cell.Rune, rune(headingText[i]))
|
assert.Equal(t, cell.Rune, rune(headingText[i]))
|
||||||
assert.Equal(t, cell.Style, textstyles.ManPageHeading)
|
assert.Equal(t, cell.Style, textstyles.ManPageHeading)
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import (
|
|||||||
//revive:disable-next-line:var-naming
|
//revive:disable-next-line:var-naming
|
||||||
const NO_BREAK_SPACE = '\xa0'
|
const NO_BREAK_SPACE = '\xa0'
|
||||||
|
|
||||||
func getWrapWidth(line []twin.Cell, maxWrapWidth int) int {
|
func getWrapWidth(line []twin.StyledRune, maxWrapWidth int) int {
|
||||||
if len(line) <= maxWrapWidth {
|
if len(line) <= maxWrapWidth {
|
||||||
panic(fmt.Errorf("cannot compute wrap width when input isn't longer than max (%d<=%d)",
|
panic(fmt.Errorf("cannot compute wrap width when input isn't longer than max (%d<=%d)",
|
||||||
len(line), maxWrapWidth))
|
len(line), maxWrapWidth))
|
||||||
@ -49,16 +49,16 @@ func getWrapWidth(line []twin.Cell, maxWrapWidth int) int {
|
|||||||
return maxWrapWidth
|
return maxWrapWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrapLine(width int, line []twin.Cell) [][]twin.Cell {
|
func wrapLine(width int, line []twin.StyledRune) [][]twin.StyledRune {
|
||||||
// Trailing space risks showing up by itself on a line, which would just
|
// Trailing space risks showing up by itself on a line, which would just
|
||||||
// look weird.
|
// look weird.
|
||||||
line = twin.TrimSpaceRight(line)
|
line = twin.TrimSpaceRight(line)
|
||||||
|
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
return [][]twin.Cell{{}}
|
return [][]twin.StyledRune{{}}
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapped := make([][]twin.Cell, 0, len(line)/width)
|
wrapped := make([][]twin.StyledRune, 0, len(line)/width)
|
||||||
for len(line) > width {
|
for len(line) > width {
|
||||||
wrapWidth := getWrapWidth(line, width)
|
wrapWidth := getWrapWidth(line, width)
|
||||||
firstPart := line[:wrapWidth]
|
firstPart := line[:wrapWidth]
|
||||||
|
@ -7,12 +7,12 @@ import (
|
|||||||
"github.com/walles/moar/twin"
|
"github.com/walles/moar/twin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func tokenize(input string) []twin.Cell {
|
func tokenize(input string) []twin.StyledRune {
|
||||||
line := NewLine(input)
|
line := NewLine(input)
|
||||||
return line.HighlightedTokens("", nil, nil).Cells
|
return line.HighlightedTokens("", nil, nil).StyledRunes
|
||||||
}
|
}
|
||||||
|
|
||||||
func rowsToString(cellLines [][]twin.Cell) string {
|
func rowsToString(cellLines [][]twin.StyledRune) string {
|
||||||
returnMe := ""
|
returnMe := ""
|
||||||
for _, cellLine := range cellLines {
|
for _, cellLine := range cellLines {
|
||||||
lineString := ""
|
lineString := ""
|
||||||
@ -33,7 +33,7 @@ func assertWrap(t *testing.T, input string, widthInScreenCells int, wrappedLines
|
|||||||
toWrap := tokenize(input)
|
toWrap := tokenize(input)
|
||||||
actual := wrapLine(widthInScreenCells, toWrap)
|
actual := wrapLine(widthInScreenCells, toWrap)
|
||||||
|
|
||||||
expected := [][]twin.Cell{}
|
expected := [][]twin.StyledRune{}
|
||||||
for _, wrappedLine := range wrappedLines {
|
for _, wrappedLine := range wrappedLines {
|
||||||
expected = append(expected, tokenize(wrappedLine))
|
expected = append(expected, tokenize(wrappedLine))
|
||||||
}
|
}
|
||||||
|
12
m/pager.go
12
m/pager.go
@ -77,8 +77,8 @@ type Pager struct {
|
|||||||
QuitIfOneScreen bool
|
QuitIfOneScreen bool
|
||||||
|
|
||||||
// Ref: https://github.com/walles/moar/issues/94
|
// Ref: https://github.com/walles/moar/issues/94
|
||||||
ScrollLeftHint twin.Cell
|
ScrollLeftHint twin.StyledRune
|
||||||
ScrollRightHint twin.Cell
|
ScrollRightHint twin.StyledRune
|
||||||
|
|
||||||
SideScrollAmount int // Should be positive
|
SideScrollAmount int // Should be positive
|
||||||
|
|
||||||
@ -184,8 +184,8 @@ func NewPager(r *Reader) *Pager {
|
|||||||
ShowStatusBar: true,
|
ShowStatusBar: true,
|
||||||
DeInit: true,
|
DeInit: true,
|
||||||
SideScrollAmount: 16,
|
SideScrollAmount: 16,
|
||||||
ScrollLeftHint: twin.NewCell('<', twin.StyleDefault.WithAttr(twin.AttrReverse)),
|
ScrollLeftHint: twin.NewStyledRune('<', twin.StyleDefault.WithAttr(twin.AttrReverse)),
|
||||||
ScrollRightHint: twin.NewCell('>', twin.StyleDefault.WithAttr(twin.AttrReverse)),
|
ScrollRightHint: twin.NewStyledRune('>', twin.StyleDefault.WithAttr(twin.AttrReverse)),
|
||||||
scrollPosition: newScrollPosition(name),
|
scrollPosition: newScrollPosition(name),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,12 +210,12 @@ func (p *Pager) setFooter(footer string) {
|
|||||||
|
|
||||||
pos := 0
|
pos := 0
|
||||||
for _, token := range footer {
|
for _, token := range footer {
|
||||||
p.screen.SetCell(pos, height-1, twin.NewCell(token, statusbarStyle))
|
p.screen.SetCell(pos, height-1, twin.NewStyledRune(token, statusbarStyle))
|
||||||
pos++
|
pos++
|
||||||
}
|
}
|
||||||
|
|
||||||
for ; pos < width; pos++ {
|
for ; pos < width; pos++ {
|
||||||
p.screen.SetCell(pos, height-1, twin.NewCell(' ', statusbarStyle))
|
p.screen.SetCell(pos, height-1, twin.NewStyledRune(' ', statusbarStyle))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
134
m/pager_test.go
134
m/pager_test.go
@ -27,19 +27,19 @@ const blueBackgroundClearToEol = "\x1b[44m\x1b[K" // No 0 before the K, should
|
|||||||
func TestUnicodeRendering(t *testing.T) {
|
func TestUnicodeRendering(t *testing.T) {
|
||||||
reader := NewReaderFromText("", "åäö")
|
reader := NewReaderFromText("", "åäö")
|
||||||
|
|
||||||
var answers = []twin.Cell{
|
var answers = []twin.StyledRune{
|
||||||
twin.NewCell('å', twin.StyleDefault),
|
twin.NewStyledRune('å', twin.StyleDefault),
|
||||||
twin.NewCell('ä', twin.StyleDefault),
|
twin.NewStyledRune('ä', twin.StyleDefault),
|
||||||
twin.NewCell('ö', twin.StyleDefault),
|
twin.NewStyledRune('ö', twin.StyleDefault),
|
||||||
}
|
}
|
||||||
|
|
||||||
contents := startPaging(t, reader).GetRow(0)
|
contents := startPaging(t, reader).GetRow(0)
|
||||||
for pos, expected := range answers {
|
for pos, expected := range answers {
|
||||||
assertCellsEqual(t, expected, contents[pos])
|
assertRunesEqual(t, expected, contents[pos])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertCellsEqual(t *testing.T, expected twin.Cell, actual twin.Cell) {
|
func assertRunesEqual(t *testing.T, expected twin.StyledRune, actual twin.StyledRune) {
|
||||||
if actual.Rune == expected.Rune && actual.Style == expected.Style {
|
if actual.Rune == expected.Rune && actual.Style == expected.Style {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -51,21 +51,21 @@ func TestFgColorRendering(t *testing.T) {
|
|||||||
reader := NewReaderFromText("",
|
reader := NewReaderFromText("",
|
||||||
"\x1b[30ma\x1b[31mb\x1b[32mc\x1b[33md\x1b[34me\x1b[35mf\x1b[36mg\x1b[37mh\x1b[0mi")
|
"\x1b[30ma\x1b[31mb\x1b[32mc\x1b[33md\x1b[34me\x1b[35mf\x1b[36mg\x1b[37mh\x1b[0mi")
|
||||||
|
|
||||||
var answers = []twin.Cell{
|
var answers = []twin.StyledRune{
|
||||||
twin.NewCell('a', twin.StyleDefault.WithForeground(twin.NewColor16(0))),
|
twin.NewStyledRune('a', twin.StyleDefault.WithForeground(twin.NewColor16(0))),
|
||||||
twin.NewCell('b', twin.StyleDefault.WithForeground(twin.NewColor16(1))),
|
twin.NewStyledRune('b', twin.StyleDefault.WithForeground(twin.NewColor16(1))),
|
||||||
twin.NewCell('c', twin.StyleDefault.WithForeground(twin.NewColor16(2))),
|
twin.NewStyledRune('c', twin.StyleDefault.WithForeground(twin.NewColor16(2))),
|
||||||
twin.NewCell('d', twin.StyleDefault.WithForeground(twin.NewColor16(3))),
|
twin.NewStyledRune('d', twin.StyleDefault.WithForeground(twin.NewColor16(3))),
|
||||||
twin.NewCell('e', twin.StyleDefault.WithForeground(twin.NewColor16(4))),
|
twin.NewStyledRune('e', twin.StyleDefault.WithForeground(twin.NewColor16(4))),
|
||||||
twin.NewCell('f', twin.StyleDefault.WithForeground(twin.NewColor16(5))),
|
twin.NewStyledRune('f', twin.StyleDefault.WithForeground(twin.NewColor16(5))),
|
||||||
twin.NewCell('g', twin.StyleDefault.WithForeground(twin.NewColor16(6))),
|
twin.NewStyledRune('g', twin.StyleDefault.WithForeground(twin.NewColor16(6))),
|
||||||
twin.NewCell('h', twin.StyleDefault.WithForeground(twin.NewColor16(7))),
|
twin.NewStyledRune('h', twin.StyleDefault.WithForeground(twin.NewColor16(7))),
|
||||||
twin.NewCell('i', twin.StyleDefault),
|
twin.NewStyledRune('i', twin.StyleDefault),
|
||||||
}
|
}
|
||||||
|
|
||||||
contents := startPaging(t, reader).GetRow(0)
|
contents := startPaging(t, reader).GetRow(0)
|
||||||
for pos, expected := range answers {
|
for pos, expected := range answers {
|
||||||
assertCellsEqual(t, expected, contents[pos])
|
assertRunesEqual(t, expected, contents[pos])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,19 +78,19 @@ func TestBrokenUtf8(t *testing.T) {
|
|||||||
// The broken UTF8 character in the middle is based on "©" = 0xc2a9
|
// The broken UTF8 character in the middle is based on "©" = 0xc2a9
|
||||||
reader := NewReaderFromText("", "abc\xc2def")
|
reader := NewReaderFromText("", "abc\xc2def")
|
||||||
|
|
||||||
var answers = []twin.Cell{
|
var answers = []twin.StyledRune{
|
||||||
twin.NewCell('a', twin.StyleDefault),
|
twin.NewStyledRune('a', twin.StyleDefault),
|
||||||
twin.NewCell('b', twin.StyleDefault),
|
twin.NewStyledRune('b', twin.StyleDefault),
|
||||||
twin.NewCell('c', twin.StyleDefault),
|
twin.NewStyledRune('c', twin.StyleDefault),
|
||||||
twin.NewCell('?', twin.StyleDefault.WithForeground(twin.NewColor16(7)).WithBackground(twin.NewColor16(1))),
|
twin.NewStyledRune('?', twin.StyleDefault.WithForeground(twin.NewColor16(7)).WithBackground(twin.NewColor16(1))),
|
||||||
twin.NewCell('d', twin.StyleDefault),
|
twin.NewStyledRune('d', twin.StyleDefault),
|
||||||
twin.NewCell('e', twin.StyleDefault),
|
twin.NewStyledRune('e', twin.StyleDefault),
|
||||||
twin.NewCell('f', twin.StyleDefault),
|
twin.NewStyledRune('f', twin.StyleDefault),
|
||||||
}
|
}
|
||||||
|
|
||||||
contents := startPaging(t, reader).GetRow(0)
|
contents := startPaging(t, reader).GetRow(0)
|
||||||
for pos, expected := range answers {
|
for pos, expected := range answers {
|
||||||
assertCellsEqual(t, expected, contents[pos])
|
assertRunesEqual(t, expected, contents[pos])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,21 +171,21 @@ func TestCodeHighlighting(t *testing.T) {
|
|||||||
|
|
||||||
packageKeywordStyle := twin.StyleDefault.WithAttr(twin.AttrBold).WithForeground(twin.NewColorHex(0x6AB825))
|
packageKeywordStyle := twin.StyleDefault.WithAttr(twin.AttrBold).WithForeground(twin.NewColorHex(0x6AB825))
|
||||||
packageNameStyle := twin.StyleDefault.WithForeground(twin.NewColorHex(0xD0D0D0))
|
packageNameStyle := twin.StyleDefault.WithForeground(twin.NewColorHex(0xD0D0D0))
|
||||||
var answers = []twin.Cell{
|
var answers = []twin.StyledRune{
|
||||||
twin.NewCell('p', packageKeywordStyle),
|
twin.NewStyledRune('p', packageKeywordStyle),
|
||||||
twin.NewCell('a', packageKeywordStyle),
|
twin.NewStyledRune('a', packageKeywordStyle),
|
||||||
twin.NewCell('c', packageKeywordStyle),
|
twin.NewStyledRune('c', packageKeywordStyle),
|
||||||
twin.NewCell('k', packageKeywordStyle),
|
twin.NewStyledRune('k', packageKeywordStyle),
|
||||||
twin.NewCell('a', packageKeywordStyle),
|
twin.NewStyledRune('a', packageKeywordStyle),
|
||||||
twin.NewCell('g', packageKeywordStyle),
|
twin.NewStyledRune('g', packageKeywordStyle),
|
||||||
twin.NewCell('e', packageKeywordStyle),
|
twin.NewStyledRune('e', packageKeywordStyle),
|
||||||
twin.NewCell(' ', packageNameStyle),
|
twin.NewStyledRune(' ', packageNameStyle),
|
||||||
twin.NewCell('m', packageNameStyle),
|
twin.NewStyledRune('m', packageNameStyle),
|
||||||
}
|
}
|
||||||
|
|
||||||
contents := startPaging(t, reader).GetRow(0)
|
contents := startPaging(t, reader).GetRow(0)
|
||||||
for pos, expected := range answers {
|
for pos, expected := range answers {
|
||||||
assertCellsEqual(t, expected, contents[pos])
|
assertRunesEqual(t, expected, contents[pos])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,22 +196,22 @@ func TestCodeHighlight_compressed(t *testing.T) {
|
|||||||
assert.NilError(t, reader._wait())
|
assert.NilError(t, reader._wait())
|
||||||
|
|
||||||
markdownHeading1Style := twin.StyleDefault.WithAttr(twin.AttrBold).WithForeground(twin.NewColorHex(0xffffff))
|
markdownHeading1Style := twin.StyleDefault.WithAttr(twin.AttrBold).WithForeground(twin.NewColorHex(0xffffff))
|
||||||
var answers = []twin.Cell{
|
var answers = []twin.StyledRune{
|
||||||
twin.NewCell('#', markdownHeading1Style),
|
twin.NewStyledRune('#', markdownHeading1Style),
|
||||||
twin.NewCell(' ', markdownHeading1Style),
|
twin.NewStyledRune(' ', markdownHeading1Style),
|
||||||
twin.NewCell('M', markdownHeading1Style),
|
twin.NewStyledRune('M', markdownHeading1Style),
|
||||||
twin.NewCell('a', markdownHeading1Style),
|
twin.NewStyledRune('a', markdownHeading1Style),
|
||||||
twin.NewCell('r', markdownHeading1Style),
|
twin.NewStyledRune('r', markdownHeading1Style),
|
||||||
twin.NewCell('k', markdownHeading1Style),
|
twin.NewStyledRune('k', markdownHeading1Style),
|
||||||
twin.NewCell('d', markdownHeading1Style),
|
twin.NewStyledRune('d', markdownHeading1Style),
|
||||||
twin.NewCell('o', markdownHeading1Style),
|
twin.NewStyledRune('o', markdownHeading1Style),
|
||||||
twin.NewCell('w', markdownHeading1Style),
|
twin.NewStyledRune('w', markdownHeading1Style),
|
||||||
twin.NewCell('n', markdownHeading1Style),
|
twin.NewStyledRune('n', markdownHeading1Style),
|
||||||
}
|
}
|
||||||
|
|
||||||
contents := startPaging(t, reader).GetRow(0)
|
contents := startPaging(t, reader).GetRow(0)
|
||||||
for pos, expected := range answers {
|
for pos, expected := range answers {
|
||||||
assertCellsEqual(t, expected, contents[pos])
|
assertRunesEqual(t, expected, contents[pos])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,7 +230,7 @@ func TestCodeHighlightingIncludes(t *testing.T) {
|
|||||||
secondIncludeLine := screen.GetRow(3)
|
secondIncludeLine := screen.GetRow(3)
|
||||||
|
|
||||||
// Both should start with "#include" colored the same way
|
// Both should start with "#include" colored the same way
|
||||||
assertCellsEqual(t, firstIncludeLine[0], secondIncludeLine[0])
|
assertRunesEqual(t, firstIncludeLine[0], secondIncludeLine[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnicodePrivateUse(t *testing.T) {
|
func TestUnicodePrivateUse(t *testing.T) {
|
||||||
@ -242,10 +242,10 @@ func TestUnicodePrivateUse(t *testing.T) {
|
|||||||
char := '\uf244'
|
char := '\uf244'
|
||||||
|
|
||||||
reader := NewReaderFromText("hello", string(char))
|
reader := NewReaderFromText("hello", string(char))
|
||||||
renderedCell := startPaging(t, reader).GetRow(0)[0]
|
renderedRune := startPaging(t, reader).GetRow(0)[0]
|
||||||
|
|
||||||
// Make sure we display this character unmodified
|
// Make sure we display this character unmodified
|
||||||
assertCellsEqual(t, twin.NewCell(char, twin.StyleDefault), renderedCell)
|
assertRunesEqual(t, twin.NewStyledRune(char, twin.StyleDefault), renderedRune)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetManPageFormat() {
|
func resetManPageFormat() {
|
||||||
@ -253,7 +253,7 @@ func resetManPageFormat() {
|
|||||||
textstyles.ManPageUnderline = twin.StyleDefault.WithAttr(twin.AttrUnderline)
|
textstyles.ManPageUnderline = twin.StyleDefault.WithAttr(twin.AttrUnderline)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testManPageFormatting(t *testing.T, input string, expected twin.Cell) {
|
func testManPageFormatting(t *testing.T, input string, expected twin.StyledRune) {
|
||||||
reader := NewReaderFromText("", input)
|
reader := NewReaderFromText("", input)
|
||||||
|
|
||||||
// Without these lines the man page tests will fail if either of these
|
// Without these lines the man page tests will fail if either of these
|
||||||
@ -264,19 +264,19 @@ func testManPageFormatting(t *testing.T, input string, expected twin.Cell) {
|
|||||||
resetManPageFormat()
|
resetManPageFormat()
|
||||||
|
|
||||||
contents := startPaging(t, reader).GetRow(0)
|
contents := startPaging(t, reader).GetRow(0)
|
||||||
assertCellsEqual(t, expected, contents[0])
|
assertRunesEqual(t, expected, contents[0])
|
||||||
assert.Equal(t, contents[1].Rune, ' ')
|
assert.Equal(t, contents[1].Rune, ' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestManPageFormatting(t *testing.T) {
|
func TestManPageFormatting(t *testing.T) {
|
||||||
testManPageFormatting(t, "n\x08n", twin.NewCell('n', twin.StyleDefault.WithAttr(twin.AttrBold)))
|
testManPageFormatting(t, "n\x08n", twin.NewStyledRune('n', twin.StyleDefault.WithAttr(twin.AttrBold)))
|
||||||
testManPageFormatting(t, "_\x08x", twin.NewCell('x', twin.StyleDefault.WithAttr(twin.AttrUnderline)))
|
testManPageFormatting(t, "_\x08x", twin.NewStyledRune('x', twin.StyleDefault.WithAttr(twin.AttrUnderline)))
|
||||||
|
|
||||||
// Non-breaking space UTF-8 encoded (0xc2a0) should render as a non-breaking unicode space (0xa0)
|
// Non-breaking space UTF-8 encoded (0xc2a0) should render as a non-breaking unicode space (0xa0)
|
||||||
testManPageFormatting(t, string([]byte{0xc2, 0xa0}), twin.NewCell(rune(0xa0), twin.StyleDefault))
|
testManPageFormatting(t, string([]byte{0xc2, 0xa0}), twin.NewStyledRune(rune(0xa0), twin.StyleDefault))
|
||||||
|
|
||||||
// Corner cases
|
// Corner cases
|
||||||
testManPageFormatting(t, "\x08", twin.NewCell('<', twin.StyleDefault.WithForeground(twin.NewColor16(7)).WithBackground(twin.NewColor16(1))))
|
testManPageFormatting(t, "\x08", twin.NewStyledRune('<', twin.StyleDefault.WithForeground(twin.NewColor16(7)).WithBackground(twin.NewColor16(1))))
|
||||||
|
|
||||||
// FIXME: Test two consecutive backspaces
|
// FIXME: Test two consecutive backspaces
|
||||||
|
|
||||||
@ -359,7 +359,7 @@ func TestFindFirstHitNoMatchBackwards(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Converts a cell row to a plain string and removes trailing whitespace.
|
// Converts a cell row to a plain string and removes trailing whitespace.
|
||||||
func rowToString(row []twin.Cell) string {
|
func rowToString(row []twin.StyledRune) string {
|
||||||
rowString := ""
|
rowString := ""
|
||||||
for _, cell := range row {
|
for _, cell := range row {
|
||||||
rowString += string(cell.Rune)
|
rowString += string(cell.Rune)
|
||||||
@ -432,7 +432,7 @@ func TestScrollToEndLongInput(t *testing.T) {
|
|||||||
// line holds the last contents line.
|
// line holds the last contents line.
|
||||||
lastContentsLine := screen.GetRow(screenHeight - 2)
|
lastContentsLine := screen.GetRow(screenHeight - 2)
|
||||||
firstContentsColumn := len("10_100 ")
|
firstContentsColumn := len("10_100 ")
|
||||||
assertCellsEqual(t, twin.NewCell('X', twin.StyleDefault), lastContentsLine[firstContentsColumn])
|
assertRunesEqual(t, twin.NewStyledRune('X', twin.StyleDefault), lastContentsLine[firstContentsColumn])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsScrolledToEnd_LongFile(t *testing.T) {
|
func TestIsScrolledToEnd_LongFile(t *testing.T) {
|
||||||
@ -581,10 +581,10 @@ func TestClearToEndOfLine_ClearFromStart(t *testing.T) {
|
|||||||
screen := startPaging(t, NewReaderFromText("TestClearToEol", blueBackgroundClearToEol))
|
screen := startPaging(t, NewReaderFromText("TestClearToEol", blueBackgroundClearToEol))
|
||||||
|
|
||||||
screenWidth, _ := screen.Size()
|
screenWidth, _ := screen.Size()
|
||||||
var expected []twin.Cell
|
var expected []twin.StyledRune
|
||||||
for len(expected) < screenWidth {
|
for len(expected) < screenWidth {
|
||||||
expected = append(expected,
|
expected = append(expected,
|
||||||
twin.NewCell(' ', twin.StyleDefault.WithBackground(twin.NewColor16(4))),
|
twin.NewStyledRune(' ', twin.StyleDefault.WithBackground(twin.NewColor16(4))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -597,12 +597,12 @@ func TestClearToEndOfLine_ClearFromNotStart(t *testing.T) {
|
|||||||
screen := startPaging(t, NewReaderFromText("TestClearToEol", "a"+blueBackgroundClearToEol))
|
screen := startPaging(t, NewReaderFromText("TestClearToEol", "a"+blueBackgroundClearToEol))
|
||||||
|
|
||||||
screenWidth, _ := screen.Size()
|
screenWidth, _ := screen.Size()
|
||||||
expected := []twin.Cell{
|
expected := []twin.StyledRune{
|
||||||
twin.NewCell('a', twin.StyleDefault),
|
twin.NewStyledRune('a', twin.StyleDefault),
|
||||||
}
|
}
|
||||||
for len(expected) < screenWidth {
|
for len(expected) < screenWidth {
|
||||||
expected = append(expected,
|
expected = append(expected,
|
||||||
twin.NewCell(' ', twin.StyleDefault.WithBackground(twin.NewColor16(4))),
|
twin.NewStyledRune(' ', twin.StyleDefault.WithBackground(twin.NewColor16(4))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -629,10 +629,10 @@ func TestClearToEndOfLine_ClearFromStartScrolledRight(t *testing.T) {
|
|||||||
pager.redraw("")
|
pager.redraw("")
|
||||||
|
|
||||||
screenWidth, _ := screen.Size()
|
screenWidth, _ := screen.Size()
|
||||||
var expected []twin.Cell
|
var expected []twin.StyledRune
|
||||||
for len(expected) < screenWidth {
|
for len(expected) < screenWidth {
|
||||||
expected = append(expected,
|
expected = append(expected,
|
||||||
twin.NewCell(' ', twin.StyleDefault.WithBackground(twin.NewColor16(4))),
|
twin.NewStyledRune(' ', twin.StyleDefault.WithBackground(twin.NewColor16(4))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,12 +21,12 @@ func (m *PagerModeGotoLine) drawFooter(_ string, _ string) {
|
|||||||
|
|
||||||
pos := 0
|
pos := 0
|
||||||
for _, token := range "Go to line number: " + m.gotoLineString {
|
for _, token := range "Go to line number: " + m.gotoLineString {
|
||||||
p.screen.SetCell(pos, height-1, twin.NewCell(token, twin.StyleDefault))
|
p.screen.SetCell(pos, height-1, twin.NewStyledRune(token, twin.StyleDefault))
|
||||||
pos++
|
pos++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a cursor
|
// Add a cursor
|
||||||
p.screen.SetCell(pos, height-1, twin.NewCell(' ', twin.StyleDefault.WithAttr(twin.AttrReverse)))
|
p.screen.SetCell(pos, height-1, twin.NewStyledRune(' ', twin.StyleDefault.WithAttr(twin.AttrReverse)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *PagerModeGotoLine) onKey(key twin.KeyCode) {
|
func (m *PagerModeGotoLine) onKey(key twin.KeyCode) {
|
||||||
|
@ -18,7 +18,7 @@ func (m PagerModeJumpToMark) drawFooter(_ string, _ string) {
|
|||||||
|
|
||||||
pos := 0
|
pos := 0
|
||||||
for _, token := range m.getMarkPrompt() {
|
for _, token := range m.getMarkPrompt() {
|
||||||
p.screen.SetCell(pos, height-1, twin.NewCell(token, twin.StyleDefault))
|
p.screen.SetCell(pos, height-1, twin.NewStyledRune(token, twin.StyleDefault))
|
||||||
pos++
|
pos++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,12 +13,12 @@ func (m PagerModeMark) drawFooter(_ string, _ string) {
|
|||||||
|
|
||||||
pos := 0
|
pos := 0
|
||||||
for _, token := range "Press any key to label your mark: " {
|
for _, token := range "Press any key to label your mark: " {
|
||||||
p.screen.SetCell(pos, height-1, twin.NewCell(token, twin.StyleDefault))
|
p.screen.SetCell(pos, height-1, twin.NewStyledRune(token, twin.StyleDefault))
|
||||||
pos++
|
pos++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a cursor
|
// Add a cursor
|
||||||
p.screen.SetCell(pos, height-1, twin.NewCell(' ', twin.StyleDefault.WithAttr(twin.AttrReverse)))
|
p.screen.SetCell(pos, height-1, twin.NewStyledRune(' ', twin.StyleDefault.WithAttr(twin.AttrReverse)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m PagerModeMark) onKey(key twin.KeyCode) {
|
func (m PagerModeMark) onKey(key twin.KeyCode) {
|
||||||
|
@ -18,17 +18,17 @@ func (m PagerModeSearch) drawFooter(_ string, _ string) {
|
|||||||
|
|
||||||
pos := 0
|
pos := 0
|
||||||
for _, token := range "Search: " + m.pager.searchString {
|
for _, token := range "Search: " + m.pager.searchString {
|
||||||
m.pager.screen.SetCell(pos, height-1, twin.NewCell(token, twin.StyleDefault))
|
m.pager.screen.SetCell(pos, height-1, twin.NewStyledRune(token, twin.StyleDefault))
|
||||||
pos++
|
pos++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a cursor
|
// Add a cursor
|
||||||
m.pager.screen.SetCell(pos, height-1, twin.NewCell(' ', twin.StyleDefault.WithAttr(twin.AttrReverse)))
|
m.pager.screen.SetCell(pos, height-1, twin.NewStyledRune(' ', twin.StyleDefault.WithAttr(twin.AttrReverse)))
|
||||||
pos++
|
pos++
|
||||||
|
|
||||||
// Clear the rest of the line
|
// Clear the rest of the line
|
||||||
for pos < width {
|
for pos < width {
|
||||||
m.pager.screen.SetCell(pos, height-1, twin.NewCell(' ', twin.StyleDefault))
|
m.pager.screen.SetCell(pos, height-1, twin.NewStyledRune(' ', twin.StyleDefault))
|
||||||
pos++
|
pos++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ type renderedLine struct {
|
|||||||
// will have a wrapIndex of 1.
|
// will have a wrapIndex of 1.
|
||||||
wrapIndex int
|
wrapIndex int
|
||||||
|
|
||||||
cells []twin.Cell
|
cells []twin.StyledRune
|
||||||
|
|
||||||
// Used for rendering clear-to-end-of-line control sequences:
|
// Used for rendering clear-to-end-of-line control sequences:
|
||||||
// https://en.wikipedia.org/wiki/ANSI_escape_code#EL
|
// https://en.wikipedia.org/wiki/ANSI_escape_code#EL
|
||||||
@ -38,7 +38,7 @@ func (p *Pager) redraw(spinner string) overflowState {
|
|||||||
p.longestLineLength = 0
|
p.longestLineLength = 0
|
||||||
|
|
||||||
lastUpdatedScreenLineNumber := -1
|
lastUpdatedScreenLineNumber := -1
|
||||||
var renderedScreenLines [][]twin.Cell
|
var renderedScreenLines [][]twin.StyledRune
|
||||||
renderedScreenLines, statusText, overflow := p.renderScreenLines()
|
renderedScreenLines, statusText, overflow := p.renderScreenLines()
|
||||||
for screenLineNumber, row := range renderedScreenLines {
|
for screenLineNumber, row := range renderedScreenLines {
|
||||||
lastUpdatedScreenLineNumber = screenLineNumber
|
lastUpdatedScreenLineNumber = screenLineNumber
|
||||||
@ -54,7 +54,7 @@ func (p *Pager) redraw(spinner string) overflowState {
|
|||||||
// This happens when we're done
|
// This happens when we're done
|
||||||
eofSpinner = "---"
|
eofSpinner = "---"
|
||||||
}
|
}
|
||||||
spinnerLine := textstyles.CellsFromString("", _EofMarkerFormat+eofSpinner, nil).Cells
|
spinnerLine := textstyles.StyledRunesFromString("", _EofMarkerFormat+eofSpinner, nil).StyledRunes
|
||||||
for column, cell := range spinnerLine {
|
for column, cell := range spinnerLine {
|
||||||
p.screen.SetCell(column, lastUpdatedScreenLineNumber+1, cell)
|
p.screen.SetCell(column, lastUpdatedScreenLineNumber+1, cell)
|
||||||
}
|
}
|
||||||
@ -71,14 +71,14 @@ func (p *Pager) redraw(spinner string) overflowState {
|
|||||||
//
|
//
|
||||||
// The lines returned by this method are decorated with horizontal scroll
|
// The lines returned by this method are decorated with horizontal scroll
|
||||||
// markers and line numbers and are ready to be output to the screen.
|
// markers and line numbers and are ready to be output to the screen.
|
||||||
func (p *Pager) renderScreenLines() (lines [][]twin.Cell, statusText string, overflow overflowState) {
|
func (p *Pager) renderScreenLines() (lines [][]twin.StyledRune, statusText string, overflow overflowState) {
|
||||||
renderedLines, statusText, overflow := p.renderLines()
|
renderedLines, statusText, overflow := p.renderLines()
|
||||||
if len(renderedLines) == 0 {
|
if len(renderedLines) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct the screen lines to return
|
// Construct the screen lines to return
|
||||||
screenLines := make([][]twin.Cell, 0, len(renderedLines))
|
screenLines := make([][]twin.StyledRune, 0, len(renderedLines))
|
||||||
for _, renderedLine := range renderedLines {
|
for _, renderedLine := range renderedLines {
|
||||||
screenLines = append(screenLines, renderedLine.cells)
|
screenLines = append(screenLines, renderedLine.cells)
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ func (p *Pager) renderScreenLines() (lines [][]twin.Cell, statusText string, ove
|
|||||||
screenWidth, _ := p.screen.Size()
|
screenWidth, _ := p.screen.Size()
|
||||||
for len(screenLines[len(screenLines)-1]) < screenWidth {
|
for len(screenLines[len(screenLines)-1]) < screenWidth {
|
||||||
screenLines[len(screenLines)-1] =
|
screenLines[len(screenLines)-1] =
|
||||||
append(screenLines[len(screenLines)-1], twin.NewCell(' ', renderedLine.trailer))
|
append(screenLines[len(screenLines)-1], twin.NewStyledRune(' ', renderedLine.trailer))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,14 +214,14 @@ func (p *Pager) renderLines() ([]renderedLine, string, overflowState) {
|
|||||||
// indent, and to (optionally) render the line number.
|
// indent, and to (optionally) render the line number.
|
||||||
func (p *Pager) renderLine(line *Line, lineNumber linenumbers.LineNumber, scrollPosition scrollPositionInternal) ([]renderedLine, overflowState) {
|
func (p *Pager) renderLine(line *Line, lineNumber linenumbers.LineNumber, scrollPosition scrollPositionInternal) ([]renderedLine, overflowState) {
|
||||||
highlighted := line.HighlightedTokens(p.linePrefix, p.searchPattern, &lineNumber)
|
highlighted := line.HighlightedTokens(p.linePrefix, p.searchPattern, &lineNumber)
|
||||||
var wrapped [][]twin.Cell
|
var wrapped [][]twin.StyledRune
|
||||||
overflow := didFit
|
overflow := didFit
|
||||||
if p.WrapLongLines {
|
if p.WrapLongLines {
|
||||||
width, _ := p.screen.Size()
|
width, _ := p.screen.Size()
|
||||||
wrapped = wrapLine(width-numberPrefixLength(p, scrollPosition), highlighted.Cells)
|
wrapped = wrapLine(width-numberPrefixLength(p, scrollPosition), highlighted.StyledRunes)
|
||||||
} else {
|
} else {
|
||||||
// All on one line
|
// All on one line
|
||||||
wrapped = [][]twin.Cell{highlighted.Cells}
|
wrapped = [][]twin.StyledRune{highlighted.StyledRunes}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(wrapped) > 1 {
|
if len(wrapped) > 1 {
|
||||||
@ -260,9 +260,9 @@ func (p *Pager) renderLine(line *Line, lineNumber linenumbers.LineNumber, scroll
|
|||||||
// * Line number, or leading whitespace for wrapped lines
|
// * Line number, or leading whitespace for wrapped lines
|
||||||
// * Scroll left indicator
|
// * Scroll left indicator
|
||||||
// * Scroll right indicator
|
// * Scroll right indicator
|
||||||
func (p *Pager) decorateLine(lineNumberToShow *linenumbers.LineNumber, contents []twin.Cell, scrollPosition scrollPositionInternal) ([]twin.Cell, overflowState) {
|
func (p *Pager) decorateLine(lineNumberToShow *linenumbers.LineNumber, contents []twin.StyledRune, scrollPosition scrollPositionInternal) ([]twin.StyledRune, overflowState) {
|
||||||
width, _ := p.screen.Size()
|
width, _ := p.screen.Size()
|
||||||
newLine := make([]twin.Cell, 0, width)
|
newLine := make([]twin.StyledRune, 0, width)
|
||||||
numberPrefixLength := numberPrefixLength(p, scrollPosition)
|
numberPrefixLength := numberPrefixLength(p, scrollPosition)
|
||||||
newLine = append(newLine, createLinePrefix(lineNumberToShow, numberPrefixLength)...)
|
newLine = append(newLine, createLinePrefix(lineNumberToShow, numberPrefixLength)...)
|
||||||
overflow := didFit
|
overflow := didFit
|
||||||
@ -282,7 +282,7 @@ func (p *Pager) decorateLine(lineNumberToShow *linenumbers.LineNumber, contents
|
|||||||
if len(newLine) == 0 {
|
if len(newLine) == 0 {
|
||||||
// Don't panic on short lines, this new Cell will be
|
// Don't panic on short lines, this new Cell will be
|
||||||
// overwritten with '<' right after this if statement
|
// overwritten with '<' right after this if statement
|
||||||
newLine = append(newLine, twin.Cell{})
|
newLine = append(newLine, twin.StyledRune{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add can-scroll-left marker
|
// Add can-scroll-left marker
|
||||||
@ -306,15 +306,15 @@ func (p *Pager) decorateLine(lineNumberToShow *linenumbers.LineNumber, contents
|
|||||||
// Generate a line number prefix of the given length.
|
// Generate a line number prefix of the given length.
|
||||||
//
|
//
|
||||||
// Can be empty or all-whitespace depending on parameters.
|
// Can be empty or all-whitespace depending on parameters.
|
||||||
func createLinePrefix(lineNumber *linenumbers.LineNumber, numberPrefixLength int) []twin.Cell {
|
func createLinePrefix(lineNumber *linenumbers.LineNumber, numberPrefixLength int) []twin.StyledRune {
|
||||||
if numberPrefixLength == 0 {
|
if numberPrefixLength == 0 {
|
||||||
return []twin.Cell{}
|
return []twin.StyledRune{}
|
||||||
}
|
}
|
||||||
|
|
||||||
lineNumberPrefix := make([]twin.Cell, 0, numberPrefixLength)
|
lineNumberPrefix := make([]twin.StyledRune, 0, numberPrefixLength)
|
||||||
if lineNumber == nil {
|
if lineNumber == nil {
|
||||||
for len(lineNumberPrefix) < numberPrefixLength {
|
for len(lineNumberPrefix) < numberPrefixLength {
|
||||||
lineNumberPrefix = append(lineNumberPrefix, twin.Cell{Rune: ' '})
|
lineNumberPrefix = append(lineNumberPrefix, twin.StyledRune{Rune: ' '})
|
||||||
}
|
}
|
||||||
return lineNumberPrefix
|
return lineNumberPrefix
|
||||||
}
|
}
|
||||||
@ -331,7 +331,7 @@ func createLinePrefix(lineNumber *linenumbers.LineNumber, numberPrefixLength int
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
lineNumberPrefix = append(lineNumberPrefix, twin.NewCell(digit, lineNumbersStyle))
|
lineNumberPrefix = append(lineNumberPrefix, twin.NewStyledRune(digit, lineNumbersStyle))
|
||||||
}
|
}
|
||||||
|
|
||||||
return lineNumberPrefix
|
return lineNumberPrefix
|
||||||
|
@ -93,7 +93,7 @@ func TestSearchHighlight(t *testing.T) {
|
|||||||
{
|
{
|
||||||
inputLine: linenumbers.LineNumber{},
|
inputLine: linenumbers.LineNumber{},
|
||||||
wrapIndex: 0,
|
wrapIndex: 0,
|
||||||
cells: []twin.Cell{
|
cells: []twin.StyledRune{
|
||||||
{Rune: 'x', Style: twin.StyleDefault},
|
{Rune: 'x', Style: twin.StyleDefault},
|
||||||
{Rune: '"', Style: twin.StyleDefault.WithAttr(twin.AttrReverse)},
|
{Rune: '"', Style: twin.StyleDefault.WithAttr(twin.AttrReverse)},
|
||||||
{Rune: '"', Style: twin.StyleDefault.WithAttr(twin.AttrReverse)},
|
{Rune: '"', Style: twin.StyleDefault.WithAttr(twin.AttrReverse)},
|
||||||
|
@ -53,7 +53,7 @@ func twinStyleFromChroma(chromaStyle *chroma.Style, chromaFormatter *chroma.Form
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatted := stringBuilder.String()
|
formatted := stringBuilder.String()
|
||||||
cells := textstyles.CellsFromString("", formatted, nil).Cells
|
cells := textstyles.StyledRunesFromString("", formatted, nil).StyledRunes
|
||||||
if len(cells) != 1 {
|
if len(cells) != 1 {
|
||||||
log.Warnf("Chroma formatter didn't return exactly one cell: %#v", cells)
|
log.Warnf("Chroma formatter didn't return exactly one cell: %#v", cells)
|
||||||
return nil
|
return nil
|
||||||
@ -151,7 +151,7 @@ func styleUI(chromaStyle *chroma.Style, chromaFormatter *chroma.Formatter, statu
|
|||||||
|
|
||||||
func TermcapToStyle(termcap string) (twin.Style, error) {
|
func TermcapToStyle(termcap string) (twin.Style, error) {
|
||||||
// Add a character to be sure we have one to take the format from
|
// Add a character to be sure we have one to take the format from
|
||||||
cells := textstyles.CellsFromString("", termcap+"x", nil).Cells
|
cells := textstyles.StyledRunesFromString("", termcap+"x", nil).StyledRunes
|
||||||
if len(cells) != 1 {
|
if len(cells) != 1 {
|
||||||
return twin.StyleDefault, fmt.Errorf("Expected styling only and no text")
|
return twin.StyleDefault, fmt.Errorf("Expected styling only and no text")
|
||||||
}
|
}
|
||||||
|
@ -30,9 +30,9 @@ const _TabSize = 4
|
|||||||
|
|
||||||
const BACKSPACE = '\b'
|
const BACKSPACE = '\b'
|
||||||
|
|
||||||
type CellsWithTrailer struct {
|
type StyledRunesWithTrailer struct {
|
||||||
Cells []twin.Cell
|
StyledRunes []twin.StyledRune
|
||||||
Trailer twin.Style
|
Trailer twin.Style
|
||||||
}
|
}
|
||||||
|
|
||||||
func isPlain(s string) bool {
|
func isPlain(s string) bool {
|
||||||
@ -110,13 +110,13 @@ func WithoutFormatting(s string, lineNumber *linenumbers.LineNumber) string {
|
|||||||
//
|
//
|
||||||
// The prefix will be prepended to the string before parsing. The lineNumber is
|
// The prefix will be prepended to the string before parsing. The lineNumber is
|
||||||
// used for error reporting.
|
// used for error reporting.
|
||||||
func CellsFromString(prefix string, s string, lineNumber *linenumbers.LineNumber) CellsWithTrailer {
|
func StyledRunesFromString(prefix string, s string, lineNumber *linenumbers.LineNumber) StyledRunesWithTrailer {
|
||||||
manPageHeading := manPageHeadingFromString(s)
|
manPageHeading := manPageHeadingFromString(s)
|
||||||
if manPageHeading != nil {
|
if manPageHeading != nil {
|
||||||
return *manPageHeading
|
return *manPageHeading
|
||||||
}
|
}
|
||||||
|
|
||||||
var cells []twin.Cell
|
var cells []twin.StyledRune
|
||||||
|
|
||||||
// Specs: https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit
|
// Specs: https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit
|
||||||
styleUnprintable := twin.StyleDefault.WithBackground(twin.NewColor16(1)).WithForeground(twin.NewColor16(7))
|
styleUnprintable := twin.StyleDefault.WithBackground(twin.NewColor16(1)).WithForeground(twin.NewColor16(7))
|
||||||
@ -127,7 +127,7 @@ func CellsFromString(prefix string, s string, lineNumber *linenumbers.LineNumber
|
|||||||
|
|
||||||
case '\x09': // TAB
|
case '\x09': // TAB
|
||||||
for {
|
for {
|
||||||
cells = append(cells, twin.Cell{
|
cells = append(cells, twin.StyledRune{
|
||||||
Rune: ' ',
|
Rune: ' ',
|
||||||
Style: style,
|
Style: style,
|
||||||
})
|
})
|
||||||
@ -140,12 +140,12 @@ func CellsFromString(prefix string, s string, lineNumber *linenumbers.LineNumber
|
|||||||
|
|
||||||
case '<27>': // Go's broken-UTF8 marker
|
case '<27>': // Go's broken-UTF8 marker
|
||||||
if UnprintableStyle == UnprintableStyleHighlight {
|
if UnprintableStyle == UnprintableStyleHighlight {
|
||||||
cells = append(cells, twin.Cell{
|
cells = append(cells, twin.StyledRune{
|
||||||
Rune: '?',
|
Rune: '?',
|
||||||
Style: styleUnprintable,
|
Style: styleUnprintable,
|
||||||
})
|
})
|
||||||
} else if UnprintableStyle == UnprintableStyleWhitespace {
|
} else if UnprintableStyle == UnprintableStyleWhitespace {
|
||||||
cells = append(cells, twin.Cell{
|
cells = append(cells, twin.StyledRune{
|
||||||
Rune: '?',
|
Rune: '?',
|
||||||
Style: twin.StyleDefault,
|
Style: twin.StyleDefault,
|
||||||
})
|
})
|
||||||
@ -154,7 +154,7 @@ func CellsFromString(prefix string, s string, lineNumber *linenumbers.LineNumber
|
|||||||
}
|
}
|
||||||
|
|
||||||
case BACKSPACE:
|
case BACKSPACE:
|
||||||
cells = append(cells, twin.Cell{
|
cells = append(cells, twin.StyledRune{
|
||||||
Rune: '<',
|
Rune: '<',
|
||||||
Style: styleUnprintable,
|
Style: styleUnprintable,
|
||||||
})
|
})
|
||||||
@ -162,12 +162,12 @@ func CellsFromString(prefix string, s string, lineNumber *linenumbers.LineNumber
|
|||||||
default:
|
default:
|
||||||
if !twin.Printable(token.Rune) {
|
if !twin.Printable(token.Rune) {
|
||||||
if UnprintableStyle == UnprintableStyleHighlight {
|
if UnprintableStyle == UnprintableStyleHighlight {
|
||||||
cells = append(cells, twin.Cell{
|
cells = append(cells, twin.StyledRune{
|
||||||
Rune: '?',
|
Rune: '?',
|
||||||
Style: styleUnprintable,
|
Style: styleUnprintable,
|
||||||
})
|
})
|
||||||
} else if UnprintableStyle == UnprintableStyleWhitespace {
|
} else if UnprintableStyle == UnprintableStyleWhitespace {
|
||||||
cells = append(cells, twin.Cell{
|
cells = append(cells, twin.StyledRune{
|
||||||
Rune: ' ',
|
Rune: ' ',
|
||||||
Style: twin.StyleDefault,
|
Style: twin.StyleDefault,
|
||||||
})
|
})
|
||||||
@ -181,14 +181,14 @@ func CellsFromString(prefix string, s string, lineNumber *linenumbers.LineNumber
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return CellsWithTrailer{
|
return StyledRunesWithTrailer{
|
||||||
Cells: cells,
|
StyledRunes: cells,
|
||||||
Trailer: trailer,
|
Trailer: trailer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consume 'x<x', where '<' is backspace and the result is a bold 'x'
|
// Consume 'x<x', where '<' is backspace and the result is a bold 'x'
|
||||||
func consumeBold(runes []rune, index int) (int, *twin.Cell) {
|
func consumeBold(runes []rune, index int) (int, *twin.StyledRune) {
|
||||||
if index+2 >= len(runes) {
|
if index+2 >= len(runes) {
|
||||||
// Not enough runes left for a bold
|
// Not enough runes left for a bold
|
||||||
return index, nil
|
return index, nil
|
||||||
@ -205,14 +205,14 @@ func consumeBold(runes []rune, index int) (int, *twin.Cell) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We have a match!
|
// We have a match!
|
||||||
return index + 3, &twin.Cell{
|
return index + 3, &twin.StyledRune{
|
||||||
Rune: runes[index],
|
Rune: runes[index],
|
||||||
Style: ManPageBold,
|
Style: ManPageBold,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consume '_<x', where '<' is backspace and the result is an underlined 'x'
|
// Consume '_<x', where '<' is backspace and the result is an underlined 'x'
|
||||||
func consumeUnderline(runes []rune, index int) (int, *twin.Cell) {
|
func consumeUnderline(runes []rune, index int) (int, *twin.StyledRune) {
|
||||||
if index+2 >= len(runes) {
|
if index+2 >= len(runes) {
|
||||||
// Not enough runes left for a underline
|
// Not enough runes left for a underline
|
||||||
return index, nil
|
return index, nil
|
||||||
@ -229,7 +229,7 @@ func consumeUnderline(runes []rune, index int) (int, *twin.Cell) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We have a match!
|
// We have a match!
|
||||||
return index + 3, &twin.Cell{
|
return index + 3, &twin.StyledRune{
|
||||||
Rune: runes[index+2],
|
Rune: runes[index+2],
|
||||||
Style: ManPageUnderline,
|
Style: ManPageUnderline,
|
||||||
}
|
}
|
||||||
@ -238,7 +238,7 @@ func consumeUnderline(runes []rune, index int) (int, *twin.Cell) {
|
|||||||
// Consume '+<+<o<o' / '+<o', where '<' is backspace and the result is a unicode bullet.
|
// 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.
|
// Used on man pages, try "man printf" on macOS for one example.
|
||||||
func consumeBullet(runes []rune, index int) (int, *twin.Cell) {
|
func consumeBullet(runes []rune, index int) (int, *twin.StyledRune) {
|
||||||
patterns := [][]byte{[]byte("+\bo"), []byte("+\b+\bo\bo")}
|
patterns := [][]byte{[]byte("+\bo"), []byte("+\b+\bo\bo")}
|
||||||
for _, pattern := range patterns {
|
for _, pattern := range patterns {
|
||||||
if index+len(pattern) > len(runes) {
|
if index+len(pattern) > len(runes) {
|
||||||
@ -259,7 +259,7 @@ func consumeBullet(runes []rune, index int) (int, *twin.Cell) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We have a match!
|
// We have a match!
|
||||||
return index + len(pattern), &twin.Cell{
|
return index + len(pattern), &twin.StyledRune{
|
||||||
Rune: '•', // Unicode bullet point
|
Rune: '•', // Unicode bullet point
|
||||||
Style: twin.StyleDefault,
|
Style: twin.StyleDefault,
|
||||||
}
|
}
|
||||||
@ -293,7 +293,7 @@ func runesFromStyledString(styledString _StyledString) string {
|
|||||||
return returnMe.String()
|
return returnMe.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func tokensFromStyledString(styledString _StyledString) []twin.Cell {
|
func tokensFromStyledString(styledString _StyledString) []twin.StyledRune {
|
||||||
runes := []rune(styledString.String)
|
runes := []rune(styledString.String)
|
||||||
|
|
||||||
hasBackspace := false
|
hasBackspace := false
|
||||||
@ -304,11 +304,11 @@ func tokensFromStyledString(styledString _StyledString) []twin.Cell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens := make([]twin.Cell, 0, len(runes))
|
tokens := make([]twin.StyledRune, 0, len(runes))
|
||||||
if !hasBackspace {
|
if !hasBackspace {
|
||||||
// Shortcut when there's no backspace based formatting to worry about
|
// Shortcut when there's no backspace based formatting to worry about
|
||||||
for _, runeValue := range runes {
|
for _, runeValue := range runes {
|
||||||
tokens = append(tokens, twin.Cell{
|
tokens = append(tokens, twin.StyledRune{
|
||||||
Rune: runeValue,
|
Rune: runeValue,
|
||||||
Style: styledString.Style,
|
Style: styledString.Style,
|
||||||
})
|
})
|
||||||
@ -339,7 +339,7 @@ func tokensFromStyledString(styledString _StyledString) []twin.Cell {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens = append(tokens, twin.Cell{
|
tokens = append(tokens, twin.StyledRune{
|
||||||
Rune: runes[index],
|
Rune: runes[index],
|
||||||
Style: styledString.Style,
|
Style: styledString.Style,
|
||||||
})
|
})
|
||||||
|
@ -20,7 +20,7 @@ import (
|
|||||||
const samplesDir = "../../sample-files"
|
const samplesDir = "../../sample-files"
|
||||||
|
|
||||||
// Convert a cells array to a plain string
|
// Convert a cells array to a plain string
|
||||||
func cellsToPlainString(cells []twin.Cell) string {
|
func cellsToPlainString(cells []twin.StyledRune) string {
|
||||||
returnMe := ""
|
returnMe := ""
|
||||||
for _, cell := range cells {
|
for _, cell := range cells {
|
||||||
returnMe += string(cell.Rune)
|
returnMe += string(cell.Rune)
|
||||||
@ -74,7 +74,7 @@ func TestTokenize(t *testing.T) {
|
|||||||
var loglines strings.Builder
|
var loglines strings.Builder
|
||||||
log.SetOutput(&loglines)
|
log.SetOutput(&loglines)
|
||||||
|
|
||||||
tokens := CellsFromString("", line, lineNumber).Cells
|
tokens := StyledRunesFromString("", line, lineNumber).StyledRunes
|
||||||
plainString := WithoutFormatting(line, lineNumber)
|
plainString := WithoutFormatting(line, lineNumber)
|
||||||
if len(tokens) != utf8.RuneCountInString(plainString) {
|
if len(tokens) != utf8.RuneCountInString(plainString) {
|
||||||
t.Errorf("%s:%s: len(tokens)=%d, len(plainString)=%d for: <%s>",
|
t.Errorf("%s:%s: len(tokens)=%d, len(plainString)=%d for: <%s>",
|
||||||
@ -127,43 +127,43 @@ func TestTokenize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUnderline(t *testing.T) {
|
func TestUnderline(t *testing.T) {
|
||||||
tokens := CellsFromString("", "a\x1b[4mb\x1b[24mc", nil).Cells
|
tokens := StyledRunesFromString("", "a\x1b[4mb\x1b[24mc", nil).StyledRunes
|
||||||
assert.Equal(t, len(tokens), 3)
|
assert.Equal(t, len(tokens), 3)
|
||||||
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
|
assert.Equal(t, tokens[0], twin.StyledRune{Rune: 'a', Style: twin.StyleDefault})
|
||||||
assert.Equal(t, tokens[1], twin.Cell{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrUnderline)})
|
assert.Equal(t, tokens[1], twin.StyledRune{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrUnderline)})
|
||||||
assert.Equal(t, tokens[2], twin.Cell{Rune: 'c', Style: twin.StyleDefault})
|
assert.Equal(t, tokens[2], twin.StyledRune{Rune: 'c', Style: twin.StyleDefault})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestManPages(t *testing.T) {
|
func TestManPages(t *testing.T) {
|
||||||
// Bold
|
// Bold
|
||||||
tokens := CellsFromString("", "ab\bbc", nil).Cells
|
tokens := StyledRunesFromString("", "ab\bbc", nil).StyledRunes
|
||||||
assert.Equal(t, len(tokens), 3)
|
assert.Equal(t, len(tokens), 3)
|
||||||
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
|
assert.Equal(t, tokens[0], twin.StyledRune{Rune: 'a', Style: twin.StyleDefault})
|
||||||
assert.Equal(t, tokens[1], twin.Cell{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrBold)})
|
assert.Equal(t, tokens[1], twin.StyledRune{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrBold)})
|
||||||
assert.Equal(t, tokens[2], twin.Cell{Rune: 'c', Style: twin.StyleDefault})
|
assert.Equal(t, tokens[2], twin.StyledRune{Rune: 'c', Style: twin.StyleDefault})
|
||||||
|
|
||||||
// Underline
|
// Underline
|
||||||
tokens = CellsFromString("", "a_\bbc", nil).Cells
|
tokens = StyledRunesFromString("", "a_\bbc", nil).StyledRunes
|
||||||
assert.Equal(t, len(tokens), 3)
|
assert.Equal(t, len(tokens), 3)
|
||||||
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
|
assert.Equal(t, tokens[0], twin.StyledRune{Rune: 'a', Style: twin.StyleDefault})
|
||||||
assert.Equal(t, tokens[1], twin.Cell{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrUnderline)})
|
assert.Equal(t, tokens[1], twin.StyledRune{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrUnderline)})
|
||||||
assert.Equal(t, tokens[2], twin.Cell{Rune: 'c', Style: twin.StyleDefault})
|
assert.Equal(t, tokens[2], twin.StyledRune{Rune: 'c', Style: twin.StyleDefault})
|
||||||
|
|
||||||
// Bullet point 1, taken from doing this on my macOS system:
|
// Bullet point 1, taken from doing this on my macOS system:
|
||||||
// env PAGER="hexdump -C" man printf | moar
|
// env PAGER="hexdump -C" man printf | moar
|
||||||
tokens = CellsFromString("", "a+\b+\bo\bob", nil).Cells
|
tokens = StyledRunesFromString("", "a+\b+\bo\bob", nil).StyledRunes
|
||||||
assert.Equal(t, len(tokens), 3)
|
assert.Equal(t, len(tokens), 3)
|
||||||
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
|
assert.Equal(t, tokens[0], twin.StyledRune{Rune: 'a', Style: twin.StyleDefault})
|
||||||
assert.Equal(t, tokens[1], twin.Cell{Rune: '•', Style: twin.StyleDefault})
|
assert.Equal(t, tokens[1], twin.StyledRune{Rune: '•', Style: twin.StyleDefault})
|
||||||
assert.Equal(t, tokens[2], twin.Cell{Rune: 'b', Style: twin.StyleDefault})
|
assert.Equal(t, tokens[2], twin.StyledRune{Rune: 'b', Style: twin.StyleDefault})
|
||||||
|
|
||||||
// Bullet point 2, taken from doing this using the "fish" shell on my macOS system:
|
// Bullet point 2, taken from doing this using the "fish" shell on my macOS system:
|
||||||
// man printf | hexdump -C | moar
|
// man printf | hexdump -C | moar
|
||||||
tokens = CellsFromString("", "a+\bob", nil).Cells
|
tokens = StyledRunesFromString("", "a+\bob", nil).StyledRunes
|
||||||
assert.Equal(t, len(tokens), 3)
|
assert.Equal(t, len(tokens), 3)
|
||||||
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
|
assert.Equal(t, tokens[0], twin.StyledRune{Rune: 'a', Style: twin.StyleDefault})
|
||||||
assert.Equal(t, tokens[1], twin.Cell{Rune: '•', Style: twin.StyleDefault})
|
assert.Equal(t, tokens[1], twin.StyledRune{Rune: '•', Style: twin.StyleDefault})
|
||||||
assert.Equal(t, tokens[2], twin.Cell{Rune: 'b', Style: twin.StyleDefault})
|
assert.Equal(t, tokens[2], twin.StyledRune{Rune: 'b', Style: twin.StyleDefault})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestManPageHeadings(t *testing.T) {
|
func TestManPageHeadings(t *testing.T) {
|
||||||
@ -181,18 +181,18 @@ func TestManPageHeadings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A line with only man page bold caps should be considered a heading
|
// A line with only man page bold caps should be considered a heading
|
||||||
for _, token := range CellsFromString("", manPageHeading, nil).Cells {
|
for _, token := range StyledRunesFromString("", manPageHeading, nil).StyledRunes {
|
||||||
assert.Equal(t, token.Style, ManPageHeading)
|
assert.Equal(t, token.Style, ManPageHeading)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A line with only non-man-page bold caps should not be considered a heading
|
// A line with only non-man-page bold caps should not be considered a heading
|
||||||
wrongKindOfBold := "\x1b[1mJOHAN HELLO"
|
wrongKindOfBold := "\x1b[1mJOHAN HELLO"
|
||||||
for _, token := range CellsFromString("", wrongKindOfBold, nil).Cells {
|
for _, token := range StyledRunesFromString("", wrongKindOfBold, nil).StyledRunes {
|
||||||
assert.Equal(t, token.Style, twin.StyleDefault.WithAttr(twin.AttrBold))
|
assert.Equal(t, token.Style, twin.StyleDefault.WithAttr(twin.AttrBold))
|
||||||
}
|
}
|
||||||
|
|
||||||
// A line with not all caps should not be considered a heading
|
// A line with not all caps should not be considered a heading
|
||||||
for _, token := range CellsFromString("", notAllCaps, nil).Cells {
|
for _, token := range StyledRunesFromString("", notAllCaps, nil).StyledRunes {
|
||||||
assert.Equal(t, token.Style, twin.StyleDefault.WithAttr(twin.AttrBold))
|
assert.Equal(t, token.Style, twin.StyleDefault.WithAttr(twin.AttrBold))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -257,9 +257,9 @@ func TestRawUpdateStyle(t *testing.T) {
|
|||||||
func TestHyperlink_escBackslash(t *testing.T) {
|
func TestHyperlink_escBackslash(t *testing.T) {
|
||||||
url := "http://example.com"
|
url := "http://example.com"
|
||||||
|
|
||||||
tokens := CellsFromString("", "a\x1b]8;;"+url+"\x1b\\bc\x1b]8;;\x1b\\d", nil).Cells
|
tokens := StyledRunesFromString("", "a\x1b]8;;"+url+"\x1b\\bc\x1b]8;;\x1b\\d", nil).StyledRunes
|
||||||
|
|
||||||
assert.DeepEqual(t, tokens, []twin.Cell{
|
assert.DeepEqual(t, tokens, []twin.StyledRune{
|
||||||
{Rune: 'a', Style: twin.StyleDefault},
|
{Rune: 'a', Style: twin.StyleDefault},
|
||||||
{Rune: 'b', Style: twin.StyleDefault.WithHyperlink(&url)},
|
{Rune: 'b', Style: twin.StyleDefault.WithHyperlink(&url)},
|
||||||
{Rune: 'c', Style: twin.StyleDefault.WithHyperlink(&url)},
|
{Rune: 'c', Style: twin.StyleDefault.WithHyperlink(&url)},
|
||||||
@ -273,9 +273,9 @@ func TestHyperlink_escBackslash(t *testing.T) {
|
|||||||
func TestHyperlink_bell(t *testing.T) {
|
func TestHyperlink_bell(t *testing.T) {
|
||||||
url := "http://example.com"
|
url := "http://example.com"
|
||||||
|
|
||||||
tokens := CellsFromString("", "a\x1b]8;;"+url+"\x07bc\x1b]8;;\x07d", nil).Cells
|
tokens := StyledRunesFromString("", "a\x1b]8;;"+url+"\x07bc\x1b]8;;\x07d", nil).StyledRunes
|
||||||
|
|
||||||
assert.DeepEqual(t, tokens, []twin.Cell{
|
assert.DeepEqual(t, tokens, []twin.StyledRune{
|
||||||
{Rune: 'a', Style: twin.StyleDefault},
|
{Rune: 'a', Style: twin.StyleDefault},
|
||||||
{Rune: 'b', Style: twin.StyleDefault.WithHyperlink(&url)},
|
{Rune: 'b', Style: twin.StyleDefault.WithHyperlink(&url)},
|
||||||
{Rune: 'c', Style: twin.StyleDefault.WithHyperlink(&url)},
|
{Rune: 'c', Style: twin.StyleDefault.WithHyperlink(&url)},
|
||||||
@ -286,7 +286,7 @@ func TestHyperlink_bell(t *testing.T) {
|
|||||||
// Test with some other ESC sequence than ESC-backslash
|
// Test with some other ESC sequence than ESC-backslash
|
||||||
func TestHyperlink_nonTerminatingEsc(t *testing.T) {
|
func TestHyperlink_nonTerminatingEsc(t *testing.T) {
|
||||||
complete := "a\x1b]8;;https://example.com\x1bbc"
|
complete := "a\x1b]8;;https://example.com\x1bbc"
|
||||||
tokens := CellsFromString("", complete, nil).Cells
|
tokens := StyledRunesFromString("", complete, nil).StyledRunes
|
||||||
|
|
||||||
// This should not be treated as any link
|
// This should not be treated as any link
|
||||||
for i := 0; i < len(complete); i++ {
|
for i := 0; i < len(complete); i++ {
|
||||||
@ -295,7 +295,7 @@ func TestHyperlink_nonTerminatingEsc(t *testing.T) {
|
|||||||
// good enough.
|
// good enough.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
assert.Equal(t, tokens[i], twin.Cell{Rune: rune(complete[i]), Style: twin.StyleDefault},
|
assert.Equal(t, tokens[i], twin.StyledRune{Rune: rune(complete[i]), Style: twin.StyleDefault},
|
||||||
"i=%d, c=%s, tokens=%v", i, string(complete[i]), tokens)
|
"i=%d, c=%s, tokens=%v", i, string(complete[i]), tokens)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -306,7 +306,7 @@ func TestHyperlink_incomplete(t *testing.T) {
|
|||||||
for l := len(complete) - 1; l >= 0; l-- {
|
for l := len(complete) - 1; l >= 0; l-- {
|
||||||
incomplete := complete[:l]
|
incomplete := complete[:l]
|
||||||
t.Run(fmt.Sprintf("l=%d incomplete=<%s>", l, strings.ReplaceAll(incomplete, "\x1b", "ESC")), func(t *testing.T) {
|
t.Run(fmt.Sprintf("l=%d incomplete=<%s>", l, strings.ReplaceAll(incomplete, "\x1b", "ESC")), func(t *testing.T) {
|
||||||
tokens := CellsFromString("", incomplete, nil).Cells
|
tokens := StyledRunesFromString("", incomplete, nil).StyledRunes
|
||||||
|
|
||||||
for i := 0; i < l; i++ {
|
for i := 0; i < l; i++ {
|
||||||
if complete[i] == '\x1b' {
|
if complete[i] == '\x1b' {
|
||||||
@ -314,7 +314,7 @@ func TestHyperlink_incomplete(t *testing.T) {
|
|||||||
// that's good enough.
|
// that's good enough.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
assert.Equal(t, tokens[i], twin.Cell{Rune: rune(complete[i]), Style: twin.StyleDefault})
|
assert.Equal(t, tokens[i], twin.StyledRune{Rune: rune(complete[i]), Style: twin.StyleDefault})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -6,24 +6,24 @@ import (
|
|||||||
"github.com/walles/moar/twin"
|
"github.com/walles/moar/twin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func manPageHeadingFromString(s string) *CellsWithTrailer {
|
func manPageHeadingFromString(s string) *StyledRunesWithTrailer {
|
||||||
// For great performance, first check the string without allocating any
|
// For great performance, first check the string without allocating any
|
||||||
// memory.
|
// memory.
|
||||||
if !parseManPageHeading(s, func(_ twin.Cell) {}) {
|
if !parseManPageHeading(s, func(_ twin.StyledRune) {}) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cells := make([]twin.Cell, 0, len(s)/2)
|
cells := make([]twin.StyledRune, 0, len(s)/2)
|
||||||
ok := parseManPageHeading(s, func(cell twin.Cell) {
|
ok := parseManPageHeading(s, func(cell twin.StyledRune) {
|
||||||
cells = append(cells, cell)
|
cells = append(cells, cell)
|
||||||
})
|
})
|
||||||
if !ok {
|
if !ok {
|
||||||
panic("man page heading state changed")
|
panic("man page heading state changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CellsWithTrailer{
|
return &StyledRunesWithTrailer{
|
||||||
Cells: cells,
|
StyledRunes: cells,
|
||||||
Trailer: twin.StyleDefault,
|
Trailer: twin.StyleDefault,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ func manPageHeadingFromString(s string) *CellsWithTrailer {
|
|||||||
// A man page heading is all caps. Also, each character is encoded as
|
// A man page heading is all caps. Also, each character is encoded as
|
||||||
// char+backspace+char, where both chars need to be the same. Whitespace is an
|
// char+backspace+char, where both chars need to be the same. Whitespace is an
|
||||||
// exception, they can be not bold.
|
// exception, they can be not bold.
|
||||||
func parseManPageHeading(s string, reportCell func(twin.Cell)) bool {
|
func parseManPageHeading(s string, reportStyledRune func(twin.StyledRune)) bool {
|
||||||
if len(s) < 3 {
|
if len(s) < 3 {
|
||||||
// We don't want to match empty strings. Also, strings of length 1 and 2
|
// We don't want to match empty strings. Also, strings of length 1 and 2
|
||||||
// cannot be man page headings since "char+backspace+char" is 3 bytes.
|
// cannot be man page headings since "char+backspace+char" is 3 bytes.
|
||||||
@ -78,7 +78,7 @@ func parseManPageHeading(s string, reportCell func(twin.Cell)) bool {
|
|||||||
|
|
||||||
if unicode.IsSpace(firstChar) {
|
if unicode.IsSpace(firstChar) {
|
||||||
// Whitespace is an exception, it can be not bold
|
// Whitespace is an exception, it can be not bold
|
||||||
reportCell(twin.Cell{Rune: firstChar, Style: ManPageHeading})
|
reportStyledRune(twin.StyledRune{Rune: firstChar, Style: ManPageHeading})
|
||||||
|
|
||||||
// Assume what we got was a new first char
|
// Assume what we got was a new first char
|
||||||
firstChar = char
|
firstChar = char
|
||||||
@ -105,7 +105,7 @@ func parseManPageHeading(s string, reportCell func(twin.Cell)) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
reportCell(twin.Cell{Rune: char, Style: ManPageHeading})
|
reportStyledRune(twin.StyledRune{Rune: char, Style: ManPageHeading})
|
||||||
state = stateExpectingFirstChar
|
state = stateExpectingFirstChar
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func isManPageHeading(s string) bool {
|
func isManPageHeading(s string) bool {
|
||||||
return parseManPageHeading(s, func(_ twin.Cell) {})
|
return parseManPageHeading(s, func(_ twin.StyledRune) {})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsManPageHeading(t *testing.T) {
|
func TestIsManPageHeading(t *testing.T) {
|
||||||
@ -34,10 +34,10 @@ func TestManPageHeadingFromString_NotBoldSpace(t *testing.T) {
|
|||||||
result := manPageHeadingFromString("A\bA B\bB")
|
result := manPageHeadingFromString("A\bA B\bB")
|
||||||
|
|
||||||
assert.Assert(t, result != nil)
|
assert.Assert(t, result != nil)
|
||||||
assert.Equal(t, len(result.Cells), 3)
|
assert.Equal(t, len(result.StyledRunes), 3)
|
||||||
assert.Equal(t, result.Cells[0], twin.Cell{Rune: 'A', Style: ManPageHeading})
|
assert.Equal(t, result.StyledRunes[0], twin.StyledRune{Rune: 'A', Style: ManPageHeading})
|
||||||
assert.Equal(t, result.Cells[1], twin.Cell{Rune: ' ', Style: ManPageHeading})
|
assert.Equal(t, result.StyledRunes[1], twin.StyledRune{Rune: ' ', Style: ManPageHeading})
|
||||||
assert.Equal(t, result.Cells[2], twin.Cell{Rune: 'B', Style: ManPageHeading})
|
assert.Equal(t, result.StyledRunes[2], twin.StyledRune{Rune: 'B', Style: ManPageHeading})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestManPageHeadingFromString_WithBoldSpace(t *testing.T) {
|
func TestManPageHeadingFromString_WithBoldSpace(t *testing.T) {
|
||||||
@ -47,8 +47,8 @@ func TestManPageHeadingFromString_WithBoldSpace(t *testing.T) {
|
|||||||
result := manPageHeadingFromString("A\bA \b B\bB")
|
result := manPageHeadingFromString("A\bA \b B\bB")
|
||||||
|
|
||||||
assert.Assert(t, result != nil)
|
assert.Assert(t, result != nil)
|
||||||
assert.Equal(t, len(result.Cells), 3)
|
assert.Equal(t, len(result.StyledRunes), 3)
|
||||||
assert.Equal(t, result.Cells[0], twin.Cell{Rune: 'A', Style: ManPageHeading})
|
assert.Equal(t, result.StyledRunes[0], twin.StyledRune{Rune: 'A', Style: ManPageHeading})
|
||||||
assert.Equal(t, result.Cells[1], twin.Cell{Rune: ' ', Style: ManPageHeading})
|
assert.Equal(t, result.StyledRunes[1], twin.StyledRune{Rune: ' ', Style: ManPageHeading})
|
||||||
assert.Equal(t, result.Cells[2], twin.Cell{Rune: 'B', Style: ManPageHeading})
|
assert.Equal(t, result.StyledRunes[2], twin.StyledRune{Rune: 'B', Style: ManPageHeading})
|
||||||
}
|
}
|
||||||
|
10
moar.go
10
moar.go
@ -365,15 +365,15 @@ func parseUnprintableStyle(styleOption string) (textstyles.UnprintableStyleT, er
|
|||||||
return 0, fmt.Errorf("Good ones are highlight or whitespace")
|
return 0, fmt.Errorf("Good ones are highlight or whitespace")
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseScrollHint(scrollHint string) (twin.Cell, error) {
|
func parseScrollHint(scrollHint string) (twin.StyledRune, error) {
|
||||||
scrollHint = strings.ReplaceAll(scrollHint, "ESC", "\x1b")
|
scrollHint = strings.ReplaceAll(scrollHint, "ESC", "\x1b")
|
||||||
hintAsLine := m.NewLine(scrollHint)
|
hintAsLine := m.NewLine(scrollHint)
|
||||||
parsedTokens := hintAsLine.HighlightedTokens("", nil, nil).Cells
|
parsedTokens := hintAsLine.HighlightedTokens("", nil, nil).StyledRunes
|
||||||
if len(parsedTokens) == 1 {
|
if len(parsedTokens) == 1 {
|
||||||
return parsedTokens[0], nil
|
return parsedTokens[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return twin.Cell{}, fmt.Errorf("Expected exactly one (optionally highlighted) character. For example: 'ESC[2m…'")
|
return twin.StyledRune{}, fmt.Errorf("Expected exactly one (optionally highlighted) character. For example: 'ESC[2m…'")
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseShiftAmount(shiftAmount string) (uint, error) {
|
func parseShiftAmount(shiftAmount string) (uint, error) {
|
||||||
@ -574,10 +574,10 @@ func pagerFromArgs(
|
|||||||
unprintableStyle := flagSetFunc(flagSet, "render-unprintable", textstyles.UnprintableStyleHighlight,
|
unprintableStyle := flagSetFunc(flagSet, "render-unprintable", textstyles.UnprintableStyleHighlight,
|
||||||
"How unprintable characters are rendered: highlight or whitespace", parseUnprintableStyle)
|
"How unprintable characters are rendered: highlight or whitespace", parseUnprintableStyle)
|
||||||
scrollLeftHint := flagSetFunc(flagSet, "scroll-left-hint",
|
scrollLeftHint := flagSetFunc(flagSet, "scroll-left-hint",
|
||||||
twin.NewCell('<', twin.StyleDefault.WithAttr(twin.AttrReverse)),
|
twin.NewStyledRune('<', twin.StyleDefault.WithAttr(twin.AttrReverse)),
|
||||||
"Shown when view can scroll left. One character with optional ANSI highlighting.", parseScrollHint)
|
"Shown when view can scroll left. One character with optional ANSI highlighting.", parseScrollHint)
|
||||||
scrollRightHint := flagSetFunc(flagSet, "scroll-right-hint",
|
scrollRightHint := flagSetFunc(flagSet, "scroll-right-hint",
|
||||||
twin.NewCell('>', twin.StyleDefault.WithAttr(twin.AttrReverse)),
|
twin.NewStyledRune('>', twin.StyleDefault.WithAttr(twin.AttrReverse)),
|
||||||
"Shown when view can scroll right. One character with optional ANSI highlighting.", parseScrollHint)
|
"Shown when view can scroll right. One character with optional ANSI highlighting.", parseScrollHint)
|
||||||
shift := flagSetFunc(flagSet, "shift", 16, "Horizontal scroll `amount` >=1, defaults to 16", parseShiftAmount)
|
shift := flagSetFunc(flagSet, "shift", 16, "Horizontal scroll `amount` >=1, defaults to 16", parseShiftAmount)
|
||||||
mouseMode := flagSetFunc(
|
mouseMode := flagSetFunc(
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
func TestParseScrollHint(t *testing.T) {
|
func TestParseScrollHint(t *testing.T) {
|
||||||
token, err := parseScrollHint("ESC[7m>")
|
token, err := parseScrollHint("ESC[7m>")
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, token, twin.Cell{
|
assert.Equal(t, token, twin.StyledRune{
|
||||||
Rune: '>',
|
Rune: '>',
|
||||||
Style: twin.StyleDefault.WithAttr(twin.AttrReverse),
|
Style: twin.StyleDefault.WithAttr(twin.AttrReverse),
|
||||||
})
|
})
|
||||||
|
@ -7,13 +7,13 @@ package twin
|
|||||||
type FakeScreen struct {
|
type FakeScreen struct {
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
cells [][]Cell
|
cells [][]StyledRune
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFakeScreen(width int, height int) *FakeScreen {
|
func NewFakeScreen(width int, height int) *FakeScreen {
|
||||||
rows := make([][]Cell, height)
|
rows := make([][]StyledRune, height)
|
||||||
for i := 0; i < height; i++ {
|
for i := 0; i < height; i++ {
|
||||||
rows[i] = make([]Cell, width)
|
rows[i] = make([]StyledRune, width)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &FakeScreen{
|
return &FakeScreen{
|
||||||
@ -30,7 +30,7 @@ func (screen *FakeScreen) Close() {
|
|||||||
func (screen *FakeScreen) Clear() {
|
func (screen *FakeScreen) Clear() {
|
||||||
// This method's contents has been copied from UnixScreen.Clear()
|
// This method's contents has been copied from UnixScreen.Clear()
|
||||||
|
|
||||||
empty := NewCell(' ', StyleDefault)
|
empty := NewStyledRune(' ', StyleDefault)
|
||||||
|
|
||||||
width, height := screen.Size()
|
width, height := screen.Size()
|
||||||
for row := 0; row < height; row++ {
|
for row := 0; row < height; row++ {
|
||||||
@ -40,7 +40,7 @@ func (screen *FakeScreen) Clear() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (screen *FakeScreen) SetCell(column int, row int, cell Cell) {
|
func (screen *FakeScreen) SetCell(column int, row int, cell StyledRune) {
|
||||||
// This method's contents has been copied from UnixScreen.Clear()
|
// This method's contents has been copied from UnixScreen.Clear()
|
||||||
|
|
||||||
if column < 0 {
|
if column < 0 {
|
||||||
@ -85,6 +85,6 @@ func (screen *FakeScreen) Events() chan Event {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (screen *FakeScreen) GetRow(row int) []Cell {
|
func (screen *FakeScreen) GetRow(row int) []StyledRune {
|
||||||
return screen.cells[row]
|
return screen.cells[row]
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ type Screen interface {
|
|||||||
|
|
||||||
Clear()
|
Clear()
|
||||||
|
|
||||||
SetCell(column int, row int, cell Cell)
|
SetCell(column int, row int, cell StyledRune)
|
||||||
|
|
||||||
// Render our contents into the terminal window
|
// Render our contents into the terminal window
|
||||||
Show()
|
Show()
|
||||||
@ -80,7 +80,7 @@ type interruptableReader interface {
|
|||||||
type UnixScreen struct {
|
type UnixScreen struct {
|
||||||
widthAccessFromSizeOnly int // Access from Size() method only
|
widthAccessFromSizeOnly int // Access from Size() method only
|
||||||
heightAccessFromSizeOnly int // Access from Size() method only
|
heightAccessFromSizeOnly int // Access from Size() method only
|
||||||
cells [][]Cell
|
cells [][]StyledRune
|
||||||
|
|
||||||
// Note that the type here doesn't matter, we only want to know whether or
|
// Note that the type here doesn't matter, we only want to know whether or
|
||||||
// not this channel has been signalled
|
// not this channel has been signalled
|
||||||
@ -556,9 +556,9 @@ func (screen *UnixScreen) Size() (width int, height int) {
|
|||||||
return screen.widthAccessFromSizeOnly, screen.heightAccessFromSizeOnly
|
return screen.widthAccessFromSizeOnly, screen.heightAccessFromSizeOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
newCells := make([][]Cell, height)
|
newCells := make([][]StyledRune, height)
|
||||||
for rowNumber := 0; rowNumber < height; rowNumber++ {
|
for rowNumber := 0; rowNumber < height; rowNumber++ {
|
||||||
newCells[rowNumber] = make([]Cell, width)
|
newCells[rowNumber] = make([]StyledRune, width)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Copy any existing contents over to the new, resized screen array
|
// FIXME: Copy any existing contents over to the new, resized screen array
|
||||||
@ -627,7 +627,7 @@ func parseTerminalBgColorResponse(responseBytes []byte) *Color {
|
|||||||
return &color
|
return &color
|
||||||
}
|
}
|
||||||
|
|
||||||
func (screen *UnixScreen) SetCell(column int, row int, cell Cell) {
|
func (screen *UnixScreen) SetCell(column int, row int, cell StyledRune) {
|
||||||
if column < 0 {
|
if column < 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -646,7 +646,7 @@ func (screen *UnixScreen) SetCell(column int, row int, cell Cell) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (screen *UnixScreen) Clear() {
|
func (screen *UnixScreen) Clear() {
|
||||||
empty := NewCell(' ', StyleDefault)
|
empty := NewStyledRune(' ', StyleDefault)
|
||||||
|
|
||||||
width, height := screen.Size()
|
width, height := screen.Size()
|
||||||
for row := 0; row < height; row++ {
|
for row := 0; row < height; row++ {
|
||||||
@ -658,7 +658,7 @@ func (screen *UnixScreen) Clear() {
|
|||||||
|
|
||||||
// Returns the rendered line, plus how many information carrying cells went into
|
// Returns the rendered line, plus how many information carrying cells went into
|
||||||
// it
|
// it
|
||||||
func renderLine(row []Cell, terminalColorCount ColorCount) (string, int) {
|
func renderLine(row []StyledRune, terminalColorCount ColorCount) (string, int) {
|
||||||
// Strip trailing whitespace
|
// Strip trailing whitespace
|
||||||
lastSignificantCellIndex := len(row) - 1
|
lastSignificantCellIndex := len(row) - 1
|
||||||
for ; lastSignificantCellIndex >= 0; lastSignificantCellIndex-- {
|
for ; lastSignificantCellIndex >= 0; lastSignificantCellIndex-- {
|
||||||
|
@ -54,7 +54,7 @@ func TestConsumeEncodedEventWithNoInput(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderLine(t *testing.T) {
|
func TestRenderLine(t *testing.T) {
|
||||||
row := []Cell{
|
row := []StyledRune{
|
||||||
{
|
{
|
||||||
Rune: '<',
|
Rune: '<',
|
||||||
Style: StyleDefault.WithAttr(AttrReverse),
|
Style: StyleDefault.WithAttr(AttrReverse),
|
||||||
@ -78,7 +78,7 @@ func TestRenderLine(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderLineEmpty(t *testing.T) {
|
func TestRenderLineEmpty(t *testing.T) {
|
||||||
row := []Cell{}
|
row := []StyledRune{}
|
||||||
|
|
||||||
rendered, count := renderLine(row, ColorCount16)
|
rendered, count := renderLine(row, ColorCount16)
|
||||||
assert.Equal(t, count, 0)
|
assert.Equal(t, count, 0)
|
||||||
@ -89,7 +89,7 @@ func TestRenderLineEmpty(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderLineLastReversed(t *testing.T) {
|
func TestRenderLineLastReversed(t *testing.T) {
|
||||||
row := []Cell{
|
row := []StyledRune{
|
||||||
{
|
{
|
||||||
Rune: '<',
|
Rune: '<',
|
||||||
Style: StyleDefault.WithAttr(AttrReverse),
|
Style: StyleDefault.WithAttr(AttrReverse),
|
||||||
@ -107,7 +107,7 @@ func TestRenderLineLastReversed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderLineLastNonSpace(t *testing.T) {
|
func TestRenderLineLastNonSpace(t *testing.T) {
|
||||||
row := []Cell{
|
row := []StyledRune{
|
||||||
{
|
{
|
||||||
Rune: 'X',
|
Rune: 'X',
|
||||||
Style: StyleDefault,
|
Style: StyleDefault,
|
||||||
@ -124,7 +124,7 @@ func TestRenderLineLastNonSpace(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderLineLastReversedPlusTrailingSpace(t *testing.T) {
|
func TestRenderLineLastReversedPlusTrailingSpace(t *testing.T) {
|
||||||
row := []Cell{
|
row := []StyledRune{
|
||||||
{
|
{
|
||||||
Rune: '<',
|
Rune: '<',
|
||||||
Style: StyleDefault.WithAttr(AttrReverse),
|
Style: StyleDefault.WithAttr(AttrReverse),
|
||||||
@ -146,7 +146,7 @@ func TestRenderLineLastReversedPlusTrailingSpace(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderLineOnlyTrailingSpaces(t *testing.T) {
|
func TestRenderLineOnlyTrailingSpaces(t *testing.T) {
|
||||||
row := []Cell{
|
row := []StyledRune{
|
||||||
{
|
{
|
||||||
Rune: ' ',
|
Rune: ' ',
|
||||||
Style: StyleDefault,
|
Style: StyleDefault,
|
||||||
@ -166,7 +166,7 @@ func TestRenderLineOnlyTrailingSpaces(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderLineLastReversedSpaces(t *testing.T) {
|
func TestRenderLineLastReversedSpaces(t *testing.T) {
|
||||||
row := []Cell{
|
row := []StyledRune{
|
||||||
{
|
{
|
||||||
Rune: ' ',
|
Rune: ' ',
|
||||||
Style: StyleDefault.WithAttr(AttrReverse),
|
Style: StyleDefault.WithAttr(AttrReverse),
|
||||||
@ -184,7 +184,7 @@ func TestRenderLineLastReversedSpaces(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderLineNonPrintable(t *testing.T) {
|
func TestRenderLineNonPrintable(t *testing.T) {
|
||||||
row := []Cell{
|
row := []StyledRune{
|
||||||
{
|
{
|
||||||
Rune: '',
|
Rune: '',
|
||||||
},
|
},
|
||||||
@ -204,7 +204,7 @@ func TestRenderLineNonPrintable(t *testing.T) {
|
|||||||
|
|
||||||
func TestRenderHyperlinkAtEndOfLine(t *testing.T) {
|
func TestRenderHyperlinkAtEndOfLine(t *testing.T) {
|
||||||
url := "https://example.com/"
|
url := "https://example.com/"
|
||||||
row := []Cell{
|
row := []StyledRune{
|
||||||
{
|
{
|
||||||
Rune: '*',
|
Rune: '*',
|
||||||
Style: StyleDefault.WithHyperlink(&url),
|
Style: StyleDefault.WithHyperlink(&url),
|
||||||
@ -221,7 +221,7 @@ func TestRenderHyperlinkAtEndOfLine(t *testing.T) {
|
|||||||
|
|
||||||
func TestMultiCharHyperlink(t *testing.T) {
|
func TestMultiCharHyperlink(t *testing.T) {
|
||||||
url := "https://example.com/"
|
url := "https://example.com/"
|
||||||
row := []Cell{
|
row := []StyledRune{
|
||||||
{
|
{
|
||||||
Rune: '-',
|
Rune: '-',
|
||||||
Style: StyleDefault.WithHyperlink(&url),
|
Style: StyleDefault.WithHyperlink(&url),
|
||||||
|
@ -5,25 +5,27 @@ import (
|
|||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Cell is a rune with a style to be written to a cell on screen
|
// StyledRune is a rune with a style to be written to a one or more cells on the
|
||||||
type Cell struct {
|
// screen. Note that a StyledRune may use more than one cell on the screen ('午'
|
||||||
|
// for example).
|
||||||
|
type StyledRune struct {
|
||||||
Rune rune
|
Rune rune
|
||||||
Style Style
|
Style Style
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCell(rune rune, style Style) Cell {
|
func NewStyledRune(rune rune, style Style) StyledRune {
|
||||||
return Cell{
|
return StyledRune{
|
||||||
Rune: rune,
|
Rune: rune,
|
||||||
Style: style,
|
Style: style,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cell Cell) String() string {
|
func (cell StyledRune) String() string {
|
||||||
return fmt.Sprint("rune='", string(cell.Rune), "' ", cell.Style)
|
return fmt.Sprint("rune='", string(cell.Rune), "' ", cell.Style)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a slice of cells with trailing whitespace cells removed
|
// Returns a slice of cells with trailing whitespace cells removed
|
||||||
func TrimSpaceRight(cells []Cell) []Cell {
|
func TrimSpaceRight(cells []StyledRune) []StyledRune {
|
||||||
for i := len(cells) - 1; i >= 0; i-- {
|
for i := len(cells) - 1; i >= 0; i-- {
|
||||||
cell := cells[i]
|
cell := cells[i]
|
||||||
if !unicode.IsSpace(cell.Rune) {
|
if !unicode.IsSpace(cell.Rune) {
|
||||||
@ -34,11 +36,11 @@ func TrimSpaceRight(cells []Cell) []Cell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// All whitespace, return empty
|
// All whitespace, return empty
|
||||||
return []Cell{}
|
return []StyledRune{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a slice of cells with leading whitespace cells removed
|
// Returns a slice of cells with leading whitespace cells removed
|
||||||
func TrimSpaceLeft(cells []Cell) []Cell {
|
func TrimSpaceLeft(cells []StyledRune) []StyledRune {
|
||||||
for i := 0; i < len(cells); i++ {
|
for i := 0; i < len(cells); i++ {
|
||||||
cell := cells[i]
|
cell := cells[i]
|
||||||
if !unicode.IsSpace(cell.Rune) {
|
if !unicode.IsSpace(cell.Rune) {
|
||||||
@ -49,7 +51,7 @@ func TrimSpaceLeft(cells []Cell) []Cell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// All whitespace, return empty
|
// All whitespace, return empty
|
||||||
return []Cell{}
|
return []StyledRune{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Printable(char rune) bool {
|
func Printable(char rune) bool {
|
@ -11,28 +11,28 @@ func TestTrimSpaceRight(t *testing.T) {
|
|||||||
// Empty
|
// Empty
|
||||||
assert.Assert(t, reflect.DeepEqual(
|
assert.Assert(t, reflect.DeepEqual(
|
||||||
TrimSpaceRight(
|
TrimSpaceRight(
|
||||||
[]Cell{},
|
[]StyledRune{},
|
||||||
),
|
),
|
||||||
[]Cell{}))
|
[]StyledRune{}))
|
||||||
|
|
||||||
// Single non-space
|
// Single non-space
|
||||||
assert.Assert(t, reflect.DeepEqual(
|
assert.Assert(t, reflect.DeepEqual(
|
||||||
TrimSpaceRight(
|
TrimSpaceRight(
|
||||||
[]Cell{{Rune: 'x'}},
|
[]StyledRune{{Rune: 'x'}},
|
||||||
),
|
),
|
||||||
[]Cell{{Rune: 'x'}}))
|
[]StyledRune{{Rune: 'x'}}))
|
||||||
|
|
||||||
// Single space
|
// Single space
|
||||||
assert.Assert(t, reflect.DeepEqual(
|
assert.Assert(t, reflect.DeepEqual(
|
||||||
TrimSpaceRight(
|
TrimSpaceRight(
|
||||||
[]Cell{{Rune: ' '}},
|
[]StyledRune{{Rune: ' '}},
|
||||||
),
|
),
|
||||||
[]Cell{}))
|
[]StyledRune{}))
|
||||||
|
|
||||||
// Non-space plus space
|
// Non-space plus space
|
||||||
assert.Assert(t, reflect.DeepEqual(
|
assert.Assert(t, reflect.DeepEqual(
|
||||||
TrimSpaceRight(
|
TrimSpaceRight(
|
||||||
[]Cell{{Rune: 'x'}, {Rune: ' '}},
|
[]StyledRune{{Rune: 'x'}, {Rune: ' '}},
|
||||||
),
|
),
|
||||||
[]Cell{{Rune: 'x'}}))
|
[]StyledRune{{Rune: 'x'}}))
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user