diff --git a/src/caps.cc b/src/caps.cc
index 4757126..af30e40 100644
--- a/src/caps.cc
+++ b/src/caps.cc
@@ -229,6 +229,7 @@ const char _vte_xterm_capability_strings[] =
         ENTRY(OSC "4;%s" BEL, "change-color-bel")
         ENTRY(OSC "6;%s" BEL, "set-current-file-uri")
         ENTRY(OSC "7;%s" BEL, "set-current-directory-uri")
+        ENTRY(OSC "8;%s" BEL, "set-current-hyperlink")
         ENTRY(OSC "10;%s" BEL, "change-foreground-color-bel")
         ENTRY(OSC "11;%s" BEL, "change-background-color-bel")
         ENTRY(OSC "12;%s" BEL, "change-cursor-background-color-bel")
@@ -266,6 +267,7 @@ const char _vte_xterm_capability_strings[] =
         ENTRY(OSC "4;%s" ST, "change-color-st")
         ENTRY(OSC "6;%s" ST, "set-current-file-uri")
         ENTRY(OSC "7;%s" ST, "set-current-directory-uri")
+        ENTRY(OSC "8;%s" ST, "set-current-hyperlink")
         ENTRY(OSC "10;%s" ST, "change-foreground-color-st")
         ENTRY(OSC "11;%s" ST, "change-background-color-st")
         ENTRY(OSC "12;%s" ST, "change-cursor-background-color-st")
diff --git a/src/debug.c b/src/debug.c
index b8dc395..6fc9648 100644
--- a/src/debug.c
+++ b/src/debug.c
@@ -52,7 +52,8 @@ _vte_debug_init(void)
     { "widget-size",  VTE_DEBUG_WIDGET_SIZE  },
     { "style",        VTE_DEBUG_STYLE        },
     { "resize",       VTE_DEBUG_RESIZE       },
-    { "regex",        VTE_DEBUG_REGEX        }
+    { "regex",        VTE_DEBUG_REGEX        },
+    { "hyperlink",    VTE_DEBUG_HYPERLINK    },
   };
 
   _vte_debug_flags = g_parse_debug_string (g_getenv("VTE_DEBUG"),
diff --git a/src/debug.h b/src/debug.h
index c211942..13c8737 100644
--- a/src/debug.h
+++ b/src/debug.h
@@ -61,7 +61,8 @@ typedef enum {
 	VTE_DEBUG_WIDGET_SIZE   = 1 << 21,
         VTE_DEBUG_STYLE         = 1 << 22,
 	VTE_DEBUG_RESIZE        = 1 << 23,
-        VTE_DEBUG_REGEX         = 1 << 24
+        VTE_DEBUG_REGEX         = 1 << 24,
+        VTE_DEBUG_HYPERLINK     = 1 << 25,
 } VteDebugFlags;
 
 void _vte_debug_init(void);
diff --git a/src/ring.cc b/src/ring.cc
index e090610..568451f 100644
--- a/src/ring.cc
+++ b/src/ring.cc
@@ -25,10 +25,14 @@
 
 #include <string.h>
 
+GString *idontknow;  // FIXME temporary
+
 /*
  * VteRing: A buffer ring
  */
 
+#define hyperlink_get(ring, id) g_array_index((ring)->hyperlinks, GString, (id))
+
 #ifdef VTE_DEBUG
 static void
 _vte_ring_validate (VteRing * ring)
@@ -53,6 +57,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));
@@ -72,7 +78,7 @@ _vte_ring_init (VteRing *ring, gulong max_rows, gboolean has_streams)
 	}
 
 	ring->last_attr_text_start_offset = 0;
-	ring->last_attr = basic_cell.attr;
+	ring->last_attr = basic_cell.attr;         // FIXME do we remember the hyperlink ID or length+target? Figure out!
 	ring->utf8_buffer = g_string_sized_new (128);
 
 	_vte_row_data_init (&ring->cached_row);
@@ -80,6 +86,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_id = 0;
+
+        idontknow = g_string_new(" --- Sorry I can't remember the link target --- ");  // FIXME temporary
+
 	_vte_ring_validate(ring);
 }
 
