Skip to content

Commit

Permalink
Merge pull request #651 from noborus/add-clear-line
Browse files Browse the repository at this point in the history
Add support of ANSI escape sequence for clear line
  • Loading branch information
noborus authored Nov 8, 2024
2 parents 0fa90de + e6bf0bd commit 07a10af
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 36 deletions.
16 changes: 12 additions & 4 deletions oviewer/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ type parseState struct {
mainc rune
combc []rune
style tcell.Style
eolStyle tcell.Style
bsContent content
tabWidth int
tabx int
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand Down
68 changes: 68 additions & 0 deletions oviewer/content_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
20 changes: 13 additions & 7 deletions oviewer/convert_es.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 18 additions & 4 deletions oviewer/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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)
Expand Down
42 changes: 21 additions & 21 deletions oviewer/draw.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)

Expand All @@ -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.
Expand All @@ -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
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 07a10af

Please sign in to comment.