From 4033a8b25c0700d953f3f9b4b95130a76977352e Mon Sep 17 00:00:00 2001 From: Roman Date: Sun, 26 Jan 2020 15:17:18 +0300 Subject: [PATCH] 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 --- buffer/buffer.go | 4 ++ buffer/cell.go | 11 ++- buffer/hyperlink.go | 5 ++ buffer/terminal_state.go | 9 ++- gui/gui.go | 29 ++++++++ gui/renderer.go | 142 +++++++++++++++++++++++++++++++++++++-- terminal/osc.go | 40 ++++++++--- terminal/sixel.go | 10 ++- terminal/terminal.go | 3 +- 9 files changed, 228 insertions(+), 25 deletions(-) create mode 100644 buffer/hyperlink.go diff --git a/buffer/buffer.go b/buffer/buffer.go index 6259cf2..f715d79 100644 --- a/buffer/buffer.go +++ b/buffer/buffer.go @@ -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 diff --git a/buffer/cell.go b/buffer/cell.go index a0fa159..62ebe72 100644 --- a/buffer/cell.go +++ b/buffer/cell.go @@ -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 } diff --git a/buffer/hyperlink.go b/buffer/hyperlink.go new file mode 100644 index 0000000..105fa3b --- /dev/null +++ b/buffer/hyperlink.go @@ -0,0 +1,5 @@ +package buffer + +type Hyperlink struct { + Uri string +} diff --git a/buffer/terminal_state.go b/buffer/terminal_state.go index 38b9972..932b7a2 100644 --- a/buffer/terminal_state.go +++ b/buffer/terminal_state.go @@ -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) { diff --git a/gui/gui.go b/gui/gui.go index 6345762..a16a7dd 100644 --- a/gui/gui.go +++ b/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() } diff --git a/gui/renderer.go b/gui/renderer.go index 9b9e6ab..f2545ef 100644 --- a/gui/renderer.go +++ b/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 diff --git a/terminal/osc.go b/terminal/osc.go index 6393018..8322156 100644 --- a/terminal/osc.go +++ b/terminal/osc.go @@ -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] == "?" { diff --git a/terminal/sixel.go b/terminal/sixel.go index 800d26e..9a45b70 100644 --- a/terminal/sixel.go +++ b/terminal/sixel.go @@ -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 + } } } diff --git a/terminal/terminal.go b/terminal/terminal.go index f80210c..d8a0022 100644 --- a/terminal/terminal.go +++ b/terminal/terminal.go @@ -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 }