@@ -101,9 +114,160 @@ _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);
 }
 
+const GString *
+_vte_ring_get_hyperlink (VteRing *ring, guint id)
+{
+        if (id == 0)
+                return NULL;
+
+	// FIXME this is temporary
+	if (id == 4095)
+		return idontknow;
+
+        g_assert_cmpuint (id, <=, ring->hyperlink_highest_used_id);
+        /* GC'd IDs have a GString allocated with len == 0, the caller must never ask about such an ID */
+        g_assert_cmpuint (hyperlink_get(ring, id).len, !=, 0);
+        return &hyperlink_get(ring, id);
+}
+
+/*
+ * Find existing ID or allocate a new one.
+ * FIXME do something more effective than a linear search
+ */
+guint
+_vte_ring_get_hyperlink_id (VteRing *ring, const char *uri)
+{
+        guint i;
+	gsize len;
+	GString *str;
+
+        len = strlen(uri);
+        if (!len) {
+                _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                                  "get_hyperlink_id: ID 0 (hyperlink closed)\n");
+                return 0;
+        }
+        if (len > VTE_HYPERLINK_LENGTH_MAX) {
+	        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                                  "get_hyperlink_id: ID 0 (overlong URI given)\n");
+	        return 0;
+        }
+
+        _vte_ring_hyperlink_gc(ring, 0);
+
+        /* Linear search for this particular URI */
+        for (i = 1; i <= ring->hyperlink_highest_used_id; i++) {
+                if (hyperlink_get(ring, i).len == len &&
+                    strcmp(hyperlink_get(ring, i).str, uri) == 0) {
+                        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                                          "get_hyperlink_id: already existing ID %d for «%s»\n",
+                                          i, uri);
+                        return i;
+                }
+        }
+
+        /* Another linear search for an empty slot where a GString is already allocated */
+        for (i = 1; i < ring->hyperlinks->len; i++) {
+                if (hyperlink_get(ring, i).len == 0) {
+                        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                                          "get_hyperlink_id: reassigning old ID %d for «%s»\n",
+                                          i, uri);
+		        /* Grow size if required, however, never shrink to avoid long-term memory fragmentation. */
+			g_string_append_len (&hyperlink_get(ring, i), uri, len);
+			ring->hyperlink_highest_used_id = MAX (ring->hyperlink_highest_used_id, i);
+			return i;
+		}
+	}
+
+        /* All allocated slots contain a different URI. Gotta allocate a new one */
+	g_assert_cmpuint(ring->hyperlink_highest_used_id, ==, ring->hyperlinks->len - 1);
+
+	if (ring->hyperlink_highest_used_id == VTE_HYPERLINK_COUNT_MAX) {
+	        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+	                          "get_hyperlink_id: ID 0 (ran out of available IDs) for «%s»\n",
+	                          uri);
+		return 0;
+	}
+
+        i = ++ring->hyperlink_highest_used_id;
+        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                          "get_hyperlink_id: brand new ID %d for «%s»\n",
+                          i, uri);
+	str = g_string_new_len (uri, len);
+	g_array_append_val(ring->hyperlinks, *str);
+	g_string_free(str, FALSE);
+
+	g_assert_cmpuint(ring->hyperlink_highest_used_id, ==, ring->hyperlinks->len - 1);
+
+	return i;
+}
+
+/*
+ * Do a round of garbage collection. Hyperlinks that no longer occur in the ring are wiped out.
+ * An optional ID not to be purged can be specified, use 0 to not specify one.
+ */
+void
+_vte_ring_hyperlink_gc (VteRing *ring, guint id_to_keep)
+{
+	guint i, j, id;
+	VteRowData *row;
+	char *used;
+
+        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                          "hyperlink: GC starting (highest used ID is %d)\n",
+                          ring->hyperlink_highest_used_id);
+
+        if (ring->hyperlink_highest_used_id == 0) {
+                _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                                  "hyperlink: GC done (no links at all, nothing to do)\n");
+                return;
+        }
+
+        /* No use in fiddling with bitmaps. One char for each ID to see if it's used. */
+        used = g_newa(char, 1 + ring->hyperlink_highest_used_id);
+        memset(used, 0, 1 + ring->hyperlink_highest_used_id);
+        used[id_to_keep] = 1;
+        used[ring->last_attr.hyperlink.id] = 1;  // FIXME depending on whether last_attr contains hyperlink id or length+target
+
+        // FIXME rather go from ring->start to ring->end
+        // FIXME what to do with cached_row???
+	for (i = 0; i <= ring->mask; i++) {
+	        row = &ring->array[i];
+	        for (j = 0; j < row->len; j++) {
+	                id = row->cells[j].attr.hyperlink.id;
+	                used[id] = 1;
+	        }
+	}
+
+        for (i = 1; i <= ring->hyperlink_highest_used_id; i++) {
+                if (!used[i] && hyperlink_get(ring, i).len != 0) {
+                        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                                          "hyperlink: GC purging link %d to %s\n",
+                                          i, hyperlink_get(ring, i).str);
+                        /* Wipe out the URI itself so it doesn't linger on in the memory for a long time */
+                        memset(hyperlink_get(ring, i).str, 0, hyperlink_get(ring, i).len);
+                        g_string_assign(&hyperlink_get(ring, i), "");
+                }
+	}
+
+        while (ring->hyperlink_highest_used_id >= 1 &&
+           hyperlink_get(ring, ring->hyperlink_highest_used_id).len == 0) {
+               ring->hyperlink_highest_used_id--;
+        }
+
+        _vte_debug_print (VTE_DEBUG_HYPERLINK,
+                          "hyperlink: GC done (highest used ID is now %d)\n",
+                          ring->hyperlink_highest_used_id);
+}
+
+
 typedef struct _VteRowRecord {
 	gsize text_start_offset;  /* offset where text of this row begins */
 	gsize attr_start_offset;  /* offset of the first character's attributes */
@@ -136,7 +300,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);
 
