From e0e9ee6610a1424e35b518a4f8b1bfe514ff5ad0 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Sun, 15 Sep 2024 13:56:50 +0200 Subject: [PATCH] Start telling screen cells from styled runes One rune can cover multiple screen cells. --- m/line.go | 18 +-- m/line_test.go | 4 +- m/linewrapper.go | 8 +- m/linewrapper_test.go | 8 +- m/pager.go | 12 +- m/pager_test.go | 134 +++++++++++----------- m/pagermode-go-to-line.go | 4 +- m/pagermode-jump-to-mark.go | 2 +- m/pagermode-mark.go | 4 +- m/pagermode-search.go | 6 +- m/screenLines.go | 34 +++--- m/screenLines_test.go | 2 +- m/styling.go | 4 +- m/textstyles/ansiTokenizer.go | 48 ++++---- m/textstyles/ansiTokenizer_test.go | 66 +++++------ m/textstyles/manPageHeading.go | 20 ++-- m/textstyles/manPageHeading_test.go | 18 +-- moar.go | 10 +- moar_test.go | 2 +- twin/fake-screen.go | 12 +- twin/screen.go | 14 +-- twin/screen_test.go | 20 ++-- twin/{cell.go => styledRune.go} | 20 ++-- twin/{cell_test.go => styledRune_test.go} | 16 +-- 24 files changed, 244 insertions(+), 242 deletions(-) rename twin/{cell.go => styledRune.go} (73%) rename twin/{cell_test.go => styledRune_test.go} (66%) diff --git a/m/line.go b/m/line.go index c367e35..4d80c7c 100644 --- a/m/line.go +++ b/m/line.go @@ -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, } } diff --git a/m/line_test.go b/m/line_test.go index d4ba2c9..0beaf44 100644 --- a/m/line_test.go +++ b/m/line_test.go @@ -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) } diff --git a/m/linewrapper.go b/m/linewrapper.go index 9540fd3..84a28cc 100644 --- a/m/linewrapper.go +++ b/m/linewrapper.go @@ -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] diff --git a/m/linewrapper_test.go b/m/linewrapper_test.go index b72dee5..4011409 100644 --- a/m/linewrapper_test.go +++ b/m/linewrapper_test.go @@ -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)) } diff --git a/m/pager.go b/m/pager.go index ff1801d..6a5a7b1 100644 --- a/m/pager.go +++ b/m/pager.go @@ -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)) } } diff --git a/m/pager_test.go b/m/pager_test.go index a230989..5ecd089 100644 --- a/m/pager_test.go +++ b/m/pager_test.go @@ -27,19 +27,19 @@ const blueBackgroundClearToEol = "\x1b[44m\x1b[K" // No 0 before the K, should func TestUnicodeRendering(t *testing.T) { 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))), ) } diff --git a/m/pagermode-go-to-line.go b/m/pagermode-go-to-line.go index 3f56ca2..eba5429 100644 --- a/m/pagermode-go-to-line.go +++ b/m/pagermode-go-to-line.go @@ -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) { diff --git a/m/pagermode-jump-to-mark.go b/m/pagermode-jump-to-mark.go index dd5132f..1418fd0 100644 --- a/m/pagermode-jump-to-mark.go +++ b/m/pagermode-jump-to-mark.go @@ -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++ } } diff --git a/m/pagermode-mark.go b/m/pagermode-mark.go index 299604f..8caa3a7 100644 --- a/m/pagermode-mark.go +++ b/m/pagermode-mark.go @@ -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) { diff --git a/m/pagermode-search.go b/m/pagermode-search.go index 3eb4568..9a58d18 100644 --- a/m/pagermode-search.go +++ b/m/pagermode-search.go @@ -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++ } } diff --git a/m/screenLines.go b/m/screenLines.go index 33887af..f0d7551 100644 --- a/m/screenLines.go +++ b/m/screenLines.go @@ -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 diff --git a/m/screenLines_test.go b/m/screenLines_test.go index 4d2837d..962288a 100644 --- a/m/screenLines_test.go +++ b/m/screenLines_test.go @@ -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)}, diff --git a/m/styling.go b/m/styling.go index aadbe1e..5171e81 100644 --- a/m/styling.go +++ b/m/styling.go @@ -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") } diff --git a/m/textstyles/ansiTokenizer.go b/m/textstyles/ansiTokenizer.go index ca13f1e..ee352c4 100644 --- a/m/textstyles/ansiTokenizer.go +++ b/m/textstyles/ansiTokenizer.go @@ -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 '�': // 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= 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 '_= 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 '+<+ 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, }) diff --git a/m/textstyles/ansiTokenizer_test.go b/m/textstyles/ansiTokenizer_test.go index 99eb196..62d82e1 100644 --- a/m/textstyles/ansiTokenizer_test.go +++ b/m/textstyles/ansiTokenizer_test.go @@ -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}) } }) } diff --git a/m/textstyles/manPageHeading.go b/m/textstyles/manPageHeading.go index 36fd92c..e9c91b3 100644 --- a/m/textstyles/manPageHeading.go +++ b/m/textstyles/manPageHeading.go @@ -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: diff --git a/m/textstyles/manPageHeading_test.go b/m/textstyles/manPageHeading_test.go index 5a2a8ea..237c34e 100644 --- a/m/textstyles/manPageHeading_test.go +++ b/m/textstyles/manPageHeading_test.go @@ -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}) } diff --git a/moar.go b/moar.go index 27a2cf0..23ed7e4 100644 --- a/moar.go +++ b/moar.go @@ -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( diff --git a/moar_test.go b/moar_test.go index e6fc8a2..b47456a 100644 --- a/moar_test.go +++ b/moar_test.go @@ -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), }) diff --git a/twin/fake-screen.go b/twin/fake-screen.go index 6628994..a1d010b 100644 --- a/twin/fake-screen.go +++ b/twin/fake-screen.go @@ -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] } diff --git a/twin/screen.go b/twin/screen.go index c4501aa..7ebcd6c 100644 --- a/twin/screen.go +++ b/twin/screen.go @@ -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-- { diff --git a/twin/screen_test.go b/twin/screen_test.go index e77caae..39f3c83 100644 --- a/twin/screen_test.go +++ b/twin/screen_test.go @@ -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), diff --git a/twin/cell.go b/twin/styledRune.go similarity index 73% rename from twin/cell.go rename to twin/styledRune.go index 2bb1f32..9344f5e 100644 --- a/twin/cell.go +++ b/twin/styledRune.go @@ -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 { diff --git a/twin/cell_test.go b/twin/styledRune_test.go similarity index 66% rename from twin/cell_test.go rename to twin/styledRune_test.go index 52c2d22..1843072 100644 --- a/twin/cell_test.go +++ b/twin/styledRune_test.go @@ -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'}})) }