Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for OSC sequences #9

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 67 additions & 6 deletions escapes.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,28 @@ type EscapeItem struct {
// Required: The line shall not contain "\n"
func ExtractTermEscapes(line string) (string, []EscapeItem) {
var termEscapes []EscapeItem
var ed EscapeDetector
var line1 strings.Builder

pos := 0
item := ""
occupiedRuneCount := 0
inEscape := false
for i, r := range []rune(line) {
if r == '\x1b' {
ed.Witness(r)
if ed.Started() {
pos = i
item = string(r)
inEscape = true
continue
}
if inEscape {
if ed.InEscape() {
item += string(r)
if r == 'm' {
if ed.Ended() {
termEscapes = append(termEscapes, EscapeItem{item, pos - occupiedRuneCount})
occupiedRuneCount += utf8.RuneCountInString(item)
inEscape = false
}
continue
}

line1.WriteRune(r)
}

Expand Down Expand Up @@ -93,3 +93,64 @@ func OffsetEscapes(escapes []EscapeItem, offset int) []EscapeItem {
}
return result
}

// EscapeDetector detect escape sequences in a stream of runes.
// Supported sequences are:
// - Select Graphic Rendition (SGR)
// - Operating System Command (OSC)
//
// See https://chromium.googlesource.com/apps/libapps/+/refs/heads/master/hterm/doc/ControlSequences.md
type EscapeDetector struct {
inEscape bool
started bool
ended bool
firstRune rune
}

func (ed *EscapeDetector) Witness(r rune) {
if ed.inEscape && ed.ended {
ed.inEscape = false
}

if !ed.inEscape {
switch r {
case '\x1b': // SGR + OSC
ed.inEscape = true
ed.started = true
ed.ended = false
ed.firstRune = r
default:
ed.ended = false
}
} else {
switch {
case ed.firstRune == '\x1b' && r == 'm': // SGR
ed.inEscape = true
ed.started = false
ed.ended = true
ed.firstRune = rune(0)
case ed.firstRune == '\x1b' && r == '\x07': // OSC
ed.inEscape = true
ed.started = false
ed.ended = true
ed.firstRune = rune(0)
default:
ed.started = false
}
}
}

// InEscape indicate that the last rune was part of a sequence
func (ed *EscapeDetector) InEscape() bool {
return ed.inEscape
}

// Started indicate that the last rune started a sequence
func (ed *EscapeDetector) Started() bool {
return ed.started
}

// Ended indicate that the last rune ended a sequence
func (ed *EscapeDetector) Ended() bool {
return ed.ended
}
47 changes: 47 additions & 0 deletions escapes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestExtractApplyTermEscapes(t *testing.T) {
Expand Down Expand Up @@ -137,3 +138,49 @@ func TestOffsetEscapes(t *testing.T) {
assert.Equal(t, tc.output, result)
}
}

func TestEscapeDetector(t *testing.T) {
input := "This \u001B[31mis an\u001B[0m example."
states := []struct {
inEscape bool
started bool
ended bool
}{
{false, false, false}, // T
{false, false, false}, // h
{false, false, false}, // i
{false, false, false}, // s
{false, false, false}, //
{true, true, false}, // \u001b
{true, false, false}, // [
{true, false, false}, // 3
{true, false, false}, // 1
{true, false, true}, // m
{false, false, false}, // i
{false, false, false}, // s
{false, false, false}, //
{false, false, false}, // a
{false, false, false}, // n
{true, true, false}, // \u001b
{true, false, false}, // [
{true, false, false}, // 0
{true, false, true}, // m
{false, false, false}, //
{false, false, false}, // e
{false, false, false}, // x
{false, false, false}, // a
{false, false, false}, // m
{false, false, false}, // p
{false, false, false}, // l
{false, false, false}, // e
{false, false, false}, // .
}

var ed EscapeDetector
for i, r := range input {
ed.Witness(r)
require.Equal(t, states[i].inEscape, ed.InEscape())
require.Equal(t, states[i].started, ed.Started())
require.Equal(t, states[i].ended, ed.Ended())
}
}
15 changes: 5 additions & 10 deletions len.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,12 @@ import (
// escape sequences.
func Len(text string) int {
length := 0
escape := false
var ed EscapeDetector

for _, char := range text {
if char == '\x1b' {
escape = true
}
if !escape {
length += runewidth.RuneWidth(char)
}
if char == 'm' {
escape = false
for _, r := range []rune(text) {
ed.Witness(r)
if !ed.InEscape() {
length += runewidth.RuneWidth(r)
}
}

Expand Down
12 changes: 3 additions & 9 deletions wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,16 +389,14 @@ func splitWord(word string, length int) (string, string) {
runes := []rune(word)
var result []rune
added := 0
escape := false

if length == 0 {
return "", word
}

var ed EscapeDetector
for _, r := range runes {
if r == '\x1b' {
escape = true
}
ed.Witness(r)

width := runewidth.RuneWidth(r)
if width+added > length {
Expand All @@ -408,16 +406,12 @@ func splitWord(word string, length int) (string, string) {

result = append(result, r)

if !escape {
if !ed.InEscape() {
added += width
if added >= length {
break
}
}

if r == 'm' {
escape = false
}
}

leftover := runes[len(result):]
Expand Down