GUACAMOLE-470: Merge support for fully configurable terminal color palette.
diff --git a/src/protocols/ssh/settings.c b/src/protocols/ssh/settings.c
index 6f71d86..2ce9790 100644
--- a/src/protocols/ssh/settings.c
+++ b/src/protocols/ssh/settings.c
@@ -122,10 +122,12 @@
 #endif
 
     /**
-     * The name of the color scheme to use. Currently valid color schemes are:
-     * "black-white", "white-black", "gray-black", and "green-black", each
-     * following the "foreground-background" pattern. By default, this will be
-     * "gray-black".
+     * The color scheme to use, as a series of semicolon-separated color-value
+     * pairs: "background: <color>", "foreground: <color>", or
+     * "color<n>: <color>", where <n> is a number from 0 to 255, and <color> is
+     * "color<n>" or an X11 color code (e.g. "aqua" or "rgb:12/34/56").
+     * The color scheme can also be one of the special values: "black-white",
+     * "white-black", "gray-black", or "green-black".
      */
     IDX_COLOR_SCHEME,
 
diff --git a/src/protocols/telnet/settings.c b/src/protocols/telnet/settings.c
index dcd75b6..6e4a773 100644
--- a/src/protocols/telnet/settings.c
+++ b/src/protocols/telnet/settings.c
@@ -100,10 +100,12 @@
     IDX_FONT_SIZE,
 
     /**
-     * The name of the color scheme to use. Currently valid color schemes are:
-     * "black-white", "white-black", "gray-black", and "green-black", each
-     * following the "foreground-background" pattern. By default, this will be
-     * "gray-black".
+     * The color scheme to use, as a series of semicolon-separated color-value
+     * pairs: "background: <color>", "foreground: <color>", or
+     * "color<n>: <color>", where <n> is a number from 0 to 255, and <color> is
+     * "color<n>" or an X11 color code (e.g. "aqua" or "rgb:12/34/56").
+     * The color scheme can also be one of the special values: "black-white",
+     * "white-black", "gray-black", or "green-black".
      */
     IDX_COLOR_SCHEME,
 
diff --git a/src/terminal/display.c b/src/terminal/display.c
index 3ad1cba..9391ec1 100644
--- a/src/terminal/display.c
+++ b/src/terminal/display.c
@@ -246,7 +246,8 @@
 
 guac_terminal_display* guac_terminal_display_alloc(guac_client* client,
         const char* font_name, int font_size, int dpi,
-        guac_terminal_color* foreground, guac_terminal_color* background) {
+        guac_terminal_color* foreground, guac_terminal_color* background,
+        const guac_terminal_color (*palette)[256]) {
 
     PangoFontMap* font_map;
     PangoFont* font;
@@ -294,6 +295,7 @@
 
     display->default_foreground = display->glyph_foreground = *foreground;
     display->default_background = display->glyph_background = *background;
+    display->default_palette = palette;
 
     /* Calculate character dimensions */
     display->char_width =
@@ -317,6 +319,9 @@
 
 void guac_terminal_display_free(guac_terminal_display* display) {
 
+    /* Free default palette. */
+    free((void*) display->default_palette);
+
     /* Free operations buffers */
     free(display->operations);
 
@@ -328,6 +333,12 @@
 void guac_terminal_display_reset_palette(guac_terminal_display* display) {
 
     /* Reinitialize palette with default values */
+    if (display->default_palette) {
+        memcpy(display->palette, *display->default_palette,
+               sizeof(GUAC_TERMINAL_INITIAL_PALETTE));
+        return;
+    }
+
     memcpy(display->palette, GUAC_TERMINAL_INITIAL_PALETTE,
             sizeof(GUAC_TERMINAL_INITIAL_PALETTE));
 
diff --git a/src/terminal/named-colors.c b/src/terminal/named-colors.c
index 89deee8..b0e579b 100644
--- a/src/terminal/named-colors.c
+++ b/src/terminal/named-colors.c
@@ -786,7 +786,7 @@
             guac_terminal_named_color_search);
 
     /* Fail if no such color is found */
-    if (color == NULL)
+    if (found == NULL)
         return 1;
 
     /* Otherwise copy the found color */
diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c
index 076305a..7096add 100644
--- a/src/terminal/terminal.c
+++ b/src/terminal/terminal.c
@@ -29,7 +29,9 @@
 #include "terminal/terminal_handlers.h"
 #include "terminal/types.h"
 #include "terminal/typescript.h"
+#include "terminal/xparsecolor.h"
 
+#include <ctype.h>
 #include <errno.h>
 #include <pthread.h>
 #include <stdarg.h>
@@ -178,6 +180,9 @@
     term->tab_interval = 8;
     memset(term->custom_tabs, 0, sizeof(term->custom_tabs));
 
+    /* Reset character attributes */
+    term->current_attributes = term->default_char.attributes;
+
     /* Reset display palette */
     guac_terminal_display_reset_palette(term->display);
 
@@ -255,53 +260,255 @@
 
 }
 
+/**
+ * Compare a non-null-terminated string to a null-terminated literal, in the
+ * same manner as strcmp().
+ *
+ * @param str_start
+ *     Start of the non-null-terminated string.
+ *
+ * @param str_end
+ *     End of the non-null-terminated string, after the last character.
+ *
+ * @param literal
+ *     The null-terminated literal to compare against.
+ *
+ * @return
+ *     Zero if the two strings are equal and non-zero otherwise.
+ */
+static int guac_terminal_color_scheme_compare_token(const char* str_start,
+        const char* str_end, const char* literal) {
+
+    const int result = strncmp(literal, str_start, str_end - str_start);
+    if (result != 0)
+        return result;
+
+    /* At this point, literal is same length or longer than
+     * | str_end - str_start |, so if the two are equal, literal should
+     * have its null-terminator at | str_end - str_start |. */
+    return (int) (unsigned char) literal[str_end - str_start];
+}
+
+/**
+ * Strip the leading and trailing spaces of a bounded string.
+ *
+ * @param[in,out] str_start
+ *     Address of a pointer to the start of the string. On return, the pointer
+ *     is advanced to after any leading spaces.
+ *
+ * @param[in,out] str_end
+ *     Address of a pointer to the end of the string, after the last character.
+ *     On return, the pointer is moved back to before any trailing spaces.
+ */
+static void guac_terminal_color_scheme_strip_spaces(const char** str_start,
+        const char** str_end) {
+
+    /* Strip leading spaces. */
+    while (*str_start < *str_end && isspace(**str_start))
+        (*str_start)++;
+
+    /* Strip trailing spaces. */
+    while (*str_end > *str_start && isspace(*(*str_end - 1)))
+        (*str_end)--;
+}
+
+/**
+ * Parse the name part of the name-value pair within the color-scheme
+ * configuration.
+ *
+ * @param client
+ *     The client that the terminal is connected to.
+ *
+ * @param name_start
+ *     Start of the name string.
+ *
+ * @param name_end
+ *     End of the name string, after the last character.
+ *
+ * @param foreground
+ *     Pointer to the foreground color.
+ *
+ * @param background
+ *     Pointer to the background color.
+ *
+ * @param palette
+ *     Pointer to the palette array.
+ *
+ * @param[out] target
+ *     On return, pointer to the color struct that corresponds to the name.
+ *
+ * @return
+ *     Zero if successful or non-zero otherwise.
+ */
+static int guac_terminal_parse_color_scheme_name(guac_client* client,
+        const char* name_start, const char* name_end,
+        guac_terminal_color* foreground, guac_terminal_color* background,
+        guac_terminal_color (*palette)[256],
+        guac_terminal_color** target) {
+
+    guac_terminal_color_scheme_strip_spaces(&name_start, &name_end);
+
+    if (!guac_terminal_color_scheme_compare_token(
+            name_start, name_end, GUAC_TERMINAL_SCHEME_FOREGROUND)) {
+        *target = foreground;
+        return 0;
+    }
+
+    if (!guac_terminal_color_scheme_compare_token(
+            name_start, name_end, GUAC_TERMINAL_SCHEME_BACKGROUND)) {
+        *target = background;
+        return 0;
+    }
+
+    /* Parse color<n> value. */
+    int index = -1;
+    if (sscanf(name_start, GUAC_TERMINAL_SCHEME_NUMBERED "%d", &index) &&
+            index >= 0 && index <= 255) {
+        *target = &(*palette)[index];
+        return 0;
+    }
+
+    guac_client_log(client, GUAC_LOG_WARNING,
+                    "Unknown color name: \"%.*s\".",
+                    name_end - name_start, name_start);
+    return 1;
+}
+
+/**
+ * Parse the value part of the name-value pair within the color-scheme
+ * configuration.
+ *
+ * @param client
+ *     The client that the terminal is connected to.
+ *
+ * @param value_start
+ *     Start of the value string.
+ *
+ * @param value_end
+ *     End of the value string, after the last character.
+ *
+ * @param palette
+ *     The current color palette.
+ *
+ * @param[out] target
+ *     On return, the parsed color.
+ *
+ * @return
+ *     Zero if successful or non-zero otherwise.
+ */
+static int guac_terminal_parse_color_scheme_value(guac_client* client,
+        const char* value_start, const char* value_end,
+        const guac_terminal_color (*palette)[256],
+        guac_terminal_color* target) {
+
+    guac_terminal_color_scheme_strip_spaces(&value_start, &value_end);
+
+    /* Parse color<n> value. */
+    int index = -1;
+    if (sscanf(value_start, GUAC_TERMINAL_SCHEME_NUMBERED "%d", &index) &&
+            index >= 0 && index <= 255) {
+        *target = (*palette)[index];
+        return 0;
+    }
+
+    /* Parse X11 value. */
+    if (!guac_terminal_xparsecolor(value_start, target))
+        return 0;
+
+    guac_client_log(client, GUAC_LOG_WARNING,
+                    "Invalid color value: \"%.*s\".",
+                    value_end - value_start, value_start);
+    return 1;
+}
+
+/**
+ * Parse a color-scheme configuration string, and return specified
+ * foreground/background colors and color palette.
+ *
+ * @param client
+ *     The client that the terminal is connected to.
+ *
+ * @param color_scheme
+ *     A semicolon-separated list of name-value pairs, i.e.
+ *     "<name>: <value> [; <name>: <value> [; ...]]".
+ *     For example, "color2: rgb:cc/33/22; background: color5".
+ *
+ * @param[out] foreground
+ *     Parsed foreground color.
+ *
+ * @param[out] background
+ *     Parsed background color.
+ *
+ * @param[in,out] palette
+ *     Parsed color palette. The caller is responsible for allocating a mutable
+ *     array on entry. On return, the array contains the parsed palette.
+ */
+static void guac_terminal_parse_color_scheme(guac_client* client,
+        const char* color_scheme, guac_terminal_color* foreground,
+        guac_terminal_color* background,
+        guac_terminal_color (*palette)[256]) {
+
+    /* Set default gray-black color scheme and initial palette. */
+    *foreground = GUAC_TERMINAL_INITIAL_PALETTE[GUAC_TERMINAL_COLOR_GRAY];
+    *background = GUAC_TERMINAL_INITIAL_PALETTE[GUAC_TERMINAL_COLOR_BLACK];
+    memcpy(palette, GUAC_TERMINAL_INITIAL_PALETTE,
+            sizeof(GUAC_TERMINAL_INITIAL_PALETTE));
+
+    /* Current char being parsed, or NULL if at end of parsing. */
+    const char* cursor = color_scheme;
+
+    while (cursor) {
+        /* Start of the current "name: value" pair. */
+        const char* pair_start = cursor;
+
+        /* End of the current name-value pair. */
+        const char* pair_end = strchr(pair_start, ';');
+        if (pair_end) {
+            cursor = pair_end + 1;
+        }
+        else {
+            pair_end = pair_start + strlen(pair_start);
+            cursor = NULL;
+        }
+
+        guac_terminal_color_scheme_strip_spaces(&pair_start, &pair_end);
+        if (pair_start >= pair_end)
+            /* Allow empty pairs, which happens, e.g., when the configuration
+             * string ends in a semi-colon. */
+            continue;
+
+        /* End of the name part of the pair. */
+        const char* name_end = memchr(pair_start, ':', pair_end - pair_start);
+        if (name_end == NULL) {
+            guac_client_log(client, GUAC_LOG_WARNING,
+                            "Expecting colon: \"%.*s\".",
+                            pair_end - pair_start, pair_start);
+            return;
+        }
+
+        /* The color that the name corresponds to. */
+        guac_terminal_color* color_target = NULL;
+
+        if (guac_terminal_parse_color_scheme_name(
+                client, pair_start, name_end, foreground, background,
+                palette, &color_target))
+            return; /* Parsing failed. */
+
+        if (guac_terminal_parse_color_scheme_value(
+                client, name_end + 1, pair_end, palette, color_target))
+            return; /* Parsing failed. */
+    }
+}
+
 guac_terminal* guac_terminal_create(guac_client* client,
         const char* font_name, int font_size, int dpi,
         int width, int height, const char* color_scheme,
         const int backspace) {
 
-    int default_foreground;
-    int default_background;
-
-    /* Default to "gray-black" color scheme if no scheme provided */
-    if (color_scheme == NULL || color_scheme[0] == '\0') {
-        default_foreground = GUAC_TERMINAL_COLOR_GRAY;
-        default_background = GUAC_TERMINAL_COLOR_BLACK;
-    }
-
-    /* Otherwise, parse color scheme */
-    else if (strcmp(color_scheme, GUAC_TERMINAL_SCHEME_GRAY_BLACK) == 0) {
-        default_foreground = GUAC_TERMINAL_COLOR_GRAY;
-        default_background = GUAC_TERMINAL_COLOR_BLACK;
-    }
-    else if (strcmp(color_scheme, GUAC_TERMINAL_SCHEME_BLACK_WHITE) == 0) {
-        default_foreground = GUAC_TERMINAL_COLOR_BLACK;
-        default_background = GUAC_TERMINAL_COLOR_WHITE;
-    }
-    else if (strcmp(color_scheme, GUAC_TERMINAL_SCHEME_GREEN_BLACK) == 0) {
-        default_foreground = GUAC_TERMINAL_COLOR_DARK_GREEN;
-        default_background = GUAC_TERMINAL_COLOR_BLACK;
-    }
-    else if (strcmp(color_scheme, GUAC_TERMINAL_SCHEME_WHITE_BLACK) == 0) {
-        default_foreground = GUAC_TERMINAL_COLOR_WHITE;
-        default_background = GUAC_TERMINAL_COLOR_BLACK;
-    }
-
-    /* If invalid, default to "gray-black" */
-    else {
-        guac_client_log(client, GUAC_LOG_WARNING,
-                "Invalid color scheme: \"%s\". Defaulting to \"gray-black\".",
-                color_scheme);
-        default_foreground = GUAC_TERMINAL_COLOR_GRAY;
-        default_background = GUAC_TERMINAL_COLOR_BLACK;
-    }
-
     /* Build default character using default colors */
     guac_terminal_char default_char = {
         .value = 0,
         .attributes = {
-            .foreground  = GUAC_TERMINAL_INITIAL_PALETTE[default_foreground],
-            .background  = GUAC_TERMINAL_INITIAL_PALETTE[default_background],
             .bold        = false,
             .half_bright = false,
             .reverse     = false,
@@ -310,6 +517,32 @@
         .width = 1
     };
 
+    /* Initialized by guac_terminal_parse_color_scheme. */
+    guac_terminal_color (*default_palette)[256] = (guac_terminal_color(*)[256])
+            malloc(sizeof(guac_terminal_color[256]));
+
+    /* Special cases. */
+    if (color_scheme == NULL || color_scheme[0] == '\0') {
+        /* guac_terminal_parse_color_scheme defaults to gray-black */
+    }
+    else if (strcmp(color_scheme, GUAC_TERMINAL_SCHEME_GRAY_BLACK) == 0) {
+        color_scheme = "foreground:color7;background:color0";
+    }
+    else if (strcmp(color_scheme, GUAC_TERMINAL_SCHEME_BLACK_WHITE) == 0) {
+        color_scheme = "foreground:color0;background:color15";
+    }
+    else if (strcmp(color_scheme, GUAC_TERMINAL_SCHEME_GREEN_BLACK) == 0) {
+        color_scheme = "foreground:color2;background:color0";
+    }
+    else if (strcmp(color_scheme, GUAC_TERMINAL_SCHEME_WHITE_BLACK) == 0) {
+        color_scheme = "foreground:color15;background:color0";
+    }
+
+    guac_terminal_parse_color_scheme(client, color_scheme,
+                                     &default_char.attributes.foreground,
+                                     &default_char.attributes.background,
+                                     default_palette);
+
     /* Calculate available display area */
     int available_width = width - GUAC_TERMINAL_SCROLLBAR_WIDTH;
     if (available_width < 0)
@@ -332,7 +565,8 @@
     term->display = guac_terminal_display_alloc(client,
             font_name, font_size, dpi,
             &default_char.attributes.foreground,
-            &default_char.attributes.background);
+            &default_char.attributes.background,
+            default_palette);
 
     /* Fail if display init failed */
     if (term->display == NULL) {
diff --git a/src/terminal/terminal/display.h b/src/terminal/terminal/display.h
index e51b07f..98337fd 100644
--- a/src/terminal/terminal/display.h
+++ b/src/terminal/terminal/display.h
@@ -139,6 +139,12 @@
     guac_terminal_color palette[256];
 
     /**
+     * The default palette. Use GUAC_TERMINAL_INITIAL_PALETTE if null.
+     * Must free on destruction if not null.
+     */
+    const guac_terminal_color (*default_palette)[256];
+
+    /**
      * Default foreground color for all glyphs.
      */
     guac_terminal_color default_foreground;
@@ -215,7 +221,8 @@
  */
 guac_terminal_display* guac_terminal_display_alloc(guac_client* client,
         const char* font_name, int font_size, int dpi,
-        guac_terminal_color* foreground, guac_terminal_color* background);
+        guac_terminal_color* foreground, guac_terminal_color* background,
+        const guac_terminal_color (*palette)[256]);
 
 /**
  * Frees the given display.
@@ -224,7 +231,7 @@
 
 /**
  * Resets the palette of the given display to the initial, default color
- * values, as defined by GUAC_TERMINAL_INITIAL_PALETTE.
+ * values, as defined by default_palette or GUAC_TERMINAL_INITIAL_PALETTE.
  *
  * @param display
  *     The display to reset.
diff --git a/src/terminal/terminal/terminal.h b/src/terminal/terminal/terminal.h
index c071b40..6094c39 100644
--- a/src/terminal/terminal/terminal.h
+++ b/src/terminal/terminal/terminal.h
@@ -83,6 +83,21 @@
  */
 #define GUAC_TERMINAL_SCHEME_WHITE_BLACK "white-black"
 
+/**
+ * Color name representing the foreground color.
+ */
+#define GUAC_TERMINAL_SCHEME_FOREGROUND "foreground"
+
+/**
+ * Color name representing the background color.
+ */
+#define GUAC_TERMINAL_SCHEME_BACKGROUND "background"
+
+/**
+ * Color name representing a numbered color.
+ */
+#define GUAC_TERMINAL_SCHEME_NUMBERED "color"
+
 typedef struct guac_terminal guac_terminal;
 
 /**
diff --git a/src/terminal/xparsecolor.c b/src/terminal/xparsecolor.c
index 5aa1fd4..779374d 100644
--- a/src/terminal/xparsecolor.c
+++ b/src/terminal/xparsecolor.c
@@ -33,6 +33,7 @@
 
     /* 12-bit RGB ("rgb:h/h/h"), zero-padded to 24-bit */
     if (sscanf(spec, "rgb:%1x/%1x/%1x", &red, &green, &blue) == 3) {
+        color->palette_index = -1; /* Not from palette. */
         color->red   = red   << 4;
         color->green = green << 4;
         color->blue  = blue  << 4;
@@ -41,6 +42,7 @@
 
     /* 24-bit RGB ("rgb:hh/hh/hh") */
     if (sscanf(spec, "rgb:%2x/%2x/%2x", &red, &green, &blue) == 3) {
+        color->palette_index = -1; /* Not from palette. */
         color->red   = red;
         color->green = green;
         color->blue  = blue;
@@ -49,6 +51,7 @@
 
     /* 36-bit RGB ("rgb:hhh/hhh/hhh"), truncated to 24-bit */
     if (sscanf(spec, "rgb:%3x/%3x/%3x", &red, &green, &blue) == 3) {
+        color->palette_index = -1; /* Not from palette. */
         color->red   = red   >> 4;
         color->green = green >> 4;
         color->blue  = blue  >> 4;
@@ -57,6 +60,7 @@
 
     /* 48-bit RGB ("rgb:hhhh/hhhh/hhhh"), truncated to 24-bit */
     if (sscanf(spec, "rgb:%4x/%4x/%4x", &red, &green, &blue) == 3) {
+        color->palette_index = -1; /* Not from palette. */
         color->red   = red   >> 8;
         color->green = green >> 8;
         color->blue  = blue  >> 8;