1
1
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:
Johan Walles 2024-09-15 13:56:50 +02:00
parent bac6d0d80e
commit e0e9ee6610
24 changed files with 244 additions and 242 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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