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

Add line numbers to escape sequence log messages

This commit is contained in:
Johan Walles 2023-11-13 08:50:37 +01:00
parent b8e86b0aef
commit 8c4a133008
11 changed files with 49 additions and 41 deletions

View File

@ -40,11 +40,11 @@ func NewLine(raw string) Line {
// Returns a representation of the string split into styled tokens. Any regexp // Returns a representation of the string split into styled tokens. Any regexp
// matches are highlighted. A nil regexp means no highlighting. // matches are highlighted. A nil regexp means no highlighting.
func (line *Line) HighlightedTokens(linePrefix string, search *regexp.Regexp) cellsWithTrailer { func (line *Line) HighlightedTokens(linePrefix string, search *regexp.Regexp, lineNumberOneBased *int) cellsWithTrailer {
plain := line.Plain() plain := line.Plain(lineNumberOneBased)
matchRanges := getMatchRanges(&plain, search) matchRanges := getMatchRanges(&plain, search)
fromString := cellsFromString(linePrefix + line.raw) fromString := cellsFromString(linePrefix+line.raw, lineNumberOneBased)
returnCells := make([]twin.Cell, 0, len(fromString.Cells)) returnCells := make([]twin.Cell, 0, len(fromString.Cells))
for _, token := range fromString.Cells { for _, token := range fromString.Cells {
style := token.Style style := token.Style
@ -69,9 +69,9 @@ func (line *Line) HighlightedTokens(linePrefix string, search *regexp.Regexp) ce
} }
// Plain returns a plain text representation of the initial string // Plain returns a plain text representation of the initial string
func (line *Line) Plain() string { func (line *Line) Plain(lineNumberOneBased *int) string {
if line.plain == nil { if line.plain == nil {
plain := withoutFormatting(line.raw) plain := withoutFormatting(line.raw, lineNumberOneBased)
line.plain = &plain line.plain = &plain
} }
return *line.plain return *line.plain
@ -103,7 +103,7 @@ func ConsumeLessTermcapEnvs() {
func termcapToStyle(termcap string) twin.Style { func termcapToStyle(termcap string) twin.Style {
// Add a character to be sure we have one to take the format from // Add a character to be sure we have one to take the format from
cells := cellsFromString(termcap + "x").Cells cells := cellsFromString(termcap+"x", nil).Cells
return cells[len(cells)-1].Style return cells[len(cells)-1].Style
} }
@ -121,7 +121,7 @@ func isPlain(s string) bool {
return true return true
} }
func withoutFormatting(s string) string { func withoutFormatting(s string, lineNumberOneBased *int) string {
if isPlain(s) { if isPlain(s) {
return s return s
} }
@ -134,7 +134,7 @@ func withoutFormatting(s string) string {
// runes. // runes.
stripped.Grow(len(s) * 2) stripped.Grow(len(s) * 2)
for _, styledString := range styledStringsFromString(s).styledStrings { for _, styledString := range styledStringsFromString(s, lineNumberOneBased).styledStrings {
for _, runeValue := range runesFromStyledString(styledString) { for _, runeValue := range runesFromStyledString(styledString) {
switch runeValue { switch runeValue {
@ -179,13 +179,13 @@ func withoutFormatting(s string) string {
} }
// Turn a (formatted) string into a series of screen cells // Turn a (formatted) string into a series of screen cells
func cellsFromString(s string) cellsWithTrailer { func cellsFromString(s string, lineNumberOneBased *int) cellsWithTrailer {
var cells []twin.Cell var cells []twin.Cell
// Specs: https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit // Specs: https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit
styleUnprintable := twin.StyleDefault.Background(twin.NewColor16(1)).Foreground(twin.NewColor16(7)) styleUnprintable := twin.StyleDefault.Background(twin.NewColor16(1)).Foreground(twin.NewColor16(7))
stringsWithTrailer := styledStringsFromString(s) stringsWithTrailer := styledStringsFromString(s, lineNumberOneBased)
for _, styledString := range stringsWithTrailer.styledStrings { for _, styledString := range stringsWithTrailer.styledStrings {
for _, token := range tokensFromStyledString(styledString) { for _, token := range tokensFromStyledString(styledString) {
switch token.Rune { switch token.Rune {

View File

@ -51,8 +51,8 @@ func TestTokenize(t *testing.T) {
var loglines strings.Builder var loglines strings.Builder
log.SetOutput(&loglines) log.SetOutput(&loglines)
tokens := cellsFromString(line.raw).Cells tokens := cellsFromString(line.raw, &lineNumber).Cells
plainString := withoutFormatting(line.raw) plainString := withoutFormatting(line.raw, &lineNumber)
if len(tokens) != utf8.RuneCountInString(plainString) { if len(tokens) != utf8.RuneCountInString(plainString) {
t.Errorf("%s:%d: len(tokens)=%d, len(plainString)=%d for: <%s>", t.Errorf("%s:%d: len(tokens)=%d, len(plainString)=%d for: <%s>",
fileName, lineNumber, fileName, lineNumber,
@ -104,7 +104,7 @@ func TestTokenize(t *testing.T) {
} }
func TestUnderline(t *testing.T) { func TestUnderline(t *testing.T) {
tokens := cellsFromString("a\x1b[4mb\x1b[24mc").Cells tokens := cellsFromString("a\x1b[4mb\x1b[24mc", nil).Cells
assert.Equal(t, len(tokens), 3) assert.Equal(t, len(tokens), 3)
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault}) assert.Equal(t, tokens[0], twin.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[1], twin.Cell{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrUnderline)})
@ -113,14 +113,14 @@ func TestUnderline(t *testing.T) {
func TestManPages(t *testing.T) { func TestManPages(t *testing.T) {
// Bold // Bold
tokens := cellsFromString("ab\bbc").Cells tokens := cellsFromString("ab\bbc", nil).Cells
assert.Equal(t, len(tokens), 3) assert.Equal(t, len(tokens), 3)
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault}) assert.Equal(t, tokens[0], twin.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[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[2], twin.Cell{Rune: 'c', Style: twin.StyleDefault})
// Underline // Underline
tokens = cellsFromString("a_\bbc").Cells tokens = cellsFromString("a_\bbc", nil).Cells
assert.Equal(t, len(tokens), 3) assert.Equal(t, len(tokens), 3)
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault}) assert.Equal(t, tokens[0], twin.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[1], twin.Cell{Rune: 'b', Style: twin.StyleDefault.WithAttr(twin.AttrUnderline)})
@ -128,7 +128,7 @@ func TestManPages(t *testing.T) {
// Bullet point 1, taken from doing this on my macOS system: // Bullet point 1, taken from doing this on my macOS system:
// env PAGER="hexdump -C" man printf | moar // env PAGER="hexdump -C" man printf | moar
tokens = cellsFromString("a+\b+\bo\bob").Cells tokens = cellsFromString("a+\b+\bo\bob", nil).Cells
assert.Equal(t, len(tokens), 3) assert.Equal(t, len(tokens), 3)
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault}) assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.Cell{Rune: '•', Style: twin.StyleDefault}) assert.Equal(t, tokens[1], twin.Cell{Rune: '•', Style: twin.StyleDefault})
@ -136,7 +136,7 @@ func TestManPages(t *testing.T) {
// Bullet point 2, taken from doing this using the "fish" shell on my macOS system: // Bullet point 2, taken from doing this using the "fish" shell on my macOS system:
// man printf | hexdump -C | moar // man printf | hexdump -C | moar
tokens = cellsFromString("a+\bob").Cells tokens = cellsFromString("a+\bob", nil).Cells
assert.Equal(t, len(tokens), 3) assert.Equal(t, len(tokens), 3)
assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault}) assert.Equal(t, tokens[0], twin.Cell{Rune: 'a', Style: twin.StyleDefault})
assert.Equal(t, tokens[1], twin.Cell{Rune: '•', Style: twin.StyleDefault}) assert.Equal(t, tokens[1], twin.Cell{Rune: '•', Style: twin.StyleDefault})
@ -203,7 +203,7 @@ func TestRawUpdateStyle(t *testing.T) {
func TestHyperlink_escBackslash(t *testing.T) { func TestHyperlink_escBackslash(t *testing.T) {
url := "http://example.com" url := "http://example.com"
tokens := cellsFromString("a\x1b]8;;" + url + "\x1b\\bc\x1b]8;;\x1b\\d").Cells tokens := cellsFromString("a\x1b]8;;"+url+"\x1b\\bc\x1b]8;;\x1b\\d", nil).Cells
assert.DeepEqual(t, tokens, []twin.Cell{ assert.DeepEqual(t, tokens, []twin.Cell{
{Rune: 'a', Style: twin.StyleDefault}, {Rune: 'a', Style: twin.StyleDefault},
@ -219,7 +219,7 @@ func TestHyperlink_escBackslash(t *testing.T) {
func TestHyperlink_bell(t *testing.T) { func TestHyperlink_bell(t *testing.T) {
url := "http://example.com" url := "http://example.com"
tokens := cellsFromString("a\x1b]8;;" + url + "\x07bc\x1b]8;;\x07d").Cells tokens := cellsFromString("a\x1b]8;;"+url+"\x07bc\x1b]8;;\x07d", nil).Cells
assert.DeepEqual(t, tokens, []twin.Cell{ assert.DeepEqual(t, tokens, []twin.Cell{
{Rune: 'a', Style: twin.StyleDefault}, {Rune: 'a', Style: twin.StyleDefault},
@ -232,7 +232,7 @@ func TestHyperlink_bell(t *testing.T) {
// Test with some other ESC sequence than ESC-backslash // Test with some other ESC sequence than ESC-backslash
func TestHyperlink_nonTerminatingEsc(t *testing.T) { func TestHyperlink_nonTerminatingEsc(t *testing.T) {
complete := "a\x1b]8;;https://example.com\x1bbc" complete := "a\x1b]8;;https://example.com\x1bbc"
tokens := cellsFromString(complete).Cells tokens := cellsFromString(complete, nil).Cells
// This should not be treated as any link // This should not be treated as any link
for i := 0; i < len(complete); i++ { for i := 0; i < len(complete); i++ {
@ -252,7 +252,7 @@ func TestHyperlink_incomplete(t *testing.T) {
for l := len(complete) - 1; l >= 0; l-- { for l := len(complete) - 1; l >= 0; l-- {
incomplete := complete[:l] incomplete := complete[:l]
t.Run(fmt.Sprintf("l=%d incomplete=<%s>", l, strings.ReplaceAll(incomplete, "\x1b", "ESC")), func(t *testing.T) { t.Run(fmt.Sprintf("l=%d incomplete=<%s>", l, strings.ReplaceAll(incomplete, "\x1b", "ESC")), func(t *testing.T) {
tokens := cellsFromString(incomplete).Cells tokens := cellsFromString(incomplete, nil).Cells
for i := 0; i < l; i++ { for i := 0; i < l; i++ {
if complete[i] == '\x1b' { if complete[i] == '\x1b' {

View File

@ -9,7 +9,7 @@ import (
func tokenize(input string) []twin.Cell { func tokenize(input string) []twin.Cell {
line := NewLine(input) line := NewLine(input)
return line.HighlightedTokens("", nil).Cells return line.HighlightedTokens("", nil, nil).Cells
} }
func rowsToString(cellLines [][]twin.Cell) string { func rowsToString(cellLines [][]twin.Cell) string {

View File

@ -515,7 +515,7 @@ func TestPageSamples(t *testing.T) {
continue continue
} }
firstPagerLine := rowToString(screen.GetRow(0)) firstPagerLine := rowToString(screen.GetRow(0))
assert.Assert(t, strings.HasPrefix(firstReaderLine.Plain(), firstPagerLine)) assert.Assert(t, strings.HasPrefix(firstReaderLine.Plain(nil), firstPagerLine))
} }
} }

View File

@ -250,10 +250,10 @@ func TestGetLongLine(t *testing.T) {
assert.Equal(t, overflow, didFit) assert.Equal(t, overflow, didFit)
line := lines.lines[0] line := lines.lines[0]
assert.Assert(t, strings.HasPrefix(line.Plain(), "1 2 3 4"), "<%s>", line) assert.Assert(t, strings.HasPrefix(line.Plain(nil), "1 2 3 4"), "<%s>", line)
assert.Assert(t, strings.HasSuffix(line.Plain(), "0123456789"), line) assert.Assert(t, strings.HasSuffix(line.Plain(nil), "0123456789"), line)
assert.Equal(t, len(line.Plain()), 100021) assert.Equal(t, len(line.Plain(nil)), 100021)
} }
func getReaderWithLineCount(totalLines int) *Reader { func getReaderWithLineCount(totalLines int) *Reader {
@ -307,7 +307,7 @@ func testCompressedFile(t *testing.T, filename string) {
} }
lines, _ := reader.GetLines(1, 5) lines, _ := reader.GetLines(1, 5)
assert.Equal(t, lines.lines[0].Plain(), "This is a compressed file", "%s", filename) assert.Equal(t, lines.lines[0].Plain(nil), "This is a compressed file", "%s", filename)
} }
func TestCompressedFiles(t *testing.T) { func TestCompressedFiles(t *testing.T) {

View File

@ -51,7 +51,7 @@ func (p *Pager) redraw(spinner string) overflowState {
// This happens when we're done // This happens when we're done
eofSpinner = "---" eofSpinner = "---"
} }
spinnerLine := cellsFromString(_EofMarkerFormat + eofSpinner).Cells spinnerLine := cellsFromString(_EofMarkerFormat+eofSpinner, nil).Cells
for column, cell := range spinnerLine { for column, cell := range spinnerLine {
p.screen.SetCell(column, lastUpdatedScreenLineNumber+1, cell) p.screen.SetCell(column, lastUpdatedScreenLineNumber+1, cell)
} }
@ -204,7 +204,7 @@ func (p *Pager) renderLines() ([]renderedLine, string, overflowState) {
// lineNumber and numberPrefixLength are required for knowing how much to // lineNumber and numberPrefixLength are required for knowing how much to
// indent, and to (optionally) render the line number. // indent, and to (optionally) render the line number.
func (p *Pager) renderLine(line *Line, lineNumber int) ([]renderedLine, overflowState) { func (p *Pager) renderLine(line *Line, lineNumber int) ([]renderedLine, overflowState) {
highlighted := line.HighlightedTokens(p.linePrefix, p.searchPattern) highlighted := line.HighlightedTokens(p.linePrefix, p.searchPattern, &lineNumber)
var wrapped [][]twin.Cell var wrapped [][]twin.Cell
overflow := didFit overflow := didFit
if p.WrapLongLines { if p.WrapLongLines {

View File

@ -165,7 +165,7 @@ func (si *scrollPositionInternal) emptyBottomLinesCount(pager *Pager) int {
break break
} }
subLines, _ := pager.renderLine(line, 0) subLines, _ := pager.renderLine(line, lineNumberOneBased)
unclaimedViewportLines -= len(subLines) unclaimedViewportLines -= len(subLines)
if unclaimedViewportLines <= 0 { if unclaimedViewportLines <= 0 {
return 0 return 0

View File

@ -64,7 +64,7 @@ func (p *Pager) findFirstHit(startPosition scrollPosition, backwards bool) *scro
return nil return nil
} }
lineText := line.Plain() lineText := line.Plain(&searchPosition)
if p.searchPattern.MatchString(lineText) { if p.searchPattern.MatchString(lineText) {
return scrollPositionFromLineNumber("findFirstHit", searchPosition) return scrollPositionFromLineNumber("findFirstHit", searchPosition)
} }

View File

@ -12,7 +12,9 @@ import (
const esc = '\x1b' const esc = '\x1b'
type styledStringSplitter struct { type styledStringSplitter struct {
input string input string
lineNumberOneBased *int
nextByteIndex int nextByteIndex int
previousByteIndex int previousByteIndex int
@ -23,7 +25,7 @@ type styledStringSplitter struct {
trailer twin.Style trailer twin.Style
} }
func styledStringsFromString(s string) styledStringsWithTrailer { func styledStringsFromString(s string, lineNumberOneBased *int) styledStringsWithTrailer {
if !strings.ContainsAny(s, "\x1b") { if !strings.ContainsAny(s, "\x1b") {
// This shortcut makes BenchmarkPlainTextSearch() perform a lot better // This shortcut makes BenchmarkPlainTextSearch() perform a lot better
return styledStringsWithTrailer{ return styledStringsWithTrailer{
@ -36,7 +38,8 @@ func styledStringsFromString(s string) styledStringsWithTrailer {
} }
splitter := styledStringSplitter{ splitter := styledStringSplitter{
input: s, input: s,
lineNumberOneBased: lineNumberOneBased,
} }
splitter.run() splitter.run()
@ -80,8 +83,13 @@ func (s *styledStringSplitter) run() {
escIndex := s.previousByteIndex escIndex := s.previousByteIndex
err := s.handleEscape() err := s.handleEscape()
if err != nil { if err != nil {
header := ""
if s.lineNumberOneBased != nil {
header = fmt.Sprintf("Line %d: ", *s.lineNumberOneBased)
}
failed := s.input[escIndex:s.nextByteIndex] failed := s.input[escIndex:s.nextByteIndex]
log.Debug("Failed to parse <", strings.ReplaceAll(failed, "\x1b", "ESC"), ">: ", err) log.Debug(header, "<", strings.ReplaceAll(failed, "\x1b", "ESC"), ">: ", err)
// Somewhere in handleEscape(), we got a character that was // Somewhere in handleEscape(), we got a character that was
// unexpected. We need to treat everything up to before that // unexpected. We need to treat everything up to before that
@ -113,7 +121,7 @@ func (s *styledStringSplitter) handleEscape() error {
return s.consumeControlSequence(char) return s.consumeControlSequence(char)
} }
return fmt.Errorf("Unhandled char after ESC: %q", char) return fmt.Errorf("Unhandled Fe sequence ESC%c", char)
} }
func (s *styledStringSplitter) consumeControlSequence(charAfterEsc rune) error { func (s *styledStringSplitter) consumeControlSequence(charAfterEsc rune) error {
@ -172,7 +180,7 @@ func (s *styledStringSplitter) handleCompleteControlSequence(charAfterEsc rune,
return nil return nil
} }
return fmt.Errorf("Expected 'm' at the end of the control sequence, got %q", lastChar) return fmt.Errorf("Unhandled CSI type %q", lastChar)
} }
func (s *styledStringSplitter) handleOsc(sequence string) error { func (s *styledStringSplitter) handleOsc(sequence string) error {

View File

@ -33,14 +33,14 @@ func TestNextCharLastChar_empty(t *testing.T) {
// https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md // https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md
func TestIgnorePromptHints(t *testing.T) { func TestIgnorePromptHints(t *testing.T) {
// From an e-mail I got titled "moar question: "--RAW-CONTROL-CHARS" equivalent" // From an e-mail I got titled "moar question: "--RAW-CONTROL-CHARS" equivalent"
result := styledStringsFromString("\x1b]133;A\x1b\\hello") result := styledStringsFromString("\x1b]133;A\x1b\\hello", nil)
assert.Equal(t, twin.StyleDefault, result.trailer) assert.Equal(t, twin.StyleDefault, result.trailer)
assert.Equal(t, 1, len(result.styledStrings)) assert.Equal(t, 1, len(result.styledStrings))
assert.Equal(t, "hello", result.styledStrings[0].String) assert.Equal(t, "hello", result.styledStrings[0].String)
assert.Equal(t, twin.StyleDefault, result.styledStrings[0].Style) assert.Equal(t, twin.StyleDefault, result.styledStrings[0].Style)
// C rather than A, different end-of-sequence, should also be ignored // C rather than A, different end-of-sequence, should also be ignored
result = styledStringsFromString("\x1b]133;C\x07hello") result = styledStringsFromString("\x1b]133;C\x07hello", nil)
assert.Equal(t, twin.StyleDefault, result.trailer) assert.Equal(t, twin.StyleDefault, result.trailer)
assert.Equal(t, 1, len(result.styledStrings)) assert.Equal(t, 1, len(result.styledStrings))
assert.Equal(t, "hello", result.styledStrings[0].String) assert.Equal(t, "hello", result.styledStrings[0].String)
@ -54,7 +54,7 @@ func TestIgnorePromptHints(t *testing.T) {
// Johan got an e-mail titled "moar question: "--RAW-CONTROL-CHARS" equivalent" // Johan got an e-mail titled "moar question: "--RAW-CONTROL-CHARS" equivalent"
// about the sequence we're testing here. // about the sequence we're testing here.
func TestColonColors(t *testing.T) { func TestColonColors(t *testing.T) {
result := styledStringsFromString("\x1b[38:5:238mhello") result := styledStringsFromString("\x1b[38:5:238mhello", nil)
assert.Equal(t, twin.StyleDefault, result.trailer) assert.Equal(t, twin.StyleDefault, result.trailer)
assert.Equal(t, 1, len(result.styledStrings)) assert.Equal(t, 1, len(result.styledStrings))
assert.Equal(t, "hello", result.styledStrings[0].String) assert.Equal(t, "hello", result.styledStrings[0].String)

View File

@ -200,7 +200,7 @@ func parseUnprintableStyle(styleOption string) (m.UnprintableStyle, error) {
func parseScrollHint(scrollHint string) (twin.Cell, error) { func parseScrollHint(scrollHint string) (twin.Cell, error) {
scrollHint = strings.ReplaceAll(scrollHint, "ESC", "\x1b") scrollHint = strings.ReplaceAll(scrollHint, "ESC", "\x1b")
hintAsLine := m.NewLine(scrollHint) hintAsLine := m.NewLine(scrollHint)
parsedTokens := hintAsLine.HighlightedTokens("", nil).Cells parsedTokens := hintAsLine.HighlightedTokens("", nil, nil).Cells
if len(parsedTokens) == 1 { if len(parsedTokens) == 1 {
return parsedTokens[0], nil return parsedTokens[0], nil
} }