diff --git a/oviewer/content.go b/oviewer/content.go index 8d4505c..16c0e15 100644 --- a/oviewer/content.go +++ b/oviewer/content.go @@ -71,6 +71,7 @@ type parseState struct { mainc rune combc []rune style tcell.Style + eolStyle tcell.Style bsContent content tabWidth int tabx int @@ -98,13 +99,20 @@ func RawStrToContents(str string, tabWidth int) contents { return parseString(newRawConverter(), str, tabWidth) } -// parseString converts a string to lineContents. -// parseString is converted character by character by Converter. -// If tabwidth is set to -1, \t is displayed instead of functioning as a tab. +// parseString converts a string to contents. +// This function wraps parseLine and is used when line styles are not needed. func parseString(conv Converter, str string, tabWidth int) contents { + lc, _ := parseLine(conv, str, tabWidth) + return lc +} + +// parseLine converts a string to lineContents and eolStyle, and returns them. +// If tabWidth is set to -1, \t is displayed instead of functioning as a tab. +func parseLine(conv Converter, str string, tabWidth int) (contents, tcell.Style) { st := &parseState{ lc: make(contents, 0, len(str)), style: tcell.StyleDefault, + eolStyle: tcell.StyleDefault, tabWidth: tabWidth, tabx: 0, bsFlag: false, @@ -125,7 +133,7 @@ func parseString(conv Converter, str string, tabWidth int) contents { st.mainc = '\n' st.combc = nil conv.convert(st) - return st.lc + return st.lc, st.eolStyle } // parseChar parses a single character. diff --git a/oviewer/content_test.go b/oviewer/content_test.go index 71a1fc0..e994391 100644 --- a/oviewer/content_test.go +++ b/oviewer/content_test.go @@ -854,3 +854,71 @@ func TestRawStrToContents(t *testing.T) { }) } } + +func Test_parseLine(t *testing.T) { + type args struct { + str string + tabWidth int + } + tests := []struct { + name string + args args + want contents + want1 tcell.Style + }{ + { + name: "testEscapeSequence", + args: args{ + str: "\x1b[31mred\x1b[m", + }, + want: contents{ + {width: 1, style: tcell.StyleDefault.Foreground(tcell.ColorMaroon), mainc: 'r'}, + {width: 1, style: tcell.StyleDefault.Foreground(tcell.ColorMaroon), mainc: 'e'}, + {width: 1, style: tcell.StyleDefault.Foreground(tcell.ColorMaroon), mainc: 'd'}, + }, + want1: tcell.StyleDefault, + }, + { + name: "testClearLine0", + args: args{ + str: "\x1b[42mt\x1b[0K", + }, + want: contents{ + {width: 1, style: tcell.StyleDefault.Background(tcell.ColorGreen), mainc: 't'}, + }, + want1: tcell.StyleDefault.Background(tcell.ColorGreen), + }, + { + name: "testClearLineBlank", + args: args{ + str: "\x1b[42mt\x1b[K", + }, + want: contents{ + {width: 1, style: tcell.StyleDefault.Background(tcell.ColorGreen), mainc: 't'}, + }, + want1: tcell.StyleDefault.Background(tcell.ColorGreen), + }, + { + name: "testClearLine1", + args: args{ + str: "\x1b[42mt\x1b[1K", // Not supported + }, + want: contents{ + {width: 1, style: tcell.StyleDefault.Background(tcell.ColorGreen), mainc: 't'}, + }, + want1: tcell.StyleDefault, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conv := newESConverter() + got, got1 := parseLine(conv, tt.args.str, tt.args.tabWidth) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseLine() got = \n%#v, want \n%#v", got, tt.want) + } + if !reflect.DeepEqual(got1, tt.want1) { + t.Errorf("parseLine() got1 = %#v, want %#v", got1, tt.want1) + } + }) + } +} diff --git a/oviewer/convert_es.go b/oviewer/convert_es.go index d0481ea..0e27927 100644 --- a/oviewer/convert_es.go +++ b/oviewer/convert_es.go @@ -75,15 +75,21 @@ func (es *escapeSequence) convert(st *parseState) bool { } return true case ansiControlSequence: - if mainc == 'm' { + switch { + case mainc == 'm': st.style = csToStyle(st.style, es.parameter.String()) - } else if mainc >= 'A' && mainc <= 'T' { - // Ignore. - } else { - if mainc >= 0x30 && mainc <= 0x3f { - es.parameter.WriteRune(mainc) - return true + case mainc == 'K': + // CSI 0 K or CSI K maintains the style after the newline + // (can change the background color of the line). + params := es.parameter.String() + if params == "" || params == "0" { + st.eolStyle = st.style } + case mainc >= 'A' && mainc <= 'T': + // Ignore. + case mainc >= '0' && mainc <= 'f': + es.parameter.WriteRune(mainc) + return true } es.state = ansiText return true diff --git a/oviewer/document.go b/oviewer/document.go index 354da39..4544ea8 100644 --- a/oviewer/document.go +++ b/oviewer/document.go @@ -12,6 +12,7 @@ import ( "sync/atomic" "time" + "github.com/gdamore/tcell/v2" lru "github.com/hashicorp/golang-lru/v2" "github.com/jwalton/gchalk" "github.com/noborus/guesswidth" @@ -204,6 +205,8 @@ type LineC struct { section int // Line number within a section. sectionNm int + // eolStyle is the style of the end of the line. + eolStyle tcell.Style } // NewDocument returns Document. @@ -442,6 +445,16 @@ func (m *Document) contents(lN int) (contents, error) { return parseString(m.conv, str, m.TabWidth), err } +func (m *Document) contentsLine(lN int) (contents, tcell.Style, error) { + if lN < 0 || lN >= m.BufEndNum() { + return nil, tcell.StyleDefault, ErrOutOfRange + } + + str, err := m.LineStr(lN) + lc, style := parseLine(m.conv, str, m.TabWidth) + return lc, style, err +} + // getLineC returns contents from line number and tabWidth. // If the line number does not exist, EOF content is returned. func (m *Document) getLineC(lN int) LineC { @@ -453,7 +466,7 @@ func (m *Document) getLineC(lN int) LineC { return line } - org, err := m.contents(lN) + org, style, err := m.contentsLine(lN) if err != nil && errors.Is(err, ErrOutOfRange) { lc := make(contents, 1) lc[0] = EOFContent @@ -466,9 +479,10 @@ func (m *Document) getLineC(lN int) LineC { } str, pos := ContentsToStr(org) line := LineC{ - lc: org, - str: str, - pos: pos, + lc: org, + str: str, + pos: pos, + eolStyle: style, } if err == nil { m.cache.Add(lN, line) diff --git a/oviewer/draw.go b/oviewer/draw.go index ec2fddc..f12ad4c 100644 --- a/oviewer/draw.go +++ b/oviewer/draw.go @@ -61,7 +61,7 @@ func (root *Root) drawBody(lX int, lN int) (int, int) { root.scr.numbers[y] = newLineNumber(lN, wrapNum) root.drawLineNumber(lN, y, line.valid) - nextLX, nextLN := root.drawLine(y, lX, lN, line.lc) + nextLX, nextLN := root.drawLine(y, lX, lN, line) if line.valid { root.coordinatesStyle(lN, y) } @@ -93,14 +93,14 @@ func (root *Root) drawHeader() { lX := 0 lN := root.scr.headerLN for y := 0; y < m.headerHeight && lN < root.scr.headerEnd; y++ { - line, ok := root.scr.lines[lN] + lineC, ok := root.scr.lines[lN] if !ok { panic(fmt.Sprintf("line is not found %d", lN)) } root.scr.numbers[y] = newLineNumber(lN, wrapNum) root.blankLineNumber(y) - lX, lN = root.drawLine(y, lX, lN, line.lc) + lX, lN = root.drawLine(y, lX, lN, lineC) // header style. root.applyStyleToLine(y, root.StyleHeader) @@ -120,14 +120,14 @@ func (root *Root) drawSectionHeader() { lX := 0 lN := root.scr.sectionHeaderLN for y := m.headerHeight; y < m.headerHeight+m.sectionHeaderHeight && lN < root.scr.sectionHeaderEnd; y++ { - line, ok := root.scr.lines[lN] + lineC, ok := root.scr.lines[lN] if !ok { panic(fmt.Sprintf("line is not found %d", lN)) } root.scr.numbers[y] = newLineNumber(lN, wrapNum) - root.drawLineNumber(lN, y, line.valid) + root.drawLineNumber(lN, y, lineC.valid) - nextLX, nextLN := root.drawLine(y, lX, lN, line.lc) + nextLX, nextLN := root.drawLine(y, lX, lN, lineC) // section header style. root.applyStyleToLine(y, root.StyleSectionLine) // markstyle is displayed above the section header. @@ -149,33 +149,33 @@ func (root *Root) drawSectionHeader() { } // drawWrapLine wraps and draws the contents and returns the next drawing position. -func (root *Root) drawLine(y int, lX int, lN int, lc contents) (int, int) { +func (root *Root) drawLine(y int, lX int, lN int, lineC LineC) (int, int) { if root.Doc.WrapMode { - return root.drawWrapLine(y, lX, lN, lc) + return root.drawWrapLine(y, lX, lN, lineC) } - return root.drawNoWrapLine(y, root.Doc.x, lN, lc) + return root.drawNoWrapLine(y, root.Doc.x, lN, lineC) } // drawWrapLine wraps and draws the contents and returns the next drawing position. -func (root *Root) drawWrapLine(y int, lX int, lN int, lc contents) (int, int) { +func (root *Root) drawWrapLine(y int, lX int, lN int, lineC LineC) (int, int) { if lX < 0 { log.Printf("Illegal lX:%d", lX) return 0, 0 } for x := 0; ; x++ { - if lX+x >= len(lc) { + if lX+x >= len(lineC.lc) { // EOL - root.clearEOL(root.scr.startX+x, y) + root.clearEOL(root.scr.startX+x, y, lineC.eolStyle) lX = 0 lN++ break } - content := lc[lX+x] + content := lineC.lc[lX+x] if x+root.scr.startX+content.width > root.scr.vWidth { // Right edge. - root.clearEOL(root.scr.startX+x, y) + root.clearEOL(root.scr.startX+x, y, tcell.StyleDefault) lX += x break } @@ -186,17 +186,17 @@ func (root *Root) drawWrapLine(y int, lX int, lN int, lc contents) (int, int) { } // drawNoWrapLine draws contents without wrapping and returns the next drawing position. -func (root *Root) drawNoWrapLine(y int, startX int, lN int, lc contents) (int, int) { +func (root *Root) drawNoWrapLine(y int, startX int, lN int, lineC LineC) (int, int) { startX = max(startX, root.minStartX) for x := 0; root.scr.startX+x < root.scr.vWidth; x++ { - if startX+x >= len(lc) { + if startX+x >= len(lineC.lc) { // EOL - root.clearEOL(root.scr.startX+x, y) + root.clearEOL(root.scr.startX+x, y, lineC.eolStyle) break } content := DefaultContent if startX+x >= 0 { - content = lc[startX+x] + content = lineC.lc[startX+x] } root.Screen.SetContent(root.scr.startX+x, y, content.mainc, content.combc, content.style) } @@ -258,15 +258,15 @@ func (root *Root) setContentString(vx int, vy int, lc contents) { } // clearEOL clears from the specified position to the right end. -func (root *Root) clearEOL(x int, y int) { +func (root *Root) clearEOL(x int, y int, style tcell.Style) { for ; x < root.scr.vWidth; x++ { - root.Screen.SetContent(x, y, ' ', nil, defaultStyle) + root.Screen.SetContent(x, y, ' ', nil, style) } } // clearY clear the specified line. func (root *Root) clearY(y int) { - root.clearEOL(0, y) + root.clearEOL(0, y, tcell.StyleDefault) } // coordinatesStyle applies the style of the coordinates.