Skip to content

Commit

Permalink
Merge pull request #664 from noborus/improve-osc-hyperlink
Browse files Browse the repository at this point in the history
Reimplemented osc hyperlink
noborus authored Dec 1, 2024

Verified

This commit was signed with the committer’s verified signature.
oliviertassinari Olivier Tassinari
2 parents df6770f + d7e3d4b commit 258bcbd
Showing 3 changed files with 138 additions and 76 deletions.
51 changes: 50 additions & 1 deletion oviewer/content_test.go
Original file line number Diff line number Diff line change
@@ -536,7 +536,7 @@ func Test_StrToContentsHyperlink(t *testing.T) {
},
{
name: "testHyperLinkfile",
args: args{line: "\x1b]8;;file:///file\afile\x1b]8;;\a", tabWidth: 8},
args: args{line: "\x1b]8;;file:///file\afile\x1b]8;;\x1b\\", tabWidth: 8},
want: contents{
{width: 1, style: tcell.StyleDefault.Url("file:///file"), mainc: rune('f'), combc: nil},
{width: 1, style: tcell.StyleDefault.Url("file:///file"), mainc: rune('i'), combc: nil},
@@ -858,6 +858,55 @@ func Test_parseLine(t *testing.T) {
},
want1: tcell.StyleDefault,
},
{
name: "testHyperLinkError",
args: args{
str: "\x1b]+8;;http://example.com\x1b\\link\x1b]8;;\x1b\\",
},
want: contents{
{width: 1, style: tcell.StyleDefault, mainc: 'l'},
{width: 1, style: tcell.StyleDefault, mainc: 'i'},
{width: 1, style: tcell.StyleDefault, mainc: 'n'},
{width: 1, style: tcell.StyleDefault, mainc: 'k'},
},
},
{
name: "testHyperLink",
args: args{
str: "\x1b]8;;http://example.com\x1b\\link\x1b]8;;\x1b\\",
},
want: contents{
{width: 1, style: tcell.StyleDefault.Url("http://example.com"), mainc: 'l'},
{width: 1, style: tcell.StyleDefault.Url("http://example.com"), mainc: 'i'},
{width: 1, style: tcell.StyleDefault.Url("http://example.com"), mainc: 'n'},
{width: 1, style: tcell.StyleDefault.Url("http://example.com"), mainc: 'k'},
},
want1: tcell.StyleDefault,
},
{
name: "testHyperLinkID",
args: args{
str: "\x1b]8;1;http://example.com\x1b\\link\x1b]8;;\x1b\\",
},
want: contents{
{width: 1, style: tcell.StyleDefault.Url("http://example.com").UrlId("1"), mainc: 'l'},
{width: 1, style: tcell.StyleDefault.Url("http://example.com").UrlId("1"), mainc: 'i'},
{width: 1, style: tcell.StyleDefault.Url("http://example.com").UrlId("1"), mainc: 'n'},
{width: 1, style: tcell.StyleDefault.Url("http://example.com").UrlId("1"), mainc: 'k'},
},
},
{
name: "testHyperLinkFile",
args: args{
str: "\x1b]8;;file:///file\afile\x1b]8;;\a",
},
want: contents{
{width: 1, style: tcell.StyleDefault.Url("file:///file"), mainc: 'f'},
{width: 1, style: tcell.StyleDefault.Url("file:///file"), mainc: 'i'},
{width: 1, style: tcell.StyleDefault.Url("file:///file"), mainc: 'l'},
{width: 1, style: tcell.StyleDefault.Url("file:///file"), mainc: 'e'},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
150 changes: 88 additions & 62 deletions oviewer/convert_es.go
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@ package oviewer

import (
"fmt"
"log"
"strconv"
"strings"
"sync"
@@ -25,9 +24,7 @@ const (
ansiControlSequence
otherSequence
systemSequence
oscHyperLink
oscParameter
oscURL
oscControlSequence
)

// csiParamStart and csiParamEnd define the range of parameters in the CSI.
@@ -48,15 +45,13 @@ const (
// escapeSequence is a structure that holds the escape sequence.
type escapeSequence struct {
parameter strings.Builder
url strings.Builder
state int
}

// newESConverter returns a new escape sequence converter.
func newESConverter() *escapeSequence {
return &escapeSequence{
parameter: strings.Builder{},
url: strings.Builder{},
state: ansiText,
}
}
@@ -92,7 +87,7 @@ func (es *escapeSequence) paraseEscapeSequence(st *parseState) bool {
st.style = tcell.StyleDefault
es.state = ansiText
return true
case ']': // Operating System Command Sequence.
case ']': // OSC(Operating System Command Sequence).
es.state = systemSequence
return true
case 'P', 'X', '^', '_': // Substrings and commands.
@@ -116,57 +111,17 @@ func (es *escapeSequence) paraseEscapeSequence(st *parseState) bool {
es.state = ansiEscape
return true
case systemSequence:
switch mainc {
case '8':
es.state = oscHyperLink
return true
case '\\':
es.state = ansiText
return true
case 0x1b:
// unknown but for compatibility.
es.state = ansiControlSequence
return true
case 0x07:
es.state = ansiText
return true
}
log.Printf("invalid char %c", mainc)
es.parseOSC(st, mainc)
return true
case oscHyperLink:
switch mainc {
case ';':
es.state = oscParameter
return true
}
es.state = ansiText
return false
case oscParameter:
if mainc != ';' {
es.parameter.WriteRune(mainc)
return true
}
urlID := es.parameter.String()
if urlID != "" {
st.style = st.style.UrlId(urlID)
}
case oscControlSequence:
parameter := es.parameter.String()
es.parameter.Reset()
es.state = oscURL
return true
case oscURL:
switch mainc {
case 0x1b:
st.style = st.style.Url(es.url.String())
es.url.Reset()
es.state = systemSequence
return true
case 0x07:
st.style = st.style.Url(es.url.String())
es.url.Reset()
if mainc == '\\' { // ST(String Terminator).
st.style = oscStyle(st, parameter)
es.state = ansiText
return true
}
es.url.WriteRune(mainc)
es.state = ansiText
return true
}
switch mainc {
@@ -339,7 +294,7 @@ func toSGRCode(paramList []string, index int) (sgrParams, error) {
str := paramList[index]
sgr := sgrParams{}
colonLists := strings.Split(str, ":")
code, err := sgrNumber(colonLists[0])
code, err := esNumber(colonLists[0])
if err != nil {
return sgrParams{}, ErrNotSuuport
}
@@ -361,10 +316,10 @@ func toSGRCode(paramList []string, index int) (sgrParams, error) {
return sgr, nil
}

// sgrNumber converts a string to a number.
// esNumber converts a string to a number.
// If the string is empty, it returns 0.
// If the string contains a non-numeric character, it returns an error.
func sgrNumber(str string) (int, error) {
func esNumber(str string) (int, error) {
if str == "" {
return 0, nil
}
@@ -390,7 +345,7 @@ func containsNonDigit(str string) bool {

// underLineStyle sets the underline style.
func underLineStyle(s OVStyle, param string) OVStyle {
n, err := sgrNumber(param)
n, err := esNumber(param)
if err != nil {
return s
}
@@ -432,7 +387,7 @@ func convertSGRColor(sgr sgrParams) (string, int, error) {
return "", 0, nil
}
inc := 1
ex, err := sgrNumber(sgr.params[0])
ex, err := esNumber(sgr.params[0])
if err != nil {
return "", inc, err
}
@@ -471,7 +426,7 @@ func parse256Color(param string) (string, error) {
if param == "" {
return "", nil
}
c, err := sgrNumber(param)
c, err := esNumber(param)
if err != nil {
return "", err
}
@@ -492,9 +447,9 @@ func parseRGBColor(red string, green string, blue string) (string, error) {
if red == "" || green == "" || blue == "" {
return "", nil
}
r, err1 := sgrNumber(red)
g, err2 := sgrNumber(green)
b, err3 := sgrNumber(blue)
r, err1 := esNumber(red)
g, err2 := esNumber(green)
b, err3 := esNumber(blue)
if err1 != nil || err2 != nil || err3 != nil {
return "", fmt.Errorf("invalid RGB color values: %v, %v, %v", red, green, blue)
}
@@ -504,3 +459,74 @@ func parseRGBColor(red string, green string, blue string) (string, error) {
color := fmt.Sprintf("#%02x%02x%02x", r, g, b)
return color, nil
}

// parseOSC parses the OSC(Operating System Command Sequence) escape sequence.
func (es *escapeSequence) parseOSC(st *parseState, mainc rune) {
switch mainc {
case '\a': // BEL is also interpreted as ST.
parameter := es.parameter.String()
es.parameter.Reset()
if isOSC(parameter) {
st.style = oscStyle(st, parameter)
}
es.state = ansiText
return
case 0x1b: // ESC.
if isOSC(es.parameter.String()) {
es.state = oscControlSequence
return
}
es.parameter.Reset()
es.state = ansiControlSequence
return
}

es.parameter.WriteRune(mainc)
}

// isOSC returns true if the parameter is an OSC escape sequence.
func isOSC(parameter string) bool {
if parameter == "" {
return false
}
params := strings.Split(parameter, ";")
if len(params) < 2 {
return false
}
code, err := esNumber(params[0])
if err != nil {
return false
}
switch code { // OSC code.
case 8: // Hyperlink.
return true
}
return false
}

// oscStyle returns tcell.Style from the OSC control sequence.
// oscStyle only supports hyperlinks.
func oscStyle(st *parseState, paramStr string) tcell.Style {
params := strings.Split(paramStr, ";")
if len(params) < 2 {
return st.style
}
code, err := esNumber(params[0])
if err != nil {
return st.style
}
switch code { // OSC code.
case 8: // Hyperlink.
if len(params) < 3 {
return st.style
}
urlID := params[1]
url := params[2]
if urlID != "" {
st.style = st.style.UrlId(urlID)
}
st.style = st.style.Url(url)
return st.style
}
return st.style
}
13 changes: 0 additions & 13 deletions oviewer/convert_es_test.go
Original file line number Diff line number Diff line change
@@ -164,19 +164,6 @@ func Test_escapeSequence_convert(t *testing.T) {
want: true,
wantState: ansiText,
},
{
name: "test-OscHyperLink",
fields: fields{
state: oscHyperLink,
},
args: args{
st: &parseState{
mainc: 'a',
},
},
want: false,
wantState: ansiText,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

0 comments on commit 258bcbd

Please sign in to comment.