[vte] widget, emulation: Add support for OSC 8 hyperlinks (HTML-like anchors)



commit c9e7cbabfe2fd682a80dd8938c317e7aed1195f4
Author: Egmont Koblinger <egmont gmail com>
Date:   Tue Apr 25 01:17:11 2017 +0200

    widget,emulation: Add support for OSC 8 hyperlinks (HTML-like anchors)
    
    The escape sequences
      OSC 8 ; params ; URI BEL
      OSC 8 ; params ; URI ST
    turn subsequent characters into an HTML-like anchor to the given address.
    Terminate the hyperlink with the same escape sequence with an empty URI.
    
    Params is an optional colon-separated list of key=value assignments. Cells
    having the same URI and "id" parameter, or cells lacking an "id" that were
    printed in a single OSC 8 run are underlined together on mouseover.
    
    For further details, see
    https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=779734

 bindings/vala/app.vala         |    7 +
 doc/reference/vte-docs.xml     |    4 +
 doc/reference/vte-sections.txt |    3 +
 perf/hyperlink-demo.txt        |   95 ++++++++++
 src/debug.c                    |    3 +-
 src/debug.h                    |    3 +-
 src/marshal.list               |    1 +
 src/ring.cc                    |  388 +++++++++++++++++++++++++++++++++++++---
 src/ring.h                     |   32 +++-
 src/vte.cc                     |  281 +++++++++++++++++++++++++++--
 src/vte/vteterminal.h          |   10 +
 src/vteapp.c                   |   19 ++-
 src/vtedefines.hh              |   30 +++
 src/vtegtk.cc                  |  121 +++++++++++++
 src/vtegtk.hh                  |    3 +
 src/vteinternal.hh             |   19 ++-
 src/vterowdata.h               |   57 ++++++-
 src/vteseq.cc                  |  114 ++++++++++++-
 18 files changed, 1132 insertions(+), 58 deletions(-)
---
diff --git a/bindings/vala/app.vala b/bindings/vala/app.vala
index 58698f9..11cd1ff 100644
--- a/bindings/vala/app.vala
+++ b/bindings/vala/app.vala
@@ -324,6 +324,7 @@ class Window : Gtk.ApplicationWindow
     if (App.Options.word_char_exceptions != null)
       terminal.set_word_char_exceptions(App.Options.word_char_exceptions);
 
+    terminal.set_allow_hyperlink(!App.Options.no_hyperlink);
     terminal.set_audible_bell(App.Options.audible);
     terminal.set_cjk_ambiguous_width(App.Options.get_cjk_ambiguous_width());
     terminal.set_cursor_blink_mode(App.Options.get_cursor_blink_mode());
@@ -628,6 +629,9 @@ class Window : Gtk.ApplicationWindow
 
 #if VALA_0_24
     if (event != null) {
+      var hyperlink = terminal.hyperlink_check_event(event);
+      if (hyperlink != null)
+        menu.append("Copy _Hyperlink", "win.copy-match::" + hyperlink);
       var match = terminal.match_check_event(event, null);
       if (match != null)
         menu.append("Copy _Match", "win.copy-match::" + match);
@@ -834,6 +838,7 @@ class App : Gtk.Application
     public static bool no_context_menu = false;
     public static bool no_double_buffer = false;
     public static bool no_geometry_hints = false;
+    public static bool no_hyperlink = false;
     public static bool no_pcre = false;
     public static bool no_rewrap = false;
     public static bool no_shell = false;
@@ -1021,6 +1026,8 @@ class App : Gtk.Application
         "Disable double-buffering", null },
       { "no-geometry-hints", 'G', 0, OptionArg.NONE, ref no_geometry_hints,
         "Allow the terminal to be resized to any dimension, not constrained to fit to an integer multiple of 
characters", null },
+      { "no-hyperlink", 'H', 0, OptionArg.NONE, ref no_hyperlink,
+        "Disable hyperlinks", null },
       { "no-rewrap", 'R', 0, OptionArg.NONE, ref no_rewrap,
         "Disable rewrapping on resize", null },
       { "no-shell", 'S', 0, OptionArg.NONE, ref no_shell,
diff --git a/doc/reference/vte-docs.xml b/doc/reference/vte-docs.xml
index 89a4e15..9e66f2b 100644
--- a/doc/reference/vte-docs.xml
+++ b/doc/reference/vte-docs.xml
@@ -102,6 +102,10 @@
     <title>Index of new symbols in 0.48</title>
     <xi:include href="xml/api-index-0.48.xml"><xi:fallback /></xi:include>
   </index>
+  <index id="api-index-0-50" role="0.50">
+    <title>Index of new symbols in 0.50</title>
+    <xi:include href="xml/api-index-0.50.xml"><xi:fallback /></xi:include>
+  </index>
 
   <xi:include href="xml/annotation-glossary.xml"><xi:fallback /></xi:include>
 
diff --git a/doc/reference/vte-sections.txt b/doc/reference/vte-sections.txt
index d4bedb1..69683dd 100644
--- a/doc/reference/vte-sections.txt
+++ b/doc/reference/vte-sections.txt
@@ -24,6 +24,8 @@ vte_terminal_set_audible_bell
 vte_terminal_get_audible_bell
 vte_terminal_set_allow_bold
 vte_terminal_get_allow_bold
+vte_terminal_set_allow_hyperlink
+vte_terminal_get_allow_hyperlink
 vte_terminal_set_scroll_on_output
 vte_terminal_set_scroll_on_keystroke
 vte_terminal_set_rewrap_on_resize
@@ -54,6 +56,7 @@ vte_terminal_get_text
 vte_terminal_get_text_include_trailing_spaces
 vte_terminal_get_text_range
 vte_terminal_get_cursor_position
+vte_terminal_hyperlink_check_event
 vte_terminal_match_add_regex
 vte_terminal_match_remove
 vte_terminal_match_remove_all
diff --git a/perf/hyperlink-demo.txt b/perf/hyperlink-demo.txt
new file mode 100644
index 0000000..6c3d1b4
--- /dev/null
+++ b/perf/hyperlink-demo.txt
@@ -0,0 +1,95 @@
+Tests for ]8;;https://bugzilla.gnome.org/show_bug.cgi?id=779734gnome-terminal #779734]8;; and 
]8;;https://gitlab.com/gnachman/iterm2/issues/5158iTerm2 #5158]8;;
+═════════════════════════════════════════════════
+
+commit 
]8;;https://git.gnome.org/browse/vte/commit/?id=a9b0b4c75a6dc7282f7cfcaef71413d69f7f0731a9b0b4c75a6dc7282f7cfcaef71413d69f7f0731]8;;
+Author: Egmont Koblinger <]8;;mailto:egmont gmail comegmont gmail com]8;;>
+Date:   Sat Oct 24 00:12:22 2015 +0200
+
+    widget: Implement smooth scrolling
+    
+    ]8;;https://bugzilla.gnome.org/show_bug.cgi?id=746690Bug #746690]8;;
+
+commit 
]8;;https://git.gnome.org/browse/vte/commit/?id=6a74baeaabb0a1ce54444611b324338f94721a5c6a74baeaabb0a1ce54444611b324338f94721a5c]8;;
+Merge: 
]8;;https://git.gnome.org/browse/vte/commit/?id=3fac4469de267f662c761ea4f247c8017ced483d3fac446]8;; 
]8;;https://git.gnome.org/browse/vte/commit/?id=56ea5810759b9943a4203f9382919f058a66f22456ea581]8;;
+Author: Christian Persch <]8;;mailto:chpe gnome orgchpe gnome org]8;;>
+Date:   Mon Apr 27 13:48:52 2015 +0200
+
+    Merge branch 'work-html' into merge-html
+
+]8;;file:///var/lib/gconf/defaults/%25gconf-tree.xmlA file with a % sign in its name (escaped as %25)]8;;
+Icons: ]8;;file:///usr/share/icons/Adwaita/256x256/apps/preferences-desktop-theme.pngTheme]8;; 
]8;;file:///usr/share/icons/Adwaita/256x256/categories/applications-graphics.pngGraphics]8;; 
]8;;file:///usr/share/icons/Adwaita/256x256/status/starred.pngStar]8;; 
]8;;file:///usr/share/icons/Adwaita/256x256/actions/system-log-out.pngExit]8;; 
]8;;file:///usr/share/icons/Adwaita/512x512/apps/utilities-terminal.pngTerminal]8;;
+Backgrounds: ]8;;file:///usr/share/backgrounds/gnome/Bokeh_Tails.jpgBokeh]8;; 
]8;;file:///usr/share/backgrounds/gnome/Chmiri.jpgChmiri]8;; 
]8;;file:///usr/share/backgrounds/gnome/Dark_Ivy.jpgIvy]8;; 
]8;;file:///usr/share/backgrounds/gnome/Flowerbed.jpgFlower]8;; 
]8;;file:///usr/share/backgrounds/gnome/Godafoss_Iceland.jpgIceland]8;; 
]8;;file:///usr/share/backgrounds/gnome/Icescape.jpgIcescape]8;; 
]8;;file:///usr/share/backgrounds/gnome/Mirror.jpgMirror]8;; 
]8;;file:///usr/share/backgrounds/gnome/Road.jpgRoad]8;; 
]8;;file:///usr/share/backgrounds/gnome/Sandstone.jpgSandstone]8;; 
]8;;file:///usr/share/backgrounds/gnome/Stones.jpgStones]8;; 
]8;;file:///usr/share/backgrounds/gnome/Waterfalls.jpgWaterfalls]8;; 
]8;;file:///usr/share/backgrounds/gnome/Waves.jpgWaves]8;;
+
+]8;;https://en.wikipedia.org/wiki/�Wiki page of � (unescaped raw Latin-1; invalid UTF-8)]8;;
+]8;;https://en.wikipedia.org/wiki/ÁWiki page of Á (unescaped raw UTF-8)]8;;
+]8;;https://en.wikipedia.org/wiki/%C3%81Wiki page of Á (escaped as %C3%81)]8;;
+]8;;https://en.wikipedia.org/wiki/%25Wiki page of % (escaped as %25)]8;;
+]8;;http://%d8%a7%d9%84%d9%85%d8%ba%d8%b1%d8%a8.icom.museumhttp://المغرب.icom.museum (with URI-escaped 
domain name)]8;;
+]8;;http://xn--4wa8awb4637h.orghttp://xn--4wa8awb4637h.org (Παν語.org)]8;;
+
+Two adjacent links pointing to the same URL:   
]8;;http://example.com/foofoo]8;;]8;;http://example.com/foofoo]8;;
+Two adjacent links pointing to different URLs: 
]8;;http://example.com/foofoo]8;;]8;;http://example.com/barbar]8;;
+
+The same two without closing the first link: 
]8;;http://example.com/foofoo]8;;http://example.com/foofoo]8;; 
]8;;http://example.com/foofoo]8;;http://example.com/barbar]8;;
+
+A URL wrapping to the next line, and a trailing whitespace: ]8;;http://example.com/foobarfoo
+bar ]8;;
+
+]8;;http://example.com/colorsMulti-colour
 link also tests that "\e[m" or "\e[0m" does not terminate the link]8;;
+
+Soft reset "\e[!p" resets attributes and terminates link: ]8;;http://example.com/softresetfoo[!pbar
+
+]8;;http://example.com/widthSome CJK and combining accents: 䀀䀁䀂ćĝm̃n̄o̅]8;;
+
+(Introducing the "under_score" character for even more fun)
+
+Explicit and implicit link: ]8;;http://example.com/under_scorehttp://example.com/under_score]8;;
+Explicit and implicit link with different targets: 
]8;;http://example.com/explicit_under_scorehttp://example.com/implicit_under_score]8;;
+Explicit and implicit link, broken into two lines: ]8;;http://example.com/under_scorehttp://examp
+le.com/under_score]8;;
+
+Explicitly underlined links ("\e[4m"):
+Explicit link only: ]8;;http://example.com/under_scoreI'm an explicit link with under_score]8;;
+Implicit link only: http://example.com/under_score
+Both:               ]8;;http://example.com/under_scorehttp://example.com/under_score]8;;
+
+Conflicting explicit and implicit links: 
http://example.com/foobar-]8;;http://example.com/explicitexplicit]8;;-rest
+
+Invisible explicit link: «]8;;http://example.com/invisibleCan you see me?]8;;»
+Invisible implicit link: «http://example.com/how_about_me»
+
+]8;;asdfghjklExplicit link with stupid target]8;;
+
+]8;;http://example.com/.........30........40........50........60........70........80........90.......100URL
 of 100 bytes]8;;
+]8;;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200URL
 of 200 bytes]8;;
+]8;;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500URL
 of 500 bytes]8;;
+]8;;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980....
 ...990......1000URL of 1000 bytes]8;;
+]8;;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980....
 
...990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500URL
 of 1500 bytes]8;;
+]8;;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980....
 
...990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......1720......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970...
 ...1980......1990......2000URL of 2000 bytes]8;;
+]8;;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980....
 
...990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......1720......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970...
 
...1980......1990......2000......2010......2020......2030......2040......2050......2060......2070......2080...URL
 of 2083 bytes]8;;
+]8;;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980....
 
...990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......1720......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970...
 
