Skip to content

Commit

Permalink
feat(term/ansi): implement csi, osc, and params sequence parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed Feb 28, 2024
1 parent bd8a315 commit 5b22701
Show file tree
Hide file tree
Showing 6 changed files with 566 additions and 0 deletions.
153 changes: 153 additions & 0 deletions exp/term/ansi/csi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package ansi

import (
"strings"
)

// CsiSequence represents a control sequence introducer (CSI) sequence.
//
// The sequence starts with a CSI sequence, CSI (0x9B) in a 8-bit environment
// or ESC [ (0x1B 0x5B) in a 7-bit environment, followed by any number of
// parameters in the range of 0x30-0x3F, then by any number of intermediate
// byte in the range of 0x20-0x2F, then finally with a single final byte in the
// range of 0x20-0x7E.
//
// CSI P..P I..I F
//
// See ECMA-48 § 5.4.
type CsiSequence string

// IsValid reports whether the control sequence is valid.
func (c CsiSequence) IsValid() bool {
if len(c) == 0 {
return false
}

var i int
if c[0] == CSI {
i++
} else if len(c) > 1 && c[0] == ESC && c[1] == '[' {
i += 2
} else {
return false
}

// Parameters in the range 0x30-0x3F.
for ; i < len(c) && c[i] >= 0x30 && c[i] <= 0x3F; i++ { // nolint: revive
}

// Intermediate bytes in the range 0x20-0x2F.
for ; i < len(c) && c[i] >= 0x20 && c[i] <= 0x2F; i++ { // nolint: revive
}

// Final byte in the range 0x40-0x7E.
return i < len(c) && c[i] >= 0x40 && c[i] <= 0x7E
}

// HasInitial reports whether the control sequence has an initial byte.
// This indicater a private sequence.
func (c CsiSequence) HasInitial() bool {
i := c.Initial()
return i != 0
}

// Initial returns the initial byte of the control sequence.
func (c CsiSequence) Initial() byte {
if len(c) == 0 {
return 0
}

i := strings.IndexFunc(string(c), func(r rune) bool {
return r >= 0x3C && r <= 0x3F
})
if i == -1 {
return 0
}

return c[i]
}

// Params returns the parameters of the control sequence.
func (c CsiSequence) Params() []byte {
if len(c) == 0 {
return []byte{}
}

start := strings.IndexFunc(string(c), func(r rune) bool {
return r >= 0x30 && r <= 0x3F
})
if start == -1 {
return []byte{}
}

end := strings.IndexFunc(string(c[start:]), func(r rune) bool {
return r < 0x30 || r > 0x3F
})
if end == -1 {
return []byte{}
}

return []byte(c[start : start+end])
}

// Intermediates returns the intermediate bytes of the control sequence.
func (c CsiSequence) Intermediates() []byte {
if len(c) == 0 {
return []byte{}
}

start := strings.IndexFunc(string(c), func(r rune) bool {
return r >= 0x20 && r <= 0x2F
})
if start == -1 {
return []byte{}
}

end := strings.IndexFunc(string(c[start:]), func(r rune) bool {
return r < 0x20 || r > 0x2F
})
if end == -1 {
return []byte{}
}

return []byte(c[start : start+end])
}

// Command returns the command byte of the control sequence.
// A CSI command byte is in the range of 0x40-0x7E. This includes ASCII
// - @
// - A-Z
// - [ \ ]
// - ^ _ `
// - a-z
// - { | }
// - ~
func (c CsiSequence) Command() byte {
i := strings.LastIndexFunc(string(c), func(r rune) bool {
return r >= 0x40 && r <= 0x7E
})
if i == -1 {
return 0
}

return c[i]
}

// IsPrivate reports whether the control sequence is a private sequence.
// This means either the first parameter byte is in the range of 0x3C-0x3F or
// the command byte is in the range of 0x70-0x7E.
func (c CsiSequence) IsPrivate() bool {
if len(c) == 0 {
return false
}

var i int
for i = 0; i < len(c); i++ {
if c[i] >= 0x30 && c[i] <= 0x3F {
break
}
}

return (c[i] >= 0x3C && c[i] <= 0x3F) ||
(c[len(c)-1] >= 0x70 && c[len(c)-1] <= 0x7E)
}
89 changes: 89 additions & 0 deletions exp/term/ansi/csi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package ansi

import "testing"

func TestCsiSequenceIsValid(t *testing.T) {
cases := []struct {
seq CsiSequence
valid bool
}{
{CsiSequence(""), false},
{CsiSequence("\x1b["), false},
{CsiSequence("\x1b]"), false},
{CsiSequence("\x9b"), false},
{CsiSequence("\x1b[?1;2:1230"), false},
{CsiSequence("\x1b[0A"), true},
{CsiSequence("\x1b[A"), true},
{CsiSequence("\x1b[ A"), true},
{CsiSequence("\x1b[ #A"), true},
{CsiSequence("\x1b[1 #A"), true},
{CsiSequence("\x1b[1; #A"), true},
{CsiSequence("\x1b[1;2 #A"), true},
{CsiSequence("\x1b[1;2:3:4 #A"), true},
{CsiSequence("\x1b[1;2:3:4: #["), true},
{CsiSequence("\x1b[1;2;3;4;5;6;7;8;9A"), true},
{CsiSequence("\x1b[?1;2A"), true},
{CsiSequence("\x1b[?1;2:123A"), true},
}
for _, c := range cases {
if got, want := c.seq.IsValid(), c.valid; got != want {
t.Errorf("got %t, want %t", got, want)
}
}
}

