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
  • Loading branch information
noborus authored Dec 1, 2024
2 parents df6770f + d7e3d4b commit 258bcbd
Show file tree
Hide file tree
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
Expand Up @@ -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},
Expand Down Expand Up @@ -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) {
Expand Down
150 changes: 88 additions & 62 deletions oviewer/convert_es.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package oviewer

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

// csiParamStart and csiParamEnd define the range of parameters in the CSI.
Expand All @@ -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,
}
}
Expand Down Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
}
Expand All @@ -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
Expand Up @@ -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) {
Expand Down

0 comments on commit 258bcbd

Please sign in to comment.