@@ -172,21 +338,30 @@ _vte_ring_freeze_row (VteRing *ring, gulong position, const VteRowData *row)
 				memset(&attr_change, 0, sizeof (attr_change));
 				attr_change.text_end_offset = ring->last_attr_text_start_offset;
 				attr_change.attr = ring->last_attr;
+				/* Replace hyperlink.id with hyperlink.length (the same field, but different semantics) */
+				hyperlink = &hyperlink_get(ring, attr_change.attr.hyperlink.id);
+				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;
+				}
 				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->len;
 				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;
+				attr_change.attr.hyperlink.length = 0;  // FIXME double check if it's okay to omit the hyperlink here
 				_vte_stream_append (ring->attr_stream, (const char *) &attr_change, sizeof (attr_change));
 				ring->last_attr = attr;
 			}
@@ -201,6 +376,9 @@ _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, 0);
 }
 
 static void
@@ -250,9 +428,10 @@ _vte_ring_thaw_row (VteRing *ring, gulong position, VteRowData *row, gboolean do
 			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);
+				record.attr_start_offset += sizeof (attr_change) + attr_change.attr.hyperlink.length;
 			}
 			attr = attr_change.attr;
+			attr.hyperlink.id = attr.hyperlink.length ? 4095 : 0;  // FIXME actual id
 		}
 
 		cell.attr = attr;
@@ -291,14 +470,14 @@ _vte_ring_thaw_row (VteRing *ring, gulong position, VteRowData *row, gboolean do
 			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);
+					attr_stream_truncate_at -= sizeof (attr_change);  // BIG BIG FIXME
 				}
 			}
 			/* 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))) {
+				if (_vte_stream_read (ring->attr_stream, attr_stream_truncate_at - sizeof (attr_change), (char *) &attr_change, sizeof (attr_change))) {  // BIG BIG FIXME
 					ring->last_attr_text_start_offset = attr_change.text_end_offset;
 				} else {
 					ring->last_attr_text_start_offset = 0;
@@ -829,7 +1008,7 @@ _vte_ring_rewrap (VteRing *ring,
 	VteCellTextOffset *marker_text_offsets;
 	VteVisualPosition *new_markers;
 	VteRowRecord old_record;
-	VteCellAttrChange attr_change;
+	VteCellAttrChange attr_change;  /* contains hyperlink.length because we don't care about the target here */
 	VteStream *new_row_stream;
 	gsize paragraph_start_text_offset;
 	gsize paragraph_end_text_offset;
