Skip to content

Commit

Permalink
Reimplemented osc hyperlink
Browse files Browse the repository at this point in the history
The OSC hyperlink has also been rewritten to match CSI.
Fixed so that the text does not disappear even
when an error occurs.
  • Loading branch information
noborus committed Nov 29, 2024
1 parent df6770f commit 8c512ee
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 80 deletions.
49 changes: 49 additions & 0 deletions oviewer/content_test.go
Original file line number Diff line number Diff line change
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
120 changes: 53 additions & 67 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,7 @@ 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)
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)
}
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()
es.state = ansiText
return true
}
es.url.WriteRune(mainc)
es.parseOSC(st, mainc)
return true
}
switch mainc {
Expand All @@ -176,6 +121,7 @@ func (es *escapeSequence) paraseEscapeSequence(st *parseState) bool {
case '\n':
return false
}
//log.Printf("mainc: %c, %#v", mainc, st.style)
return false
}

Expand Down Expand Up @@ -339,7 +285,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 +307,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 +336,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 +378,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 +417,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 +438,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 +450,43 @@ 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 '\x1b': // ESC.
return
case '\\', '\a': // ST(String Terminator) (or BEL).
st.style = oscStyle(st, es.parameter.String())
es.parameter.Reset()
es.state = ansiText
return
}
es.parameter.WriteRune(mainc)
}

// oscStyle returns tcell.Style from the OSC control sequence.
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
}
65 changes: 52 additions & 13 deletions oviewer/convert_es_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package oviewer

import (
"reflect"
"strings"
"testing"

"github.com/gdamore/tcell/v2"
Expand Down Expand Up @@ -164,19 +165,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 Expand Up @@ -550,3 +538,54 @@ func Test_colorName(t *testing.T) {
})
}
}

func Test_escapeSequence_parseOSC(t *testing.T) {
type fields struct {
parameter string
state int
}
type args struct {
st *parseState
mainc rune
}
type want struct {
style tcell.Style
}
tests := []struct {
name string
fields fields
args args
want want
}{
{
name: "test-OSC",
fields: fields{
parameter: "8;;http://example.com",
state: ansiControlSequence,
},
args: args{
st: &parseState{
mainc: '\\',
},
mainc: 0x07,
},
want: want{
style: tcell.StyleDefault.Url("http://example.com"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parameter := strings.Builder{}
parameter.WriteString(tt.fields.parameter)
es := &escapeSequence{
parameter: parameter,
state: tt.fields.state,
}
es.parseOSC(tt.args.st, tt.args.mainc)
if tt.args.st.style != tt.want.style {
t.Errorf("escapeSequence.parseOSC() = %v, want %v", tt.args.st.style, tt.want.style)
}
})
}
}

0 comments on commit 8c512ee

Please sign in to comment.