summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2024-08-14 08:43:27 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2024-08-14 08:51:34 +0900
commitd90a969c00401a11d86a238eef29048dea42398a (patch)
treebe0bbec4ede7c5bc4f7024592ac955045ff980b7 /src
parent2c8a96bb27d3da089ab89d47143dd3c6854d1295 (diff)
downloadfzf-d90a969c00401a11d86a238eef29048dea42398a.tar.gz
Add support for hyperlinks in preview window
Close #2165
Diffstat (limited to 'src')
-rw-r--r--src/ansi.go38
-rw-r--r--src/result_test.go8
-rw-r--r--src/terminal.go14
-rw-r--r--src/tui/light.go8
-rw-r--r--src/tui/tcell.go20
-rw-r--r--src/tui/tui.go2
6 files changed, 80 insertions, 10 deletions
diff --git a/src/ansi.go b/src/ansi.go
index 5f7c8be4..65168f7e 100644
--- a/src/ansi.go
+++ b/src/ansi.go
@@ -1,6 +1,7 @@
package fzf
import (
+ "fmt"
"strconv"
"strings"
"unicode/utf8"
@@ -13,22 +14,28 @@ type ansiOffset struct {
color ansiState
}
+type url struct {
+ uri string
+ params string
+}
+
type ansiState struct {
fg tui.Color
bg tui.Color
attr tui.Attr
lbg tui.Color
+ url *url
}
func (s *ansiState) colored() bool {
- return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0
+ return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0 || s.url != nil
}
func (s *ansiState) equals(t *ansiState) bool {
if t == nil {
return !s.colored()
}
- return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg
+ return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg && s.url == t.url
}
func (s *ansiState) ToString() string {
@@ -60,7 +67,11 @@ func (s *ansiState) ToString() string {
}
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
- return "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
+ ret = "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
+ if s.url != nil {
+ ret = fmt.Sprintf("\x1b]8;%s;%s\x1b\\%s\x1b]8;;\x1b", s.url.params, s.url.uri, ret)
+ }
+ return ret
}
func toAnsiString(color tui.Color, offset int) string {
@@ -98,10 +109,19 @@ func matchOperatingSystemCommand(s string) int {
if s[i] == '\x07' {
return i + 1
}
+ // `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
+ // ------
if s[i] == '\x1b' && i < len(s)-1 && s[i+1] == '\\' {
return i + 2
}
}
+
+ // `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
+ // ------------
+ if i < len(s) && s[:i+1] == "\x1b]8;;\x1b" {
+ return i + 1
+ }
+
return -1
}
@@ -328,13 +348,21 @@ func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
var state ansiState
if prevState == nil {
- state = ansiState{-1, -1, 0, -1}
+ state = ansiState{-1, -1, 0, -1, nil}
} else {
- state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
+ state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg, prevState.url}
}
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
state.lbg = prevState.bg
+ } else if ansiCode == "\x1b]8;;\x1b" { // End of a hyperlink
+ state.url = nil
+ } else if strings.HasPrefix(ansiCode, "\x1b]8;") && strings.HasSuffix(ansiCode, "\x1b\\") {
+ if paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
+ params := ansiCode[4 : 4+paramsEnd]
+ uri := ansiCode[5+paramsEnd : len(ansiCode)-2]
+ state.url = &url{uri: uri, params: params}
+ }
}
return state
}
diff --git a/src/result_test.go b/src/result_test.go
index 2f818a9b..c11e1ab5 100644
--- a/src/result_test.go
+++ b/src/result_test.go
@@ -124,10 +124,10 @@ func TestColorOffset(t *testing.T) {
item := Result{
item: &Item{
colors: &[]ansiOffset{
- {[2]int32{0, 20}, ansiState{1, 5, 0, -1}},
- {[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1}},
- {[2]int32{30, 32}, ansiState{3, 7, 0, -1}},
- {[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1}}}}}
+ {[2]int32{0, 20}, ansiState{1, 5, 0, -1, nil}},
+ {[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1, nil}},
+ {[2]int32{30, 32}, ansiState{3, 7, 0, -1, nil}},
+ {[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1, nil}}}}}
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
diff --git a/src/terminal.go b/src/terminal.go
index 29df02b2..62d5ae92 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -1068,7 +1068,7 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
// // unless the part has a non-default ANSI state
loc := whiteSuffix.FindStringIndex(trimmed)
if loc != nil {
- blankState := ansiOffset{[2]int32{int32(loc[0]), int32(loc[1])}, ansiState{-1, -1, tui.AttrClear, -1}}
+ blankState := ansiOffset{[2]int32{int32(loc[0]), int32(loc[1])}, ansiState{-1, -1, tui.AttrClear, -1, nil}}
if item.colors != nil {
lastColor := (*item.colors)[len(*item.colors)-1]
if lastColor.offset[1] < int32(loc[1]) {
@@ -2668,12 +2668,21 @@ Loop:
var fillRet tui.FillReturn
prefixWidth := 0
+ var url *url
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
trimmed := []rune(str)
isTrimmed := false
if !t.previewOpts.wrap {
trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X())
}
+ if url == nil && ansi != nil && ansi.url != nil {
+ url = ansi.url
+ t.pwindow.LinkBegin(url.uri, url.params)
+ }
+ if url != nil && (ansi == nil || ansi.url == nil) {
+ url = nil
+ t.pwindow.LinkEnd()
+ }
str, width := t.processTabs(trimmed, prefixWidth)
if width > prefixWidth {
prefixWidth = width
@@ -2687,6 +2696,9 @@ Loop:
return !isTrimmed &&
(fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine)
})
+ if url != nil {
+ t.pwindow.LinkEnd()
+ }
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
if fillRet == tui.FillNextLine {
continue
diff --git a/src/tui/light.go b/src/tui/light.go
index 3ed24f71..187ac667 100644
--- a/src/tui/light.go
+++ b/src/tui/light.go
@@ -1118,6 +1118,14 @@ func (w *LightWindow) setBg() string {
return "\x1b[m"
}
+func (w *LightWindow) LinkBegin(uri string, params string) {
+ w.renderer.queued.WriteString("\x1b]8;" + params + ";" + uri + "\x1b\\")
+}
+
+func (w *LightWindow) LinkEnd() {
+ w.renderer.queued.WriteString("\x1b]8;;\x1b\\")
+}
+
func (w *LightWindow) Fill(text string) FillReturn {
w.Move(w.posy, w.posx)
code := w.setBg()
diff --git a/src/tui/tcell.go b/src/tui/tcell.go
index 16ce452d..d80cd58d 100644
--- a/src/tui/tcell.go
+++ b/src/tui/tcell.go
@@ -4,6 +4,7 @@ package tui
import (
"os"
+ "regexp"
"time"
"github.com/gdamore/tcell/v2"
@@ -49,6 +50,8 @@ type TcellWindow struct {
lastY int
moveCursor bool
borderStyle BorderStyle
+ uri *string
+ params *string
}
func (w *TcellWindow) Top() int {
@@ -666,6 +669,13 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
Italic(a&Attr(tcell.AttrItalic) != 0)
+ if w.uri != nil {
+ style = style.Url(*w.uri)
+ if md := regexp.MustCompile(`id=([^:]+)`).FindStringSubmatch(*w.params); len(md) > 1 {
+ style = style.UrlId(md[1])
+ }
+ }
+
gr := uniseg.NewGraphemes(text)
Loop:
for gr.Next() {
@@ -716,6 +726,16 @@ func (w *TcellWindow) Fill(str string) FillReturn {
return w.fillString(str, w.normal)
}
+func (w *TcellWindow) LinkBegin(uri string, params string) {
+ w.uri = &uri
+ w.params = &params
+}
+
+func (w *TcellWindow) LinkEnd() {
+ w.uri = nil
+ w.params = nil
+}
+
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
if fg == colDefault {
fg = w.normal.Fg()
diff --git a/src/tui/tui.go b/src/tui/tui.go
index f3e58f41..240f7ce7 100644
--- a/src/tui/tui.go
+++ b/src/tui/tui.go
@@ -564,6 +564,8 @@ type Window interface {
CPrint(color ColorPair, text string)
Fill(text string) FillReturn
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
+ LinkBegin(uri string, params string)
+ LinkEnd()
Erase()
EraseMaybe() bool
}