@@ -875,6 +1054,7 @@ _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;
+		attr_change.attr.hyperlink.length = hyperlink_get(ring, ring->last_attr.hyperlink.id).len;  // FIXME double check me!
 		attr_change.text_end_offset = _vte_stream_head (ring->text_stream);
 	}
 
@@ -918,9 +1098,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;
 			if (!_vte_stream_read(ring->attr_stream, attr_offset, (char *) &attr_change, sizeof (attr_change))) {
 				attr_change.attr = ring->last_attr;
+				attr_change.attr.hyperlink.length = hyperlink_get(ring, ring->last_attr.hyperlink.id).len;  // FIXME double check me!
 				attr_change.text_end_offset = _vte_stream_head (ring->text_stream);
 			}
 		}
@@ -934,9 +1115,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;
 				if (!_vte_stream_read(ring->attr_stream, attr_offset, (char *) &attr_change, sizeof (attr_change))) {
 					attr_change.attr = ring->last_attr;
+					attr_change.attr.hyperlink.length = hyperlink_get(ring, ring->last_attr.hyperlink.id).len;  // FIXME double check me!
 					attr_change.text_end_offset = _vte_stream_head (ring->text_stream);
 				}
 			}
diff --git a/src/ring.h b/src/ring.h
index b56d82e..e805767 100644
--- a/src/ring.h
+++ b/src/ring.h
@@ -56,7 +56,11 @@ struct _VteRing {
 	gulong writable, mask;
 	VteRowData *array;
 
-	/* Storage */
+	/* Storage:
+	 * row_stream contains records of type VteRowRecord.
+	 * text_stream is the text in UTF-8.
+	 * attr_stream contains records of VteCellAttrChange, each followed by an URI
+	 * of length as in the record's attr.hyperlink.length, not NUL terminated. */
 	VteStream *attr_stream, *text_stream, *row_stream;
 	gsize last_attr_text_start_offset;
 	VteCellAttr last_attr;
@@ -67,6 +71,8 @@ struct _VteRing {
 
 	gboolean has_streams;
         gulong visible_rows;  /* to keep at least a screenful of lines in memory, bug 646098 comment 12 */
+        GArray *hyperlinks;  /* contains GStrings. [0] is an empty GString, [1] to [VTE_HYPERLINK_COUNT_MAX] contain the URIs */
+        guint hyperlink_highest_used_id;
 };
 
 #define _vte_ring_contains(__ring, __position) \
@@ -81,6 +87,9 @@ VteRowData *_vte_ring_index_writable (VteRing *ring, gulong position);
 
 void _vte_ring_init (VteRing *ring, gulong max_rows, gboolean has_streams);
 void _vte_ring_fini (VteRing *ring);
+const GString *_vte_ring_get_hyperlink (VteRing *ring, guint id);
+guint _vte_ring_get_hyperlink_id (VteRing *ring, const char *uri);
+void _vte_ring_hyperlink_gc (VteRing *ring, guint id_to_keep);
 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 3f2b9bb..2c226be 100644
--- a/src/vte.cc
+++ b/src/vte.cc
@@ -1818,6 +1818,32 @@ VteTerminalPrivate::regex_match_check(GdkEvent *event,
         return regex_match_check(col, row - (long)m_screen->scroll_delta, tag);
 }
 
+char *
+VteTerminalPrivate::get_hyperlink(GdkEvent *event)
+{
+        long col, row;
+        VteRowData const* row_data;
+        VteCell const* cell;
+        const GString *uri_string;
+
+        if (!rowcol_from_event(event, &col, &row))
+                return NULL;
+
+        row_data = find_row_data(row);
+        if (!row_data)
+                return NULL;
+        cell = _vte_row_data_get (row_data, col);
+        if (!cell)
+                return NULL;
+
+        uri_string = _vte_ring_get_hyperlink(m_screen->row_data, cell->attr.hyperlink.id);
+
+        if (uri_string)
+                return g_strndup(uri_string->str, uri_string->len);
+        else
+                return NULL;
+}
+
 bool
 VteTerminalPrivate::regex_match_check_extra(GdkEvent *event,
                                             VteRegex **regexes,
@@ -5985,7 +6011,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.id  == attr2->hyperlink.id);
 }
 
 /*
@@ -8561,6 +8588,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,
@@ -8579,9 +8607,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);
 	}
 
@@ -8614,7 +8644,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;
@@ -8648,7 +8678,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,
@@ -8879,6 +8918,7 @@ VteTerminalPrivate::draw_cells_with_attributes(struct _vte_draw_text_request *it
 					cells[j].attr.italic,
 					cells[j].attr.underline,
 					cells[j].attr.strikethrough,
+					cells[j].attr.hyperlink.id != 0,
 					FALSE, FALSE, column_width, height);
 		j += g_unichar_to_utf8(items[i].c, scratch_buf);
 	}
@@ -8905,7 +8945,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;
@@ -9040,7 +9081,8 @@ VteTerminalPrivate::draw_rows(VteScreen *screen_,
 			while (cell->c == 0 || cell->attr.invisible ||
 					(cell->c == ' ' &&
 					 !cell->attr.underline &&
-					 !cell->attr.strikethrough) ||
+					 !cell->attr.strikethrough &&
+					 cell->attr.hyperlink.id == 0) ||
 					cell->attr.fragment) {
 				if (++i >= end_column) {
 					goto fg_skip_row;
@@ -9055,6 +9097,7 @@ VteTerminalPrivate::draw_rows(VteScreen *screen_,
 			determine_colors(cell, selected, &fore, &back);
 			underline = cell->attr.underline;
 			strikethrough = cell->attr.strikethrough;
+			hyperlink = (cell->attr.hyperlink.id != 0);
 			bold = cell->attr.bold;
 			italic = cell->attr.italic;
 			if (m_show_match) {
@@ -9089,7 +9132,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++;
@@ -9121,6 +9164,10 @@ VteTerminalPrivate::draw_rows(VteScreen *screen_,
 					if (nstrikethrough != strikethrough) {
 						break;
 					}
+					nhyperlink = (cell->attr.hyperlink.id != 0);
+					if (nhyperlink != hyperlink) {
+						break;
+					}
 					/* Break up matched/not-matched text. */
 					nhilite = false;
 					if (m_show_match) {
@@ -9172,7 +9219,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
@@ -9377,6 +9424,7 @@ VteTerminalPrivate::paint_cursor()
                                                         cell->attr.italic,
                                                         cell->attr.underline,
                                                         cell->attr.strikethrough,
+                                                        cell->attr.hyperlink.id != 0,
                                                         FALSE,
                                                         FALSE,
                                                         width,
@@ -9463,6 +9511,7 @@ VteTerminalPrivate::paint_im_preedit_string()
 						FALSE,
 						FALSE,
 						FALSE,
+						FALSE,
 						TRUE,
 						width, height);
 		}