...1980......1990......2000......2010......2020......2030......2040......2050......2060......2070......2080....URL
 of 2084 bytes]8;;
+
+]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250;http://example.com/idID
 of 250 bytes once,]8;; 
]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250;http://example.com/idtwice]8;;
+]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.;http://example.com/idID
 of 251 bytes once,]8;; 
]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.;http://example.com/idtwice]8;;
+
+]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.
 
......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......1720
 
......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970......1980......1990......2000......2010......2020......2030......2040......2050......2060......2070......2080...ID
 of 250 bytes + URL of 2083 bytes]8;;
+]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730
 
.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......172
 
0......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970......1980......1990......2000......2010......2020......2030......2040......2050......2060......2070......2080...ID
 of 251 bytes + URL of 2083 bytes]8;;
+]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730.
 
......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......1720
 
......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970......1980......1990......2000......2010......2020......2030......2040......2050......2060......2070......2080....ID
 of 250 bytes + URL of 2084 bytes]8;;
+]8;id=........10........20........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.;http://example.com/.........30........40........50........60........70........80........90.......100.......110.......120.......130.......140.......150.......160.......170.......180.......190.......200.......210.......220.......230.......240.......250.......260.......270.......280.......290.......300.......310.......320.......330.......340.......350.......360.......370.......380.......390.......400.......410.......420.......430.......440.......450.......460.......470.......480.......490.......500.......510.......520.......530.......540.......550.......560.......570.......580.......590.......600.......610.......620.......630.......640.......650.......660.......670.......680.......690.......700.......710.......720.......730
 
.......740.......750.......760.......770.......780.......790.......800.......810.......820.......830.......840.......850.......860.......870.......880.......890.......900.......910.......920.......930.......940.......950.......960.......970.......980.......990......1000......1010......1020......1030......1040......1050......1060......1070......1080......1090......1100......1110......1120......1130......1140......1150......1160......1170......1180......1190......1200......1210......1220......1230......1240......1250......1260......1270......1280......1290......1300......1310......1320......1330......1340......1350......1360......1370......1380......1390......1400......1410......1420......1430......1440......1450......1460......1470......1480......1490......1500......1510......1520......1530......1540......1550......1560......1570......1580......1590......1600......1610......1620......1630......1640......1650......1660......1670......1680......1690......1700......1710......172
 
0......1730......1740......1750......1760......1770......1780......1790......1800......1810......1820......1830......1840......1850......1860......1870......1880......1890......1900......1910......1920......1930......1940......1950......1960......1970......1980......1990......2000......2010......2020......2030......2040......2050......2060......2070......2080....ID
 of 251 bytes + URL of 2084 bytes]8;;
+
+]8;;http://example.com/ST\ST (aka ESC \) instead of BEL]8;;\
+8;;http://example.com/C1œC1 (U+009D [UTF-8: 0xC2 0x9D] as OSC and U+009C [UTF-8: 0xC2 0x9C] as ST)8;;œ 
(note: not all terminal emulators support C1 in UTF-8)
+
+Cursor movement within the same OSC 8 run: ]8;;http://example.com/cursormoveright]8;;
+
+Alternating URIs, all with the same ID. Either all foos or all bars should be underlined on hover:
+]8;id=1;http://example.com/foofoo]8;;]8;id=1;http://example.com/barbar]8;;]8;foo=bar:id=1;http://example.com/foofoo]8;;]8;id=1;http://example.com/barbar]8;;]8;id=1:baz=quux;http://example.com/foofoo]8;;]8;id=1;http://example.com/barbar]8;;]8;foo=bar:id=1:baz=quux;http://example.com/foofoo]8;;
+
+Screenshot from an imaginary text editor:
+╔═ file1 ════╗
+║          ╔═ file2 ═══╗
+║]8;id=imaginary-text-editor-file1;http://example.comhttp://exa]8;;║Lorem ipsum║
+║]8;id=imaginary-text-editor-file1;http://example.comle.com]8;;    ║ dolor sit ║
+║          ║amet, conse║
+╚══════════║ctetur adip║
+           ╚═══════════╝
diff --git a/src/debug.c b/src/debug.c
index b8dc395..6fc9648 100644
--- a/src/debug.c
+++ b/src/debug.c
@@ -52,7 +52,8 @@ _vte_debug_init(void)
     { "widget-size",  VTE_DEBUG_WIDGET_SIZE  },
     { "style",        VTE_DEBUG_STYLE        },
     { "resize",       VTE_DEBUG_RESIZE       },
-    { "regex",        VTE_DEBUG_REGEX        }
+    { "regex",        VTE_DEBUG_REGEX        },
+    { "hyperlink",    VTE_DEBUG_HYPERLINK    },
   };
 
   _vte_debug_flags = g_parse_debug_string (g_getenv("VTE_DEBUG"),
diff --git a/src/debug.h b/src/debug.h
index c211942..13c8737 100644
--- a/src/debug.h
+++ b/src/debug.h
@@ -61,7 +61,8 @@ typedef enum {
        VTE_DEBUG_WIDGET_SIZE   = 1 << 21,
         VTE_DEBUG_STYLE         = 1 << 22,
        VTE_DEBUG_RESIZE        = 1 << 23,
-        VTE_DEBUG_REGEX         = 1 << 24
+        VTE_DEBUG_REGEX         = 1 << 24,
+        VTE_DEBUG_HYPERLINK     = 1 << 25,
 } VteDebugFlags;
 
 void _vte_debug_init(void);
diff --git a/src/marshal.list b/src/marshal.list
index 0276422..1e4d0c1 100644
--- a/src/marshal.list
+++ b/src/marshal.list
@@ -1,4 +1,5 @@
 VOID:INT,INT
 VOID:OBJECT,OBJECT
+VOID:STRING,BOXED
 VOID:STRING,UINT
 VOID:UINT,UINT
diff --git a/src/ring.cc b/src/ring.cc
index 1243f62..d91dc97 100644
--- a/src/ring.cc
+++ b/src/ring.cc
@@ -29,6 +29,8 @@
  * VteRing: A buffer ring
  */
 
+#define hyperlink_get(ring, idx) ((GString *) g_ptr_array_index((ring)->hyperlinks, (idx)))
+
 #ifdef VTE_DEBUG
 static void
 _vte_ring_validate (VteRing * ring)
@@ -53,6 +55,8 @@ _vte_ring_validate (VteRing * ring)
 void
 _vte_ring_init (VteRing *ring, gulong max_rows, gboolean has_streams)
 {
+        GString *empty_str;
+
        _vte_debug_print(VTE_DEBUG_RING, "New ring %p.\n", ring);
 
        memset (ring, 0, sizeof (*ring));
@@ -80,6 +84,14 @@ _vte_ring_init (VteRing *ring, gulong max_rows, gboolean has_streams)
 
         ring->visible_rows = 0;
 
+        ring->hyperlinks = g_ptr_array_new();
+        empty_str = g_string_new_len("", 0);
+        g_ptr_array_add(ring->hyperlinks, empty_str);
+        ring->hyperlink_highest_used_idx = 0;
+        ring->hyperlink_current_idx = 0;
+        ring->hyperlink_hover_idx = 0;
+        ring->hyperlink_maybe_gc_counter = 0;
+
        _vte_ring_validate(ring);
 }
 
@@ -101,6 +113,10 @@ _vte_ring_fini (VteRing *ring)
 
        g_string_free (ring->utf8_buffer, TRUE);
 
+        for (i = 0; i < ring->hyperlinks->len; i++)
+                g_string_free (hyperlink_get(ring, i), TRUE);
+        g_ptr_array_free (ring->hyperlinks, TRUE);
+
        _vte_row_data_fini (&ring->cached_row);
 }
 
@@ -118,6 +134,186 @@ typedef struct _VteCellTextOffset {
        gint eol_cells;       /* -1 if over a character, >=0 if at EOL or beyond */
 } VteCellTextOffset;
 
