1
1
mirror of https://github.com/walles/moar.git synced 2024-11-21 16:04:20 +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
// 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)
matchRanges := getMatchRanges(&plain, search)
fromString := textstyles.CellsFromString(linePrefix, line.raw, lineNumber)
returnCells := make([]twin.Cell, 0, len(fromString.Cells))
for _, token := range fromString.Cells {
fromString := textstyles.StyledRunesFromString(linePrefix, line.raw, lineNumber)
returnRunes := make([]twin.StyledRune, 0, len(fromString.StyledRunes))
for _, token := range fromString.StyledRunes {
style := token.Style
if matchRanges.InRange(len(returnCells)) {
if matchRanges.InRange(len(returnRunes)) {
if standoutStyle != nil {
style = *standoutStyle
} 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,
Style: style,
})
}
return textstyles.CellsWithTrailer{
Cells: returnCells,
Trailer: fromString.Trailer,
return textstyles.StyledRunesWithTrailer{
StyledRunes: returnRunes,
Trailer: fromString.Trailer,
}
}

View File

@ -25,8 +25,8 @@ func TestHighlightedTokensWithManPageHeading(t *testing.T) {
line := NewLine(manPageHeading)
highlighted := line.HighlightedTokens(prefix, nil, nil)
assert.Equal(t, len(highlighted.Cells), len(headingText))
for i, cell := range highlighted.Cells {
assert.Equal(t, len(highlighted.StyledRunes), len(headingText))
for i, cell := range highlighted.StyledRunes {
assert.Equal(t, cell.Rune, rune(headingText[i]))
assert.Equal(t, cell.Style, textstyles.ManPageHeading)
}

View File

@ -12,7 +12,7 @@ import (
//revive:disable-next-line:var-naming
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 {
panic(fmt.Errorf("cannot compute wrap width when input isn't longer than max (%d<=%d)",
len(line), maxWrapWidth))
@ -49,16 +49,16 @@ func getWrapWidth(line []twin.Cell, maxWrapWidth int) int {
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
// look weird.
line = twin.TrimSpaceRight(line)
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 {
wrapWidth := getWrapWidth(line, width)
firstPart := line[:wrapWidth]

View File

@ -7,12 +7,12 @@ import (
"github.com/walles/moar/twin"
)
func tokenize(input string) []twin.Cell {
func tokenize(input string) []twin.StyledRune {
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 := ""
for _, cellLine := range cellLines {
lineString := ""
@ -33,7 +33,7 @@ func assertWrap(t *testing.T, input string, widthInScreenCells int, wrappedLines
toWrap := tokenize(input)
actual := wrapLine(widthInScreenCells, toWrap)
expected := [][]twin.Cell{}
expected := [][]twin.StyledRune{}
for _, wrappedLine := range wrappedLines {
expected = append(expected, tokenize(wrappedLine))
}

View File

@ -77,8 +77,8 @@ type Pager struct {
QuitIfOneScreen bool
// Ref: https://github.com/walles/moar/issues/94
ScrollLeftHint twin.Cell
ScrollRightHint twin.Cell
ScrollLeftHint twin.StyledRune
ScrollRightHint twin.StyledRune
SideScrollAmount int // Should be positive
@ -184,8 +184,8 @@ func NewPager(r *Reader) *Pager {
ShowStatusBar: true,
DeInit: true,
SideScrollAmount: 16,
ScrollLeftHint: twin.NewCell('<', twin.StyleDefault.WithAttr(twin.AttrReverse)),
ScrollRightHint: twin.NewCell('>', twin.StyleDefault.WithAttr(twin.AttrReverse)),
ScrollLeftHint: twin.NewStyledRune('<', twin.StyleDefault.WithAttr(twin.AttrReverse)),
ScrollRightHint: twin.NewStyledRune('>', twin.StyleDefault.WithAttr(twin.AttrReverse)),
scrollPosition: newScrollPosition(name),
}
@ -210,12 +210,12 @@ func (p *Pager) setFooter(footer string) {
pos := 0
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++
}
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) {
reader := NewReaderFromText("", "åäö")
var answers = []twin.Cell{
twin.NewCell('å', twin.StyleDefault),
twin.NewCell('ä', twin.StyleDefault),
twin.NewCell('ö', twin.StyleDefault),
var answers = []twin.StyledRune{
twin.NewStyledRune('å', twin.StyleDefault),
twin.NewStyledRune('ä', twin.StyleDefault),
twin.NewStyledRune('ö', twin.StyleDefault),
}
contents := startPaging(t, reader).GetRow(0)
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 {
return
}
@ -51,21 +51,21 @@ func TestFgColorRendering(t *testing.T) {
reader := NewReaderFromText("",
"\x1b[30ma\x1b[31mb\x1b[32mc\x1b[33md\x1b[34me\x1b[35mf\x1b[36mg\x1b[37mh\x1b[0mi")
var answers = []twin.Cell{
twin.NewCell('a', twin.StyleDefault.WithForeground(twin.NewColor16(0))),
twin.NewCell('b', twin.StyleDefault.WithForeground(twin.NewColor16(1))),
twin.NewCell('c', twin.StyleDefault.WithForeground(twin.NewColor16(2))),
twin.NewCell('d', twin.StyleDefault.WithForeground(twin.NewColor16(3))),
twin.NewCell('e', twin.StyleDefault.WithForeground(twin.NewColor16(4))),
twin.NewCell('f', twin.StyleDefault.WithForeground(twin.NewColor16(5))),
twin.NewCell('g', twin.StyleDefault.WithForeground(twin.NewColor16(6))),
twin.NewCell('h', twin.StyleDefault.WithForeground(twin.NewColor16(7))),
twin.NewCell('i', twin.StyleDefault),
var answers = []twin.StyledRune{
twin.NewStyledRune('a', twin.StyleDefault.WithForeground(twin.NewColor16(0))),
twin.NewStyledRune('b', twin.StyleDefault.WithForeground(twin.NewColor16(1))),
twin.NewStyledRune('c', twin.StyleDefault.WithForeground(twin.NewColor16(2))),
twin.NewStyledRune('d', twin.StyleDefault.WithForeground(twin.NewColor16(3))),
twin.NewStyledRune('e', twin.StyleDefault.WithForeground(twin.NewColor16(4))),
twin.NewStyledRune('f', twin.StyleDefault.WithForeground(twin.NewColor16(5))),
twin.NewStyledRune('g', twin.StyleDefault.WithForeground(twin.NewColor16(6))),
twin.NewStyledRune('h', twin.StyleDefault.WithForeground(twin.NewColor16(7))),
twin.NewStyledRune('i', twin.StyleDefault),
}
contents := startPaging(t, reader).GetRow(0)
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
reader := NewReaderFromText("", "abc\xc2def")
var answers = []twin.Cell{
twin.NewCell('a', twin.StyleDefault),
twin.NewCell('b', twin.StyleDefault),
twin.NewCell('c', twin.StyleDefault),
twin.NewCell('?', twin.StyleDefault.WithForeground(twin.NewColor16(7)).WithBackground(twin.NewColor16(1))),
twin.NewCell('d', twin.StyleDefault),
twin.NewCell('e', twin.StyleDefault),
twin.NewCell('f', twin.StyleDefault),
var answers = []twin.StyledRune{
twin.NewStyledRune('a', twin.StyleDefault),
twin.NewStyledRune('b', twin.StyleDefault),
twin.NewStyledRune('c', twin.StyleDefault),
twin.NewStyledRune('?', twin.StyleDefault.WithForeground(twin.NewColor16(7)).WithBackground(twin.NewColor16(1))),
twin.NewStyledRune('d', twin.StyleDefault),
twin.NewStyledRune('e', twin.StyleDefault),
twin.NewStyledRune('f', twin.StyleDefault),
}
contents := startPaging(t, reader).GetRow(0)
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))
packageNameStyle := twin.StyleDefault.WithForeground(twin.NewColorHex(0xD0D0D0))
var answers = []twin.Cell{
twin.NewCell('p', packageKeywordStyle),
twin.NewCell('a', packageKeywordStyle),
twin.NewCell('c', packageKeywordStyle),
twin.NewCell('k', packageKeywordStyle),
twin.NewCell('a', packageKeywordStyle),
twin.NewCell('g', packageKeywordStyle),
twin.NewCell('e', packageKeywordStyle),
twin.NewCell(' ', packageNameStyle),
twin.NewCell('m', packageNameStyle),
var answers = []twin.StyledRune{
twin.NewStyledRune('p', packageKeywordStyle),
twin.NewStyledRune('a', packageKeywordStyle),
twin.NewStyledRune('c', packageKeywordStyle),
twin.NewStyledRune('k', packageKeywordStyle),
twin.NewStyledRune('a', packageKeywordStyle),
twin.NewStyledRune('g', packageKeywordStyle),
twin.NewStyledRune('e', packageKeywordStyle),
twin.NewStyledRune(' ', packageNameStyle),
twin.NewStyledRune('m', packageNameStyle),
}
contents := startPaging(t, reader).GetRow(0)
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())
markdownHeading1Style := twin.StyleDefault.WithAttr(twin.AttrBold).WithForeground(twin.NewColorHex(0xffffff))
var answers = []twin.Cell{
twin.NewCell('#', markdownHeading1Style),
twin.NewCell(' ', markdownHeading1Style),
twin.NewCell('M', markdownHeading1Style),
twin.NewCell('a', markdownHeading1Style),
twin.NewCell('r', markdownHeading1Style),
twin.NewCell('k', markdownHeading1Style),
twin.NewCell('d', markdownHeading1Style),
twin.NewCell('o', markdownHeading1Style),
twin.NewCell('w', markdownHeading1Style),
twin.NewCell('n', markdownHeading1Style),
var answers = []twin.StyledRune{
twin.NewStyledRune('#', markdownHeading1Style),
twin.NewStyledRune(' ', markdownHeading1Style),
twin.NewStyledRune('M', markdownHeading1Style),
twin.NewStyledRune('a', markdownHeading1Style),
twin.NewStyledRune('r', markdownHeading1Style),
twin.NewStyledRune('k', markdownHeading1Style),
twin.NewStyledRune('d', markdownHeading1Style),
twin.NewStyledRune('o', markdownHeading1Style),
twin.NewStyledRune('w', markdownHeading1Style),
twin.NewStyledRune('n', markdownHeading1Style),
}
contents := startPaging(t, reader).GetRow(0)
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)
// 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) {
@ -242,10 +242,10 @@ func TestUnicodePrivateUse(t *testing.T) {
char := '\uf244'
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
assertCellsEqual(t, twin.NewCell(char, twin.StyleDefault), renderedCell)
assertRunesEqual(t, twin.NewStyledRune(char, twin.StyleDefault), renderedRune)
}
func resetManPageFormat() {
@ -253,7 +253,7 @@ func resetManPageFormat() {
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)
// 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()
contents := startPaging(t, reader).GetRow(0)
assertCellsEqual(t, expected, contents[0])
assertRunesEqual(t, expected, contents[0])
assert.Equal(t, contents[1].Rune, ' ')
}
func TestManPageFormatting(t *testing.T) {
testManPageFormatting(t, "n\x08n", twin.NewCell('n', twin.StyleDefault.WithAttr(twin.AttrBold)))
testManPageFormatting(t, "_\x08x", twin.NewCell('x', twin.StyleDefault.WithAttr(twin.AttrUnderline)))
testManPageFormatting(t, "n\x08n", twin.NewStyledRune('n', twin.StyleDefault.WithAttr(twin.AttrBold)))
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)
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
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
@ -359,7 +359,7 @@ func TestFindFirstHitNoMatchBackwards(t *testing.T) {
}
// 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 := ""
for _, cell := range row {
rowString += string(cell.Rune)
@ -432,7 +432,7 @@ func TestScrollToEndLongInput(t *testing.T) {
// line holds the last contents line.
lastContentsLine := screen.GetRow(screenHeight - 2)
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) {
@ -581,10 +581,10 @@ func TestClearToEndOfLine_ClearFromStart(t *testing.T) {
screen := startPaging(t, NewReaderFromText("TestClearToEol", blueBackgroundClearToEol))
screenWidth, _ := screen.Size()
var expected []twin.Cell
var expected []twin.StyledRune
for len(expected) < screenWidth {
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))
screenWidth, _ := screen.Size()
expected := []twin.Cell{
twin.NewCell('a', twin.StyleDefault),
expected := []twin.StyledRune{
twin.NewStyledRune('a', twin.StyleDefault),
}
for len(expected) < screenWidth {
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("")
screenWidth, _ := screen.Size()
var expected []twin.Cell
var expected []twin.StyledRune
for len(expected) < screenWidth {
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
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++
}
// 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) {

View File

@ -18,7 +18,7 @@ func (m PagerModeJumpToMark) drawFooter(_ string, _ string) {
pos := 0
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++
}
}

View File

@ -13,12 +13,12 @@ func (m PagerModeMark) drawFooter(_ string, _ string) {
pos := 0
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++
}
// 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) {

View File

@ -18,17 +18,17 @@ func (m PagerModeSearch) drawFooter(_ string, _ string) {
pos := 0
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++
}
// 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++
// Clear the rest of the line
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++
}
}

View File

@ -22,7 +22,7 @@ type renderedLine struct {
// will have a wrapIndex of 1.
wrapIndex int
cells []twin.Cell
cells []twin.StyledRune
// Used for rendering clear-to-end-of-line control sequences:
// https://en.wikipedia.org/wiki/ANSI_escape_code#EL
@ -38,7 +38,7 @@ func (p *Pager) redraw(spinner string) overflowState {
p.longestLineLength = 0
lastUpdatedScreenLineNumber := -1
var renderedScreenLines [][]twin.Cell
var renderedScreenLines [][]twin.StyledRune
renderedScreenLines, statusText, overflow := p.renderScreenLines()
for screenLineNumber, row := range renderedScreenLines {
lastUpdatedScreenLineNumber = screenLineNumber
@ -54,7 +54,7 @@ func (p *Pager) redraw(spinner string) overflowState {
// This happens when we're done
eofSpinner = "---"
}
spinnerLine := textstyles.CellsFromString("", _EofMarkerFormat+eofSpinner, nil).Cells
spinnerLine := textstyles.StyledRunesFromString("", _EofMarkerFormat+eofSpinner, nil).StyledRunes
for column, cell := range spinnerLine {
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
// 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()
if len(renderedLines) == 0 {
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 {
screenLines = append(screenLines, renderedLine.cells)
@ -90,7 +90,7 @@ func (p *Pager) renderScreenLines() (lines [][]twin.Cell, statusText string, ove
screenWidth, _ := p.screen.Size()
for len(screenLines[len(screenLines)-1]) < screenWidth {
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.
func (p *Pager) renderLine(line *Line, lineNumber linenumbers.LineNumber, scrollPosition scrollPositionInternal) ([]renderedLine, overflowState) {
highlighted := line.HighlightedTokens(p.linePrefix, p.searchPattern, &lineNumber)
var wrapped [][]twin.Cell
var wrapped [][]twin.StyledRune
overflow := didFit
if p.WrapLongLines {
width, _ := p.screen.Size()
wrapped = wrapLine(width-numberPrefixLength(p, scrollPosition), highlighted.Cells)
wrapped = wrapLine(width-numberPrefixLength(p, scrollPosition), highlighted.StyledRunes)
} else {
// All on one line
wrapped = [][]twin.Cell{highlighted.Cells}
wrapped = [][]twin.StyledRune{highlighted.StyledRunes}
}
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
// * Scroll left 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()
newLine := make([]twin.Cell, 0, width)
newLine := make([]twin.StyledRune, 0, width)
numberPrefixLength := numberPrefixLength(p, scrollPosition)
newLine = append(newLine, createLinePrefix(lineNumberToShow, numberPrefixLength)...)
overflow := didFit
@ -282,7 +282,7 @@ func (p *Pager) decorateLine(lineNumberToShow *linenumbers.LineNumber, contents
if len(newLine) == 0 {
// Don't panic on short lines, this new Cell will be
// overwritten with '<' right after this if statement
newLine = append(newLine, twin.Cell{})
newLine = append(newLine, twin.StyledRune{})
}
// 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.
//
// 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 {
return []twin.Cell{}
return []twin.StyledRune{}
}
lineNumberPrefix := make([]twin.Cell, 0, numberPrefixLength)
lineNumberPrefix := make([]twin.StyledRune, 0, numberPrefixLength)
if lineNumber == nil {
for len(lineNumberPrefix) < numberPrefixLength {
lineNumberPrefix = append(lineNumberPrefix, twin.Cell{Rune: ' '})
lineNumberPrefix = append(lineNumberPrefix, twin.StyledRune{Rune: ' '})
}
return lineNumberPrefix
}
@ -331,7 +331,7 @@ func createLinePrefix(lineNumber *linenumbers.LineNumber, numberPrefixLength int
break
}
lineNumberPrefix = append(lineNumberPrefix, twin.NewCell(digit, lineNumbersStyle))
lineNumberPrefix = append(lineNumberPrefix, twin.NewStyledRune(digit, lineNumbersStyle))
}
return lineNumberPrefix

View File

@ -93,7 +93,7 @@ func TestSearchHighlight(t *testing.T) {
{
inputLine: linenumbers.LineNumber{},
wrapIndex: 0,
cells: []twin.Cell{
cells: []twin.StyledRune{
{Rune: 'x', Style: twin.StyleDefault},
{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()
cells := textstyles.CellsFromString("", formatted, nil).Cells
cells := textstyles.StyledRunesFromString("", formatted, nil).StyledRunes
if len(cells) != 1 {
log.Warnf("Chroma formatter didn't return exactly one cell: %#v", cells)
return nil
@ -151,7 +151,7 @@ func styleUI(chromaStyle *chroma.Style, chromaFormatter *chroma.Formatter, statu
func TermcapToStyle(termcap string) (twin.Style, error) {
// 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 {
return twin.StyleDefault, fmt.Errorf("Expected styling only and no text")
}

View File

@ -30,9 +30,9 @@ const _TabSize = 4
const BACKSPACE = '\b'
type CellsWithTrailer struct {
Cells []twin.Cell
Trailer twin.Style
type StyledRunesWithTrailer struct {
StyledRunes []twin.StyledRune
Trailer twin.Style
}
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
// 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)
if manPageHeading != nil {
return *manPageHeading
}
var cells []twin.Cell
var cells []twin.StyledRune
// 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))
@ -127,7 +127,7 @@ func CellsFromString(prefix string, s string, lineNumber *linenumbers.LineNumber
case '\x09': // TAB
for {
cells = append(cells, twin.Cell{
cells = append(cells, twin.StyledRune{
Rune: ' ',
Style: style,
})
@ -140,12 +140,12 @@ func CellsFromString(prefix string, s string, lineNumber *linenumbers.LineNumber
case '<27>': // Go's broken-UTF8 marker
if UnprintableStyle == UnprintableStyleHighlight {
cells = append(cells, twin.Cell{
cells = append(cells, twin.StyledRune{
Rune: '?',
Style: styleUnprintable,
})
} else if UnprintableStyle == UnprintableStyleWhitespace {
cells = append(cells, twin.Cell{
cells = append(cells, twin.StyledRune{
Rune: '?',
Style: twin.StyleDefault,
})
@ -154,7 +154,7 @@ func CellsFromString(prefix string, s string, lineNumber *linenumbers.LineNumber
}
case BACKSPACE:
cells = append(cells, twin.Cell{
cells = append(cells, twin.StyledRune{
Rune: '<',
Style: styleUnprintable,
})
@ -162,12 +162,12 @@ func CellsFromString(prefix string, s string, lineNumber *linenumbers.LineNumber
default:
if !twin.Printable(token.Rune) {
if UnprintableStyle == UnprintableStyleHighlight {
cells = append(cells, twin.Cell{
cells = append(cells, twin.StyledRune{
Rune: '?',
Style: styleUnprintable,
})
} else if UnprintableStyle == UnprintableStyleWhitespace {
cells = append(cells, twin.Cell{
cells = append(cells, twin.StyledRune{
Rune: ' ',
Style: twin.StyleDefault,
})
@ -181,14 +181,14 @@ func CellsFromString(prefix string, s string, lineNumber *linenumbers.LineNumber
}
})
return CellsWithTrailer{
Cells: cells,
Trailer: trailer,
return StyledRunesWithTrailer{
StyledRunes: cells,
Trailer: trailer,
}
}
// 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) {
// Not enough runes left for a bold
return index, nil
@ -205,14 +205,14 @@ func consumeBold(runes []rune, index int) (int, *twin.Cell) {
}
// We have a match!
return index + 3, &twin.Cell{
return index + 3, &twin.StyledRune{
Rune: runes[index],
Style: ManPageBold,
}
}
// 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) {
// Not enough runes left for a underline
return index, nil
@ -229,7 +229,7 @@ func consumeUnderline(runes []rune, index int) (int, *twin.Cell) {
}
// We have a match!
return index + 3, &twin.Cell{
return index + 3, &twin.StyledRune{
Rune: runes[index+2],
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.
//
// 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")}
for _, pattern := range patterns {
if index+len(pattern) > len(runes) {
@ -259,7 +259,7 @@ func consumeBullet(runes []rune, index int) (int, *twin.Cell) {
}
// We have a match!
return index + len(pattern), &twin.Cell{
return index + len(pattern), &twin.StyledRune{
Rune: '•', // Unicode bullet point
Style: twin.StyleDefault,
}
@ -293,7 +293,7 @@ func runesFromStyledString(styledString _StyledString) string {
return returnMe.String()
}
func tokensFromStyledString(styledString _StyledString) []twin.Cell {
func tokensFromStyledString(styledString _StyledString) []twin.StyledRune {
runes := []rune(styledString.String)
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 {
// Shortcut when there's no backspace based formatting to worry about
for _, runeValue := range runes {
tokens = append(tokens, twin.Cell{
tokens = append(tokens, twin.StyledRune{
Rune: runeValue,
Style: styledString.Style,
})
@ -339,7 +339,7 @@ func tokensFromStyledString(styledString _StyledString) []twin.Cell {
continue
}
tokens = append(tokens, twin.Cell{
tokens = append(tokens, twin.StyledRune{
Rune: runes[index],
Style: styledString.Style,
})

View File

@ -20,7 +20,7 @@ import (
const samplesDir = "../../sample-files"
// Convert a cells array to a plain string
func cellsToPlainString(cells []twin.Cell) string {
func cellsToPlainString(cells []twin.StyledRune) string {
returnMe := ""
for _, cell := range cells {
returnMe += string(cell.Rune)
@ -74,7 +74,7 @@ func TestTokenize(t *testing.T) {
var loglines strings.Builder
log.SetOutput(&loglines)
tokens := CellsFromString("", line, lineNumber).Cells
tokens := StyledRunesFromString("", line, lineNumber).StyledRunes
plainString := WithoutFormatting(line, lineNumber)
if len(tokens) != utf8.RuneCountInString(plainString) {
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) {
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, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.Cell{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrUnderline)})
assert.Equal(t, tokens[2], twin.Cell{Rune: 'c', Style: twin.StyleDefault})
assert.Equal(t, tokens[0], twin.StyledRune{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.StyledRune{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrUnderline)})
assert.Equal(t, tokens[2], twin.StyledRune{Rune: 'c', Style: twin.StyleDefault})
}
func TestManPages(t *testing.T) {
// Bold
tokens := CellsFromString("", "ab\bbc", nil).Cells
tokens := StyledRunesFromString("", "ab\bbc", nil).StyledRunes
assert.Equal(t, len(tokens), 3)
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.Cell{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrBold)})
assert.Equal(t, tokens[2], twin.Cell{Rune: 'c', Style: twin.StyleDefault})
assert.Equal(t, tokens[0], twin.StyledRune{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.StyledRune{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrBold)})
assert.Equal(t, tokens[2], twin.StyledRune{Rune: 'c', Style: twin.StyleDefault})
// Underline
tokens = CellsFromString("", "a_\bbc", nil).Cells
tokens = StyledRunesFromString("", "a_\bbc", nil).StyledRunes
assert.Equal(t, len(tokens), 3)
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.Cell{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrUnderline)})
assert.Equal(t, tokens[2], twin.Cell{Rune: 'c', Style: twin.StyleDefault})
assert.Equal(t, tokens[0], twin.StyledRune{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.StyledRune{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrUnderline)})
assert.Equal(t, tokens[2], twin.StyledRune{Rune: 'c', Style: twin.StyleDefault})
// Bullet point 1, taken from doing this on my macOS system:
// 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, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.Cell{Rune: '•', Style: twin.StyleDefault})
assert.Equal(t, tokens[2], twin.Cell{Rune: 'b', Style: twin.StyleDefault})
assert.Equal(t, tokens[0], twin.StyledRune{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.StyledRune{Rune: '•', 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:
// 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, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.Cell{Rune: '•', Style: twin.StyleDefault})
assert.Equal(t, tokens[2], twin.Cell{Rune: 'b', Style: twin.StyleDefault})
assert.Equal(t, tokens[0], twin.StyledRune{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.StyledRune{Rune: '•', Style: twin.StyleDefault})
assert.Equal(t, tokens[2], twin.StyledRune{Rune: 'b', Style: twin.StyleDefault})
}
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
for _, token := range CellsFromString("", manPageHeading, nil).Cells {
for _, token := range StyledRunesFromString("", manPageHeading, nil).StyledRunes {
assert.Equal(t, token.Style, ManPageHeading)
}
// A line with only non-man-page bold caps should not be considered a heading
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))
}
// 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))
}
}
@ -257,9 +257,9 @@ func TestRawUpdateStyle(t *testing.T) {
func TestHyperlink_escBackslash(t *testing.T) {
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: 'b', 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) {
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: 'b', 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
func TestHyperlink_nonTerminatingEsc(t *testing.T) {
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
for i := 0; i < len(complete); i++ {
@ -295,7 +295,7 @@ func TestHyperlink_nonTerminatingEsc(t *testing.T) {
// good enough.
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)
}
}
@ -306,7 +306,7 @@ func TestHyperlink_incomplete(t *testing.T) {
for l := len(complete) - 1; l >= 0; l-- {
incomplete := complete[:l]
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++ {
if complete[i] == '\x1b' {
@ -314,7 +314,7 @@ func TestHyperlink_incomplete(t *testing.T) {
// that's good enough.
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"
)
func manPageHeadingFromString(s string) *CellsWithTrailer {
func manPageHeadingFromString(s string) *StyledRunesWithTrailer {
// For great performance, first check the string without allocating any
// memory.
if !parseManPageHeading(s, func(_ twin.Cell) {}) {
if !parseManPageHeading(s, func(_ twin.StyledRune) {}) {
return nil
}
cells := make([]twin.Cell, 0, len(s)/2)
ok := parseManPageHeading(s, func(cell twin.Cell) {
cells := make([]twin.StyledRune, 0, len(s)/2)
ok := parseManPageHeading(s, func(cell twin.StyledRune) {
cells = append(cells, cell)
})
if !ok {
panic("man page heading state changed")
}
return &CellsWithTrailer{
Cells: cells,
Trailer: twin.StyleDefault,
return &StyledRunesWithTrailer{
StyledRunes: cells,
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
// char+backspace+char, where both chars need to be the same. Whitespace is an
// 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 {
// 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.
@ -78,7 +78,7 @@ func parseManPageHeading(s string, reportCell func(twin.Cell)) bool {
if unicode.IsSpace(firstChar) {
// 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
firstChar = char
@ -105,7 +105,7 @@ func parseManPageHeading(s string, reportCell func(twin.Cell)) bool {
return false
}
reportCell(twin.Cell{Rune: char, Style: ManPageHeading})
reportStyledRune(twin.StyledRune{Rune: char, Style: ManPageHeading})
state = stateExpectingFirstChar
default:

View File

@ -8,7 +8,7 @@ import (
)
func isManPageHeading(s string) bool {
return parseManPageHeading(s, func(_ twin.Cell) {})
return parseManPageHeading(s, func(_ twin.StyledRune) {})
}
func TestIsManPageHeading(t *testing.T) {
@ -34,10 +34,10 @@ func TestManPageHeadingFromString_NotBoldSpace(t *testing.T) {
result := manPageHeadingFromString("A\bA B\bB")
assert.Assert(t, result != nil)
assert.Equal(t, len(result.Cells), 3)
assert.Equal(t, result.Cells[0], twin.Cell{Rune: 'A', Style: ManPageHeading})
assert.Equal(t, result.Cells[1], twin.Cell{Rune: ' ', Style: ManPageHeading})
assert.Equal(t, result.Cells[2], twin.Cell{Rune: 'B', Style: ManPageHeading})
assert.Equal(t, len(result.StyledRunes), 3)
assert.Equal(t, result.StyledRunes[0], twin.StyledRune{Rune: 'A', Style: ManPageHeading})
assert.Equal(t, result.StyledRunes[1], twin.StyledRune{Rune: ' ', Style: ManPageHeading})
assert.Equal(t, result.StyledRunes[2], twin.StyledRune{Rune: 'B', Style: ManPageHeading})
}
func TestManPageHeadingFromString_WithBoldSpace(t *testing.T) {
@ -47,8 +47,8 @@ func TestManPageHeadingFromString_WithBoldSpace(t *testing.T) {
result := manPageHeadingFromString("A\bA \b B\bB")
assert.Assert(t, result != nil)
assert.Equal(t, len(result.Cells), 3)
assert.Equal(t, result.Cells[0], twin.Cell{Rune: 'A', Style: ManPageHeading})
assert.Equal(t, result.Cells[1], twin.Cell{Rune: ' ', Style: ManPageHeading})
assert.Equal(t, result.Cells[2], twin.Cell{Rune: 'B', Style: ManPageHeading})
assert.Equal(t, len(result.StyledRunes), 3)
assert.Equal(t, result.StyledRunes[0], twin.StyledRune{Rune: 'A', Style: ManPageHeading})
assert.Equal(t, result.StyledRunes[1], twin.StyledRune{Rune: ' ', 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")
}
func parseScrollHint(scrollHint string) (twin.Cell, error) {
func parseScrollHint(scrollHint string) (twin.StyledRune, error) {
scrollHint = strings.ReplaceAll(scrollHint, "ESC", "\x1b")
hintAsLine := m.NewLine(scrollHint)
parsedTokens := hintAsLine.HighlightedTokens("", nil, nil).Cells
parsedTokens := hintAsLine.HighlightedTokens("", nil, nil).StyledRunes
if len(parsedTokens) == 1 {
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) {
@ -574,10 +574,10 @@ func pagerFromArgs(
unprintableStyle := flagSetFunc(flagSet, "render-unprintable", textstyles.UnprintableStyleHighlight,
"How unprintable characters are rendered: highlight or whitespace", parseUnprintableStyle)
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)
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)
shift := flagSetFunc(flagSet, "shift", 16, "Horizontal scroll `amount` >=1, defaults to 16", parseShiftAmount)
mouseMode := flagSetFunc(

View File

@ -10,7 +10,7 @@ import (
func TestParseScrollHint(t *testing.T) {
token, err := parseScrollHint("ESC[7m>")
assert.NilError(t, err)
assert.Equal(t, token, twin.Cell{
assert.Equal(t, token, twin.StyledRune{
Rune: '>',
Style: twin.StyleDefault.WithAttr(twin.AttrReverse),
})

View File

@ -7,13 +7,13 @@ package twin
type FakeScreen struct {
width int
height int
cells [][]Cell
cells [][]StyledRune
}
func NewFakeScreen(width int, height int) *FakeScreen {
rows := make([][]Cell, height)
rows := make([][]StyledRune, height)
for i := 0; i < height; i++ {
rows[i] = make([]Cell, width)
rows[i] = make([]StyledRune, width)
}
return &FakeScreen{
@ -30,7 +30,7 @@ func (screen *FakeScreen) Close() {
func (screen *FakeScreen) Clear() {
// This method's contents has been copied from UnixScreen.Clear()
empty := NewCell(' ', StyleDefault)
empty := NewStyledRune(' ', StyleDefault)
width, height := screen.Size()
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()
if column < 0 {
@ -85,6 +85,6 @@ func (screen *FakeScreen) Events() chan Event {
return nil
}
func (screen *FakeScreen) GetRow(row int) []Cell {
func (screen *FakeScreen) GetRow(row int) []StyledRune {
return screen.cells[row]
}

View File

@ -36,7 +36,7 @@ type Screen interface {
Clear()
SetCell(column int, row int, cell Cell)
SetCell(column int, row int, cell StyledRune)
// Render our contents into the terminal window
Show()
@ -80,7 +80,7 @@ type interruptableReader interface {
type UnixScreen struct {
widthAccessFromSizeOnly 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
// not this channel has been signalled
@ -556,9 +556,9 @@ func (screen *UnixScreen) Size() (width int, height int) {
return screen.widthAccessFromSizeOnly, screen.heightAccessFromSizeOnly
}
newCells := make([][]Cell, height)
newCells := make([][]StyledRune, height)
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
@ -627,7 +627,7 @@ func parseTerminalBgColorResponse(responseBytes []byte) *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 {
return
}
@ -646,7 +646,7 @@ func (screen *UnixScreen) SetCell(column int, row int, cell Cell) {
}
func (screen *UnixScreen) Clear() {
empty := NewCell(' ', StyleDefault)
empty := NewStyledRune(' ', StyleDefault)
width, height := screen.Size()
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
// it
func renderLine(row []Cell, terminalColorCount ColorCount) (string, int) {
func renderLine(row []StyledRune, terminalColorCount ColorCount) (string, int) {
// Strip trailing whitespace
lastSignificantCellIndex := len(row) - 1
for ; lastSignificantCellIndex >= 0; lastSignificantCellIndex-- {

View File

@ -54,7 +54,7 @@ func TestConsumeEncodedEventWithNoInput(t *testing.T) {
}
func TestRenderLine(t *testing.T) {
row := []Cell{
row := []StyledRune{
{
Rune: '<',
Style: StyleDefault.WithAttr(AttrReverse),
@ -78,7 +78,7 @@ func TestRenderLine(t *testing.T) {
}
func TestRenderLineEmpty(t *testing.T) {
row := []Cell{}
row := []StyledRune{}
rendered, count := renderLine(row, ColorCount16)
assert.Equal(t, count, 0)
@ -89,7 +89,7 @@ func TestRenderLineEmpty(t *testing.T) {
}
func TestRenderLineLastReversed(t *testing.T) {
row := []Cell{
row := []StyledRune{
{
Rune: '<',
Style: StyleDefault.WithAttr(AttrReverse),
@ -107,7 +107,7 @@ func TestRenderLineLastReversed(t *testing.T) {
}
func TestRenderLineLastNonSpace(t *testing.T) {
row := []Cell{
row := []StyledRune{
{
Rune: 'X',
Style: StyleDefault,
@ -124,7 +124,7 @@ func TestRenderLineLastNonSpace(t *testing.T) {
}
func TestRenderLineLastReversedPlusTrailingSpace(t *testing.T) {
row := []Cell{
row := []StyledRune{
{
Rune: '<',
Style: StyleDefault.WithAttr(AttrReverse),
@ -146,7 +146,7 @@ func TestRenderLineLastReversedPlusTrailingSpace(t *testing.T) {
}
func TestRenderLineOnlyTrailingSpaces(t *testing.T) {
row := []Cell{
row := []StyledRune{
{
Rune: ' ',
Style: StyleDefault,
@ -166,7 +166,7 @@ func TestRenderLineOnlyTrailingSpaces(t *testing.T) {
}
func TestRenderLineLastReversedSpaces(t *testing.T) {
row := []Cell{
row := []StyledRune{
{
Rune: ' ',
Style: StyleDefault.WithAttr(AttrReverse),
@ -184,7 +184,7 @@ func TestRenderLineLastReversedSpaces(t *testing.T) {
}
func TestRenderLineNonPrintable(t *testing.T) {
row := []Cell{
row := []StyledRune{
{
Rune: '',
},
@ -204,7 +204,7 @@ func TestRenderLineNonPrintable(t *testing.T) {
func TestRenderHyperlinkAtEndOfLine(t *testing.T) {
url := "https://example.com/"
row := []Cell{
row := []StyledRune{
{
Rune: '*',
Style: StyleDefault.WithHyperlink(&url),
@ -221,7 +221,7 @@ func TestRenderHyperlinkAtEndOfLine(t *testing.T) {
func TestMultiCharHyperlink(t *testing.T) {
url := "https://example.com/"
row := []Cell{
row := []StyledRune{
{
Rune: '-',
Style: StyleDefault.WithHyperlink(&url),

View File

@ -5,25 +5,27 @@ import (
"unicode"
)
// Cell is a rune with a style to be written to a cell on screen
type Cell struct {
// StyledRune is a rune with a style to be written to a one or more cells on the
// screen. Note that a StyledRune may use more than one cell on the screen ('午'
// for example).
type StyledRune struct {
Rune rune
Style Style
}
func NewCell(rune rune, style Style) Cell {
return Cell{
func NewStyledRune(rune rune, style Style) StyledRune {
return StyledRune{
Rune: rune,
Style: style,
}
}
func (cell Cell) String() string {
func (cell StyledRune) String() string {
return fmt.Sprint("rune='", string(cell.Rune), "' ", cell.Style)
}
// 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-- {
cell := cells[i]
if !unicode.IsSpace(cell.Rune) {
@ -34,11 +36,11 @@ func TrimSpaceRight(cells []Cell) []Cell {
}
// All whitespace, return empty
return []Cell{}
return []StyledRune{}
}
// 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++ {
cell := cells[i]
if !unicode.IsSpace(cell.Rune) {
@ -49,7 +51,7 @@ func TrimSpaceLeft(cells []Cell) []Cell {
}
// All whitespace, return empty
return []Cell{}
return []StyledRune{}
}
func Printable(char rune) bool {

View File

@ -11,28 +11,28 @@ func TestTrimSpaceRight(t *testing.T) {
// Empty
assert.Assert(t, reflect.DeepEqual(
TrimSpaceRight(
[]Cell{},
[]StyledRune{},
),
[]Cell{}))
[]StyledRune{}))
// Single non-space
assert.Assert(t, reflect.DeepEqual(
TrimSpaceRight(
[]Cell{{Rune: 'x'}},
[]StyledRune{{Rune: 'x'}},
),
[]Cell{{Rune: 'x'}}))
[]StyledRune{{Rune: 'x'}}))
// Single space
assert.Assert(t, reflect.DeepEqual(
TrimSpaceRight(
[]Cell{{Rune: ' '}},
[]StyledRune{{Rune: ' '}},
),
[]Cell{}))
[]StyledRune{}))
// Non-space plus space
assert.Assert(t, reflect.DeepEqual(
TrimSpaceRight(
[]Cell{{Rune: 'x'}, {Rune: ' '}},
[]StyledRune{{Rune: 'x'}, {Rune: ' '}},
),
[]Cell{{Rune: 'x'}}))
[]StyledRune{{Rune: 'x'}}))
}