func TestCsiSequenceParams(t *testing.T) {
cases := []struct {
seq CsiSequence
params string
}{
{CsiSequence("\x1b[012;3"), ""},
{CsiSequence("\x1b[A"), ""},
{CsiSequence("\x1b[0A"), "0"},
{CsiSequence("\x1b[1;2;3;4;5;6;7;8;9A"), "1;2;3;4;5;6;7;8;9"},
{CsiSequence("\x1b[?1;2A"), "?1;2"},
{CsiSequence("\x1b[?1;2:123A"), "?1;2:123"},
}
for _, c := range cases {
if got, want := string(c.seq.Params()), c.params; got != want {
t.Errorf("got %q, want %q", got, want)
}
}
}

func TestCsiSequenceIntermediates(t *testing.T) {
cases := []struct {
seq CsiSequence
intermediate string
}{
{CsiSequence("\x1b[0A"), ""},
{CsiSequence("\x1b[1;2;3;4;5;6;7;8;9A"), ""},
{CsiSequence("\x1b[?1;2A"), ""},
{CsiSequence("\x1b[?1;2:123A"), ""},
{CsiSequence("\x1b[?1;2:123 A"), " "},
{CsiSequence("\x1b[123 #!A"), " #!"},
}
for _, c := range cases {
if got, want := string(c.seq.Intermediates()), c.intermediate; got != want {
t.Errorf("got %q, want %q", got, want)
}
}
}

func TestCsiSequenceCommand(t *testing.T) {
cases := []struct {
seq CsiSequence
command byte
}{
{CsiSequence(""), 0},
{CsiSequence("\x1b[0A"), 'A'},
{CsiSequence("\x1b[1;2;3;4;5;6;7;8;9A"), 'A'},
{CsiSequence("\x1b[?1;2A"), 'A'},
{CsiSequence("\x1b[?1;2:123A"), 'A'},
}
for _, c := range cases {
if got, want := c.seq.Command(), c.command; got != want {
t.Errorf("got %q, want %q", got, want)
}
}
}
141 changes: 141 additions & 0 deletions exp/term/ansi/osc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package ansi

import (
"strings"
"unicode"
)

// OscSequence represents an OSC sequence.
//
// The sequence starts with a OSC sequence, OSC (0x9D) in a 8-bit environment
// or ESC ] (0x1B 0x5D) in a 7-bit environment, followed by positive integer identifier,
// then by arbitrary data terminated by a ST (0x9C) in a 8-bit environment,
// ESC \ (0x1B 0x5C) in a 7-bit environment, or BEL (0x07) for backwards compatibility.
//
// OSC Ps ; Pt ST
// OSC Ps ; Pt BEL
//
// See ECMA-48 § 5.7.
type OscSequence string

// IsValid reports whether the control sequence is valid.
// We allow UTF-8 in the data.
func (o OscSequence) IsValid() bool {
if len(o) == 0 {
return false
}

var i int
if o[0] == OSC {
i++
} else if len(o) > 1 && o[0] == ESC && o[1] == ']' {
i += 2
} else {
return false
}

// Osc data
start := i
end := -1
for ; i < len(o) && o[i] >= 0x20 && o[i] <= 0xFF && o[i] != ST && o[i] != BEL && o[i] != ESC; i++ { // nolint: revive
if end == -1 && o[i] == ';' {
end = i
}
}
if end == -1 {
end = i
}

// Identifier must be all digits.
for j := start; j < end; j++ {
if !unicode.IsDigit(rune(o[j])) {
return false
}
}

// Terminator is one of the following:
// - ST (0x9C)
// - ESC \ (0x1B 0x5C)
// - BEL (0x07)
return i < len(o) &&
(o[i] == ST || o[i] == BEL || (i+1 < len(o) && o[i] == ESC && o[i+1] == '\\'))
}

// Identifier returns the identifier of the control sequence.
func (o OscSequence) Identifier() string {
if len(o) == 0 {
return ""
}

start := strings.IndexFunc(string(o), func(r rune) bool {
return r >= '0' && r <= '9'
})
if start == -1 {
return ""
}
end := strings.Index(string(o), ";")
if end == -1 {
for i := len(o) - 1; i > start; i-- {
if o[i] == ST || o[i] == BEL || o[i] == ESC {
end = i
break
}
}
}
if end == -1 || start >= end {
return ""
}

id := string(o[start:end])
for _, r := range id {
if !unicode.IsDigit(r) {
return ""
}
}

return id
}

// Data returns the data of the control sequence.
func (o OscSequence) Data() string {
if len(o) == 0 {
return ""
}

start := strings.Index(string(o), ";")
if start == -1 {
return ""
}

end := -1
for i := len(o) - 1; i > start; i-- {
if o[i] == ST || o[i] == BEL || o[i] == ESC {
end = i
break
}
}
if end == -1 || start >= end {
return ""
}

return string(o[start+1 : end])
}

// Terminator returns the terminator of the control sequence.
func (o OscSequence) Terminator() string {
if len(o) == 0 {
return ""
}

i := len(o) - 1
for ; i > 0; i-- {
if o[i] == ST || o[i] == BEL || o[i] == ESC {
break
}
}
if i == -1 {
return ""
}

return string(o[i:])
}
Loading

0 comments on commit 5b22701

Please sign in to comment.