[03/16] Introduce ui_file_style

Message ID 20181128001435.12703-4-tom@tromey.com
State New
Headers show
Series
  • Add styling to the gdb CLI and TUI
Related show

Commit Message

Tom Tromey Nov. 28, 2018, 12:14 a.m.
This introduces the new ui_file_style class and various helpers.  This
class represents a terminal style and provides methods for parsing and
emitting the corresponding ANSI terminal escape sequences.

gdb/ChangeLog
2018-11-27  Tom Tromey  <tom@tromey.com>

	* unittests/style-selftests.c: New file.
	* ui-style.c: New file.
	* ui-style.h: New file.
	* ui-file.h: Include ui-style.h.
	* Makefile.in (COMMON_SFILES): Add ui-style.c.
	(HFILES_NO_SRCDIR): Add ui-style.h.
	(SUBDIR_UNITTESTS_SRCS): Add style-selftests.c.
---
 gdb/ChangeLog                   |  10 +
 gdb/Makefile.in                 |   3 +
 gdb/ui-file.h                   |   1 +
 gdb/ui-style.c                  | 412 ++++++++++++++++++++++++++++++++
 gdb/ui-style.h                  | 215 +++++++++++++++++
 gdb/unittests/style-selftests.c | 109 +++++++++
 6 files changed, 750 insertions(+)
 create mode 100644 gdb/ui-style.c
 create mode 100644 gdb/ui-style.h
 create mode 100644 gdb/unittests/style-selftests.c

-- 
2.17.2

Comments

Joel Brobecker Dec. 24, 2018, 3:40 a.m. | #1
On Tue, Nov 27, 2018 at 05:14:22PM -0700, Tom Tromey wrote:
> This introduces the new ui_file_style class and various helpers.  This

> class represents a terminal style and provides methods for parsing and

> emitting the corresponding ANSI terminal escape sequences.

> 

> gdb/ChangeLog

> 2018-11-27  Tom Tromey  <tom@tromey.com>

> 

> 	* unittests/style-selftests.c: New file.

> 	* ui-style.c: New file.

> 	* ui-style.h: New file.

> 	* ui-file.h: Include ui-style.h.

> 	* Makefile.in (COMMON_SFILES): Add ui-style.c.

> 	(HFILES_NO_SRCDIR): Add ui-style.h.

> 	(SUBDIR_UNITTESTS_SRCS): Add style-selftests.c.


FWIW, this looks good overall. I have one minor comment, and
a question or two...

Having the unit tests is absolutely awesome!

> +/* See ui-style.h.  */

> +

> +void

> +ui_file_style::color::get_rgb (uint8_t *rgb) const

