diff --git a/bindings/vala/app.vala b/bindings/vala/app.vala
index 58698f92..11cd1ff8 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 89a4e159..9e66f2b9 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 d4bedb16..69683dd6 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 00000000..6c3d1b4b
--- /dev/null
+++ b/perf/hyperlink-demo.txt
@@ -0,0 +1,95 @@
+[1mTests 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;;[m
+═════════════════════════════════════════════════
+
+[33mcommit ]8;;https://git.gnome.org/browse/vte/commit/?id=a9b0b4c75a6dc7282f7cfcaef71413d69f7f0731a9b0b4c75a6dc7282f7cfcaef71413d69f7f0731]8;;[m
+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;;
+
+[33mcommit ]8;;https://git.gnome.org/browse/vte/commit/?id=6a74baeaabb0a1ce54444611b324338f94721a5c6a74baeaabb0a1ce54444611b324338f94721a5c]8;;[m
+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/colors[31mM[32mu[33ml[34mt[35mi[36m-[0;48;5;21mc[0;48;5;27mo[0;48;5;33ml[0;48;5;39mo[0;48;5;45mu[0;48;5;51mr[m [1ml[0;3mi[0;4mn[0;9mk[m also tests that "\e[m" or "\e[0m" does [3mnot[0m terminate the link]8;;
+
+Soft reset "\e[!p" resets attributes and terminates link: ]8;;http://example.com/softreset[3;31mfoo[!pbar
+
+]8;;http://example.com/widthSome CJK and combining accents: 䀀䀁䀂ćĝ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;;
+
+[4mExplicitly underlined links ("\e[4m"):[m
+Explicit link only: [4m]8;;http://example.com/under_scoreI'm an explicit link with under_score]8;;[m
+Implicit link only: [4mhttp://example.com/under_score[m
+Both:               [4m]8;;http://example.com/under_scorehttp://example.com/under_score]8;;[m
+
+Conflicting explicit and implicit links: http://example.com/foobar-]8;;http://example.com/explicitexplicit]8;;-rest
+
+Invisible explicit link: «[8m]8;;http://example.com/invisibleCan you see me?]8;;[m»
+Invisible implicit link: «[8mhttp://example.com/how_about_me[m»
+
+]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......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 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......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 251 bytes + URL of 2084 bytes]8;;
+
+]8;;http://example.com/ST\ST (aka ESC \) instead of BEL]8;;\
+8;;http://example.com/C1C1 (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/cursormove[C[C[Cright]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 b8dc3956..6fc9648a 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 c2119427..13c8737b 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 0276422e..1e4d0c1b 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 36d82e12..c06e68ff 100644
--- a/src/ring.cc
+++ b/src/ring.cc
@@ -29,6 +29,8 @@
  * VteRing: A buffer ring
  */
 
+#define hyperlink_get(ring, idx) g_array_index((ring)->hyperlinks, GString *, (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,13 @@ _vte_ring_init (VteRing *ring, gulong max_rows, gboolean has_streams)
 
         ring->visible_rows = 0;
 
+        ring->hyperlinks = g_array_new(FALSE, FALSE, sizeof (GString *));
+        empty_str = g_string_new_len("", 0);
+        g_array_append_val(ring->hyperlinks, empty_str);
+        ring->hyperlink_highest_used_idx = 0;
+        ring->hyperlink_current_idx = 0;
+        ring->hyperlink_hover_idx = 0;
+
 	_vte_ring_validate(ring);
 }
 
@@ -101,6 +112,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_array_free (ring->hyperlinks, TRUE);
+
 	_vte_row_data_fini (&ring->cached_row);
 }
 
@@ -118,6 +133,168 @@ 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);
+
+        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_assign(hyperlink_get(ring, idx), "");
+                }
+	}
+
+        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);
+}
+
+/*
+ * 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_array_append_val(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 +313,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 do_hyperlink_gc = FALSE;
 
 	_vte_debug_print (VTE_DEBUG_RING, "Freezing row %lu.\n", position);
 
@@ -166,28 +345,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);
+					do_hyperlink_gc = 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);
+					do_hyperlink_gc = 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 +398,20 @@ _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);
+
+	if (do_hyperlink_gc)
+		_vte_ring_hyperlink_gc(ring);
 }
 
+/* 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 +419,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,19 +457,46 @@ _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. Why???
+							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_HYPERLINK) {
+                        /* Debug: Reverse the colors for the stream's contents. */
+                        if (!do_truncate) {
+                                cell.attr.reverse = !cell.attr.reverse;
+                        }
+                }
 		cell.c = g_utf8_get_char (p);
 
 		q = g_utf8_next_char (p);
@@ -268,38 +509,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;
 				}
@@ -341,12 +606,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)
 {
@@ -355,13 +614,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);
 
@@ -402,7 +717,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
@@ -874,7 +1189,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);
 	}
 
@@ -918,9 +1234,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);
 			}
 		}
@@ -934,9 +1251,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 b56d82e2..2a813d7d 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,15 @@ struct _VteRing {
 
 	gboolean has_streams;
         gulong visible_rows;  /* to keep at least a screenful of lines in memory, bug 646098 comment 12 */
+
+        GArray *hyperlinks;  /* The hyperlink pool. Contains GString* pointers.
+                                [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 FIXME for what? */
+        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. */
 };
 
 #define _vte_ring_contains(__ring, __position) \
@@ -81,6 +105,8 @@ 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);
+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 9ea0b031..b4742d6d 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,
@@ -5488,6 +5533,146 @@ 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 last_row = last_displayed_row();
+        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);
+
+        // FIXME warning: cannot optimize possibly infinite loops [-Wunsafe-loop-optimizations]
+        for (row = first_row; row <= last_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 +6177,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 +7089,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 +7150,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 +7301,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 +7993,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 +8105,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 +8247,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 +8546,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 +8768,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 +8787,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 +8824,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 +8858,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 +9098,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 +9125,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 +9261,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 +9277,13 @@ 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 (m_allow_hyperlink && cell->attr.hyperlink_idx != 0 &&
+			    cell->attr.hyperlink_idx == m_hyperlink_hover_idx) {
+				hilite = true;
+			} else if ((!m_allow_hyperlink || m_hyperlink_hover_idx == 0) && m_show_match) {
 				hilite = m_match_span.contains(row, i);
 			} else {
 				hilite = false;
@@ -9097,7 +9315,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 +9347,16 @@ 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 (m_allow_hyperlink && cell->attr.hyperlink_idx != 0 &&
+					    cell->attr.hyperlink_idx == m_hyperlink_hover_idx) {
+						nhilite = true;
+					} else if ((!m_allow_hyperlink || 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,
@@ -9471,6 +9697,7 @@ VteTerminalPrivate::paint_im_preedit_string()
 						FALSE,
 						FALSE,
 						FALSE,
+						FALSE,
 						TRUE,
 						width, height);
 		}
@@ -9743,6 +9970,28 @@ 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
+                                                         // Setting hover_idx should go through a setter method
+		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 +10281,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 +10664,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 3b22c4a5..40cdc4f5 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 ac321be5..7ecd2fc0 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 7f56291a..3afc5328 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 02479500..35701f0d 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 5340725d..d1ade949 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 61dcb956..7eea9419 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 2e73fd4e..f21a3be8 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 be2868f0..ee339c1b 100644
--- a/src/vteseq.cc
+++ b/src/vteseq.cc
@@ -1,3 +1,4 @@
+
 /*
  * Copyright (C) 2001-2004 Red Hat, Inc.
  *
@@ -387,6 +388,18 @@ 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
+                                                 // Setting hover_idx should go through a setter method
+        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 +2140,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 +2284,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;
@@ -2453,7 +2466,101 @@ VteTerminalPrivate::set_current_file_uri_changed(char* uri /* adopted */)
 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. */