diff --git a/src/vte/vteterminal.h b/src/vte/vteterminal.h
index 3b22c4a..bb1b50f 100644
--- a/src/vte/vteterminal.h
+++ b/src/vte/vteterminal.h
@@ -371,6 +371,9 @@ char *vte_terminal_match_check_event(VteTerminal *terminal,
                                      GdkEvent *event,
                                      int *tag) _VTE_GNUC_NONNULL(1) _VTE_GNUC_NONNULL(2) G_GNUC_MALLOC;
 _VTE_PUBLIC
+char *vte_terminal_get_hyperlink(VteTerminal *terminal,
+                                 GdkEvent *event) _VTE_GNUC_NONNULL(1) _VTE_GNUC_NONNULL(2) G_GNUC_MALLOC;
+_VTE_PUBLIC
 gboolean vte_terminal_event_check_regex_simple(VteTerminal *terminal,
                                                GdkEvent *event,
                                                VteRegex **regexes,
diff --git a/src/vteapp.c b/src/vteapp.c
index ac321be..f0233a0 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_get_hyperlink(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);
diff --git a/src/vtedefines.hh b/src/vtedefines.hh
index 7f56291..432eff8 100644
--- a/src/vtedefines.hh
+++ b/src/vtedefines.hh
@@ -93,3 +93,12 @@
 
 #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.
+ * Make sure there are enough bits to store this in VteCellAttr.hyperlink.length */
+#define VTE_HYPERLINK_LENGTH_MAX        2083
+/* Maximum number of URIs in the ring for a given screen (as in "normal" vs "alternate" screen)
+ * at a time. A few more technical IDs are also required.
+ * Make sure there are enough bits to store this in VteCellAttr.hyperlink.id */
+#define VTE_HYPERLINK_COUNT_MAX         4092
diff --git a/src/vtegtk.cc b/src/vtegtk.cc
index 21282e1..3e105f0 100644
--- a/src/vtegtk.cc
+++ b/src/vtegtk.cc
@@ -1804,6 +1804,31 @@ vte_terminal_match_check_event(VteTerminal *terminal,
 }
 
 /**
+ * vte_terminal_get_hyperlink:
+ * @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, or not being
+ * a valid UTF-8 string.
+ *
+ * Returns: (transfer full): a newly allocated string containing the target of the hyperlink
+ *
+ * Since: 0.50
+ */
+char *
+vte_terminal_get_hyperlink(VteTerminal *terminal,
+                           GdkEvent *event)
+{
+        g_return_val_if_fail(VTE_IS_TERMINAL(terminal), FALSE);
+        return IMPL(terminal)->get_hyperlink(event);
+}
+
+/**
  * vte_terminal_event_check_regex_simple:
  * @terminal: a #VteTerminal
  * @event: a #GdkEvent
diff --git a/src/vteinternal.hh b/src/vteinternal.hh
index ff5b93c..57bdfb0 100644
--- a/src/vteinternal.hh
+++ b/src/vteinternal.hh
@@ -690,6 +690,7 @@ public:
                         bool italic,
                         bool underline,
                         bool strikethrough,
+                        bool hyperlink,
                         bool hilite,
                         bool boxed,
                         int column_width,
@@ -968,6 +969,8 @@ public:
         void match_hilite_update(vte::view::coords const& pos);
         void match_hilite(vte::view::coords const& pos);
 
+	char *get_hyperlink(GdkEvent *event);
+
         bool rowcol_from_event(GdkEvent *event,
                                long *column,
                                long *row);
@@ -1173,6 +1176,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* uri /* adopted */);
         inline void set_keypad_mode(VteKeymode mode);
         inline void seq_erase_in_display(long param);
         inline void seq_erase_in_line(long param);
