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 ""
|
||||
}
|
||||
|
||||
if cell.IsHyperlink() {
|
||||
return cell.hyperlink.Uri
|
||||
}
|
||||
|
||||
candidate := string(cell.Rune())
|
||||
|
||||
// First, move forward
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package buffer
|
||||
|
||||
type Hyperlink struct {
|
||||
Uri string
|
||||
}
|
|
@ -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) {
|
||||
|
|
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()
|
||||
}
|
||||
|
|
142
gui/renderer.go
142
gui/renderer.go
|
@ -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
|
||||
|
|
|
@ -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] == "?" {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue