Hyperlinks gnome-terminal style (OSC 8 sequence) (#263)

* #147 Hyperlinks. Step 1. Set/unset

* #147 Hyperlinks, corrected set/unset

* #147 set up hyperlinks and render them

* #147. Click on hyperlink

Co-authored-by: Liam Galvin <liam@liam-galvin.co.uk>
This commit is contained in:
Roman 2020-01-26 15:17:18 +03:00 committed by GitHub
parent 8f0027dae1
commit 4033a8b25c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 228 additions and 25 deletions

View File

@ -80,6 +80,10 @@ func (buffer *Buffer) GetURLAtPosition(col uint16, viewRow uint16) string {
return ""
}
if cell.IsHyperlink() {
return cell.hyperlink.Uri
}
candidate := string(cell.Rune())
// First, move forward

View File

@ -5,9 +5,10 @@ import (
)
type Cell struct {
r rune
attr CellAttributes
image *image.RGBA
r rune
attr CellAttributes
image *image.RGBA
hyperlink *Hyperlink
}
type CellAttributes struct {
@ -35,6 +36,10 @@ func (cell *Cell) Attr() CellAttributes {
return cell.attr
}
func (cell *Cell) IsHyperlink() bool {
return cell.hyperlink != nil
}
func (cell *Cell) Rune() rune {
return cell.r
}

5
buffer/hyperlink.go Normal file
View File

@ -0,0 +1,5 @@
package buffer
type Hyperlink struct {
Uri string
}

View File

@ -18,6 +18,7 @@ type TerminalState struct {
tabStops map[uint16]struct{}
Charsets []*map[rune]rune // array of 2 charsets, nil means ASCII (no conversion)
CurrentCharset int // active charset index in Charsets array, valid values are 0 or 1
CurrentHyperlink *Hyperlink
}
// NewTerminalMode creates a new terminal state
@ -41,7 +42,11 @@ func NewTerminalState(viewCols uint16, viewLines uint16, attr CellAttributes, ma
func (terminalState *TerminalState) DefaultCell(applyEffects bool) Cell {
attr := terminalState.CursorAttr
if !applyEffects {
var hyperlink *Hyperlink
if applyEffects {
// fully-fledged cell
hyperlink = terminalState.CurrentHyperlink
} else {
attr.Blink = false
attr.Bold = false
attr.Dim = false
@ -49,7 +54,7 @@ func (terminalState *TerminalState) DefaultCell(applyEffects bool) Cell {
attr.Underline = false
attr.Dim = false
}
return Cell{attr: attr}
return Cell{attr: attr, hyperlink: hyperlink}
}
func (terminalState *TerminalState) SetVerticalMargins(top uint, bottom uint) {

View File

@ -734,6 +734,35 @@ func (gui *GUI) renderTerminalData(shouldLock bool) {
}
}
}
// hyperlinks
for y := 0; y < lineCount; y++ {
if y < len(lines) {
span := 0
colour := [3]float32{0, 0, 0}
cells := lines[y].Cells()
var x int
for x = 0; x < colCount && x < len(cells); x++ {
cell := cells[x]
if span > 0 && (!cell.IsHyperlink() || colour != cell.Fg()) {
gui.renderer.DrawLinkLine(span, uint(x-span), uint(y), colour)
span = 0
}
colour = cell.Fg()
if cell.IsHyperlink() {
span++
}
}
if span > 0 {
gui.renderer.DrawLinkLine(span, uint(x-span), uint(y), colour)
}
}
}
gui.renderScrollbar()
}

View File

@ -29,6 +29,16 @@ type OpenGLRenderer struct {
rectRenderer *rectangleRenderer
}
type line struct {
vao uint32
vbo uint32
cv uint32
colourAttr uint32
colour [3]float32
points []float32
prog uint32
}
func (r *OpenGLRenderer) CellWidth() float32 {
return r.cellWidth
}
@ -37,12 +47,116 @@ func (r *OpenGLRenderer) CellHeight() float32 {
return r.cellHeight
}
func (r *OpenGLRenderer) newLine(x1 float32, y1 float32, x2 float32, y2 float32, dash float32, colourAttr uint32) *line {
l := &line{}
halfAreaWidth := float32(r.areaWidth / 2)
halfAreaHeight := float32(r.areaHeight / 2)
x1 = (x1 - halfAreaWidth) / halfAreaWidth
y1 = -(y1 - (halfAreaHeight)) / halfAreaHeight
x2 = (x2 - halfAreaWidth) / halfAreaWidth
y2 = -(y2 - (halfAreaHeight)) / halfAreaHeight
var xgap float32
var tan float32
if x2-x1 != 0 {
tan = (y2 - y1) / (x2 - x1)
xgap = dash / float32(math.Cos(math.Atan(float64(tan)))) / halfAreaWidth
}
l.points = []float32{
x1, y1, 0,
}
if xgap == 0 {
l.points = append(l.points, x2, y2, 0)
} else {
end := x1 + xgap
for {
var y float32
if end >= x2 {
end = x2
y = y2
} else {
y = y1 + tan*(end-x1)
}
l.points = append(l.points, end, y, 0)
start := end + xgap
if start >= x2 {
break
}
y = y1 + tan*(start-x1)
l.points = append(l.points, start, y, 0)
end = start + xgap
}
}
l.colourAttr = colourAttr
l.prog = r.program
// SHAPE
gl.GenBuffers(1, &l.vbo)
gl.BindBuffer(gl.ARRAY_BUFFER, l.vbo)
gl.BufferData(gl.ARRAY_BUFFER, 4*len(l.points), gl.Ptr(&l.points[0]), gl.STATIC_DRAW)
gl.GenVertexArrays(1, &l.vao)
gl.BindVertexArray(l.vao)
gl.EnableVertexAttribArray(0)
gl.BindBuffer(gl.ARRAY_BUFFER, l.vbo)
gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil)
// colour
gl.GenBuffers(1, &l.cv)
return l
}
func (l *line) Draw() {
gl.UseProgram(l.prog)
gl.BindVertexArray(l.vao)
gl.DrawArrays(gl.LINES, 0, int32(len(l.points)/3))
}
func (l *line) setColour(colour [3]float32) {
if l.colour == colour {
return
}
c := make([]float32, len(l.points))
for i := 0; i < len(c); i += 3 {
c[i] = colour[0]
c[i+1] = colour[1]
c[i+2] = colour[2]
}
gl.UseProgram(l.prog)
gl.BindBuffer(gl.ARRAY_BUFFER, l.cv)
gl.BufferData(gl.ARRAY_BUFFER, len(c)*4, gl.Ptr(c), gl.STATIC_DRAW)
gl.EnableVertexAttribArray(l.colourAttr)
gl.VertexAttribPointer(l.colourAttr, 3, gl.FLOAT, false, 0, gl.PtrOffset(0))
l.colour = colour
}
func (l *line) Free() {
gl.DeleteVertexArrays(1, &l.vao)
gl.DeleteBuffers(1, &l.vbo)
gl.DeleteBuffers(1, &l.cv)
l.vao = 0
l.vbo = 0
l.cv = 0
}
func NewOpenGLRenderer(config *config.Config, fontMap *FontMap, areaX int, areaY int, areaWidth int, areaHeight int, colourAttr uint32, program uint32) (*OpenGLRenderer, error) {
rectRenderer, err := newRectangleRenderer()
if err != nil {
return nil, err
}
r := &OpenGLRenderer{
areaWidth: areaWidth,
areaHeight: areaHeight,
@ -123,20 +237,36 @@ func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, colour
}
}
func (r *OpenGLRenderer) getUndelineThickness() float32 {
thickness := r.cellHeight / 16
if thickness < 1 {
thickness = 1
}
return thickness
}
// DrawUnderline draws a line under 'span' characters starting at (col, row)
func (r *OpenGLRenderer) DrawUnderline(span int, col uint, row uint, colour [3]float32) {
//calculate coordinates
x := float32(float32(col) * r.cellWidth)
y := (float32(row+1))*r.cellHeight + r.fontMap.DefaultFont().MinY()*0.25
thickness := r.cellHeight / 16
if thickness < 1 {
thickness = 1
}
thickness := r.getUndelineThickness()
r.rectRenderer.render(x, y, r.cellWidth*float32(span), thickness, colour)
}
func (r *OpenGLRenderer) DrawLinkLine(span int, col uint, row uint, colour [3]float32) {
//calculate coordinates
x := float32(float32(col) * r.cellWidth)
y := (float32(row+1))*r.cellHeight + r.fontMap.DefaultFont().MinY()*0.5
line := r.newLine(x, y, x+r.cellWidth*float32(span), y, r.cellWidth/4, r.colourAttr)
line.setColour(colour)
line.Draw()
line.Free()
}
func (r *OpenGLRenderer) DrawCellText(text string, col uint, row uint, alpha float32, colour [3]float32, bold bool) {
var f *glfont.Font

View File

@ -3,6 +3,8 @@ package terminal
import (
"fmt"
"strings"
"github.com/liamg/aminal/buffer"
)
func oscHandler(pty chan rune, terminal *Terminal) error {
@ -10,18 +12,28 @@ func oscHandler(pty chan rune, terminal *Terminal) error {
params := []string{}
param := ""
for {
b := <-pty
if terminal.IsOSCTerminator(b) {
params = append(params, param)
break
{
isEscaped := false // flag if the previous character was escape
for {
b := <-pty
if terminal.IsOSCTerminator(b, isEscaped) {
params = append(params, param)
break
}
if isEscaped {
isEscaped = false
} else if b == 0x1b {
isEscaped = true
continue
}
if b == ';' {
params = append(params, param)
param = ""
continue
}
param = fmt.Sprintf("%s%c", param, b)
}
if b == ';' {
params = append(params, param)
param = ""
continue
}
param = fmt.Sprintf("%s%c", param, b)
}
if len(params) == 0 {
@ -39,6 +51,12 @@ func oscHandler(pty chan rune, terminal *Terminal) error {
switch pS[0] {
case "0", "2":
terminal.SetTitle(pT)
case "8": // hyperlink
if len(params) > 2 && len(params[2]) > 0 {
terminal.terminalState.CurrentHyperlink = &buffer.Hyperlink{Uri: params[2]}
} else {
terminal.terminalState.CurrentHyperlink = nil
}
case "10": // get/set foreground colour
if len(pS) > 1 {
if pS[1] == "?" {

View File

@ -11,14 +11,20 @@ import (
"github.com/liamg/aminal/sixel"
)
type boolFormRuneFunc func(rune) bool
type boolFormRuneFunc func(rune, bool) bool
func swallowByFunction(pty chan rune, isTerminator boolFormRuneFunc) {
isEscaped := false
for {
b := <-pty
if isTerminator(b) {
if isTerminator(b, isEscaped) {
break
}
if isEscaped {
isEscaped = false
} else if b == 0x1b {
isEscaped = true
}
}
}

View File

@ -154,7 +154,8 @@ func (terminal *Terminal) GetMouseExtMode() MouseExtMode {
return terminal.mouseExtMode
}
func (terminal *Terminal) IsOSCTerminator(char rune) bool {
func (terminal *Terminal) IsOSCTerminator(char rune, isEscaped bool) bool {
// @todo handle isEscaped flag
_, ok := terminal.platformDependentSettings.OSCTerminators[char]
return ok
}