+
+static inline VteRowData *
+_vte_ring_writable_index (VteRing *ring, gulong position)
+{
+       return &ring->array[position & ring->mask];
+}
+
+
+#define SET_BIT(buf, n) buf[(n) / 8] |= (1 << ((n) % 8))
+#define GET_BIT(buf, n) ((buf[(n) / 8] >> ((n) % 8)) & 1)
+
+/*
+ * Do a round of garbage collection. Hyperlinks that no longer occur in the ring are wiped out.
+ */
+static void
+_vte_ring_hyperlink_gc (VteRing *ring)
+{
+        gulong i, j;
+        hyperlink_idx_t idx;
+        VteRowData *row;
+        char *used;
+
+        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                          "hyperlink: GC starting (highest used idx is %d)\n",
+                          ring->hyperlink_highest_used_idx);
+
+        ring->hyperlink_maybe_gc_counter = 0;
+
+        if (ring->hyperlink_highest_used_idx == 0) {
+                _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                                  "hyperlink: GC done (no links at all, nothing to do)\n");
+                return;
+        }
+
+        /* One bit for each idx to see if it's used. */
+        used = (char *) g_malloc0 (ring->hyperlink_highest_used_idx / 8 + 1);
+
+        /* A few special values not to be garbage collected. */
+        SET_BIT(used, ring->hyperlink_current_idx);
+        SET_BIT(used, ring->hyperlink_hover_idx);
+        SET_BIT(used, ring->last_attr.hyperlink_idx);
+
+        for (i = ring->writable; i < ring->end; i++) {
+                row = _vte_ring_writable_index (ring, i);
+                for (j = 0; j < row->len; j++) {
+                        idx = row->cells[j].attr.hyperlink_idx;
+                        SET_BIT(used, idx);
+                }
+        }
+
+        for (idx = 1; idx <= ring->hyperlink_highest_used_idx; idx++) {
+                if (!GET_BIT(used, idx) && hyperlink_get(ring, idx)->len != 0) {
+                        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                                          "hyperlink: GC purging link %d to id;uri=\"%s\"\n",
+                                          idx, hyperlink_get(ring, idx)->str);
+                        /* Wipe out the ID and URI itself so it doesn't linger on in the memory for a long 
time */
+                        memset(hyperlink_get(ring, idx)->str, 0, hyperlink_get(ring, idx)->len);
+                        g_string_truncate (hyperlink_get(ring, idx), 0);
+                }
+        }
+
+        while (ring->hyperlink_highest_used_idx >= 1 && hyperlink_get(ring, 
ring->hyperlink_highest_used_idx)->len == 0) {
+               ring->hyperlink_highest_used_idx--;
+        }
+
+        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                          "hyperlink: GC done (highest used idx is now %d)\n",
+                          ring->hyperlink_highest_used_idx);
+
+        g_free (used);
+}
+
+/*
+ * Cumulate the given value, and do a GC when 65536 is reached.
+ */
+void
+_vte_ring_hyperlink_maybe_gc (VteRing *ring, gulong increment)
+{
+        ring->hyperlink_maybe_gc_counter += increment;
+
+        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                          "hyperlink: maybe GC, counter at %ld\n",
+                          ring->hyperlink_maybe_gc_counter);
+
+        if (ring->hyperlink_maybe_gc_counter >= 65536)
+                _vte_ring_hyperlink_gc (ring);
+}
+
+/*
+ * Find existing idx for the hyperlink or allocate a new one.
+ *
+ * Returns 0 if given no hyperlink or an empty one, or if the pool is full.
+ * Returns the idx (either already existing or newly allocated) from 1 up to
+ * VTE_HYPERLINK_COUNT_MAX inclusive otherwise.
+ *
+ * FIXME do something more effective than a linear search
+ */
+static hyperlink_idx_t
+_vte_ring_get_hyperlink_idx_no_update_current (VteRing *ring, const char *hyperlink)
+{
+        hyperlink_idx_t idx;
+        gsize len;
+        GString *str;
+
+        if (!hyperlink || !hyperlink[0])
+                return 0;
+
+        len = strlen(hyperlink);
+
+        /* Linear search for this particular URI */
+        for (idx = 1; idx <= ring->hyperlink_highest_used_idx; idx++) {
+                if (strcmp(hyperlink_get(ring, idx)->str, hyperlink) == 0) {
+                        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                                          "get_hyperlink_idx: already existing idx %d for id;uri=\"%s\"\n",
+                                          idx, hyperlink);
+                        return idx;
+                }
+        }
+
+        /* FIXME it's the second time we're GCing if coming from _vte_ring_get_hyperlink_idx */
+        _vte_ring_hyperlink_gc(ring);
+
+        /* Another linear search for an empty slot where a GString is already allocated */
+        for (idx = 1; idx < ring->hyperlinks->len; idx++) {
+                if (hyperlink_get(ring, idx)->len == 0) {
+                        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                                          "get_hyperlink_idx: reassigning old idx %d for id;uri=\"%s\"\n",
+                                          idx, hyperlink);
+                        /* Grow size if required, however, never shrink to avoid long-term memory 
fragmentation. */
+                        g_string_append_len (hyperlink_get(ring, idx), hyperlink, len);
+                        ring->hyperlink_highest_used_idx = MAX (ring->hyperlink_highest_used_idx, idx);
+                        return idx;
+                }
+        }
+
+        /* All allocated slots are in use. Gotta allocate a new one */
+        g_assert_cmpuint(ring->hyperlink_highest_used_idx + 1, ==, ring->hyperlinks->len);
+
+        /* VTE_HYPERLINK_COUNT_MAX should be big enough for this not to happen under
+           normal circumstances. Anyway, it's cheap to protect against extreme ones. */
+        if (ring->hyperlink_highest_used_idx == VTE_HYPERLINK_COUNT_MAX) {
+                _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                                  "get_hyperlink_idx: idx 0 (ran out of available idxs) for id;uri=\"%s\"\n",
+                                  hyperlink);
+                return 0;
+        }
+
+        idx = ++ring->hyperlink_highest_used_idx;
+        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                          "get_hyperlink_idx: brand new idx %d for id;uri=\"%s\"\n",
+                          idx, hyperlink);
+        str = g_string_new_len (hyperlink, len);
+        g_ptr_array_add(ring->hyperlinks, str);
+
+        g_assert_cmpuint(ring->hyperlink_highest_used_idx + 1, ==, ring->hyperlinks->len);
+
+        return idx;
+}
+
+/*
+ * Find existing idx for the hyperlink or allocate a new one.
+ *
+ * Returns 0 if given no hyperlink or an empty one, or if the pool is full.
+ * Returns the idx (either already existing or newly allocated) from 1 up to
+ * VTE_HYPERLINK_COUNT_MAX inclusive otherwise.
+ *
+ * The current idx is also updated, in order not to be garbage collected.
+ */
+guint
+_vte_ring_get_hyperlink_idx (VteRing *ring, const char *hyperlink)
+{
+        /* Release current idx and do a round of GC to possibly purge its hyperlink,
+         * even if new hyperlink is NULL or empty. */
+        ring->hyperlink_current_idx = 0;
+        _vte_ring_hyperlink_gc(ring);
+
+        ring->hyperlink_current_idx = _vte_ring_get_hyperlink_idx_no_update_current(ring, hyperlink);
+        return ring->hyperlink_current_idx;
+}
+
 static gboolean
 _vte_ring_read_row_record (VteRing *ring, VteRowRecord *record, gulong position)
 {
@@ -136,7 +332,9 @@ _vte_ring_freeze_row (VteRing *ring, gulong position, const VteRowData *row)
        VteRowRecord record;
        VteCell *cell;
        GString *buffer = ring->utf8_buffer;
+        GString *hyperlink;
        int i;
+        gboolean froze_hyperlink = FALSE;
 
        _vte_debug_print (VTE_DEBUG_RING, "Freezing row %lu.\n", position);
 
@@ -166,28 +364,46 @@ _vte_ring_freeze_row (VteRing *ring, gulong position, const VteRowData *row)
                attr = cell->attr;
                if (G_LIKELY (!attr.fragment)) {
                        VteCellAttrChange attr_change;
+                        guint16 hyperlink_length;
 
                        if (memcmp(&ring->last_attr, &attr, sizeof (VteCellAttr)) != 0) {
                                ring->last_attr_text_start_offset = record.text_start_offset + buffer->len;
                                memset(&attr_change, 0, sizeof (attr_change));
                                attr_change.text_end_offset = ring->last_attr_text_start_offset;
-                               attr_change.attr = ring->last_attr;
+                                _attrcpy(&attr_change.attr, &ring->last_attr);
+                                hyperlink = hyperlink_get(ring, ring->last_attr.hyperlink_idx);
+                                attr_change.attr.hyperlink_length = hyperlink->len;
                                _vte_stream_append (ring->attr_stream, (const char *) &attr_change, sizeof 
(attr_change));
+                                if (G_UNLIKELY (hyperlink->len != 0)) {
+                                        _vte_stream_append (ring->attr_stream, hyperlink->str, 
hyperlink->len);
+                                        froze_hyperlink = TRUE;
+                                }
+                                hyperlink_length = attr_change.attr.hyperlink_length;
+                                _vte_stream_append (ring->attr_stream, (const char *) &hyperlink_length, 2);
                                if (!buffer->len)
                                        /* This row doesn't use last_attr, adjust */
-                                       record.attr_start_offset += sizeof (attr_change);
+                                        record.attr_start_offset += sizeof (attr_change) + hyperlink_length 
+ 2;
                                ring->last_attr = attr;
                        }
 
                        num_chars = _vte_unistr_strlen (cell->c);
                        if (num_chars > 1) {
+                                /* Combining chars */
                                attr.columns = 0;
                                ring->last_attr_text_start_offset = record.text_start_offset + buffer->len
                                                                  + g_unichar_to_utf8 (_vte_unistr_get_base 
(cell->c), NULL);
                                memset(&attr_change, 0, sizeof (attr_change));
                                attr_change.text_end_offset = ring->last_attr_text_start_offset;
-                               attr_change.attr = ring->last_attr;
+                                _attrcpy(&attr_change.attr, &ring->last_attr);
+                                hyperlink = hyperlink_get(ring, ring->last_attr.hyperlink_idx);
+                                attr_change.attr.hyperlink_length = hyperlink->len;
                                _vte_stream_append (ring->attr_stream, (const char *) &attr_change, sizeof 
(attr_change));
+                                if (G_UNLIKELY (hyperlink->len != 0)) {
+                                        _vte_stream_append (ring->attr_stream, hyperlink->str, 
hyperlink->len);
+                                        froze_hyperlink = TRUE;
+                                }
+                                hyperlink_length = attr_change.attr.hyperlink_length;
+                                _vte_stream_append (ring->attr_stream, (const char *) &hyperlink_length, 2);
                                ring->last_attr = attr;
                        }
 
@@ -201,10 +417,21 @@ _vte_ring_freeze_row (VteRing *ring, gulong position, const VteRowData *row)
 
        _vte_stream_append (ring->text_stream, buffer->str, buffer->len);
        _vte_ring_append_row_record (ring, &record, position);
+
+        /* After freezing some hyperlinks, do a hyperlink GC. The constant is totally arbitrary, feel free 
to fine tune. */
+        if (froze_hyperlink)
+                _vte_ring_hyperlink_maybe_gc(ring, 1024);
 }
 
+/* If do_truncate (data is placed back from the stream to the ring), real new hyperlink idxs are looked up 
or allocated.
+ *
+ * If !do_truncate (data is fetched only to be displayed), hyperlinked cells are given the pseudo idx 
VTE_HYPERLINK_IDX_TARGET_IN_STREAM,
+ * except for the hyperlink_hover_idx which gets this real idx. This is important for hover underlining.
+ *
+ * Optionally updates the hyperlink parameter to point to the ring-owned hyperlink target. */
 static void
-_vte_ring_thaw_row (VteRing *ring, gulong position, VteRowData *row, gboolean do_truncate)
+_vte_ring_thaw_row (VteRing *ring, gulong position, VteRowData *row, gboolean do_truncate,
+                    int hyperlink_column, const char **hyperlink)
 {
        VteRowRecord records[2], record;
        VteCellAttr attr;
@@ -212,6 +439,13 @@ _vte_ring_thaw_row (VteRing *ring, gulong position, VteRowData *row, gboolean do
        VteCell cell;
        const char *p, *q, *end;
        GString *buffer = ring->utf8_buffer;
+        char hyperlink_readbuf[VTE_HYPERLINK_TOTAL_LENGTH_MAX + 1];
+
+        hyperlink_readbuf[0] = '\0';
+        if (hyperlink) {
+                ring->hyperlink_buf[0] = '\0';
+                *hyperlink = ring->hyperlink_buf;
+        }
 
        _vte_debug_print (VTE_DEBUG_RING, "Thawing row %lu.\n", position);
 
@@ -243,20 +477,41 @@ _vte_ring_thaw_row (VteRing *ring, gulong position, VteRowData *row, gboolean do
        p = buffer->str;
        end = p + buffer->len;
        while (p < end) {
-
                if (record.text_start_offset >= ring->last_attr_text_start_offset) {
                        attr = ring->last_attr;
+                        strcpy(hyperlink_readbuf, hyperlink_get(ring, attr.hyperlink_idx)->str);
                } else {
                        if (record.text_start_offset >= attr_change.text_end_offset) {
                                if (!_vte_stream_read (ring->attr_stream, record.attr_start_offset, (char *) 
&attr_change, sizeof (attr_change)))
                                        return;
                                record.attr_start_offset += sizeof (attr_change);
+                                g_assert_cmpuint (attr_change.attr.hyperlink_length, <=, 
VTE_HYPERLINK_TOTAL_LENGTH_MAX);
+                                if (attr_change.attr.hyperlink_length && !_vte_stream_read 
(ring->attr_stream, record.attr_start_offset, hyperlink_readbuf, attr_change.attr.hyperlink_length))
+                                        return;
+                                hyperlink_readbuf[attr_change.attr.hyperlink_length] = '\0';
+                                record.attr_start_offset += attr_change.attr.hyperlink_length + 2;
+
+                                _attrcpy(&attr, &attr_change.attr);
+                                attr.hyperlink_idx = 0;
+                                if (G_UNLIKELY (attr_change.attr.hyperlink_length)) {
+                                        if (do_truncate) {
+                                                /* Find the existing idx or allocate a new one, just as when 
receiving an OSC 8 escape sequence.
+                                                 * Do not update the current idx though. */
+                                                attr.hyperlink_idx = 
_vte_ring_get_hyperlink_idx_no_update_current (ring, hyperlink_readbuf);
+                                        } else {
+                                                /* Use a special hyperlink idx, except if to be underlined 
because the hyperlink is the same as the hovered cell's. */
+                                                attr.hyperlink_idx = VTE_HYPERLINK_IDX_TARGET_IN_STREAM;
+                                                if (ring->hyperlink_hover_idx != 0 && 
strcmp(hyperlink_readbuf, hyperlink_get(ring, ring->hyperlink_hover_idx)->str) == 0) {
+                                                        /* FIXME here we're calling the expensive strcmp() 
above and _vte_ring_get_hyperlink_idx_no_update_current() way too many times. */
+                                                        attr.hyperlink_idx = 
_vte_ring_get_hyperlink_idx_no_update_current(ring, hyperlink_readbuf);
+                                                }
+                                        }
+                                }
                        }
-                       attr = attr_change.attr;
                }
 
                cell.attr = attr;
-                _VTE_DEBUG_IF(VTE_DEBUG_RING) {
+                _VTE_DEBUG_IF(VTE_DEBUG_RING | VTE_DEBUG_HYPERLINK) {
                         /* Debug: Reverse the colors for the stream's contents. */
                         if (!do_truncate) {
                                 cell.attr.reverse = !cell.attr.reverse;
@@ -274,38 +529,62 @@ _vte_ring_thaw_row (VteRing *ring, gulong position, VteRowData *row, gboolean do
                                row->cells[row->len - 1].c = _vte_unistr_append_unichar (row->cells[row->len 
- 1].c, cell.c);
                        } else {
                                cell.attr.columns = 1;
+                                if (row->len == hyperlink_column && hyperlink != NULL)
+                                        *hyperlink = strcpy(ring->hyperlink_buf, hyperlink_readbuf);
                                _vte_row_data_append (row, &cell);
                        }
                } else {
+                        if (row->len == hyperlink_column && hyperlink != NULL)
+                                *hyperlink = strcpy(ring->hyperlink_buf, hyperlink_readbuf);
                        _vte_row_data_append (row, &cell);
                        if (cell.attr.columns > 1) {
                                /* Add the fragments */
                                int i, columns = cell.attr.columns;
                                cell.attr.fragment = 1;
                                cell.attr.columns = 1;
-                               for (i = 1; i < columns; i++)
+                                for (i = 1; i < columns; i++) {
+                                        if (row->len == hyperlink_column && hyperlink != NULL)
+                                                *hyperlink = strcpy(ring->hyperlink_buf, hyperlink_readbuf);
                                        _vte_row_data_append (row, &cell);
+                                }
                        }
                }
        }
 
+        /* FIXME this is extremely complicated (by design), figure out something better.
+           This is the only place where we need to walk backwards in attr_stream,
+           which is the reason for the hyperlink's length being repeated after the hyperlink itself. */
        if (do_truncate) {
                gsize attr_stream_truncate_at = records[0].attr_start_offset;
                _vte_debug_print (VTE_DEBUG_RING, "Truncating\n");
                if (records[0].text_start_offset <= ring->last_attr_text_start_offset) {
                        /* Check the previous attr record. If its text ends where truncating, this attr 
record also needs to be removed. */
-                       if (_vte_stream_read (ring->attr_stream, attr_stream_truncate_at - sizeof 
(attr_change), (char *) &attr_change, sizeof (attr_change))) {
-                               if (records[0].text_start_offset == attr_change.text_end_offset) {
-                                       _vte_debug_print (VTE_DEBUG_RING, "... at attribute change\n");
-                                       attr_stream_truncate_at -= sizeof (attr_change);
+                        guint16 hyperlink_length;
+                        if (_vte_stream_read (ring->attr_stream, attr_stream_truncate_at - 2, (char *) 
&hyperlink_length, 2)) {
+                                g_assert_cmpuint (hyperlink_length, <=, VTE_HYPERLINK_TOTAL_LENGTH_MAX);
+                                if (_vte_stream_read (ring->attr_stream, attr_stream_truncate_at - 2 - 
hyperlink_length - sizeof (attr_change), (char *) &attr_change, sizeof (attr_change))) {
+                                        if (records[0].text_start_offset == attr_change.text_end_offset) {
+                                                _vte_debug_print (VTE_DEBUG_RING, "... at attribute 
change\n");
+                                                attr_stream_truncate_at -= sizeof (attr_change) + 
hyperlink_length + 2;
+                                        }
                                }
                        }
                        /* Reconstruct last_attr from the first record of attr_stream that we cut off,
                           last_attr_text_start_offset from the last record that we keep. */
                        if (_vte_stream_read (ring->attr_stream, attr_stream_truncate_at, (char *) 
&attr_change, sizeof (attr_change))) {
-                               ring->last_attr = attr_change.attr;
-                               if (_vte_stream_read (ring->attr_stream, attr_stream_truncate_at - sizeof 
(attr_change), (char *) &attr_change, sizeof (attr_change))) {
-                                       ring->last_attr_text_start_offset = attr_change.text_end_offset;
+                                _attrcpy(&ring->last_attr, &attr_change.attr);
+                                ring->last_attr.hyperlink_idx = 0;
+                                if (attr_change.attr.hyperlink_length && _vte_stream_read 
(ring->attr_stream, attr_stream_truncate_at + sizeof (attr_change), (char *) &hyperlink_readbuf, 
attr_change.attr.hyperlink_length)) {
+                                        hyperlink_readbuf[attr_change.attr.hyperlink_length] = '\0';
+                                        ring->last_attr.hyperlink_idx = _vte_ring_get_hyperlink_idx (ring, 
hyperlink_readbuf);
+                                }
+                                if (_vte_stream_read (ring->attr_stream, attr_stream_truncate_at - 2, (char 
*) &hyperlink_length, 2)) {
+                                        g_assert_cmpuint (hyperlink_length, <=, 
VTE_HYPERLINK_TOTAL_LENGTH_MAX);
+                                        if (_vte_stream_read (ring->attr_stream, attr_stream_truncate_at - 2 
- hyperlink_length - sizeof (attr_change), (char *) &attr_change, sizeof (attr_change))) {
+                                                ring->last_attr_text_start_offset = 
attr_change.text_end_offset;
+                                        } else {
+                                                ring->last_attr_text_start_offset = 0;
+                                        }
                                } else {
                                        ring->last_attr_text_start_offset = 0;
                                }
@@ -347,12 +626,6 @@ _vte_ring_reset (VteRing *ring)
         return ring->end;
 }
 
-static inline VteRowData *
-_vte_ring_writable_index (VteRing *ring, gulong position)
-{
-       return &ring->array[position & ring->mask];
-}
-
 const VteRowData *
 _vte_ring_index (VteRing *ring, gulong position)
 {
@@ -361,13 +634,69 @@ _vte_ring_index (VteRing *ring, gulong position)
 
        if (ring->cached_row_num != position) {
                _vte_debug_print(VTE_DEBUG_RING, "Caching row %lu.\n", position);
-               _vte_ring_thaw_row (ring, position, &ring->cached_row, FALSE);
+                _vte_ring_thaw_row (ring, position, &ring->cached_row, FALSE, -1, NULL);
                ring->cached_row_num = position;
        }
 
        return &ring->cached_row;
 }
 
+/*
+ * Returns the hyperlink idx at the given position.
+ *
+ * Updates the hyperlink parameter to point to the hyperlink's target.
+ * The buffer is owned by the ring and must not be modified by the caller.
+ *
+ * Optionally also updates the internal concept of the hovered idx. In this case,
+ * a real idx is looked up or newly allocated in the hyperlink pool even if the
+ * cell is scrolled out to the streams.
+ * This is to be able to underline all cells that share the same hyperlink.
+ *
+ * Otherwise cells from the stream might get the pseudo idx VTE_HYPERLINK_IDX_TARGET_IN_STREAM.
+ */
+hyperlink_idx_t
+_vte_ring_get_hyperlink_at_position (VteRing *ring, gulong position, int col, bool update_hover_idx, const 
char **hyperlink)
+{
+        hyperlink_idx_t idx;
+        const char *hp;
+
+        if (hyperlink == NULL)
+                hyperlink = &hp;
+        *hyperlink = NULL;
+
+        if (update_hover_idx) {
+                /* Invalidate the cache because new hover idx might result in new idxs to report. */
+                ring->cached_row_num = (gulong) -1;
+        }
+
+        if (G_UNLIKELY (position == (gulong) -1 || col == -1)) {
+                if (update_hover_idx)
+                        ring->hyperlink_hover_idx = 0;
+                return 0;
+        }
+
+        if (G_LIKELY (position >= ring->writable)) {
+                VteRowData *row = _vte_ring_writable_index (ring, position);
+                if (col >= _vte_row_data_length(row)) {
+                        if (update_hover_idx)
+                                ring->hyperlink_hover_idx = 0;
+                        return 0;
+                }
+                *hyperlink = hyperlink_get(ring, row->cells[col].attr.hyperlink_idx)->str;
+                idx = row->cells[col].attr.hyperlink_idx;
+        } else {
+                _vte_ring_thaw_row (ring, position, &ring->cached_row, FALSE, col, hyperlink);
+                /* Note: Intentionally don't set cached_row_num. We're about to update
+                 * ring->hyperlink_hover_idx which makes some idxs no longer valid. */
+                idx = _vte_ring_get_hyperlink_idx_no_update_current(ring, *hyperlink);
+        }
+        if (**hyperlink == '\0')
+                *hyperlink = NULL;
+        if (update_hover_idx)
+                ring->hyperlink_hover_idx = idx;
+        return idx;
+}
+
 static void _vte_ring_ensure_writable (VteRing *ring, gulong position);
 static void _vte_ring_ensure_writable_room (VteRing *ring);
 
@@ -408,7 +737,7 @@ _vte_ring_thaw_one_row (VteRing *ring)
 
        row = _vte_ring_writable_index (ring, ring->writable);
 
-       _vte_ring_thaw_row (ring, ring->writable, row, TRUE);
+        _vte_ring_thaw_row (ring, ring->writable, row, TRUE, -1, NULL);
 }
 
 static void
@@ -880,7 +1209,8 @@ _vte_ring_rewrap (VteRing *ring,
 
        attr_offset = old_record.attr_start_offset;
        if (!_vte_stream_read(ring->attr_stream, attr_offset, (char *) &attr_change, sizeof (attr_change))) {
-               attr_change.attr = ring->last_attr;
+                _attrcpy(&attr_change.attr, &ring->last_attr);
+                attr_change.attr.hyperlink_length = hyperlink_get(ring, ring->last_attr.hyperlink_idx)->len;
                attr_change.text_end_offset = _vte_stream_head (ring->text_stream);
        }
 
@@ -924,9 +1254,10 @@ _vte_ring_rewrap (VteRing *ring,
                /* Wrap the paragraph */
                if (attr_change.text_end_offset <= text_offset) {
                        /* Attr change at paragraph boundary, advance to next attr. */
-                       attr_offset += sizeof (attr_change);
+                        attr_offset += sizeof (attr_change) + attr_change.attr.hyperlink_length + 2;
                        if (!_vte_stream_read(ring->attr_stream, attr_offset, (char *) &attr_change, sizeof 
(attr_change))) {
-                               attr_change.attr = ring->last_attr;
+                                _attrcpy(&attr_change.attr, &ring->last_attr);
+                                attr_change.attr.hyperlink_length = hyperlink_get(ring, 
ring->last_attr.hyperlink_idx)->len;
                                attr_change.text_end_offset = _vte_stream_head (ring->text_stream);
                        }
                }
@@ -940,9 +1271,10 @@ _vte_ring_rewrap (VteRing *ring,
                        gsize runlength;  /* number of bytes we process in one run: identical attributes, 
within paragraph */
                        if (attr_change.text_end_offset <= text_offset) {
                                /* Attr change at line boundary, advance to next attr. */
-                               attr_offset += sizeof (attr_change);
+                                attr_offset += sizeof (attr_change) + attr_change.attr.hyperlink_length + 2;
                                if (!_vte_stream_read(ring->attr_stream, attr_offset, (char *) &attr_change, 
sizeof (attr_change))) {
-                                       attr_change.attr = ring->last_attr;
+                                        _attrcpy(&attr_change.attr, &ring->last_attr);
+                                        attr_change.attr.hyperlink_length = hyperlink_get(ring, 
ring->last_attr.hyperlink_idx)->len;
                                        attr_change.text_end_offset = _vte_stream_head (ring->text_stream);
                                }
                        }
diff --git a/src/ring.h b/src/ring.h
index b56d82e..fe7ba55 100644
--- a/src/ring.h
+++ b/src/ring.h
@@ -32,13 +32,15 @@
 G_BEGIN_DECLS
 
 
+typedef guint32 hyperlink_idx_t;
+
 typedef struct _VteVisualPosition {
        long row, col;
 } VteVisualPosition;
 
 typedef struct _VteCellAttrChange {
        gsize text_end_offset;  /* offset of first character no longer using this attr */
-       VteCellAttr attr;
+        VteStreamCellAttr attr;
 } VteCellAttrChange;
 
 
@@ -56,7 +58,20 @@ struct _VteRing {
        gulong writable, mask;
        VteRowData *array;
 
-       /* Storage */
+        /* Storage:
+         *
+         * row_stream contains records of VteRowRecord for each physical row.
+         * (This stream is regenerated when the contents rewrap on resize.)
+         *
+         * text_stream is the text in UTF-8.
+         *
+         * attr_stream contains entries that consist of:
+         *  - a VteCellAttrChange.
+         *  - a string of attr.hyperlink_length length containing the (typically empty) hyperlink data.
+         *    As far as the ring is concerned, this hyperlink data is opaque. Only the caller cares that
+         *    if nonempty, it actually contains the ID and URI separated with a semicolon. Not NUL 
terminated.
+         *  - 2 bytes repeating attr.hyperlink_length so that we can walk backwards.
+         */
        VteStream *attr_stream, *text_stream, *row_stream;
        gsize last_attr_text_start_offset;
        VteCellAttr last_attr;
@@ -67,6 +82,16 @@ struct _VteRing {
 
        gboolean has_streams;
         gulong visible_rows;  /* to keep at least a screenful of lines in memory, bug 646098 comment 12 */
+
+        GPtrArray *hyperlinks;  /* The hyperlink pool. Contains GString* items.
+                                   [0] points to an empty GString, [1] to [VTE_HYPERLINK_COUNT_MAX] contain 
the id;uri pairs. */
+        char hyperlink_buf[VTE_HYPERLINK_TOTAL_LENGTH_MAX + 1];  /* One more hyperlink buffer to get the 
value if it's not placed in the pool. */
+        hyperlink_idx_t hyperlink_highest_used_idx;  /* 0 if no hyperlinks at all in the pool. */
+        hyperlink_idx_t hyperlink_current_idx;  /* The hyperlink idx used for newly created cells.
+                                                   Must not be GC'd even if doesn't occur onscreen. */
+        hyperlink_idx_t hyperlink_hover_idx;  /* The hyperlink idx of the hovered cell.
+                                                 An idx is allocated on hover even if the cell is scrolled 
out to the streams. */
+        gulong hyperlink_maybe_gc_counter;  /* Do a GC when it reaches 65536. */
 };
 
 #define _vte_ring_contains(__ring, __position) \
@@ -81,6 +106,9 @@ VteRowData *_vte_ring_index_writable (VteRing *ring, gulong position);
 
 void _vte_ring_init (VteRing *ring, gulong max_rows, gboolean has_streams);
 void _vte_ring_fini (VteRing *ring);
+void _vte_ring_hyperlink_maybe_gc (VteRing *ring, gulong increment);
+hyperlink_idx_t _vte_ring_get_hyperlink_idx (VteRing *ring, const char *hyperlink);
+hyperlink_idx_t _vte_ring_get_hyperlink_at_position (VteRing *ring, gulong position, int col, bool 
update_hover_idx, const char **hyperlink);
 long _vte_ring_reset (VteRing *ring);
 void _vte_ring_resize (VteRing *ring, gulong max_rows);
 void _vte_ring_shrink (VteRing *ring, gulong max_len);
diff --git a/src/vte.cc b/src/vte.cc
index 9ea0b03..3cb8343 100644
--- a/src/vte.cc
+++ b/src/vte.cc
@@ -264,9 +264,12 @@ VteTerminalPrivate::ring_remove(vte::grid::row_t position)
 
 /* Reset defaults for character insertion. */
 void
-VteTerminalPrivate::reset_default_attributes()
+VteTerminalPrivate::reset_default_attributes(bool reset_hyperlink)
 {
+        hyperlink_idx_t hyperlink_idx_save = m_defaults.attr.hyperlink_idx;
         m_defaults = m_color_defaults = m_fill_defaults = basic_cell;
+        if (!reset_hyperlink)
+                m_defaults.attr.hyperlink_idx = hyperlink_idx_save;
 }
 
 //FIXMEchpe this function is bad
@@ -919,6 +922,18 @@ VteTerminalPrivate::emit_paste_clipboard()
        g_signal_emit(m_terminal, signals[SIGNAL_PASTE_CLIPBOARD], 0);
 }
 
+/* Emit a "hyperlink_hover_uri_changed" signal. */
+void
+VteTerminalPrivate::emit_hyperlink_hover_uri_changed(const GdkRectangle *bbox)
+{
+        GObject *object = G_OBJECT(m_terminal);
+
+        _vte_debug_print(VTE_DEBUG_SIGNALS,
+                         "Emitting `hyperlink-hover-uri-changed'.\n");
+        g_signal_emit(m_terminal, signals[SIGNAL_HYPERLINK_HOVER_URI_CHANGED], 0, m_hyperlink_hover_uri, 
bbox);
+        g_object_notify_by_pspec(object, pspecs[PROP_HYPERLINK_HOVER_URI]);
+}
+
 void
 VteTerminalPrivate::deselect_all()
 {
@@ -1802,6 +1817,32 @@ VteTerminalPrivate::rowcol_from_event(GdkEvent *event,
 }
 
 char *
+VteTerminalPrivate::hyperlink_check(GdkEvent *event)
+{
+        long col, row;
+        const char *hyperlink;
+        const char *separator;
+
+        if (!m_allow_hyperlink || !rowcol_from_event(event, &col, &row))
+                return NULL;
+
+        _vte_ring_get_hyperlink_at_position(m_screen->row_data, row, col, false, &hyperlink);
+
+        if (hyperlink != NULL) {
+                /* URI is after the first semicolon */
+                separator = strchr(hyperlink, ';');
+                g_assert(separator != NULL);
+                hyperlink = separator + 1;
+        }
+
+        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                          "hyperlink_check: \"%s\"\n",
+                          hyperlink);
+
+        return g_strdup(hyperlink);
+}
+
+char *
 VteTerminalPrivate::regex_match_check(GdkEvent *event,
                                       int *tag)
 {
@@ -2258,7 +2299,11 @@ VteTerminalPrivate::apply_mouse_cursor()
                 return;
 
         if (m_mouse_cursor_visible) {
-                if ((guint)m_match_tag < m_match_regexes->len) {
+                if (m_hyperlink_hover_idx != 0) {
+                        _vte_debug_print(VTE_DEBUG_CURSOR,
+                                        "Setting hyperlink mouse cursor.\n");
+                        gdk_window_set_cursor(m_event_window, m_mouse_hyperlink_cursor);
+                } else if ((guint)m_match_tag < m_match_regexes->len) {
                         struct vte_match_regex *regex =
                                 &g_array_index(m_match_regexes,
                                               struct vte_match_regex,
@@ -3854,6 +3899,9 @@ next_match:
        /* Tell the input method where the cursor is. */
         im_update_cursor();
 
+        /* After processing some data, do a hyperlink GC. The multiplier is totally arbitrary, feel free to 
fine tune. */
+        _vte_ring_hyperlink_maybe_gc(m_screen->row_data, wcount * 4);
+
        _vte_debug_print (VTE_DEBUG_WORK, ")");
        _vte_debug_print (VTE_DEBUG_IO,
                        "%ld chars and %ld bytes in %" G_GSIZE_FORMAT " chunks left to process.\n",
@@ -5488,6 +5536,145 @@ VteTerminalPrivate::maybe_send_mouse_drag(vte::grid::coords const& unconfined_ro
 }
 
 /*
+ * VteTerminalPrivate::hyperlink_invalidate_and_get_bbox
+ *
+ * Invalidates cells belonging to the non-zero hyperlink idx, in order to
+ * stop highlighting the previously hovered hyperlink or start highlighting
+ * the new one. Optionally stores the coordinates of the bounding box.
+ */
+void
+VteTerminalPrivate::hyperlink_invalidate_and_get_bbox(hyperlink_idx_t idx, GdkRectangle *bbox)
+{
+        auto first_row = first_displayed_row();
+        auto end_row = last_displayed_row() + 1;
+        vte::grid::row_t row, top = LONG_MAX, bottom = -1;
+        vte::grid::column_t col, left = LONG_MAX, right = -1;
+        const VteRowData *rowdata;
+
+        g_assert (idx != 0);
+
+        for (row = first_row; row < end_row; row++) {
+                rowdata = _vte_ring_index(m_screen->row_data, row);
+                if (rowdata != NULL) {
+                        for (col = 0; col < rowdata->len; col++) {
+                                if (G_UNLIKELY (rowdata->cells[col].attr.hyperlink_idx == idx)) {
+                                        invalidate_cells(col, 1, row, 1);
+                                        top = MIN(top, row);
+                                        bottom = MAX(bottom, row);
+                                        left = MIN(left, col);
+                                        right = MAX(right, col);
+                                }
+                        }
+                }
+        }
+
+        if (bbox == NULL)
+                return;
+
+        /* If bbox != NULL, we're looking for the new hovered hyperlink which always has onscreen bits. */
+        g_assert (top != LONG_MAX && bottom != -1 && left != LONG_MAX && right != -1);
+
+        auto allocation = get_allocated_rect();
+        bbox->x = allocation.x + m_padding.left + left * m_char_width;
+        bbox->y = allocation.y + m_padding.top + row_to_pixel(top);
+        bbox->width = (right - left + 1) * m_char_width;
+        bbox->height = (bottom - top + 1) * m_char_height;
+        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                          "Hyperlink bounding box: x=%d y=%d w=%d h=%d\n",
+                          bbox->x, bbox->y, bbox->width, bbox->height);
+}
+
+/*
+ * VteTerminalPrivate::hyperlink_hilite_update:
+ *
+ * Checks the coordinates for hyperlink. Updates m_hyperlink_hover_idx
+ * and m_hyperlink_hover_uri, and schedules to update the highlighting.
+ */
+void
+VteTerminalPrivate::hyperlink_hilite_update(vte::view::coords const& pos)
+{
+        const VteRowData *rowdata;
+        hyperlink_idx_t new_hyperlink_hover_idx = 0;
+        GdkRectangle bbox;
+        const char *separator;
+
+        if (!m_allow_hyperlink)
+                return;
+
+        glong col = pos.x / m_char_width;
+        glong row = pixel_to_row(pos.y);
+
+        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                         "hyperlink_hilite_update\n");
+
+        rowdata = find_row_data(row);
+        if (rowdata && col < rowdata->len) {
+                new_hyperlink_hover_idx = rowdata->cells[col].attr.hyperlink_idx;
+        }
+        if (new_hyperlink_hover_idx == m_hyperlink_hover_idx) {
+                _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                                  "hyperlink did not change\n");
+                return;
+        }
+
+        /* Invalidate cells of the old hyperlink. */
+        if (m_hyperlink_hover_idx != 0) {
+                hyperlink_invalidate_and_get_bbox(m_hyperlink_hover_idx, NULL);
+        }
+
+        /* This might be different from new_hyperlink_hover_idx. If in the stream, that one contains
+         * the pseudo idx VTE_HYPERLINK_IDX_TARGET_IN_STREAM and now a real idx is allocated.
+         * Plus, the ring's internal belief of the hovered hyperlink is also updated. */
+        m_hyperlink_hover_idx = _vte_ring_get_hyperlink_at_position(m_screen->row_data, row, col, true, 
&m_hyperlink_hover_uri);
+
+        /* Invalidate cells of the new hyperlink. Get the bounding box. */
+        if (m_hyperlink_hover_idx != 0) {
+                /* URI is after the first semicolon */
+                separator = strchr(m_hyperlink_hover_uri, ';');
+                g_assert(separator != NULL);
+                m_hyperlink_hover_uri = separator + 1;
+
+                hyperlink_invalidate_and_get_bbox(m_hyperlink_hover_idx, &bbox);
+                g_assert(bbox.width > 0 && bbox.height > 0);
+        }
+        _vte_debug_print(VTE_DEBUG_HYPERLINK,
+                         "Hover idx: %d \"%s\"\n",
+                         m_hyperlink_hover_idx,
+                         m_hyperlink_hover_uri);
+
+        /* Underlining hyperlinks has precedence over regex matches. So when the hovered hyperlink changes,
+         * the regex match might need to become or stop being underlined. */
+        invalidate_match_span();
+
+        apply_mouse_cursor();
+
+        emit_hyperlink_hover_uri_changed(m_hyperlink_hover_idx != 0 ? &bbox : NULL);
+}
+
+/*
+ * VteTerminalPrivate::hyperlink_hilite:
+ *
+ * If the mouse moved to a new cell, updates the hyperlinks via hyperlink_hilite_update().
+ */
+void
+VteTerminalPrivate::hyperlink_hilite(vte::view::coords const& pos)
+{
+        /* if the cursor is not above a cell, skip */
+        if (!view_coords_visible(pos))
+                return;
+
+        /* If the pointer hasn't moved to another character cell, then we
+         * need do nothing. Note: Don't use mouse_last_row as that's relative
+         * to insert_delta, and we care about the absolute row number. */
+        if (grid_coords_from_view_coords(pos) ==
+             confined_grid_coords_from_view_coords(m_mouse_last_position)) {
+                return;
+        }
+
+       hyperlink_hilite_update(pos);
+}
+
+/*
  * VteTerminalPrivate::match_hilite_clear:
  *
  * Reset match variables and invalidate the old match region if highlighted.
@@ -5992,7 +6179,8 @@ vte_terminal_cellattr_equal(VteCellAttr const *attr1,
                attr1->strikethrough == attr2->strikethrough &&
                attr1->reverse       == attr2->reverse   &&
                attr1->blink         == attr2->blink     &&
-               attr1->invisible     == attr2->invisible);
+                attr1->invisible     == attr2->invisible &&
+                attr1->hyperlink_idx  == attr2->hyperlink_idx);
 }
 
 /*
@@ -6903,6 +7091,7 @@ VteTerminalPrivate::widget_motion_notify(GdkEventMotion *event)
                match_hilite_hide();
        } else if (pos != m_mouse_last_position) {
                /* Hilite any matches. */
+                hyperlink_hilite(pos);
                match_hilite(pos);
                /* Show the cursor. */
                 set_pointer_autohidden(false);
@@ -6963,6 +7152,7 @@ VteTerminalPrivate::widget_button_press(GdkEventButton *event)
         auto pos = view_coords_from_event(base_event);
         auto rowcol = grid_coords_from_view_coords(pos);
 
+        hyperlink_hilite(pos);
        match_hilite(pos);
 
         set_pointer_autohidden(false);
@@ -7113,6 +7303,7 @@ VteTerminalPrivate::widget_button_release(GdkEventButton *event)
         auto pos = view_coords_from_event(base_event);
         auto rowcol = grid_coords_from_view_coords(pos);
 
+        hyperlink_hilite(pos);
        match_hilite(pos);
 
         set_pointer_autohidden(false);
@@ -7804,7 +7995,7 @@ VteTerminalPrivate::VteTerminalPrivate(VteTerminal *t) :
        _vte_ring_init (m_normal_screen.row_data, VTE_SCROLLBACK_INIT, TRUE);
        m_screen = &m_normal_screen;
 
-       reset_default_attributes();
+        reset_default_attributes(true);
 
         /* Initialize charset modes. */
         m_character_replacements[0] = VTE_CHARACTER_REPLACEMENT_NONE;
@@ -7916,6 +8107,9 @@ VteTerminalPrivate::VteTerminalPrivate(VteTerminal *t) :
         m_font_scale = 1.;
        m_has_fonts = FALSE;
 
+        m_allow_hyperlink = FALSE;
+        m_hyperlink_auto_id = 0;
+
        /* Not all backends generate GdkVisibilityNotify, so mark the
         * window as unobscured initially. */
        m_visibility_state = GDK_VISIBILITY_UNOBSCURED;
@@ -8055,6 +8249,8 @@ VteTerminalPrivate::widget_unrealize()
        m_mouse_default_cursor = NULL;
        g_object_unref(m_mouse_mousing_cursor);
        m_mouse_mousing_cursor = NULL;
+        g_object_unref(m_mouse_hyperlink_cursor);
+        m_mouse_hyperlink_cursor = NULL;
        g_object_unref(m_mouse_inviso_cursor);
        m_mouse_inviso_cursor = NULL;
 
@@ -8352,6 +8548,11 @@ VteTerminalPrivate::widget_realize()
         m_mouse_cursor_over_widget = TRUE;  /* FIXME figure out the actual value, although it's safe to err 
in this direction */
        m_mouse_default_cursor = widget_cursor_new(VTE_DEFAULT_CURSOR);
        m_mouse_mousing_cursor = widget_cursor_new(VTE_MOUSING_CURSOR);
+        if (_vte_debug_on(VTE_DEBUG_HYPERLINK))
+                /* Differ from the standard regex match cursor in debug mode. */
+                m_mouse_hyperlink_cursor = widget_cursor_new(VTE_HYPERLINK_CURSOR_DEBUG);
+        else
+                m_mouse_hyperlink_cursor = widget_cursor_new(VTE_HYPERLINK_CURSOR);
        m_mouse_inviso_cursor = widget_cursor_new(GDK_BLANK_CURSOR);
 
        /* Create a GDK window for the widget. */
@@ -8569,6 +8770,7 @@ VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
                                bool italic,
                                bool underline,
                                bool strikethrough,
+                               bool hyperlink,
                                bool hilite,
                                bool boxed,
                                int column_width,
@@ -8587,9 +8789,11 @@ VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
                }
                tmp = g_string_free (str, FALSE);
                g_printerr ("draw_cells('%s', fore=%d, back=%d, bold=%d,"
-                               " ul=%d, strike=%d, hilite=%d, boxed=%d)\n",
+                                " ul=%d, strike=%d,"
+                                " hyperlink=%d, hilite=%d, boxed=%d)\n",
                                tmp, fore, back, bold,
-                               underline, strikethrough, hilite, boxed);
+                                underline, strikethrough,
+                                hyperlink, hilite, boxed);
                g_free (tmp);
        }
 
@@ -8622,7 +8826,7 @@ VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
                        _vte_draw_get_style(bold, italic));
 
        /* Draw whatever SFX are required. */
-       if (underline | strikethrough | hilite | boxed) {
+        if (underline | strikethrough | hyperlink | hilite | boxed) {
                i = 0;
                do {
                        x = items[i].x;
@@ -8656,7 +8860,16 @@ VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
                                                        y + row_height - 1,
                                                        VTE_LINE_WIDTH,
                                                        &fg, VTE_DRAW_OPAQUE);
-                       }
+                        } else if (hyperlink) {
+                                for (double j = 0.125; j < columns; j += 0.5) {
+                                        _vte_draw_fill_rectangle(m_draw,
+                                                                 x + j * column_width,
+                                                                 y + row_height - 1,
+                                                                 column_width * 0.25,
+                                                                 1,
+                                                                 &fg, VTE_DRAW_OPAQUE);
+                                }
+                        }
                        if (boxed) {
                                 _vte_draw_draw_rectangle(m_draw,
                                                x, y,
@@ -8887,6 +9100,7 @@ VteTerminalPrivate::draw_cells_with_attributes(struct _vte_draw_text_request *it
                                        cells[j].attr.italic,
                                        cells[j].attr.underline,
                                        cells[j].attr.strikethrough,
+                                        m_allow_hyperlink && cells[j].attr.hyperlink_idx != 0,
                                        FALSE, FALSE, column_width, height);
                j += g_unichar_to_utf8(items[i].c, scratch_buf);
        }
@@ -8913,7 +9127,8 @@ VteTerminalPrivate::draw_rows(VteScreen *screen_,
         vte::grid::column_t i, j;
         long x, y;
        guint fore, nfore, back, nback;
-       gboolean underline, nunderline, bold, nbold, italic, nitalic, hilite, nhilite,
+        gboolean underline, nunderline, bold, nbold, italic, nitalic,
+                 hyperlink, nhyperlink, hilite, nhilite,
                 selected, nselected, strikethrough, nstrikethrough;
        guint item_count;
        const VteCell *cell;
@@ -9048,7 +9263,8 @@ VteTerminalPrivate::draw_rows(VteScreen *screen_,
                        while (cell->c == 0 || cell->attr.invisible ||
                                        (cell->c == ' ' &&
                                         !cell->attr.underline &&
-                                        !cell->attr.strikethrough) ||
+                                         !cell->attr.strikethrough &&
+                                         (!m_allow_hyperlink || cell->attr.hyperlink_idx == 0)) ||
                                        cell->attr.fragment) {
                                if (++i >= end_column) {
                                        goto fg_skip_row;
@@ -9063,9 +9279,12 @@ VteTerminalPrivate::draw_rows(VteScreen *screen_,
                        determine_colors(cell, selected, &fore, &back);
                        underline = cell->attr.underline;
                        strikethrough = cell->attr.strikethrough;
+                        hyperlink = (m_allow_hyperlink && cell->attr.hyperlink_idx != 0);
                        bold = cell->attr.bold;
                        italic = cell->attr.italic;
-                       if (m_show_match) {
+                        if (cell->attr.hyperlink_idx != 0 && cell->attr.hyperlink_idx == 
m_hyperlink_hover_idx) {
+                                hilite = true;
+                        } else if (m_hyperlink_hover_idx == 0 && m_show_match) {
                                hilite = m_match_span.contains(row, i);
                        } else {
                                hilite = false;
@@ -9097,7 +9316,7 @@ VteTerminalPrivate::draw_rows(VteScreen *screen_,
                                                /* only break the run if we
                                                 * are drawing attributes
                                                 */
-                                               if (underline || strikethrough || hilite) {
+                                                if (underline || strikethrough || hyperlink || hilite) {
                                                        break;
                                                } else {
                                                        j++;
@@ -9129,9 +9348,15 @@ VteTerminalPrivate::draw_rows(VteScreen *screen_,
                                        if (nstrikethrough != strikethrough) {
                                                break;
                                        }
+                                        nhyperlink = (m_allow_hyperlink && cell->attr.hyperlink_idx != 0);
+                                        if (nhyperlink != hyperlink) {
+                                                break;
+                                        }
                                        /* Break up matched/not-matched text. */
                                        nhilite = false;
-                                       if (m_show_match) {
+                                        if (cell->attr.hyperlink_idx != 0 && cell->attr.hyperlink_idx == 
m_hyperlink_hover_idx) {
+                                                nhilite = true;
+                                        } else if (m_hyperlink_hover_idx == 0 && m_show_match) {
                                                nhilite = m_match_span.contains(row, j);
                                        }
                                        if (nhilite != hilite) {
@@ -9180,7 +9405,7 @@ fg_draw:
                                        item_count,
                                        fore, back, FALSE, FALSE,
                                        bold, italic, underline,
-                                       strikethrough, hilite, FALSE,
+                                        strikethrough, hyperlink, hilite, FALSE,
                                        column_width, row_height);
                        item_count = 1;
                        /* We'll need to continue at the first cell which didn't
@@ -9385,6 +9610,7 @@ VteTerminalPrivate::paint_cursor()
                                                         cell->attr.italic,
                                                         cell->attr.underline,
                                                         cell->attr.strikethrough,
+                                                        m_allow_hyperlink && cell->attr.hyperlink_idx != 0,
                                                         FALSE,
                                                         FALSE,
                                                         width,
@@ -9470,6 +9696,7 @@ VteTerminalPrivate::paint_im_preedit_string()
                                                FALSE,
                                                FALSE,
                                                FALSE,
+                                                FALSE,
                                                FALSE,
                                                TRUE,
                                                width, height);
@@ -9743,6 +9970,27 @@ VteTerminalPrivate::set_allow_bold(bool setting)
 }
 
 bool
+VteTerminalPrivate::set_allow_hyperlink(bool setting)
+{
+        if (setting == m_allow_hyperlink)
+                return false;
+
+        if (setting == false) {
+                m_hyperlink_hover_idx = _vte_ring_get_hyperlink_at_position(m_screen->row_data, -1, -1, 
true, NULL);
+                g_assert (m_hyperlink_hover_idx == 0);
+                m_hyperlink_hover_uri = NULL;
+                emit_hyperlink_hover_uri_changed(NULL);  /* FIXME only emit if really changed */
+                m_defaults.attr.hyperlink_idx = _vte_ring_get_hyperlink_idx(m_screen->row_data, NULL);
+                g_assert (m_defaults.attr.hyperlink_idx == 0);
+        }
+
+        m_allow_hyperlink = setting;
+        invalidate_all();
+
+        return true;
+}
+
+bool
 VteTerminalPrivate::set_scroll_on_output(bool scroll)
 {
         if (scroll == m_scroll_on_output)
@@ -10032,7 +10280,7 @@ VteTerminalPrivate::reset(bool clear_tabstops,
                m_palette[i].sources[VTE_COLOR_SOURCE_ESCAPE].is_set = FALSE;
        /* Reset the default attributes.  Reset the alternate attribute because
         * it's not a real attribute, but we need to treat it as one here. */
-       reset_default_attributes();
+        reset_default_attributes(true);
         /* Reset charset modes. */
         m_character_replacements[0] = VTE_CHARACTER_REPLACEMENT_NONE;
         m_character_replacements[1] = VTE_CHARACTER_REPLACEMENT_NONE;
@@ -10415,9 +10663,10 @@ VteTerminalPrivate::emit_pending_signals()
                 m_text_deleted_flag = false;
        }
        if (m_contents_changed_pending) {
-               /* Update dingus match set. */
+                /* Update hyperlink and dingus match set. */
                match_contents_clear();
                if (m_mouse_cursor_visible) {
+                        hyperlink_hilite_update(m_mouse_last_position);
                        match_hilite_update(m_mouse_last_position);
                }
 
diff --git a/src/vte/vteterminal.h b/src/vte/vteterminal.h
index 3b22c4a..40cdc4f 100644
--- a/src/vte/vteterminal.h
+++ b/src/vte/vteterminal.h
@@ -289,6 +289,12 @@ void vte_terminal_set_allow_bold(VteTerminal *terminal,
 _VTE_PUBLIC
 gboolean vte_terminal_get_allow_bold(VteTerminal *terminal) _VTE_GNUC_NONNULL(1);
 
+_VTE_PUBLIC
+void vte_terminal_set_allow_hyperlink(VteTerminal *terminal,
+                                      gboolean allow_hyperlink) _VTE_GNUC_NONNULL(1);
+_VTE_PUBLIC
+gboolean vte_terminal_get_allow_hyperlink(VteTerminal *terminal) _VTE_GNUC_NONNULL(1);
+
 /* Check if the terminal is the current selection owner. */
 _VTE_PUBLIC
 gboolean vte_terminal_get_has_selection(VteTerminal *terminal) _VTE_GNUC_NONNULL(1);
@@ -342,6 +348,10 @@ void vte_terminal_get_cursor_position(VteTerminal *terminal,
                                      glong *column,
                                       glong *row) _VTE_GNUC_NONNULL(1);
 
+_VTE_PUBLIC
+char *vte_terminal_hyperlink_check_event(VteTerminal *terminal,
+                                         GdkEvent *event) _VTE_GNUC_NONNULL(1) _VTE_GNUC_NONNULL(2) 
G_GNUC_MALLOC;
+
 /* Add a matching expression, returning the tag the widget assigns to that
  * expression. */
 _VTE_PUBLIC
diff --git a/src/vteapp.c b/src/vteapp.c
index ac321be..865df64 100644
--- a/src/vteapp.c
+++ b/src/vteapp.c
@@ -143,6 +143,7 @@ button_pressed(GtkWidget *widget, GdkEventButton *event, gpointer data)
 {
        VteTerminal *terminal;
        char *match;
+        char *hyperlink;
        int tag;
         gboolean has_extra_match;
         char *extra_match = NULL;
@@ -151,6 +152,14 @@ button_pressed(GtkWidget *widget, GdkEventButton *event, gpointer data)
        case 3:
                terminal = VTE_TERMINAL(widget);
 
+                hyperlink = vte_terminal_hyperlink_check_event(terminal,
+                                                               (GdkEvent*)event);
+                if (hyperlink)
+                        g_print("Hyperlink: %s\n", hyperlink);
+                else
+                        g_print("Not hyperlink\n");
+                g_free(hyperlink);
+
                match = vte_terminal_match_check_event(terminal,
                                                        (GdkEvent*)event,
                                                        &tag);
@@ -621,7 +630,8 @@ main(int argc, char **argv)
                 icon_title = FALSE, shell = TRUE,
                 reverse = FALSE, use_geometry_hints = TRUE,
                 use_scrolled_window = FALSE,
-                show_object_notifications = FALSE, rewrap = TRUE;
+                show_object_notifications = FALSE, rewrap = TRUE,
+                hyperlink = TRUE;
        char *geometry = NULL;
        gint lines = -1;
        const char *message = "Launching interactive shell...\r\n";
@@ -661,6 +671,11 @@ main(int argc, char **argv)
                        G_OPTION_ARG_NONE, &use_gregex,
                        "Use GRegex instead of PCRE2", NULL
                },
+                {
+                        "no-hyperlink", 'H', G_OPTION_FLAG_REVERSE,
+                        G_OPTION_ARG_NONE, &hyperlink,
+                        "Disable hyperlinks inside the terminal", NULL
+                },
                {
                        "no-rewrap", 'R', G_OPTION_FLAG_REVERSE,
                        G_OPTION_ARG_NONE, &rewrap,
@@ -1060,6 +1075,8 @@ main(int argc, char **argv)
                 pango_font_description_free(desc);
         }
 
+        vte_terminal_set_allow_hyperlink(terminal, hyperlink);
+
        /* Match "abcdefg". */
        if (!no_builtin_dingus) {
                 add_dingus (terminal, (char **) builtin_dingus);
diff --git a/src/vtedefines.hh b/src/vtedefines.hh
index 7f56291..3afc532 100644
--- a/src/vtedefines.hh
+++ b/src/vtedefines.hh
@@ -66,6 +66,8 @@
 #define VTE_SCROLLBACK_INIT            512
 #define VTE_DEFAULT_CURSOR             GDK_XTERM
 #define VTE_MOUSING_CURSOR             GDK_LEFT_PTR
+#define VTE_HYPERLINK_CURSOR           GDK_HAND2
+#define VTE_HYPERLINK_CURSOR_DEBUG     GDK_SPIDER
 #define VTE_TAB_MAX                    999
 #define VTE_ADJUSTMENT_PRIORITY                G_PRIORITY_DEFAULT_IDLE
 #define VTE_INPUT_RETRY_PRIORITY       G_PRIORITY_HIGH
@@ -93,3 +95,31 @@
 
 #define VTE_FONT_SCALE_MIN (.25)
 #define VTE_FONT_SCALE_MAX (4.)
+
+/* Maximum length of a URI in the OSC 8 escape sequences. There's no de jure limit,
+ * 2000-ish the de facto standard, and Internet Explorer supports 2083.
+ * See also the comment of VTE_HYPERLINK_TOTAL_LENGTH_MAX. */
+#define VTE_HYPERLINK_URI_LENGTH_MAX    2083
+
+/* Maximum number of URIs in the ring for a given screen (as in "normal" vs "alternate" screen)
+ * at a time. Idx 0 is a placeholder for no hyperlink, URIs have indexes from 1 to
+ * VTE_HYPERLINK_COUNT_MAX inclusive, plus one more technical idx is also required, see below.
+ * This is just a safety cap because the number of URIs is bound by the number of cells in the ring
+ * (excluding the stream) which should be way lower than this at sane window sizes.
+ * Make sure there are enough bits to store them in VteCellAttr.hyperlink_idx.
+ * Also make sure _vte_ring_hyperlink_gc() can allocate a large enough bitmap. */
+#define VTE_HYPERLINK_COUNT_MAX         ((1 << 20) - 2)
+
+/* Used when thawing a row from the stream in order to display it, to denote
+ * hyperlinks whose target is currently irrelevant.
+ * Make sure there are enough bits to store this in VteCellAttr.hyperlink_idx */
+#define VTE_HYPERLINK_IDX_TARGET_IN_STREAM      (VTE_HYPERLINK_COUNT_MAX + 1)
+
+/* Max length allowed in the id= parameter of an OSC 8 sequence.
+ * See also the comment of VTE_HYPERLINK_TOTAL_LENGTH_MAX. */
+#define VTE_HYPERLINK_ID_LENGTH_MAX     250
+
+/* Max length of all the hyperlink data stored in the streams as a string.
+ * Currently the hyperlink data is the ID and URI and a separator in between.
+ * Make sure there are enough bits to store this in VteStreamCellAttr.hyperlink_length */
+#define VTE_HYPERLINK_TOTAL_LENGTH_MAX  (VTE_HYPERLINK_ID_LENGTH_MAX + 1 + VTE_HYPERLINK_URI_LENGTH_MAX)
diff --git a/src/vtegtk.cc b/src/vtegtk.cc
index 0247950..529ad83 100644
--- a/src/vtegtk.cc
+++ b/src/vtegtk.cc
@@ -417,6 +417,9 @@ vte_terminal_get_property (GObject *object,
                 case PROP_ALLOW_BOLD:
                         g_value_set_boolean (value, vte_terminal_get_allow_bold (terminal));
                         break;
+                case PROP_ALLOW_HYPERLINK:
+                        g_value_set_boolean (value, vte_terminal_get_allow_hyperlink (terminal));
+                        break;
                 case PROP_AUDIBLE_BELL:
                         g_value_set_boolean (value, vte_terminal_get_audible_bell (terminal));
                         break;
@@ -450,6 +453,9 @@ vte_terminal_get_property (GObject *object,
                 case PROP_FONT_SCALE:
                         g_value_set_double (value, vte_terminal_get_font_scale (terminal));
                         break;
+                case PROP_HYPERLINK_HOVER_URI:
+                        g_value_set_string (value, impl->m_hyperlink_hover_uri);
+                        break;
                 case PROP_ICON_TITLE:
                         g_value_set_string (value, vte_terminal_get_icon_title (terminal));
                         break;
@@ -512,6 +518,9 @@ vte_terminal_set_property (GObject *object,
                 case PROP_ALLOW_BOLD:
                         vte_terminal_set_allow_bold (terminal, g_value_get_boolean (value));
                         break;
+                case PROP_ALLOW_HYPERLINK:
+                        vte_terminal_set_allow_hyperlink (terminal, g_value_get_boolean (value));
+                        break;
                 case PROP_AUDIBLE_BELL:
                         vte_terminal_set_audible_bell (terminal, g_value_get_boolean (value));
                         break;
@@ -567,6 +576,7 @@ vte_terminal_set_property (GObject *object,
                         /* Not writable */
                 case PROP_CURRENT_DIRECTORY_URI:
                 case PROP_CURRENT_FILE_URI:
+                case PROP_HYPERLINK_HOVER_URI:
                 case PROP_ICON_TITLE:
                 case PROP_WINDOW_TITLE:
                         g_assert_not_reached ();
@@ -798,6 +808,33 @@ vte_terminal_class_init(VteTerminalClass *klass)
                              G_TYPE_NONE, 0);
 
         /**
+         * VteTerminal::hyperlink-hover-changed:
+         * @vteterminal: the object which received the signal
+         * @uri: the nonempty target URI under the mouse, or NULL
+         * @bbox: the bounding box of the hyperlink anchor text, or NULL
+         *
+         * Emitted when the hovered hyperlink changes.
+         *
+         * @uri and @bbox are owned by VTE, must not be modified, and might
+         * change after the signal handlers returns.
+         *
+         * The signal is not re-emitted when the bounding box changes for the
+         * same hyperlink. This might change in a future VTE version without notice.
+         *
+         * Since: 0.50
+         */
+        signals[SIGNAL_HYPERLINK_HOVER_URI_CHANGED] =
+                g_signal_new(I_("hyperlink-hover-uri-changed"),
+                             G_OBJECT_CLASS_TYPE(klass),
+                             G_SIGNAL_RUN_LAST,
+                             0,
+                             NULL,
+                             NULL,
+                             _vte_marshal_VOID__STRING_BOXED,
+                             G_TYPE_NONE,
+                             2, G_TYPE_STRING, GDK_TYPE_RECTANGLE | G_SIGNAL_TYPE_STATIC_SCOPE);
+
+        /**
          * VteTerminal::encoding-changed:
          * @vteterminal: the object which received the signal
          *
@@ -1219,6 +1256,18 @@ vte_terminal_class_init(VteTerminalClass *klass)
                                       (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | 
G_PARAM_EXPLICIT_NOTIFY));
 
         /**
+         * VteTerminal:allow-hyperlink:
+         *
+         * Controls whether or not hyperlinks (OSC 8 escape sequence) are recognized and displayed.
+         *
+         * Since: 0.50
+         */
+        pspecs[PROP_ALLOW_HYPERLINK] =
+                g_param_spec_boolean ("allow-hyperlink", NULL, NULL,
+                                      TRUE,
+                                      (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | 
G_PARAM_EXPLICIT_NOTIFY));
+
+        /**
          * VteTerminal:audible-bell:
          *
          * Controls whether or not the terminal will beep when the child outputs the
@@ -1455,6 +1504,18 @@ vte_terminal_class_init(VteTerminalClass *klass)
                                      (GParamFlags) (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | 
G_PARAM_EXPLICIT_NOTIFY));
 
         /**
+         * VteTerminal:hyperlink-hover-uri:
+         *
+         * The currently hovered hyperlink URI, or %NULL if unset.
+         *
+         * Since: 0.50
+         */
+        pspecs[PROP_HYPERLINK_HOVER_URI] =
+                g_param_spec_string ("hyperlink-hover-uri", NULL, NULL,
+                                     NULL,
+                                     (GParamFlags) (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | 
G_PARAM_EXPLICIT_NOTIFY));
+
+        /**
          * VteTerminal:word-char-exceptions:
          *
          * The set of characters which will be considered parts of a word
@@ -1804,6 +1865,30 @@ vte_terminal_match_check_event(VteTerminal *terminal,
 }
 
 /**
+ * vte_terminal_hyperlink_check_event:
+ * @terminal: a #VteTerminal
+ * @event: a #GdkEvent
+ *
+ * Returns a nonempty string: the target of the explicit hyperlink (printed using the OSC 8
+ * escape sequence) at the position of the event, or %NULL.
+ *
+ * Proper use of the escape sequence should result in URI-encoded URIs with a proper scheme
+ * like "http://";, "https://";, "file://", "mailto:"; etc. This is, however, not enforced by VTE.
+ * The caller must tolerate the returned string potentially not being a valid URI.
+ *
+ * Returns: (transfer full): a newly allocated string containing the target of the hyperlink
+ *
+ * Since: 0.50
+ */
+char *
+vte_terminal_hyperlink_check_event(VteTerminal *terminal,
+                                   GdkEvent *event)
+{
+        g_return_val_if_fail(VTE_IS_TERMINAL(terminal), FALSE);
+        return IMPL(terminal)->hyperlink_check(event);
+}
+
+/**
  * vte_terminal_event_check_regex_simple:
  * @terminal: a #VteTerminal
  * @event: a #GdkEvent
@@ -2744,6 +2829,42 @@ vte_terminal_set_allow_bold(VteTerminal *terminal,
 }
 
 /**
+ * vte_terminal_get_allow_hyperlink:
+ * @terminal: a #VteTerminal
+ *
+ * Checks whether or not hyperlinks (OSC 8 escape sequence) are allowed.
+ *
+ * Returns: %TRUE if hyperlinks are enabled, %FALSE if not
+ *
+ * Since: 0.50
+ */
+gboolean
+vte_terminal_get_allow_hyperlink(VteTerminal *terminal)
+{
+        g_return_val_if_fail(VTE_IS_TERMINAL(terminal), FALSE);
+        return IMPL(terminal)->m_allow_hyperlink;
+}
+
+/**
+ * vte_terminal_set_allow_hyperlink:
+ * @terminal: a #VteTerminal
+ * @allow_hyperlink: %TRUE if the terminal should allow hyperlinks
+ *
+ * Controls whether or not hyperlinks (OSC 8 escape sequence) are allowed.
+ *
+ * Since: 0.50
+ */
+void
+vte_terminal_set_allow_hyperlink(VteTerminal *terminal,
+                                 gboolean allow_hyperlink)
+{
+        g_return_if_fail(VTE_IS_TERMINAL(terminal));
+
+        if (IMPL(terminal)->set_allow_hyperlink(allow_hyperlink != FALSE))
+                g_object_notify_by_pspec(G_OBJECT(terminal), pspecs[PROP_ALLOW_HYPERLINK]);
+}
+
+/**
  * vte_terminal_get_audible_bell:
  * @terminal: a #VteTerminal
  *
diff --git a/src/vtegtk.hh b/src/vtegtk.hh
index 5340725..d1ade94 100644
--- a/src/vtegtk.hh
+++ b/src/vtegtk.hh
@@ -39,6 +39,7 @@ enum {
         SIGNAL_DEICONIFY_WINDOW,
         SIGNAL_ENCODING_CHANGED,
         SIGNAL_EOF,
+        SIGNAL_HYPERLINK_HOVER_URI_CHANGED,
         SIGNAL_ICON_TITLE_CHANGED,
         SIGNAL_ICONIFY_WINDOW,
         SIGNAL_INCREASE_FONT_SIZE,
@@ -63,6 +64,7 @@ extern guint signals[LAST_SIGNAL];
 enum {
         PROP_0,
         PROP_ALLOW_BOLD,
+        PROP_ALLOW_HYPERLINK,
         PROP_AUDIBLE_BELL,
         PROP_BACKSPACE_BINDING,
         PROP_CJK_AMBIGUOUS_WIDTH,
@@ -74,6 +76,7 @@ enum {
         PROP_ENCODING,
         PROP_FONT_DESC,
         PROP_FONT_SCALE,
+        PROP_HYPERLINK_HOVER_URI,
         PROP_ICON_TITLE,
         PROP_INPUT_ENABLED,
         PROP_MOUSE_POINTER_AUTOHIDE,
diff --git a/src/vteinternal.hh b/src/vteinternal.hh
index 61dcb95..7eea941 100644
--- a/src/vteinternal.hh
+++ b/src/vteinternal.hh
@@ -479,6 +479,7 @@ public:
         gboolean m_mouse_cursor_visible;     /* derived value really containing if it's actually visible */
         GdkCursor* m_mouse_default_cursor;
         GdkCursor* m_mouse_mousing_cursor;
+        GdkCursor* m_mouse_hyperlink_cursor;
        GdkCursor* m_mouse_inviso_cursor;
 
        /* Input method support. */
@@ -530,6 +531,12 @@ public:
         guint m_hscroll_policy : 1; /* unused */
         guint m_vscroll_policy : 1;
 
+        /* Hyperlinks */
+        gboolean m_allow_hyperlink;
+        hyperlink_idx_t m_hyperlink_hover_idx;
+        const char *m_hyperlink_hover_uri; /* data is owned by the ring */
+        long m_hyperlink_auto_id;
+
 public:
 
         // FIXMEchpe inline!
@@ -692,6 +699,7 @@ public:
                         bool italic,
                         bool underline,
                         bool strikethrough,
+                        bool hyperlink,
                         bool hilite,
                         bool boxed,
                         int column_width,
@@ -887,7 +895,7 @@ public:
         bool cell_is_selected(vte::grid::column_t col,
                               vte::grid::row_t) const;
 
-        void reset_default_attributes();
+        void reset_default_attributes(bool reset_hyperlink);
 
         void ensure_font();
         void update_font();
@@ -955,12 +963,17 @@ public:
                                 guint rows);
         void emit_copy_clipboard();
         void emit_paste_clipboard();
+        void emit_hyperlink_hover_uri_changed(const GdkRectangle *bbox);
 
         void clear_tabstop(int column); // FIXMEchpe vte::grid::column_t ?
         bool get_tabstop(int column);
         void set_tabstop(int column);
         void set_default_tabstops();
 
+        void hyperlink_invalidate_and_get_bbox(hyperlink_idx_t idx, GdkRectangle *bbox);
+        void hyperlink_hilite_update(vte::view::coords const& pos);
+        void hyperlink_hilite(vte::view::coords const& pos);
+
         void match_contents_clear();
         void match_contents_refresh();
         void set_cursor_from_regex_match(struct vte_match_regex *regex);
@@ -975,6 +988,8 @@ public:
                                long *column,
                                long *row);
 
+        char *hyperlink_check(GdkEvent *event);
+
         bool regex_match_check_extra(GdkEvent *event,
                                      VteRegex **regexes,
                                      gsize n_regexes,
@@ -1076,6 +1091,7 @@ public:
 
         bool set_audible_bell(bool setting);
         bool set_allow_bold(bool setting);
+        bool set_allow_hyperlink(bool setting);
         bool set_backspace_binding(VteEraseBinding binding);
         bool set_background_alpha(double alpha);
         bool set_cjk_ambiguous_width(int width);
@@ -1176,6 +1192,7 @@ public:
         inline void seq_send_secondary_device_attributes();
         inline void set_current_directory_uri_changed(char* uri /* adopted */);
         inline void set_current_file_uri_changed(char* uri /* adopted */);
+        inline void set_current_hyperlink(char* hyperlink_params /* adopted */, char* uri /* adopted */);
         inline void set_keypad_mode(VteKeymode mode);
         inline void seq_erase_in_display(long param);
         inline void seq_erase_in_line(long param);
diff --git a/src/vterowdata.h b/src/vterowdata.h
index 2e73fd4..3e85194 100644
--- a/src/vterowdata.h
+++ b/src/vterowdata.h
@@ -21,6 +21,8 @@
 #ifndef vterowdata_h_included
 #define vterowdata_h_included
 
+#include <string.h>
+
 #include "vteunistr.h"
 #include "vtemacros.h"
 #include "vtedefines.hh"
@@ -30,13 +32,17 @@ G_BEGIN_DECLS
 #define VTE_TAB_WIDTH_BITS             4  /* Has to be able to store the value of 8. */
 #define VTE_TAB_WIDTH_MAX              ((1 << VTE_TAB_WIDTH_BITS) - 1)
 
+#define VTE_CELL_ATTR_COMMON_BYTES      8  /* The number of common bytes in VteCellAttr and 
VteStreamCellAttr */
+
 /*
  * VteCellAttr: A single cell style attributes
  *
  * Ordered by most commonly changed attributes, to
  * optimize the compact representation.
  *
- * When adding new attributes, remember to update basic_cell below too.
+ * When adding new attributes, keep in sync with VteStreamCellAttr and
+ * update VTE_CELL_ATTR_COMMON_BYTES accordingly.
+ * Also don't forget to update basic_cell below!
  */
 
 typedef struct _VteCellAttr {
@@ -59,9 +65,38 @@ typedef struct _VteCellAttr {
        guint64 dim: 1;         /* also known as faint, half intensity etc. */
 
        guint64 invisible: 1;
-        /* 1 bit unused */
+        guint64 padding_unused_1: 1;
+        /* 8-byte boundary */
+        guint32 hyperlink_idx;  /* a unique hyperlink index at a time for the ring's cells,
+                                   0 means not a hyperlink, VTE_HYPERLINK_IDX_TARGET_IN_STREAM
+                                   means the target is irrelevant/unknown at the moment.
+                                   If bitpacking, choose a size big enough to hold a different idx
+                                   for every cell in the ring but not yet in the stream
+                                   (currently the height rounded up to the next power of two, times width)
+                                   for supported VTE sizes, and update VTE_HYPERLINK_IDX_TARGET_IN_STREAM. */
+        guint32 padding_unused_2;
 } VteCellAttr;
-G_STATIC_ASSERT (sizeof (VteCellAttr) == 8);
+G_STATIC_ASSERT (sizeof (VteCellAttr) == 16);
+G_STATIC_ASSERT (offsetof (VteCellAttr, hyperlink_idx) == VTE_CELL_ATTR_COMMON_BYTES);
+
+/*
+ * VteStreamCellAttr: Variant of VteCellAttr to be stored in attr_stream.
+ *
+ * When adding new attributes, keep in sync with VteCellAttr and
+ * update VTE_CELL_ATTR_COMMON_BYTES accordingly.
+ */
+
+typedef struct _VTE_GNUC_PACKED _VteStreamCellAttr {
+        guint64 fragment: 1;
+        guint64 columns: VTE_TAB_WIDTH_BITS;
+        guint64 remaining_main_attributes: 59;  /* All the non-hyperlink related attributes from VteCellAttr.
+                                                   We don't individually access them in the stream, so 
there's
+                                                   no point in repeating each field separately. */
+        /* 8-byte boundary */
+        guint16 hyperlink_length;       /* make sure it fits VTE_HYPERLINK_TOTAL_LENGTH_MAX */
+} VteStreamCellAttr;
+G_STATIC_ASSERT (sizeof (VteStreamCellAttr) == 10);
+G_STATIC_ASSERT (offsetof (VteStreamCellAttr, hyperlink_length) == VTE_CELL_ATTR_COMMON_BYTES);
 
 /*
  * VteCell: A single cell's data
@@ -71,7 +106,7 @@ typedef struct _VTE_GNUC_PACKED _VteCell {
        vteunistr c;
        VteCellAttr attr;
 } VteCell;
-G_STATIC_ASSERT (sizeof (VteCell) == 12);
+G_STATIC_ASSERT (sizeof (VteCell) == 20);
 
 static const VteCell basic_cell = {
        0,
@@ -90,7 +125,10 @@ static const VteCell basic_cell = {
                0, /* blink */
                0, /* half */
 
-               0  /* invisible */
+                0, /* invisible */
+                0, /* padding_unused_1 */
+                0, /* hyperlink_idx */
+                0, /* padding_unused_2 */
        }
 };
 
@@ -135,6 +173,15 @@ _vte_row_data_get_writable (VteRowData *row, gulong col)
        return &row->cells[col];
 }
 
+/*
+ * Copy the common attributes from VteCellAttr to VteStreamCellAttr or vice versa.
+ */
+static inline void
+_attrcpy (void *dst, void *src)
+{
+        memcpy(dst, src, VTE_CELL_ATTR_COMMON_BYTES);
+}
+
 void _vte_row_data_init (VteRowData *row);
 void _vte_row_data_clear (VteRowData *row);
 void _vte_row_data_fini (VteRowData *row);
diff --git a/src/vteseq.cc b/src/vteseq.cc
index be2868f..4661f7a 100644
--- a/src/vteseq.cc
+++ b/src/vteseq.cc
@@ -1,3 +1,4 @@
+
 /*
  * Copyright (C) 2001-2004 Red Hat, Inc.
  *
@@ -387,6 +388,17 @@ VteTerminalPrivate::seq_switch_screen(VteScreen *new_screen)
 {
         /* if (new_screen == m_screen) return; ? */
 
+        /* The two screens use different hyperlink pools, so carrying on the idx
+         * wouldn't make sense and could lead to crashes.
+         * Ideally we'd carry the target URI itself, but I'm just lazy.
+         * Also, run a GC before we switch away from that screen. */
+        m_hyperlink_hover_idx = _vte_ring_get_hyperlink_at_position(m_screen->row_data, -1, -1, true, NULL);
+        g_assert (m_hyperlink_hover_idx == 0);
+        m_hyperlink_hover_uri = NULL;
+        emit_hyperlink_hover_uri_changed(NULL);  /* FIXME only emit if really changed */
+        m_defaults.attr.hyperlink_idx = _vte_ring_get_hyperlink_idx(m_screen->row_data, NULL);
+        g_assert (m_defaults.attr.hyperlink_idx == 0);
+
         /* cursor.row includes insert_delta, adjust accordingly */
         auto cr = m_screen->cursor.row - m_screen->insert_delta;
         m_screen = new_screen;
@@ -2127,7 +2139,7 @@ vte_sequence_handler_character_attributes (VteTerminalPrivate *that, GValueArray
                param = g_value_get_long(value);
                switch (param) {
                case 0:
-                       that->reset_default_attributes();
+                        that->reset_default_attributes(false);
                        break;
                case 1:
                         that->m_defaults.attr.bold = 1;
@@ -2271,7 +2283,7 @@ vte_sequence_handler_character_attributes (VteTerminalPrivate *that, GValueArray
        }
        /* If we had no parameters, default to the defaults. */
        if (i == 0) {
-               that->reset_default_attributes();
+                that->reset_default_attributes(false);
        }
        /* Save the new colors. */
         that->m_color_defaults.attr.fore = that->m_defaults.attr.fore;
@@ -2450,10 +2462,106 @@ VteTerminalPrivate::set_current_file_uri_changed(char* uri /* adopted */)
         m_current_file_uri_changed = uri;
 }
 
+/* Handle OSC 8 hyperlinks.
+ * See bug 779734 and https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda. */
 static void
 vte_sequence_handler_set_current_hyperlink (VteTerminalPrivate *that, GValueArray *params)
 {
-        /* Accept but ignore to prepare for the forthcoming hyperlink feature (bug #779734) */
+        GValue *value;
+        char *hyperlink_params;
+        char *uri;
+
+        hyperlink_params = NULL;
+        uri = NULL;
+        if (params != NULL && params->n_values > 1) {
+                value = g_value_array_get_nth(params, 0);
+
+                if (G_VALUE_HOLDS_POINTER(value)) {
+                        hyperlink_params = that->ucs4_to_utf8((const guchar *)g_value_get_pointer (value));
+                } else if (G_VALUE_HOLDS_STRING(value)) {
+                        /* Copy the string into the buffer. */
+                        hyperlink_params = g_value_dup_string(value);
+                }
+
+                value = g_value_array_get_nth(params, 1);
+
+                if (G_VALUE_HOLDS_POINTER(value)) {
+                        uri = that->ucs4_to_utf8((const guchar *)g_value_get_pointer (value));
+                } else if (G_VALUE_HOLDS_STRING(value)) {
+                        /* Copy the string into the buffer. */
+                        uri = g_value_dup_string(value);
+                }
+        }
+
+        that->set_current_hyperlink(hyperlink_params, uri);
+}
+
+void
+VteTerminalPrivate::set_current_hyperlink(char *hyperlink_params /* adopted */, char* uri /* adopted */)
+{
+        guint idx;
+        char *id = NULL;
+        char idbuf[24];
+
+        if (!m_allow_hyperlink)
+                return;
+
+        /* Get the "id" parameter */
+        if (hyperlink_params) {
+                if (strncmp(hyperlink_params, "id=", 3) == 0) {
+                        id = hyperlink_params + 3;
+                } else {
+                        id = strstr(hyperlink_params, ":id=");
+                        if (id)
+                                id += 4;
+                }
+        }
+        if (id) {
+                *strchrnul(id, ':') = '\0';
+        }
+        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                          "OSC 8: id=\"%s\" uri=\"%s\"\n",
+                          id, uri);
+
+        if (uri && strlen(uri) > VTE_HYPERLINK_URI_LENGTH_MAX) {
+                _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                                  "Overlong URI ignored: \"%s\"\n",
+                                  uri);
+                uri[0] = '\0';
+        }
+
+        if (id && strlen(id) > VTE_HYPERLINK_ID_LENGTH_MAX) {
+                _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                                  "Overlong \"id\" ignored: \"%s\"\n",
+                                  id);
+                id[0] = '\0';
+        }
+
+        if (uri && uri[0]) {
+                /* The hyperlink, as we carry around and store in the streams, is "id;uri" */
+                char *hyperlink;
+
+                if (!id || !id[0]) {
+                        /* Automatically generate a unique ID string. The colon makes sure
+                         * it cannot conflict with an explicitly specified one. */
+                        sprintf(idbuf, ":%ld", m_hyperlink_auto_id++);
+                        id = idbuf;
+                        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                                          "Autogenerated id=\"%s\"\n",
+                                          id);
+                }
+                hyperlink = g_strdup_printf("%s;%s", id, uri);
+                idx = _vte_ring_get_hyperlink_idx(m_screen->row_data, hyperlink);
+                g_free (hyperlink);
+        } else {
+                /* idx = 0; also remove the previous current_idx so that it can be GC'd now. */
+                idx = _vte_ring_get_hyperlink_idx(m_screen->row_data, NULL);
+        }
+
+        m_defaults.attr.hyperlink_idx = idx;
+
+        g_free(hyperlink_params);
+        g_free(uri);
 }
 
 /* Restrict the scrolling region. */


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]