> +{

> +  if (m_simple)

> +    {

> +      /* Can't call this for a basic color.  */


I am trying to understand this comment, as well as the corresponding
comment describing this method (in ui-style.h):

+       This may not be called for simple colors or for the "NONE"
+       color.  */

Does it look like you can, in fact, call this method on
a simple color?

> +  ui_file_style ()

> +  {

> +  }


Sorry for the obvious C++ question - Does this constructor means
that the default constructor results in an object with undefined
data? Or is that the same as...

    ui_file_style () = default;

It seems that it would be the latter case, but then I don't see
why we would want that...

> +/* Skip an ANSI escape sequence in BUF.  BUF must begin with an ESC

> +   character.  Return true if an escape sequence was successfully

> +   skipped; false otherwise.  In either case, N_READ is updated to

> +   reflect the number of chars read from BUF.  */

> +

> +extern bool skip_ansi_escape (const char *buf, int *bytes);


I think there might be a copy-pasto in the comment: N_READ -> BYTES?

-- 
Joel
Tom Tromey Dec. 28, 2018, 6:54 p.m. | #2
>>>>> "Joel" == Joel Brobecker <brobecker@adacore.com> writes:


>> +      /* Can't call this for a basic color.  */


Joel> I am trying to understand this comment, as well as the corresponding
Joel> comment describing this method (in ui-style.h):

Joel> +       This may not be called for simple colors or for the "NONE"
Joel> +       color.  */

Joel> Does it look like you can, in fact, call this method on
Joel> a simple color?

Yeah.  The naming was confusing here, so I have cleaned it up by
renaming "enum simple_color" to "enum basic_color".  You can call this
method for any "simple" (scalar) color that isn't a basic color or NONE,
simply because we don't have RGB values for these.

>> +  ui_file_style ()

>> +  {

>> +  }


Joel> Sorry for the obvious C++ question - Does this constructor means
Joel> that the default constructor results in an object with undefined
Joel> data? Or is that the same as...

Joel>     ui_file_style () = default;

It's the same, I have changed the code to use the "= default" form.

Joel> I think there might be a copy-pasto in the comment: N_READ -> BYTES?

I fixed this.

Tom

Patch

diff --git a/gdb/Makefile.in b/gdb/Makefile.in
index 3be058f052..ce0d799b7d 100644
--- a/gdb/Makefile.in
+++ b/gdb/Makefile.in
@@ -426,6 +426,7 @@  SUBDIR_UNITTESTS_SRCS = \
 	unittests/scoped_mmap-selftests.c \
 	unittests/scoped_restore-selftests.c \
 	unittests/string_view-selftests.c \
+	unittests/style-selftests.c \
 	unittests/tracepoint-selftests.c \
 	unittests/unpack-selftests.c \
 	unittests/utils-selftests.c \
@@ -1124,6 +1125,7 @@  COMMON_SFILES = \
 	typeprint.c \
 	ui-file.c \
 	ui-out.c \
+	ui-style.c \
 	user-regs.c \
 	utils.c \
 	valarith.c \
@@ -1397,6 +1399,7 @@  HFILES_NO_SRCDIR = \
 	typeprint.h \
 	ui-file.h \
 	ui-out.h \
+	ui-style.h \
 	user-regs.h \
 	utils.h \
 	valprint.h \
diff --git a/gdb/ui-file.h b/gdb/ui-file.h
index 2cf5f83d47..b780dff4d5 100644
--- a/gdb/ui-file.h
+++ b/gdb/ui-file.h
@@ -20,6 +20,7 @@ 
 #define UI_FILE_H
 
 #include <string>
+#include "ui-style.h"
 
 /* The abstract ui_file base class.  */
 
diff --git a/gdb/ui-style.c b/gdb/ui-style.c
new file mode 100644
index 0000000000..a6e102e7b2
--- /dev/null
+++ b/gdb/ui-style.c
@@ -0,0 +1,412 @@ 
+/* Styling for ui_file
+   Copyright (C) 2018 Free Software Foundation, Inc.
+
+   This file is part of GDB.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+#include "defs.h"
+#include "ui-style.h"
+
+/* A regular expression that is used for matching ANSI terminal escape
+   sequences.  */
+
+static const char *ansi_regex_text =
+  /* Introduction.  */
+  "^\033\\["
+#define DATA_SUBEXP 1
+  /* Capture parameter and intermediate bytes.  */
+  "("
+  /* Parameter bytes.  */
+  "[\x30-\x3f]*"
+  /* Intermediate bytes.  */
+  "[\x20-\x2f]*"
+  /* End the first capture.  */
+  ")"
+  /* The final byte.  */
+#define FINAL_SUBEXP 2
+  "([\x40-\x7e])";
+
+/* The number of subexpressions to allocate space for, including the
+   "0th" whole match subexpression.  */
+#define NUM_SUBEXPRESSIONS 3
+
+/* The compiled form of ansi_regex_text.  */
+
+static regex_t ansi_regex;
+
+/* This maps bright colors to RGB triples.  The index is the bright
+   color index, starting with bright black.  The values come from
+   xterm.  */
+
+static const uint8_t bright_colors[][3] = {
+  { 127, 127, 127 },		/* Black.  */
+  { 255, 0, 0 },		/* Red.  */
+  { 0, 255, 0 },		/* Green.  */
+  { 255, 255, 0 },		/* Yellow.  */
+  { 92, 92, 255 },		/* Blue.  */
+  { 255, 0, 255 },		/* Magenta.  */
+  { 0, 255, 255 },		/* Cyan.  */
+  { 255, 255, 255 }		/* White.  */
+};
+
+/* See ui-style.h.  */
+
+bool
+ui_file_style::color::append_ansi (bool is_fg, std::string *str) const
+{
+  if (m_simple)
+    {
+      if (m_value >= BLACK && m_value <= WHITE)
+	str->append (std::to_string (m_value + (is_fg ? 30 : 40)));
+      else if (m_value > WHITE && m_value <= WHITE + 8)
+	str->append (std::to_string (m_value - WHITE + (is_fg ? 90 : 100)));
+      else if (m_value != -1)
+	{
+	  str->append (is_fg ? "38;5;" : "48;5;");
+	  str->append (std::to_string (m_value));
+	}
+      else
+	return false;
+    }
+  else
+    {
+      str->append (is_fg ? "38;2;" : "48;2;");
+      str->append (std::to_string (m_red)
+		   + ";" + std::to_string (m_green)
+		   + ";" + std::to_string (m_blue));
+    }
+  return true;
+}
+
+/* See ui-style.h.  */
+
+void
+ui_file_style::color::get_rgb (uint8_t *rgb) const
+{
+  if (m_simple)
+    {
+      /* Can't call this for a basic color.  */
+      if (m_value >= 8 && m_value <= 15)
+	memcpy (rgb, bright_colors[m_value - 8], 3 * sizeof (uint8_t));
+      else if (m_value >= 16 && m_value <= 231)
+	{
+	  int value = m_value;
+	  value -= 16;
+	  /* This obscure formula seems to be what terminals actually
+	     do.  */
+	  int component = value / 36;
+	  rgb[0] = component == 0 ? 0 : (55 + component * 40);
+	  value %= 36;
+	  component = value / 6;
+	  rgb[1] = component == 0 ? 0 : (55 + component * 40);
+	  value %= 6;
+	  rgb[2] = value == 0 ? 0 : (55 + value * 40);
+	}
+      else if (m_value >= 232)
+	{
+	  uint8_t v = (m_value - 232) * 10 + 8;
+	  rgb[0] = v;
+	  rgb[1] = v;
+	  rgb[2] = v;
+	}
+      else
+	gdb_assert_not_reached ("get_rgb called on invalid color");
+    }
+  else
+    {
+      rgb[0] = m_red;
+      rgb[1] = m_green;
+      rgb[2] = m_blue;
+    }
+}
+
+/* See ui-style.h.  */
+
+std::string
+ui_file_style::to_ansi () const
+{
+  std::string result ("\033[");
+  bool need_semi = m_foreground.append_ansi (true, &result);
+  if (!m_background.is_none ())
+    {
+      if (need_semi)
+	result.push_back (';');
+      m_background.append_ansi (false, &result);
+      need_semi = true;
+    }
+  if (m_intensity != NORMAL)
+    {
+      if (need_semi)
+	result.push_back (';');
+      result.append (std::to_string (m_intensity));
+      need_semi = true;
+    }
+  if (m_reverse)
+    {
+      if (need_semi)
+	result.push_back (';');
+      result.push_back ('7');
+    }
+  result.push_back ('m');
+  return result;
+}
+
+/* Read a ";" and a number from STRING.  Return the number of
+   characters read and put the number into *NUM.  */
+
+static bool
+read_semi_number (const char *string, int *idx, long *num)
+{
+  if (string[*idx] != ';')
+    return false;
+  ++*idx;
+  if (string[*idx] < '0' || string[*idx] > '9')
+    return false;
+  char *tail;
+  *num = strtol (string + *idx, &tail, 10);
+  *idx = tail - string;
+  return true;
+}
+
+/* A helper for ui_file_style::parse that reads an extended color
+   sequence; that is, and 8- or 24- bit color.  */
+
+static bool
+extended_color (const char *str, int *idx, ui_file_style::color *color)
+{
+  long value;
+
+  if (!read_semi_number (str, idx, &value))
+    return false;
+
+  if (value == 5)
+    {
+      /* 8-bit color.  */
+      if (!read_semi_number (str, idx, &value))
+	return false;
+
+      if (value >= 0 && value <= 255)
+	*color = ui_file_style::color (value);
+      else
+	return false;
+    }
+  else if (value == 2)
+    {
+      /* 24-bit color.  */
+      long r, g, b;
+      if (!read_semi_number (str, idx, &r)
+	  || r > 255
+	  || !read_semi_number (str, idx, &g)
+	  || g > 255
+	  || !read_semi_number (str, idx, &b)
+	  || b > 255)
+	return false;
+      *color = ui_file_style::color (r, g, b);
+    }
+  else
+    {
+      /* Unrecognized sequence.  */
+      return false;
+    }
+
+  return true;
+}
+
+/* See ui-style.h.  */
+
+bool
+ui_file_style::parse (const char *buf, size_t *n_read)
+{
+  regmatch_t subexps[NUM_SUBEXPRESSIONS];
+
+  int match = regexec (&ansi_regex, buf, ARRAY_SIZE (subexps), subexps, 0);
+  if (match == REG_NOMATCH)
+    {
+      *n_read = 0;
+      return false;
+    }
+  /* Other failures mean the regexp is broken.  */
+  gdb_assert (match == 0);
+  /* The regexp is anchored.  */
+  gdb_assert (subexps[0].rm_so == 0);
+  /* The final character exists.  */
+  gdb_assert (subexps[FINAL_SUBEXP].rm_eo - subexps[FINAL_SUBEXP].rm_so == 1);
+
+  if (buf[subexps[FINAL_SUBEXP].rm_so] != 'm')
+    {
+      /* We don't handle this sequence, so just drop it.  */
+      *n_read = subexps[0].rm_eo;
+      return false;
+    }
+
+  /* Examine each setting in the match and apply it to the result.
+     See the Select Graphic Rendition section of
+     https://en.wikipedia.org/wiki/ANSI_escape_code.  In essence each
+     code is just a number, separated by ";"; there are some more
+     wrinkles but we don't support them all..  */
+
+  /* "\033[m" means the same thing as "\033[0m", so handle that
+     specially here.  */
+  if (subexps[DATA_SUBEXP].rm_so == subexps[DATA_SUBEXP].rm_eo)
+    *this = ui_file_style ();
+
+  for (regoff_t i = subexps[DATA_SUBEXP].rm_so;
+       i < subexps[DATA_SUBEXP].rm_eo;
+       ++i)
+    {
+      if (buf[i] == ';')
+	{
+	  /* Skip.  */
+	}
+      else if (buf[i] >= '0' && buf[i] <= '9')
+	{
+	  char *tail;
+	  long value = strtol (buf + i, &tail, 10);
+	  i = tail - buf;
+
+	  switch (value)
+	    {
+	    case 0:
+	      /* Reset.  */
+	      *this = ui_file_style ();
+	      break;
+	    case 1:
+	      /* Bold.  */
+	      m_intensity = BOLD;
+	      break;
+	    case 2:
+	      /* Dim.  */
+	      m_intensity = DIM;
+	      break;
+	    case 7:
+	      /* Reverse.  */
+	      m_reverse = true;
+	      break;
+	    case 21:
+	      m_intensity = NORMAL;
+	      break;
+	    case 22:
+	      /* Normal.  */
+	      m_intensity = NORMAL;
+	      break;
+	    case 27:
+	      /* Inverse off.  */
+	      m_reverse = false;
+	      break;
+
+	    case 30:
+	    case 31:
+	    case 32:
+	    case 33:
+	    case 34:
+	    case 35:
+	    case 36:
+	    case 37:
+	      /* Note: not 38.  */
+	    case 39:
+	      m_foreground = color (value - 30);
+	      break;
+
+	    case 40:
+	    case 41:
+	    case 42:
+	    case 43:
+	    case 44:
+	    case 45:
+	    case 46:
+	    case 47:
+	      /* Note: not 48.  */
+	    case 49:
+	      m_background = color (value - 40);
+	      break;
+
+	    case 90:
+	    case 91:
+	    case 92:
+	    case 93:
+	    case 94:
+	    case 95:
+	    case 96:
+	    case 97:
+	      m_foreground = color (value - 90 + 8);
+	      break;
+
+	    case 100:
+	    case 101:
+	    case 102:
+	    case 103:
+	    case 104:
+	    case 105:
+	    case 106:
+	    case 107:
+	      m_background = color (value - 100 + 8);
+	      break;
+
+	    case 38:
+	      /* If we can't parse the extended color, fail.  */
+	      if (!extended_color (buf, &i, &m_foreground))
+		{
+		  *n_read = subexps[0].rm_eo;
+		  return false;
+		}
+	      break;
+
+	    case 48:
+	      /* If we can't parse the extended color, fail.  */
+	      if (!extended_color (buf, &i, &m_background))
+		{
+		  *n_read = subexps[0].rm_eo;
+		  return false;
+		}
+	      break;
+
+	    default:
+	      /* Ignore everything else.  */
+	      break;
+	    }
+	}
+      else
+	{
+	  /* Unknown, let's just ignore.  */
+	}
+    }
+
+  *n_read = subexps[0].rm_eo;
+  return true;
+}
+
+/* See ui-style.h.  */
+
+bool
+skip_ansi_escape (const char *buf, int *bytes)
+{
+  regmatch_t subexps[NUM_SUBEXPRESSIONS];
+
+  int match = regexec (&ansi_regex, buf, ARRAY_SIZE (subexps), subexps, 0);
+  if (match == REG_NOMATCH || buf[subexps[FINAL_SUBEXP].rm_so] != 'm')
+    return false;
+
+  *bytes = subexps[FINAL_SUBEXP].rm_eo;
+  return true;
+}
+
+void
+_initialize_ui_style ()
+{
+  int code = regcomp (&ansi_regex, ansi_regex_text, REG_EXTENDED);
+  /* If the regular expression was incorrect, it was a programming
+     error.  */
+  gdb_assert (code == 0);
+}
diff --git a/gdb/ui-style.h b/gdb/ui-style.h
new file mode 100644
index 0000000000..d08139cef2
--- /dev/null
+++ b/gdb/ui-style.h
@@ -0,0 +1,215 @@ 
+/* Styling for ui_file
+   Copyright (C) 2018 Free Software Foundation, Inc.
+
+   This file is part of GDB.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+#ifndef UI_STYLE_H
+#define UI_STYLE_H
+
+/* Styles that can be applied to a ui_file.  */
+struct ui_file_style
+{
+  /* One of the simple colors that can be handled by ANSI
+     terminals.  */
+  enum simple_color
+  {
+    NONE = -1,
+    BLACK,
+    RED,
+    GREEN,
+    YELLOW,
+    BLUE,
+    MAGENTA,
+    CYAN,
+    WHITE
+  };
+
+  /* Representation of a terminal color.  */
+  class color
+  {
+  public:
+
+    color (simple_color c)
+      : m_simple (true),
+	m_value (c)
+    {
+    }
+
+    color (int c)
+      : m_simple (true),
+	m_value (c)
+    {
+      gdb_assert (c >= -1 && c <= 255);
+    }
+
+    color (uint8_t r, uint8_t g, uint8_t b)
+      : m_simple (false),
+	m_red (r),
+	m_green (g),
+	m_blue (b)
+    {
+    }
+
+    bool operator== (const color &other) const
+    {
+      if (m_simple != other.m_simple)
+	return false;
+      if (m_simple)
+	return m_value == other.m_value;
+      return (m_red == other.m_red && m_green == other.m_green
+	      && m_blue == other.m_blue);
+    }
+
+    bool operator< (const color &other) const
+    {
+      if (m_simple != other.m_simple)
+	return m_simple < other.m_simple;
+      if (m_simple)
+	return m_value < other.m_value;
+      if (m_red < other.m_red)
+	return true;
+      if (m_red == other.m_red)
+	{
+	  if (m_green < other.m_green)
+	    return true;
+	  if (m_green == other.m_green)
+	    return m_blue < other.m_blue;
+	}
+      return false;
+    }
+
+    /* Return true if this is the "NONE" color, false otherwise.  */
+    bool is_none () const
+    {
+      return m_simple && m_value == NONE;
+    }
+
+    /* Return true if this is one of the basic colors, false
+       otherwise.  */
+    bool is_basic () const
+    {
+      return m_simple && m_value >= BLACK && m_value <= WHITE;
+    }
+
+    /* Return the value of a basic color.  */
+    int get_value () const
+    {
+      gdb_assert (is_basic ());
+      return m_value;
+    }
+
+    /* Fill in RGB with the red/green/blue values for this color.
+       This may not be called for simple colors or for the "NONE"
+       color.  */
+    void get_rgb (uint8_t *rgb) const;
+
+    /* Append the ANSI terminal escape sequence for this color to STR.
+       IS_FG indicates whether this is a foreground or background
+       color.  Returns true if any characters were written; returns
+       false otherwise (which can only happen for the "NONE"
+       color).  */
+    bool append_ansi (bool is_fg, std::string *str) const;
+
+  private:
+
+    bool m_simple;
+    int m_value;
+    uint8_t m_red, m_green, m_blue;
+  };
+
+  /* Intensity settings that are available.  */
+  enum intensity
+  {
+    NORMAL = 0,
+    BOLD,
+    DIM
+  };
+
+  ui_file_style ()
+  {
+  }
+
+  ui_file_style (color f, color b, intensity i = NORMAL)
+    : m_foreground (f),
+      m_background (b),
+      m_intensity (i)
+  {
+  }
+
+  bool operator== (const ui_file_style &other) const
+  {
+    return (m_foreground == other.m_foreground
+	    && m_background == other.m_background
+	    && m_intensity == other.m_intensity
+	    && m_reverse == other.m_reverse);
+  }
+
+  bool operator!= (const ui_file_style &other) const
+  {
+    return !(*this == other);
+  }
+
+  /* Return the ANSI escape sequence for this style.  */
+  std::string to_ansi () const;
+
+  /* Return true if this style specified reverse display; false
+     otherwise.  */
+  bool is_reverse () const
+  {
+    return m_reverse;
+  }
+
+  /* Return the foreground color of this style.  */
+  const color &get_foreground () const
+  {
+    return m_foreground;
+  }
+
+  /* Return the background color of this style.  */
+  const color &get_background () const
+  {
+    return m_background;
+  }
+
+  /* Return the intensity of this style.  */
+  intensity get_intensity () const
+  {
+    return m_intensity;
+  }
+
+  /* Parse an ANSI escape sequence in BUF, modifying this style.  BUF
+     must begin with an ESC character.  Return true if an escape
+     sequence was successfully parsed; false otherwise.  In either
+     case, N_READ is updated to reflect the number of chars read from
+     BUF.  */
+  bool parse (const char *buf, size_t *n_read);
+
+private:
+
+  color m_foreground = NONE;
+  color m_background = NONE;
+  intensity m_intensity = NORMAL;
+  bool m_reverse = false;
+};
+
+/* Skip an ANSI escape sequence in BUF.  BUF must begin with an ESC
+   character.  Return true if an escape sequence was successfully
+   skipped; false otherwise.  In either case, N_READ is updated to
+   reflect the number of chars read from BUF.  */
+
+extern bool skip_ansi_escape (const char *buf, int *bytes);
+
+#endif /* UI_STYLE_H */
diff --git a/gdb/unittests/style-selftests.c b/gdb/unittests/style-selftests.c
new file mode 100644
index 0000000000..108ed6ccd2
--- /dev/null
+++ b/gdb/unittests/style-selftests.c
@@ -0,0 +1,109 @@ 
+/* Self tests for ui_file_style
+
+   Copyright (C) 2018 Free Software Foundation, Inc.
+
+   This file is part of GDB.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+#include "defs.h"
+#include "selftest.h"
+#include "ui-style.h"
+
+namespace selftests {
+namespace style {
+
+#define CHECK_RGB(R, G, B) \
+  SELF_CHECK (rgb[0] == (R) && rgb[1] == (G) && rgb[2] == (B))
+
+static void
+run_tests ()
+{
+  ui_file_style style;
+  size_t n_read;
+  uint8_t rgb[3];
+
+  SELF_CHECK (style.parse ("\033[m", &n_read));
+  SELF_CHECK (n_read == 3);
+  SELF_CHECK (style.get_foreground ().is_none ());
+  SELF_CHECK (style.get_background ().is_none ());
+  SELF_CHECK (style.get_intensity () == ui_file_style::NORMAL);
+  SELF_CHECK (!style.is_reverse ());
+  SELF_CHECK (style.to_ansi () == "\033[m");
+
+  style = ui_file_style ();
+  SELF_CHECK (style.parse ("\033[0m", &n_read));
+  SELF_CHECK (n_read == 4);
+  SELF_CHECK (style.get_foreground ().is_none ());
+  SELF_CHECK (style.get_background ().is_none ());
+  SELF_CHECK (style.get_intensity () == ui_file_style::NORMAL);
+  SELF_CHECK (!style.is_reverse ());
+  /* This particular case does not round-trip identically, but the
+     difference is unimportant.  */
+  SELF_CHECK (style.to_ansi () == "\033[m");
+
+  SELF_CHECK (style.parse ("\033[7m", &n_read));
+  SELF_CHECK (n_read == 4);
+  SELF_CHECK (style.get_foreground ().is_none ());
+  SELF_CHECK (style.get_background ().is_none ());
+  SELF_CHECK (style.get_intensity () == ui_file_style::NORMAL);
+  SELF_CHECK (style.is_reverse ());
+  SELF_CHECK (style.to_ansi () == "\033[7m");
+
+  style = ui_file_style ();
+  SELF_CHECK (style.parse ("\033[32;1m", &n_read));
+  SELF_CHECK (n_read == 7);
+  SELF_CHECK (style.get_foreground ().is_basic ());
+  SELF_CHECK (style.get_foreground ().get_value () == ui_file_style::GREEN);
+  SELF_CHECK (style.get_background ().is_none ());
+  SELF_CHECK (style.get_intensity () == ui_file_style::BOLD);
+  SELF_CHECK (!style.is_reverse ());
+  SELF_CHECK (style.to_ansi () == "\033[32;1m");
+
+  style = ui_file_style ();
+  SELF_CHECK (style.parse ("\033[38;5;112;48;5;249m", &n_read));
+  SELF_CHECK (n_read == 20);
+  SELF_CHECK (!style.get_foreground ().is_basic ());
+  style.get_foreground ().get_rgb (rgb);
+  CHECK_RGB (0x87, 0xd7, 0);
+  SELF_CHECK (!style.get_background ().is_basic ());
+  style.get_background ().get_rgb (rgb);
+  CHECK_RGB (0xb2, 0xb2, 0xb2);
+  SELF_CHECK (style.get_intensity () == ui_file_style::NORMAL);
+  SELF_CHECK (!style.is_reverse ());
+  SELF_CHECK (style.to_ansi () == "\033[38;5;112;48;5;249m");
+
+  style = ui_file_style ();
+  SELF_CHECK (style.parse ("\033[38;2;83;84;85;48;2;0;1;254;2;7m", &n_read));
+  SELF_CHECK (n_read == 33);
+  SELF_CHECK (!style.get_foreground ().is_basic ());
+  style.get_foreground ().get_rgb (rgb);
+  CHECK_RGB (83, 84, 85);
+  SELF_CHECK (!style.get_background ().is_basic ());
+  style.get_background ().get_rgb (rgb);
+  CHECK_RGB (0, 1, 254);
+  SELF_CHECK (style.get_intensity () == ui_file_style::DIM);
+  SELF_CHECK (style.is_reverse ());
+  SELF_CHECK (style.to_ansi () == "\033[38;2;83;84;85;48;2;0;1;254;2;7m");
+}
+
+} /* namespace style */
+} /* namespace selftests */
+
+void
+_initialize_style_selftest ()
+{
+  selftests::register_test ("style",
+			    selftests::style::run_tests);
+}