diff --git a/src/vte.cc b/src/vte.cc
index 9976cf36..1280ecf3 100644
--- a/src/vte.cc
+++ b/src/vte.cc
@@ -6206,6 +6206,17 @@ VteTerminalPrivate::cellattr_to_html(VteCellAttr const* attr,
 		g_string_prepend(string, "<i>");
 		g_string_append(string, "</i>");
 	}
+        /* <u> should be inside <font> so that it inherits its color by default */
+        if (attr->underline) {
+                const char *styles[] = {"", "single", "double", "wavy"};
+                char *tag;
+
+                tag = g_strdup_printf("<u style=\"text-decoration-style:%s\">",
+                                      styles[attr->underline]);
+                g_string_prepend(string, tag);
+                g_free(tag);
+                g_string_append(string, "</u>");
+        }
 	if (attr->fore != VTE_DEFAULT_FG || attr->reverse) {
 		vte::color::rgb color;
                 char *tag;
@@ -6232,10 +6243,6 @@ VteTerminalPrivate::cellattr_to_html(VteCellAttr const* attr,
 		g_free(tag);
 		g_string_append(string, "</span>");
 	}
-	if (attr->underline) {
-		g_string_prepend(string, "<u>");
-		g_string_append(string, "</u>");
-	}
 	if (attr->strikethrough) {
 		g_string_prepend(string, "<strike>");
 		g_string_append(string, "</strike>");
@@ -7575,6 +7582,10 @@ VteTerminalPrivate::apply_font_metrics(int cell_width,
         /* FIXME take these from pango_font_metrics_get_{underline,strikethrough}_{position,thickness} */
         m_underline_position = MIN (char_spacing.top + char_ascent + m_line_thickness, cell_height - m_line_thickness);
         m_underline_thickness = m_line_thickness;
+        m_double_underline_position = MIN (char_spacing.top + char_ascent + m_line_thickness, cell_height - 2 * m_line_thickness);
+        m_double_underline_thickness = 2 * m_line_thickness;
+        m_undercurl_position = MIN (char_spacing.top + char_ascent + m_line_thickness, cell_height - _vte_draw_get_undercurl_height(cell_width, m_line_thickness));
+        m_undercurl_thickness = m_line_thickness;
         m_strikethrough_position = char_spacing.top + char_ascent - char_height / 4;
         m_strikethrough_thickness = m_line_thickness;
         m_regex_underline_position = char_spacing.top + char_height - 1;  /* FIXME */
@@ -8093,6 +8104,10 @@ VteTerminalPrivate::VteTerminalPrivate(VteTerminal *t) :
 	m_line_thickness = 1;
 	m_underline_position = 1;
         m_underline_thickness = 1;
+        m_double_underline_position = 1;
+        m_double_underline_thickness = 1;
+        m_undercurl_position = 1.;
+        m_undercurl_thickness = 1;
 	m_strikethrough_position = 1;
         m_strikethrough_thickness = 1;
         m_regex_underline_position = 1;
@@ -8893,7 +8908,7 @@ VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
                                bool draw_default_bg,
                                bool bold,
                                bool italic,
-                               bool underline,
+                               guint underline,
                                bool strikethrough,
                                bool hyperlink,
                                bool hilite,
@@ -8959,14 +8974,25 @@ VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
 			for (columns = 0; i < n && items[i].y == y; i++) {
 				columns += items[i].columns;
 			}
-			if (underline) {
+			if (underline == 1 || underline == 2) {
+                                long position = underline == 1 ? m_underline_position : m_double_underline_position;
+                                long thickness = underline == 1 ? m_underline_thickness : m_double_underline_thickness;
                                 _vte_draw_draw_line(m_draw,
                                                     x,
-                                                    y + m_underline_position,
+                                                    y + position,
                                                     x + (columns * column_width) - 1,
-                                                    y + m_underline_position + m_underline_thickness - 1,
+                                                    y + position + thickness - 1,
                                                     VTE_LINE_WIDTH,
                                                     &fg, VTE_DRAW_OPAQUE);
+                        } else if (underline == 3) {
+                                for (int j = 0; j < columns; j++) {
+                                        _vte_draw_draw_undercurl(m_draw,
+                                                                 x + j * column_width,
+                                                                 y + m_undercurl_position,
+                                                                 column_width,
+                                                                 m_undercurl_thickness,
+                                                                 &fg, VTE_DRAW_OPAQUE);
+                                }
 			}
 			if (strikethrough) {
                                 _vte_draw_draw_line(m_draw,
@@ -9251,8 +9277,8 @@ VteTerminalPrivate::draw_rows(VteScreen *screen_,
         vte::grid::row_t row, rows;
         vte::grid::column_t i, j;
         long x, y;
-	guint fore, nfore, back, nback;
-        gboolean underline, nunderline, bold, nbold, italic, nitalic,
+        guint fore, nfore, back, nback, underline, nunderline;
+        gboolean bold, nbold, italic, nitalic,
                  hyperlink, nhyperlink, hilite, nhilite,
 		 selected, nselected, strikethrough, nstrikethrough;
 	guint item_count;
diff --git a/src/vtedraw.cc b/src/vtedraw.cc
index c99d02e7..0b75cb0a 100644
--- a/src/vtedraw.cc
+++ b/src/vtedraw.cc
@@ -1656,3 +1656,49 @@ _vte_draw_draw_line(struct _vte_draw *draw,
                                  MAX(line_width, xp - x + 1), MAX(line_width, yp - y + 1),
                                  color, alpha);
 }
+
+static inline double
+_vte_draw_get_undercurl_rad(gint width)
+{
+        return width / 2. / sqrt(2);
+}
+
+static inline double
+_vte_draw_get_undercurl_arc_height(gint width)
+{
+        return _vte_draw_get_undercurl_rad(width) * (1. - sqrt(2) / 2.);
+}
+
+double
+_vte_draw_get_undercurl_height(gint width, int line_width)
+{
+        return 2 * _vte_draw_get_undercurl_arc_height(width) + line_width;
+}
+
+void
+_vte_draw_draw_undercurl(struct _vte_draw *draw,
+                         gint x, double y, gint width,
+                         int line_width,
+                         vte::color::rgb const *color, double alpha)
+{
+        double rad = _vte_draw_get_undercurl_rad(width);
+        double yc = y + _vte_draw_get_undercurl_height(width, line_width) / 2;
+
+        g_assert(draw->cr);
+
+        _vte_debug_print (VTE_DEBUG_DRAW,
+                        "draw_undercurl (%d, %f, %d, color=(%d,%d,%d,%.3f))\n",
+                        x,y,width,
+                        color->red, color->green, color->blue,
+                        alpha);
+
+        cairo_set_operator (draw->cr, CAIRO_OPERATOR_OVER);
+        cairo_new_sub_path(draw->cr);
+        /* First quarter circle, similar to the left half of the tilde symbol. */
+        cairo_arc (draw->cr, x + width / 4., yc + width / 4., rad, M_PI * 5 / 4, M_PI * 7 / 4);
+        /* Second quarter circle, similar to the right half of the tilde symbol. */
+        cairo_arc_negative (draw->cr, x + width * 3 / 4., yc - width / 4., rad, M_PI * 3 / 4, M_PI / 4);
+        _vte_draw_set_source_color_alpha (draw, color, alpha);
+        cairo_set_line_width (draw->cr, line_width);
+        cairo_stroke (draw->cr);
+}
diff --git a/src/vtedraw.hh b/src/vtedraw.hh
index 505d71bf..745b8413 100644
--- a/src/vtedraw.hh
+++ b/src/vtedraw.hh
@@ -93,6 +93,15 @@ void _vte_draw_draw_line(struct _vte_draw *draw,
                          int line_width,
                          vte::color::rgb const *color, double alpha);
 
+double
+_vte_draw_get_undercurl_height(gint width, int line_width);
+
+void
+_vte_draw_draw_undercurl(struct _vte_draw *draw,
+                         gint x, double y, gint width,
+                         int line_width,
+                         vte::color::rgb const *color, double alpha);
+
 G_END_DECLS
 
 #endif
diff --git a/src/vteinternal.hh b/src/vteinternal.hh
index bff62e01..e3733b29 100644
--- a/src/vteinternal.hh
+++ b/src/vteinternal.hh
@@ -653,6 +653,10 @@ public:
         long m_line_thickness;
         long m_underline_position;
         long m_underline_thickness;
+        long m_double_underline_position;
+        long m_double_underline_thickness;
+        double m_undercurl_position;
+        long m_undercurl_thickness;
         long m_strikethrough_position;
         long m_strikethrough_thickness;
         long m_regex_underline_position;
@@ -835,7 +839,7 @@ public:
                         bool draw_default_bg,
                         bool bold,
                         bool italic,
-                        bool underline,
+                        guint underline,
                         bool strikethrough,
                         bool hyperlink,
                         bool hilite,
diff --git a/src/vterowdata.h b/src/vterowdata.h
index 72412b41..fd88af73 100644
--- a/src/vterowdata.h
+++ b/src/vterowdata.h
@@ -54,7 +54,7 @@ typedef struct _VteCellAttr {
 	/* 4-byte boundary */
 	guint64 back: 25;	/* see vtedefines.hh */
 
-	guint64 underline: 1;
+	guint64 underline: 2;   /* 0: none, 1: single, 2: double, 3: curly */
 	guint64 strikethrough: 1;
 
 	guint64 reverse: 1;
@@ -62,7 +62,6 @@ typedef struct _VteCellAttr {
 	guint64 dim: 1;		/* also known as faint, half intensity etc. */
 
 	guint64 invisible: 1;
-        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
@@ -71,7 +70,7 @@ typedef struct _VteCellAttr {
                                    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;
+        guint32 padding_unused_1;
 } VteCellAttr;
 G_STATIC_ASSERT (sizeof (VteCellAttr) == 16);
 G_STATIC_ASSERT (offsetof (VteCellAttr, hyperlink_idx) == VTE_CELL_ATTR_COMMON_BYTES);
@@ -123,9 +122,8 @@ static const VteCell basic_cell = {
 		0, /* half */
 
                 0, /* invisible */
-                0, /* padding_unused_1 */
                 0, /* hyperlink_idx */
-                0, /* padding_unused_2 */
+                0, /* padding_unused_1 */
 	}
 };
 
diff --git a/src/vteseq.cc b/src/vteseq.cc
index 6ac3cbb3..23b345ad 100644
--- a/src/vteseq.cc
+++ b/src/vteseq.cc
@@ -1860,24 +1860,34 @@ VteTerminalPrivate::seq_character_attributes(vte::parser::Params const& params)
 		if (G_UNLIKELY(params.has_subparams_at_unchecked(i))) {
                         auto subparams = params.subparams_at_unchecked(i);
 
-                        long param0;
+                        long param0, param1;
                         if (G_UNLIKELY(!subparams.number_at(0, param0)))
                                 continue;
-			if (G_UNLIKELY (param0 != 38 && param0 != 48))
-				continue;
 
-			unsigned int index = 1;
-			auto color = parse_sgr_38_48_parameters(subparams, &index);
-			/* Bail out on additional colon-separated values. */
-			if (G_UNLIKELY(index != subparams.size() - 1))
-				continue;
-			if (G_LIKELY (color != -1)) {
-				if (param0 == 38) {
-                                        m_defaults.attr.fore = color;
-				} else {
-                                        m_defaults.attr.back = color;
-				}
-			}
+                        switch (param0) {
+                        case 4:
+                                if (subparams.number_at(1, param1) && param1 >= 0 && param1 <= 3)
+				        m_defaults.attr.underline = param1;
+                                break;
+                        case 38:
+                        case 48:
+                        {
+                                unsigned int index = 1;
+                                auto color = parse_sgr_38_48_parameters(subparams, &index);
+                                /* Bail out on additional colon-separated values. */
+                                if (G_UNLIKELY(index != subparams.size() - 1))
+                                        continue;
+                                if (G_LIKELY (color != -1)) {
+                                        if (param0 == 38) {
+                                                m_defaults.attr.fore = color;
+                                        } else {
+                                                m_defaults.attr.back = color;
+                                        }
+                                }
+                                break;
+                        }
+                        }
+
 			continue;
 		}
 		/* If this parameter is not a number either, skip it. */
@@ -1913,7 +1923,9 @@ VteTerminalPrivate::seq_character_attributes(vte::parser::Params const& params)
 		case 9:
                         m_defaults.attr.strikethrough = 1;
 			break;
-		case 21: /* Error in old versions of linux console. */
+                case 21:
+                        m_defaults.attr.underline = 2;
+                        break;
 		case 22: /* ECMA 48. */
                         m_defaults.attr.bold = 0;
                         m_defaults.attr.dim = 0;
