-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(term/ansi): implement csi, osc, and params sequence parsing
- Loading branch information
1 parent
bd8a315
commit 5b22701
Showing
6 changed files
with
566 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:]) | ||
} |
Oops, something went wrong.