diff --git a/src/vterowdata.h b/src/vterowdata.h
index 2e73fd4..72abfce 100644
--- a/src/vterowdata.h
+++ b/src/vterowdata.h
@@ -59,9 +59,17 @@ 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 */
+	union {
+		guint16 id: 12;      /* a unique ID at a time for the ring's cells,
+		                        0 means not a hyperlink, actual hyperlinks have
+		                        IDs between 1 and VTE_HYPERLINK_COUNT_MAX, inclusive */
+		guint16 length: 12;  /* length of the URI in attr_stream */
+	} hyperlink;
+	guint64 padding_unused_2: 48;  /* should be 52, the union does some padding, arrrgh */
 } VteCellAttr;
-G_STATIC_ASSERT (sizeof (VteCellAttr) == 8);
+G_STATIC_ASSERT (sizeof (VteCellAttr) == 16);
 
 /*
  * VteCell: A single cell's data
@@ -71,7 +79,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 +98,10 @@ static const VteCell basic_cell = {
 		0, /* blink */
 		0, /* half */
 
-		0  /* invisible */
+		0, /* invisible */
+		0, /* padding_unused_1 */
+		0, /* hyperlink.id */
+		0, /* padding_unused_2 */
 	}
 };
 
diff --git a/src/vteseq-n.gperf b/src/vteseq-n.gperf
index 573f045..3e60835 100644
--- a/src/vteseq-n.gperf
+++ b/src/vteseq-n.gperf
@@ -169,5 +169,6 @@ struct vteseq_n_struct {
 #"reset-mouse-cursor-foreground-color", VTE_SEQUENCE_HANDLER_NULL
 "set-current-directory-uri", VTE_SEQUENCE_HANDLER(vte_sequence_handler_set_current_directory_uri)
 "set-current-file-uri", VTE_SEQUENCE_HANDLER(vte_sequence_handler_set_current_file_uri)
+"set-current-hyperlink", VTE_SEQUENCE_HANDLER(vte_sequence_handler_set_current_hyperlink)
 "urxvt-777", VTE_SEQUENCE_HANDLER(vte_sequence_handler_urxvt_777)
 "iterm2-1337", VTE_SEQUENCE_HANDLER(vte_sequence_handler_iterm2_1337)
diff --git a/src/vteseq.cc b/src/vteseq.cc
index 663833a..9a4b63a 100644
--- a/src/vteseq.cc
+++ b/src/vteseq.cc
@@ -385,13 +385,18 @@ VteTerminalPrivate::seq_normal_screen()
 void
 VteTerminalPrivate::seq_switch_screen(VteScreen *new_screen)
 {
-        /* if (new_screen == m_screen) return; ? */
+        /* if (new_screen == m_screen) return; ? -- yes please */
 
         /* cursor.row includes insert_delta, adjust accordingly */
         auto cr = m_screen->cursor.row - m_screen->insert_delta;
         m_screen = new_screen;
         m_screen->cursor.row = cr + m_screen->insert_delta;
 
+        /* The two screens use different hyperlink pools, so carrying on the ID
+         * wouldn't make sense and could lead to crashes.
+         * Ideally we'd carry the target URI itself, but I'm just lazy. */
+        m_defaults.attr.hyperlink.id = 0;
+
         /* Make sure the ring is large enough */
         ensure_row();
 }
@@ -2087,6 +2092,7 @@ vte_sequence_handler_character_attributes (VteTerminalPrivate *that, GValueArray
 	unsigned int i;
 	GValue *value;
 	long param;
+	guint hyperlink_id_save;
 	/* The default parameter is zero. */
 	param = 0;
 	/* Step through each numeric parameter. */
@@ -2127,7 +2133,9 @@ vte_sequence_handler_character_attributes (VteTerminalPrivate *that, GValueArray
 		param = g_value_get_long(value);
 		switch (param) {
 		case 0:
+			hyperlink_id_save = that->m_defaults.attr.hyperlink.id;  // FIXME HACK
 			that->reset_default_attributes();
+			that->m_defaults.attr.hyperlink.id = hyperlink_id_save;  // FIXME HACK
 			break;
 		case 1:
                         that->m_defaults.attr.bold = 1;
@@ -2271,7 +2279,9 @@ vte_sequence_handler_character_attributes (VteTerminalPrivate *that, GValueArray
 	}
 	/* If we had no parameters, default to the defaults. */
 	if (i == 0) {
+		hyperlink_id_save = that->m_defaults.attr.hyperlink.id;  // FIXME HACK
 		that->reset_default_attributes();
+		that->m_defaults.attr.hyperlink.id = hyperlink_id_save;  // FIXME HACK
 	}
 	/* Save the new colors. */
         that->m_color_defaults.attr.fore = that->m_defaults.attr.fore;
@@ -2450,6 +2460,38 @@ VteTerminalPrivate::set_current_file_uri_changed(char* uri /* adopted */)
         m_current_file_uri_changed = uri;
 }
 
+static void
+vte_sequence_handler_set_current_hyperlink (VteTerminalPrivate *that, GValueArray *params)
+{
+        GValue *value;
+        char *uri;
+
+        uri = NULL;
+        if (params != NULL && params->n_values > 0) {
+                value = g_value_array_get_nth(params, 0);
+
+                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(uri);
+}
+
+void
+VteTerminalPrivate::set_current_hyperlink(char* uri /* adopted */)
+{
+        guint id;
+
+        id = _vte_ring_get_hyperlink_id(m_screen->row_data, uri);
+        m_defaults.attr.hyperlink.id = id;
+
+        g_free(uri);
+}
+
 /* Restrict the scrolling region. */
 static void
 vte_sequence_handler_set_scrolling_region_from_start (VteTerminalPrivate *that, GValueArray *params)
