mirror of https://github.com/liamg/aminal.git
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:
parent
8f0027dae1
commit
4033a8b25c
|
@ -80,6 +80,10 @@ func (buffer *Buffer) GetURLAtPosition(col uint16, viewRow uint16) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cell.IsHyperlink() {
|
||||||
|
return cell.hyperlink.Uri
|
||||||
|
}
|
||||||
|
|
||||||
candidate := string(cell.Rune())
|
candidate := string(cell.Rune())
|
||||||
|
|
||||||
// First, move forward
|
// First, move forward
|
||||||
|
|
|
@ -5,9 +5,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cell struct {
|
type Cell struct {
|
||||||
r rune
|
r rune
|
||||||
attr CellAttributes
|
attr CellAttributes
|
||||||
image *image.RGBA
|
image *image.RGBA
|
||||||
|
hyperlink *Hyperlink
|
||||||
}
|
}
|
||||||
|
|
||||||
type CellAttributes struct {
|
type CellAttributes struct {
|
||||||
|
@ -35,6 +36,10 @@ func (cell *Cell) Attr() CellAttributes {
|
||||||
return cell.attr
|
return cell.attr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) IsHyperlink() bool {
|
||||||
|
return cell.hyperlink != nil
|
||||||
|
}
|
||||||
|
|
||||||
func (cell *Cell) Rune() rune {
|
func (cell *Cell) Rune() rune {
|
||||||
return cell.r
|
return cell.r
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package buffer
|
||||||
|
|
||||||
|
type Hyperlink struct {
|
||||||
|
Uri string
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ type TerminalState struct {
|
||||||
tabStops map[uint16]struct{}
|
tabStops map[uint16]struct{}
|
||||||
Charsets []*map[rune]rune // array of 2 charsets, nil means ASCII (no conversion)
|
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
|
CurrentCharset int // active charset index in Charsets array, valid values are 0 or 1
|
||||||
|
CurrentHyperlink *Hyperlink
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTerminalMode creates a new terminal state
|
// 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 {
|
func (terminalState *TerminalState) DefaultCell(applyEffects bool) Cell {
|
||||||
attr := terminalState.CursorAttr
|
attr := terminalState.CursorAttr
|
||||||
if !applyEffects {
|
var hyperlink *Hyperlink
|
||||||
|
if applyEffects {
|
||||||
|
// fully-fledged cell
|
||||||
|
hyperlink = terminalState.CurrentHyperlink
|
||||||
|
} else {
|
||||||
attr.Blink = false
|
attr.Blink = false
|
||||||
attr.Bold = false
|
attr.Bold = false
|
||||||
attr.Dim = false
|
attr.Dim = false
|
||||||
|
@ -49,7 +54,7 @@ func (terminalState *TerminalState) DefaultCell(applyEffects bool) Cell {
|
||||||
attr.Underline = false
|
attr.Underline = false
|
||||||
attr.Dim = false
|
attr.Dim = false
|
||||||
}
|
}
|
||||||
return Cell{attr: attr}
|
return Cell{attr: attr, hyperlink: hyperlink}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (terminalState *TerminalState) SetVerticalMargins(top uint, bottom uint) {
|
func (terminalState *TerminalState) SetVerticalMargins(top uint, bottom uint) {
|
||||||
|
|
29
gui/gui.go
29
gui/gui.go
|
@ -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()
|
gui.renderScrollbar()
|
||||||
}
|
}
|
||||||
|
|
142
gui/renderer.go
142
gui/renderer.go
|
@ -29,6 +29,16 @@ type OpenGLRenderer struct {
|
||||||
rectRenderer *rectangleRenderer
|
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 {
|
func (r *OpenGLRenderer) CellWidth() float32 {
|
||||||
return r.cellWidth
|
return r.cellWidth
|
||||||
}
|
}
|
||||||
|
@ -37,12 +47,116 @@ func (r *OpenGLRenderer) CellHeight() float32 {
|
||||||
return r.cellHeight
|
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) {
|
func NewOpenGLRenderer(config *config.Config, fontMap *FontMap, areaX int, areaY int, areaWidth int, areaHeight int, colourAttr uint32, program uint32) (*OpenGLRenderer, error) {
|
||||||
rectRenderer, err := newRectangleRenderer()
|
rectRenderer, err := newRectangleRenderer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
r := &OpenGLRenderer{
|
r := &OpenGLRenderer{
|
||||||
areaWidth: areaWidth,
|
areaWidth: areaWidth,
|
||||||
areaHeight: areaHeight,
|
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)
|
// DrawUnderline draws a line under 'span' characters starting at (col, row)
|
||||||
func (r *OpenGLRenderer) DrawUnderline(span int, col uint, row uint, colour [3]float32) {
|
func (r *OpenGLRenderer) DrawUnderline(span int, col uint, row uint, colour [3]float32) {
|
||||||
//calculate coordinates
|
//calculate coordinates
|
||||||
x := float32(float32(col) * r.cellWidth)
|
x := float32(float32(col) * r.cellWidth)
|
||||||
y := (float32(row+1))*r.cellHeight + r.fontMap.DefaultFont().MinY()*0.25
|
y := (float32(row+1))*r.cellHeight + r.fontMap.DefaultFont().MinY()*0.25
|
||||||
|
|
||||||
thickness := r.cellHeight / 16
|
thickness := r.getUndelineThickness()
|
||||||
if thickness < 1 {
|
|
||||||
thickness = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
r.rectRenderer.render(x, y, r.cellWidth*float32(span), thickness, colour)
|
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) {
|
func (r *OpenGLRenderer) DrawCellText(text string, col uint, row uint, alpha float32, colour [3]float32, bold bool) {
|
||||||
|
|
||||||
var f *glfont.Font
|
var f *glfont.Font
|
||||||
|
|
|
@ -3,6 +3,8 @@ package terminal
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/liamg/aminal/buffer"
|
||||||
)
|
)
|
||||||
|
|
||||||
func oscHandler(pty chan rune, terminal *Terminal) error {
|
func oscHandler(pty chan rune, terminal *Terminal) error {
|
||||||
|
@ -10,18 +12,28 @@ func oscHandler(pty chan rune, terminal *Terminal) error {
|
||||||
params := []string{}
|
params := []string{}
|
||||||
param := ""
|
param := ""
|
||||||
|
|
||||||
for {
|
{
|
||||||
b := <-pty
|
isEscaped := false // flag if the previous character was escape
|
||||||
if terminal.IsOSCTerminator(b) {
|
|
||||||
params = append(params, param)
|
for {
|
||||||
break
|
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 {
|
if len(params) == 0 {
|
||||||
|
@ -39,6 +51,12 @@ func oscHandler(pty chan rune, terminal *Terminal) error {
|
||||||
switch pS[0] {
|
switch pS[0] {
|
||||||
case "0", "2":
|
case "0", "2":
|
||||||
terminal.SetTitle(pT)
|
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
|
case "10": // get/set foreground colour
|
||||||
if len(pS) > 1 {
|
if len(pS) > 1 {
|
||||||
if pS[1] == "?" {
|
if pS[1] == "?" {
|
||||||
|
|
|
@ -11,14 +11,20 @@ import (
|
||||||
"github.com/liamg/aminal/sixel"
|
"github.com/liamg/aminal/sixel"
|
||||||
)
|
)
|
||||||
|
|
||||||
type boolFormRuneFunc func(rune) bool
|
type boolFormRuneFunc func(rune, bool) bool
|
||||||
|
|
||||||
func swallowByFunction(pty chan rune, isTerminator boolFormRuneFunc) {
|
func swallowByFunction(pty chan rune, isTerminator boolFormRuneFunc) {
|
||||||
|
isEscaped := false
|
||||||
for {
|
for {
|
||||||
b := <-pty
|
b := <-pty
|
||||||
if isTerminator(b) {
|
if isTerminator(b, isEscaped) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if isEscaped {
|
||||||
|
isEscaped = false
|
||||||
|
} else if b == 0x1b {
|
||||||
|
isEscaped = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -154,7 +154,8 @@ func (terminal *Terminal) GetMouseExtMode() MouseExtMode {
|
||||||
return terminal.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]
|
_, ok := terminal.platformDependentSettings.OSCTerminators[char]
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue