diff --git a/src/fish_indent.cpp b/src/fish_indent.cpp index c74ddb4f0..26c67e014 100644 --- a/src/fish_indent.cpp +++ b/src/fish_indent.cpp @@ -275,7 +275,7 @@ static std::string ansi_colorize(const wcstring &text, assert(colors.size() == text.size()); outputter_t outp; - highlight_spec_t last_color = highlight_spec_normal; + highlight_spec_t last_color = highlight_role_t::normal; for (size_t i = 0; i < text.size(); i++) { highlight_spec_t color = colors.at(i); if (color != last_color) { @@ -292,47 +292,47 @@ static std::string ansi_colorize(const wcstring &text, /// for the various colors. static const wchar_t *html_class_name_for_color(highlight_spec_t spec) { #define P(x) L"fish_color_" #x - switch (spec & HIGHLIGHT_SPEC_PRIMARY_MASK) { - case highlight_spec_normal: { + switch (spec.foreground) { + case highlight_role_t::normal: { return P(normal); } - case highlight_spec_error: { + case highlight_role_t::error: { return P(error); } - case highlight_spec_command: { + case highlight_role_t::command: { return P(command); } - case highlight_spec_statement_terminator: { + case highlight_role_t::statement_terminator: { return P(statement_terminator); } - case highlight_spec_param: { + case highlight_role_t::param: { return P(param); } - case highlight_spec_comment: { + case highlight_role_t::comment: { return P(comment); } - case highlight_spec_match: { + case highlight_role_t::match: { return P(match); } - case highlight_spec_search_match: { + case highlight_role_t::search_match: { return P(search_match); } - case highlight_spec_operator: { + case highlight_role_t::operat: { return P(operator); } - case highlight_spec_escape: { + case highlight_role_t::escape: { return P(escape); } - case highlight_spec_quote: { + case highlight_role_t::quote: { return P(quote); } - case highlight_spec_redirection: { + case highlight_role_t::redirection: { return P(redirection); } - case highlight_spec_autosuggestion: { + case highlight_role_t::autosuggestion: { return P(autosuggestion); } - case highlight_spec_selection: { + case highlight_role_t::selection: { return P(selection); } default: { return P(other); } @@ -347,7 +347,7 @@ static std::string html_colorize(const wcstring &text, assert(colors.size() == text.size()); wcstring html = L"
";
-    highlight_spec_t last_color = highlight_spec_normal;
+    highlight_spec_t last_color = highlight_role_t::normal;
     for (size_t i = 0; i < text.size(); i++) {
         // Handle colors.
         highlight_spec_t color = colors.at(i);
diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp
index 08d4d08ed..1f5489d18 100644
--- a/src/fish_tests.cpp
+++ b/src/fish_tests.cpp
@@ -4242,9 +4242,9 @@ static void test_highlighting() {
     // Here are the components of our source and the colors we expect those to be.
     struct highlight_component_t {
         const wchar_t *txt;
-        int color;
+        highlight_spec_t color;
         bool nospace;
-        highlight_component_t(const wchar_t *txt, int color, bool nospace = false)
+        highlight_component_t(const wchar_t *txt, highlight_spec_t color, bool nospace = false)
             : txt(txt), color(color), nospace(nospace) {}
     };
     const bool ns = true;
@@ -4252,182 +4252,184 @@ static void test_highlighting() {
     using highlight_component_list_t = std::vector;
     std::vector highlight_tests;
 
-    highlight_tests.push_back(
-        {{L"echo", highlight_spec_command},
-         {L"test/fish_highlight_test/foo", highlight_spec_param | highlight_modifier_valid_path},
-         {L"&", highlight_spec_statement_terminator}});
+    highlight_spec_t param_valid_path{highlight_role_t::param};
+    param_valid_path.valid_path = true;
+
+    highlight_tests.push_back({{L"echo", highlight_role_t::command},
+                               {L"test/fish_highlight_test/foo", param_valid_path},
+                               {L"&", highlight_role_t::statement_terminator}});
 
     highlight_tests.push_back({
-        {L"command", highlight_spec_command},
-        {L"echo", highlight_spec_command},
-        {L"abc", highlight_spec_param},
-        {L"test/fish_highlight_test/foo", highlight_spec_param | highlight_modifier_valid_path},
-        {L"&", highlight_spec_statement_terminator},
+        {L"command", highlight_role_t::command},
+        {L"echo", highlight_role_t::command},
+        {L"abc", highlight_role_t::param},
+        {L"test/fish_highlight_test/foo", param_valid_path},
+        {L"&", highlight_role_t::statement_terminator},
     });
 
     highlight_tests.push_back({
-        {L"if command ls", highlight_spec_command},
-        {L"; ", highlight_spec_statement_terminator},
-        {L"echo", highlight_spec_command},
-        {L"abc", highlight_spec_param},
-        {L"; ", highlight_spec_statement_terminator},
-        {L"/bin/definitely_not_a_command", highlight_spec_error},
-        {L"; ", highlight_spec_statement_terminator},
-        {L"end", highlight_spec_command},
+        {L"if command ls", highlight_role_t::command},
+        {L"; ", highlight_role_t::statement_terminator},
+        {L"echo", highlight_role_t::command},
+        {L"abc", highlight_role_t::param},
+        {L"; ", highlight_role_t::statement_terminator},
+        {L"/bin/definitely_not_a_command", highlight_role_t::error},
+        {L"; ", highlight_role_t::statement_terminator},
+        {L"end", highlight_role_t::command},
     });
 
     // Verify that cd shows errors for non-directories.
     highlight_tests.push_back({
-        {L"cd", highlight_spec_command},
-        {L"test/fish_highlight_test", highlight_spec_param | highlight_modifier_valid_path},
+        {L"cd", highlight_role_t::command},
+        {L"test/fish_highlight_test", param_valid_path},
     });
 
     highlight_tests.push_back({
-        {L"cd", highlight_spec_command},
-        {L"test/fish_highlight_test/foo", highlight_spec_error},
+        {L"cd", highlight_role_t::command},
+        {L"test/fish_highlight_test/foo", highlight_role_t::error},
     });
 
     highlight_tests.push_back({
-        {L"cd", highlight_spec_command},
-        {L"--help", highlight_spec_param},
-        {L"-h", highlight_spec_param},
-        {L"definitely_not_a_directory", highlight_spec_error},
+        {L"cd", highlight_role_t::command},
+        {L"--help", highlight_role_t::param},
+        {L"-h", highlight_role_t::param},
+        {L"definitely_not_a_directory", highlight_role_t::error},
     });
 
     // Command substitutions.
     highlight_tests.push_back({
-        {L"echo", highlight_spec_command},
-        {L"param1", highlight_spec_param},
-        {L"(", highlight_spec_operator},
-        {L"ls", highlight_spec_command},
-        {L"param2", highlight_spec_param},
-        {L")", highlight_spec_operator},
-        {L"|", highlight_spec_statement_terminator},
-        {L"cat", highlight_spec_command},
+        {L"echo", highlight_role_t::command},
+        {L"param1", highlight_role_t::param},
+        {L"(", highlight_role_t::operat},
+        {L"ls", highlight_role_t::command},
+        {L"param2", highlight_role_t::param},
+        {L")", highlight_role_t::operat},
+        {L"|", highlight_role_t::statement_terminator},
+        {L"cat", highlight_role_t::command},
     });
 
     // Redirections substitutions.
     highlight_tests.push_back({
-        {L"echo", highlight_spec_command},
-        {L"param1", highlight_spec_param},
+        {L"echo", highlight_role_t::command},
+        {L"param1", highlight_role_t::param},
 
         // Input redirection.
-        {L"<", highlight_spec_redirection},
-        {L"/bin/echo", highlight_spec_redirection},
+        {L"<", highlight_role_t::redirection},
+        {L"/bin/echo", highlight_role_t::redirection},
 
         // Output redirection to a valid fd.
-        {L"1>&2", highlight_spec_redirection},
+        {L"1>&2", highlight_role_t::redirection},
 
         // Output redirection to an invalid fd.
-        {L"2>&", highlight_spec_redirection},
-        {L"LOL", highlight_spec_error},
+        {L"2>&", highlight_role_t::redirection},
+        {L"LOL", highlight_role_t::error},
 
         // Just a param, not a redirection.
-        {L"test/blah", highlight_spec_param},
+        {L"test/blah", highlight_role_t::param},
 
         // Input redirection from directory.
-        {L"<", highlight_spec_redirection},
-        {L"test/", highlight_spec_error},
+        {L"<", highlight_role_t::redirection},
+        {L"test/", highlight_role_t::error},
 
         // Output redirection to an invalid path.
-        {L"3>", highlight_spec_redirection},
-        {L"/not/a/valid/path/nope", highlight_spec_error},
+        {L"3>", highlight_role_t::redirection},
+        {L"/not/a/valid/path/nope", highlight_role_t::error},
 
         // Output redirection to directory.
-        {L"3>", highlight_spec_redirection},
-        {L"test/nope/", highlight_spec_error},
+        {L"3>", highlight_role_t::redirection},
+        {L"test/nope/", highlight_role_t::error},
 
         // Redirections to overflow fd.
-        {L"99999999999999999999>&2", highlight_spec_error},
-        {L"2>&", highlight_spec_redirection},
-        {L"99999999999999999999", highlight_spec_error},
+        {L"99999999999999999999>&2", highlight_role_t::error},
+        {L"2>&", highlight_role_t::redirection},
+        {L"99999999999999999999", highlight_role_t::error},
 
         // Output redirection containing a command substitution.
-        {L"4>", highlight_spec_redirection},
-        {L"(", highlight_spec_operator},
-        {L"echo", highlight_spec_command},
-        {L"test/somewhere", highlight_spec_param},
-        {L")", highlight_spec_operator},
+        {L"4>", highlight_role_t::redirection},
+        {L"(", highlight_role_t::operat},
+        {L"echo", highlight_role_t::command},
+        {L"test/somewhere", highlight_role_t::param},
+        {L")", highlight_role_t::operat},
 
         // Just another param.
-        {L"param2", highlight_spec_param},
+        {L"param2", highlight_role_t::param},
     });
 
     highlight_tests.push_back({
-        {L"end", highlight_spec_error},
-        {L";", highlight_spec_statement_terminator},
-        {L"if", highlight_spec_command},
-        {L"end", highlight_spec_error},
+        {L"end", highlight_role_t::error},
+        {L";", highlight_role_t::statement_terminator},
+        {L"if", highlight_role_t::command},
+        {L"end", highlight_role_t::error},
     });
 
     highlight_tests.push_back({
-        {L"echo", highlight_spec_command},
-        {L"'single_quote", highlight_spec_error},
+        {L"echo", highlight_role_t::command},
+        {L"'single_quote", highlight_role_t::error},
     });
 
     highlight_tests.push_back({
-        {L"echo", highlight_spec_command},
-        {L"$foo", highlight_spec_operator},
-        {L"\"", highlight_spec_quote},
-        {L"$bar", highlight_spec_operator},
-        {L"\"", highlight_spec_quote},
-        {L"$baz[", highlight_spec_operator},
-        {L"1 2..3", highlight_spec_param},
-        {L"]", highlight_spec_operator},
+        {L"echo", highlight_role_t::command},
+        {L"$foo", highlight_role_t::operat},
+        {L"\"", highlight_role_t::quote},
+        {L"$bar", highlight_role_t::operat},
+        {L"\"", highlight_role_t::quote},
+        {L"$baz[", highlight_role_t::operat},
+        {L"1 2..3", highlight_role_t::param},
+        {L"]", highlight_role_t::operat},
     });
 
     highlight_tests.push_back({
-        {L"for", highlight_spec_command},
-        {L"i", highlight_spec_param},
-        {L"in", highlight_spec_command},
-        {L"1 2 3", highlight_spec_param},
-        {L";", highlight_spec_statement_terminator},
-        {L"end", highlight_spec_command},
+        {L"for", highlight_role_t::command},
+        {L"i", highlight_role_t::param},
+        {L"in", highlight_role_t::command},
+        {L"1 2 3", highlight_role_t::param},
+        {L";", highlight_role_t::statement_terminator},
+        {L"end", highlight_role_t::command},
     });
 
     highlight_tests.push_back({
-        {L"echo", highlight_spec_command},
-        {L"$$foo[", highlight_spec_operator},
-        {L"1", highlight_spec_param},
-        {L"][", highlight_spec_operator},
-        {L"2", highlight_spec_param},
-        {L"]", highlight_spec_operator},
-        {L"[3]", highlight_spec_param},  // two dollar signs, so last one is not an expansion
+        {L"echo", highlight_role_t::command},
+        {L"$$foo[", highlight_role_t::operat},
+        {L"1", highlight_role_t::param},
+        {L"][", highlight_role_t::operat},
+        {L"2", highlight_role_t::param},
+        {L"]", highlight_role_t::operat},
+        {L"[3]", highlight_role_t::param},  // two dollar signs, so last one is not an expansion
     });
 
     highlight_tests.push_back({
-        {L"cat", highlight_spec_command},
-        {L"/dev/null", highlight_spec_param},
-        {L"|", highlight_spec_statement_terminator},
+        {L"cat", highlight_role_t::command},
+        {L"/dev/null", highlight_role_t::param},
+        {L"|", highlight_role_t::statement_terminator},
         // This is bogus, but we used to use "less" here and that doesn't have to be installed.
-        {L"cat", highlight_spec_command},
-        {L"2>", highlight_spec_redirection},
+        {L"cat", highlight_role_t::command},
+        {L"2>", highlight_role_t::redirection},
     });
 
     highlight_tests.push_back({
-        {L"if", highlight_spec_command},
-        {L"true", highlight_spec_command},
-        {L"&&", highlight_spec_operator},
-        {L"false", highlight_spec_command},
-        {L";", highlight_spec_statement_terminator},
-        {L"or", highlight_spec_operator},
-        {L"false", highlight_spec_command},
-        {L"||", highlight_spec_operator},
-        {L"true", highlight_spec_command},
-        {L";", highlight_spec_statement_terminator},
-        {L"and", highlight_spec_operator},
-        {L"not", highlight_spec_operator},
-        {L"!", highlight_spec_operator},
-        {L"true", highlight_spec_command},
-        {L";", highlight_spec_statement_terminator},
-        {L"end", highlight_spec_command},
+        {L"if", highlight_role_t::command},
+        {L"true", highlight_role_t::command},
+        {L"&&", highlight_role_t::operat},
+        {L"false", highlight_role_t::command},
+        {L";", highlight_role_t::statement_terminator},
+        {L"or", highlight_role_t::operat},
+        {L"false", highlight_role_t::command},
+        {L"||", highlight_role_t::operat},
+        {L"true", highlight_role_t::command},
+        {L";", highlight_role_t::statement_terminator},
+        {L"and", highlight_role_t::operat},
+        {L"not", highlight_role_t::operat},
+        {L"!", highlight_role_t::operat},
+        {L"true", highlight_role_t::command},
+        {L";", highlight_role_t::statement_terminator},
+        {L"end", highlight_role_t::command},
     });
 
     highlight_tests.push_back({
-        {L"echo", highlight_spec_command},
-        {L"%self", highlight_spec_operator},
-        {L"not%self", highlight_spec_param},
-        {L"self%not", highlight_spec_param},
+        {L"echo", highlight_role_t::command},
+        {L"%self", highlight_role_t::operat},
+        {L"not%self", highlight_role_t::param},
+        {L"self%not", highlight_role_t::param},
     });
 
     auto &vars = parser_t::principal_parser().vars();
@@ -4435,21 +4437,21 @@ static void test_highlighting() {
     vars.set(L"VARIABLE_IN_COMMAND", ENV_LOCAL, {L"a"});
     vars.set(L"VARIABLE_IN_COMMAND2", ENV_LOCAL, {L"at"});
     highlight_tests.push_back(
-        {{L"/bin/ca", highlight_spec_command, ns}, {L"*", highlight_spec_operator, ns}});
+        {{L"/bin/ca", highlight_role_t::command, ns}, {L"*", highlight_role_t::operat, ns}});
 
-    highlight_tests.push_back({{L"/bin/c", highlight_spec_command, ns},
-                               {L"{$VARIABLE_IN_COMMAND}", highlight_spec_operator, ns},
-                               {L"*", highlight_spec_operator, ns}});
+    highlight_tests.push_back({{L"/bin/c", highlight_role_t::command, ns},
+                               {L"{$VARIABLE_IN_COMMAND}", highlight_role_t::operat, ns},
+                               {L"*", highlight_role_t::operat, ns}});
 
-    highlight_tests.push_back({{L"/bin/c", highlight_spec_command, ns},
-                               {L"{$VARIABLE_IN_COMMAND}", highlight_spec_operator, ns},
-                               {L"*", highlight_spec_operator, ns}});
+    highlight_tests.push_back({{L"/bin/c", highlight_role_t::command, ns},
+                               {L"{$VARIABLE_IN_COMMAND}", highlight_role_t::operat, ns},
+                               {L"*", highlight_role_t::operat, ns}});
 
-    highlight_tests.push_back({{L"/bin/c", highlight_spec_command, ns},
-                               {L"$VARIABLE_IN_COMMAND2", highlight_spec_operator, ns}});
+    highlight_tests.push_back({{L"/bin/c", highlight_role_t::command, ns},
+                               {L"$VARIABLE_IN_COMMAND2", highlight_role_t::operat, ns}});
 
-    highlight_tests.push_back({{L"$EMPTY_VARIABLE", highlight_spec_error}});
-    highlight_tests.push_back({{L"\"$EMPTY_VARIABLE\"", highlight_spec_error}});
+    highlight_tests.push_back({{L"$EMPTY_VARIABLE", highlight_role_t::error}});
+    highlight_tests.push_back({{L"\"$EMPTY_VARIABLE\"", highlight_role_t::error}});
 
     for (const highlight_component_list_t &components : highlight_tests) {
         // Generate the text.
@@ -4458,7 +4460,7 @@ static void test_highlighting() {
         for (const highlight_component_t &comp : components) {
             if (!text.empty() && !comp.nospace) {
                 text.push_back(L' ');
-                expected_colors.push_back(0);
+                expected_colors.push_back(highlight_spec_t{});
             }
             text.append(comp.txt);
             expected_colors.resize(text.size(), comp.color);
diff --git a/src/highlight.cpp b/src/highlight.cpp
index 679c5ac4b..1f18e49b6 100644
--- a/src/highlight.cpp
+++ b/src/highlight.cpp
@@ -39,71 +39,127 @@ namespace g = grammar;
 
 #define CURSOR_POSITION_INVALID ((size_t)(-1))
 
-/// Number of elements in the highlight_var array.
-#define VAR_COUNT (sizeof(highlight_var) / sizeof(wchar_t *))
-static const wchar_t *const highlight_var[] = {
-    [highlight_spec_normal]                       = L"fish_color_normal",
-    [highlight_spec_error]                        = L"fish_color_error",
-    [highlight_spec_command]                      = L"fish_color_command",
-    [highlight_spec_statement_terminator]         = L"fish_color_end",
-    [highlight_spec_param]                        = L"fish_color_param",
-    [highlight_spec_comment]                      = L"fish_color_comment",
-    [highlight_spec_match]                        = L"fish_color_match",
-    [highlight_spec_search_match]                 = L"fish_color_search_match",
-    [highlight_spec_operator]                     = L"fish_color_operator",
-    [highlight_spec_escape]                       = L"fish_color_escape",
-    [highlight_spec_quote]                        = L"fish_color_quote",
-    [highlight_spec_redirection]                  = L"fish_color_redirection",
-    [highlight_spec_autosuggestion]               = L"fish_color_autosuggestion",
-    [highlight_spec_selection]                    = L"fish_color_selection",
-    [highlight_spec_pager_progress]               = L"fish_pager_color_progress",
-    [highlight_spec_pager_background]             = L"fish_pager_color_background",
-    [highlight_spec_pager_prefix]                 = L"fish_pager_color_prefix",
-    [highlight_spec_pager_completion]             = L"fish_pager_color_completion",
-    [highlight_spec_pager_description]            = L"fish_pager_color_description",
-    [highlight_spec_pager_secondary_background]   = L"fish_pager_color_secondary_background",
-    [highlight_spec_pager_secondary_prefix]       = L"fish_pager_color_secondary_prefix",
-    [highlight_spec_pager_secondary_completion]   = L"fish_pager_color_secondary_completion",
-    [highlight_spec_pager_secondary_description]  = L"fish_pager_color_secondary_description",
-    [highlight_spec_pager_selected_background]    = L"fish_pager_color_selected_background",
-    [highlight_spec_pager_selected_prefix]        = L"fish_pager_color_selected_prefix",
-    [highlight_spec_pager_selected_completion]    = L"fish_pager_color_selected_completion",
-    [highlight_spec_pager_selected_description]   = L"fish_pager_color_selected_description",
-};
-static_assert(VAR_COUNT == HIGHLIGHT_SPEC_MAX, "Every color spec has a corresponding env var");
+static const wchar_t *get_highlight_var_name(highlight_role_t role) {
+    switch (role) {
+        case highlight_role_t::normal:
+            return L"fish_color_normal";
+        case highlight_role_t::error:
+            return L"fish_color_error";
+        case highlight_role_t::command:
+            return L"fish_color_command";
+        case highlight_role_t::statement_terminator:
+            return L"fish_color_end";
+        case highlight_role_t::param:
+            return L"fish_color_param";
+        case highlight_role_t::comment:
+            return L"fish_color_comment";
+        case highlight_role_t::match:
+            return L"fish_color_match";
+        case highlight_role_t::search_match:
+            return L"fish_color_search_match";
+        case highlight_role_t::operat:
+            return L"fish_color_operator";
+        case highlight_role_t::escape:
+            return L"fish_color_escape";
+        case highlight_role_t::quote:
+            return L"fish_color_quote";
+        case highlight_role_t::redirection:
+            return L"fish_color_redirection";
+        case highlight_role_t::autosuggestion:
+            return L"fish_color_autosuggestion";
+        case highlight_role_t::selection:
+            return L"fish_color_selection";
+        case highlight_role_t::pager_progress:
+            return L"fish_pager_color_progress";
+        case highlight_role_t::pager_background:
+            return L"fish_pager_color_background";
+        case highlight_role_t::pager_prefix:
+            return L"fish_pager_color_prefix";
+        case highlight_role_t::pager_completion:
+            return L"fish_pager_color_completion";
+        case highlight_role_t::pager_description:
+            return L"fish_pager_color_description";
+        case highlight_role_t::pager_secondary_background:
+            return L"fish_pager_color_secondary_background";
+        case highlight_role_t::pager_secondary_prefix:
+            return L"fish_pager_color_secondary_prefix";
+        case highlight_role_t::pager_secondary_completion:
+            return L"fish_pager_color_secondary_completion";
+        case highlight_role_t::pager_secondary_description:
+            return L"fish_pager_color_secondary_description";
+        case highlight_role_t::pager_selected_background:
+            return L"fish_pager_color_selected_background";
+        case highlight_role_t::pager_selected_prefix:
+            return L"fish_pager_color_selected_prefix";
+        case highlight_role_t::pager_selected_completion:
+            return L"fish_pager_color_selected_completion";
+        case highlight_role_t::pager_selected_description:
+            return L"fish_pager_color_selected_description";
+    }
+    DIE("invalid highlight role");
+}
 
 // Table used to fetch fallback highlights in case the specified one
 // wasn't set.
-static const highlight_spec_t fallbacks[] = {
-    [highlight_spec_normal]                       = highlight_spec_normal,
-    [highlight_spec_error]                        = highlight_spec_normal,
-    [highlight_spec_command]                      = highlight_spec_normal,
-    [highlight_spec_statement_terminator]         = highlight_spec_normal,
-    [highlight_spec_param]                        = highlight_spec_normal,
-    [highlight_spec_comment]                      = highlight_spec_normal,
-    [highlight_spec_match]                        = highlight_spec_normal,
-    [highlight_spec_search_match]                 = highlight_spec_normal,
-    [highlight_spec_operator]                     = highlight_spec_normal,
-    [highlight_spec_escape]                       = highlight_spec_normal,
-    [highlight_spec_quote]                        = highlight_spec_normal,
-    [highlight_spec_redirection]                  = highlight_spec_normal,
-    [highlight_spec_autosuggestion]               = highlight_spec_normal,
-    [highlight_spec_selection]                    = highlight_spec_normal,
-    [highlight_spec_pager_progress]               = highlight_spec_normal,
-    [highlight_spec_pager_background]             = highlight_spec_normal,
-    [highlight_spec_pager_prefix]                 = highlight_spec_normal,
-    [highlight_spec_pager_completion]             = highlight_spec_normal,
-    [highlight_spec_pager_description]            = highlight_spec_normal,
-    [highlight_spec_pager_secondary_background]   = highlight_spec_pager_background,
-    [highlight_spec_pager_secondary_prefix]       = highlight_spec_pager_prefix,
-    [highlight_spec_pager_secondary_completion]   = highlight_spec_pager_completion,
-    [highlight_spec_pager_secondary_description]  = highlight_spec_pager_description,
-    [highlight_spec_pager_selected_background]    = highlight_spec_search_match,
-    [highlight_spec_pager_selected_prefix]        = highlight_spec_pager_prefix,
-    [highlight_spec_pager_selected_completion]    = highlight_spec_pager_completion,
-    [highlight_spec_pager_selected_description]   = highlight_spec_pager_description,
-};
-static_assert(sizeof(fallbacks) / sizeof(fallbacks[0]) == HIGHLIGHT_SPEC_MAX, "No missing fallbacks");
+static highlight_role_t get_fallback(highlight_role_t role) {
+    switch (role) {
+        case highlight_role_t::normal:
+            return highlight_role_t::normal;
+        case highlight_role_t::error:
+            return highlight_role_t::normal;
+        case highlight_role_t::command:
+            return highlight_role_t::normal;
+        case highlight_role_t::statement_terminator:
+            return highlight_role_t::normal;
+        case highlight_role_t::param:
+            return highlight_role_t::normal;
+        case highlight_role_t::comment:
+            return highlight_role_t::normal;
+        case highlight_role_t::match:
+            return highlight_role_t::normal;
+        case highlight_role_t::search_match:
+            return highlight_role_t::normal;
+        case highlight_role_t::operat:
+            return highlight_role_t::normal;
+        case highlight_role_t::escape:
+            return highlight_role_t::normal;
+        case highlight_role_t::quote:
+            return highlight_role_t::normal;
+        case highlight_role_t::redirection:
+            return highlight_role_t::normal;
+        case highlight_role_t::autosuggestion:
+            return highlight_role_t::normal;
+        case highlight_role_t::selection:
+            return highlight_role_t::normal;
+        case highlight_role_t::pager_progress:
+            return highlight_role_t::normal;
+        case highlight_role_t::pager_background:
+            return highlight_role_t::normal;
+        case highlight_role_t::pager_prefix:
+            return highlight_role_t::normal;
+        case highlight_role_t::pager_completion:
+            return highlight_role_t::normal;
+        case highlight_role_t::pager_description:
+            return highlight_role_t::normal;
+        case highlight_role_t::pager_secondary_background:
+            return highlight_role_t::pager_background;
+        case highlight_role_t::pager_secondary_prefix:
+            return highlight_role_t::pager_prefix;
+        case highlight_role_t::pager_secondary_completion:
+            return highlight_role_t::pager_completion;
+        case highlight_role_t::pager_secondary_description:
+            return highlight_role_t::pager_description;
+        case highlight_role_t::pager_selected_background:
+            return highlight_role_t::search_match;
+        case highlight_role_t::pager_selected_prefix:
+            return highlight_role_t::pager_prefix;
+        case highlight_role_t::pager_selected_completion:
+            return highlight_role_t::pager_completion;
+        case highlight_role_t::pager_selected_description:
+            return highlight_role_t::pager_description;
+    }
+    DIE("invalid highlight role");
+}
 
 /// Determine if the filesystem containing the given fd is case insensitive for lookups regardless
 /// of whether it preserves the case when saving a pathname.
@@ -292,30 +348,19 @@ static bool plain_statement_get_expanded_command(const wcstring &src,
     return err == EXPAND_OK || err == EXPAND_WILDCARD_MATCH;
 }
 
-rgb_color_t highlight_get_color(highlight_spec_t highlight, bool is_background) {
+rgb_color_t highlight_get_color(const highlight_spec_t &highlight, bool is_background) {
     // TODO: rationalize this principal_vars.
     const auto &vars = env_stack_t::principal();
     rgb_color_t result = rgb_color_t::normal();
+    highlight_role_t role = is_background ? highlight.background : highlight.foreground;
 
-    bool treat_as_background = is_background;
-
-    // Get the primary variable.
-    size_t idx = highlight_get_primary(highlight);
-    if (idx >= VAR_COUNT) {
-        return rgb_color_t::normal();
-    }
-
-    auto var = vars.get(highlight_var[idx]);
-
-    // debug( 1, L"%d -> %d -> %ls", highlight, idx, val );
-
-    if (!var) var = vars.get(highlight_var[fallbacks[idx]]);
-    if (!var) var = vars.get(highlight_var[0]);
-
-    if (var) result = parse_color(*var, treat_as_background);
+    auto var = vars.get(get_highlight_var_name(role));
+    if (!var) var = vars.get(get_highlight_var_name(get_fallback(role)));
+    if (!var) var = vars.get(get_highlight_var_name(highlight_role_t::normal));
+    if (var) result = parse_color(*var, is_background);
 
     // Handle modifiers.
-    if (highlight & highlight_modifier_valid_path) {
+    if (!is_background && highlight.valid_path) {
         auto var2 = vars.get(L"fish_color_valid_path");
         if (var2) {
             rgb_color_t result2 = parse_color(*var2, is_background);
@@ -331,7 +376,7 @@ rgb_color_t highlight_get_color(highlight_spec_t highlight, bool is_background)
         }
     }
 
-    if (highlight & highlight_modifier_force_underline) {
+    if (!is_background && highlight.force_underline) {
         result.set_underline(true);
     }
 
@@ -435,9 +480,9 @@ static size_t color_variable(const wchar_t *in, size_t in_len,
         // Our color depends on the next char.
         wchar_t next = in[idx + 1];
         if (next == L'$' || valid_var_name_char(next)) {
-            colors[idx] = highlight_spec_operator;
+            colors[idx] = highlight_role_t::operat;
         } else {
-            colors[idx] = highlight_spec_error;
+            colors[idx] = highlight_role_t::error;
         }
         idx++;
         dollar_count++;
@@ -445,7 +490,7 @@ static size_t color_variable(const wchar_t *in, size_t in_len,
 
     // Handle a sequence of variable characters.
     while (valid_var_name_char(in[idx])) {
-        colors[idx++] = highlight_spec_operator;
+        colors[idx++] = highlight_role_t::operat;
     }
 
     // Handle a slice, up to dollar_count of them. Note that we currently don't do any validation of
@@ -456,8 +501,8 @@ static size_t color_variable(const wchar_t *in, size_t in_len,
         if (located == 1) {
             size_t slice_begin_idx = slice_begin - in, slice_end_idx = slice_end - in;
             assert(slice_end_idx > slice_begin_idx);
-            colors[slice_begin_idx] = highlight_spec_operator;
-            colors[slice_end_idx] = highlight_spec_operator;
+            colors[slice_begin_idx] = highlight_role_t::operat;
+            colors[slice_end_idx] = highlight_role_t::operat;
             idx = slice_end_idx + 1;
         } else if (located == 0) {
             // not a slice
@@ -468,7 +513,7 @@ static size_t color_variable(const wchar_t *in, size_t in_len,
             // double-quoted string that doesn't happen. As such, color the variable + the slice
             // start red. Coloring any more than that looks bad, unless we're willing to try and
             // detect where the double-quoted string ends, and I'd rather not do that.
-            std::fill(colors, colors + idx + 1, (highlight_spec_t)highlight_spec_error);
+            std::fill(colors, colors + idx + 1, highlight_role_t::error);
             break;
         }
     }
@@ -480,14 +525,14 @@ static size_t color_variable(const wchar_t *in, size_t in_len,
 static void color_string_internal(const wcstring &buffstr, highlight_spec_t base_color,
                                   std::vector::iterator colors) {
     // Clarify what we expect.
-    assert((base_color == highlight_spec_param || base_color == highlight_spec_command) &&
+    assert((base_color == highlight_role_t::param || base_color == highlight_role_t::command) &&
            "Unexpected base color");
     const size_t buff_len = buffstr.size();
     std::fill(colors, colors + buff_len, base_color);
 
     // Hacky support for %self which must be an unquoted literal argument.
     if (buffstr == PROCESS_EXPAND_SELF_STR) {
-        std::fill_n(colors, wcslen(PROCESS_EXPAND_SELF_STR), highlight_spec_operator);
+        std::fill_n(colors, wcslen(PROCESS_EXPAND_SELF_STR), highlight_role_t::operat);
         return;
     }
 
@@ -498,7 +543,7 @@ static void color_string_internal(const wcstring &buffstr, highlight_spec_t base
         switch (mode) {
             case e_unquoted: {
                 if (c == L'\\') {
-                    int fill_color = highlight_spec_escape;  // may be set to highlight_error
+                    auto fill_color = highlight_role_t::escape;  // may be set to highlight_error
                     const size_t backslash_pos = in_pos;
                     size_t fill_end = backslash_pos;
 
@@ -508,7 +553,7 @@ static void color_string_internal(const wcstring &buffstr, highlight_spec_t base
 
                     if (escaped_char == L'\0') {
                         fill_end = in_pos;
-                        fill_color = highlight_spec_error;
+                        fill_color = highlight_role_t::error;
                     } else if (wcschr(L"~%", escaped_char)) {
                         if (in_pos == 1) {
                             fill_end = in_pos + 1;
@@ -571,7 +616,7 @@ static void color_string_internal(const wcstring &buffstr, highlight_spec_t base
                         fill_end = in_pos;
 
                         // It's an error if we exceeded the max value.
-                        if (res > max_val) fill_color = highlight_spec_error;
+                        if (res > max_val) fill_color = highlight_role_t::error;
 
                         // Subtract one from in_pos, so that the increment in the loop will move to
                         // the next character.
@@ -584,7 +629,7 @@ static void color_string_internal(const wcstring &buffstr, highlight_spec_t base
                     switch (c) {
                         case L'~': {
                             if (in_pos == 0) {
-                                colors[in_pos] = highlight_spec_operator;
+                                colors[in_pos] = highlight_role_t::operat;
                             }
                             break;
                         }
@@ -598,39 +643,39 @@ static void color_string_internal(const wcstring &buffstr, highlight_spec_t base
                         }
                         case L'?': {
                             if (!feature_test(features_t::qmark_noglob)) {
-                                colors[in_pos] = highlight_spec_operator;
+                                colors[in_pos] = highlight_role_t::operat;
                             }
                             break;
                         }
                         case L'*':
                         case L'(':
                         case L')': {
-                            colors[in_pos] = highlight_spec_operator;
+                            colors[in_pos] = highlight_role_t::operat;
                             break;
                         }
                         case L'{': {
-                            colors[in_pos] = highlight_spec_operator;
+                            colors[in_pos] = highlight_role_t::operat;
                             bracket_count++;
                             break;
                         }
                         case L'}': {
-                            colors[in_pos] = highlight_spec_operator;
+                            colors[in_pos] = highlight_role_t::operat;
                             bracket_count--;
                             break;
                         }
                         case L',': {
                             if (bracket_count > 0) {
-                                colors[in_pos] = highlight_spec_operator;
+                                colors[in_pos] = highlight_role_t::operat;
                             }
                             break;
                         }
                         case L'\'': {
-                            colors[in_pos] = highlight_spec_quote;
+                            colors[in_pos] = highlight_role_t::quote;
                             mode = e_single_quoted;
                             break;
                         }
                         case L'\"': {
-                            colors[in_pos] = highlight_spec_quote;
+                            colors[in_pos] = highlight_role_t::quote;
                             mode = e_double_quoted;
                             break;
                         }
@@ -643,14 +688,14 @@ static void color_string_internal(const wcstring &buffstr, highlight_spec_t base
             }
             // Mode 1 means single quoted string, i.e 'foo'.
             case e_single_quoted: {
-                colors[in_pos] = highlight_spec_quote;
+                colors[in_pos] = highlight_role_t::quote;
                 if (c == L'\\') {
                     // backslash
                     if (in_pos + 1 < buff_len) {
                         const wchar_t escaped_char = buffstr.at(in_pos + 1);
                         if (escaped_char == L'\\' || escaped_char == L'\'') {
-                            colors[in_pos] = highlight_spec_escape;      // backslash
-                            colors[in_pos + 1] = highlight_spec_escape;  // escaped char
+                            colors[in_pos] = highlight_role_t::escape;      // backslash
+                            colors[in_pos + 1] = highlight_role_t::escape;  // escaped char
                             in_pos += 1;                                 // skip over backslash
                         }
                     }
@@ -664,7 +709,7 @@ static void color_string_internal(const wcstring &buffstr, highlight_spec_t base
                 // Slices are colored in advance, past `in_pos`, and we don't want to overwrite
                 // that.
                 if (colors[in_pos] == base_color) {
-                    colors[in_pos] = highlight_spec_quote;
+                    colors[in_pos] = highlight_role_t::quote;
                 }
                 switch (c) {
                     case L'"': {
@@ -676,8 +721,8 @@ static void color_string_internal(const wcstring &buffstr, highlight_spec_t base
                         if (in_pos + 1 < buff_len) {
                             const wchar_t escaped_char = buffstr.at(in_pos + 1);
                             if (wcschr(L"\\\"\n$", escaped_char)) {
-                                colors[in_pos] = highlight_spec_escape;      // backslash
-                                colors[in_pos + 1] = highlight_spec_escape;  // escaped char
+                                colors[in_pos] = highlight_role_t::escape;      // backslash
+                                colors[in_pos + 1] = highlight_role_t::escape;  // escaped char
                                 in_pos += 1;                                 // skip over backslash
                             }
                         }
@@ -778,7 +823,7 @@ void highlighter_t::color_command(tnode_t node) {
     // Get an iterator to the colors associated with the argument.
     const size_t arg_start = source_range->start;
     const color_array_t::iterator colors = color_array.begin() + arg_start;
-    color_string_internal(cmd_str, highlight_spec_command, colors);
+    color_string_internal(cmd_str, highlight_role_t::command, colors);
 }
 
 // node does not necessarily have type symbol_argument here.
@@ -793,7 +838,7 @@ void highlighter_t::color_argument(tnode_t node) {
     const color_array_t::iterator arg_colors = color_array.begin() + arg_start;
 
     // Color this argument without concern for command substitutions.
-    color_string_internal(arg_str, highlight_spec_param, arg_colors);
+    color_string_internal(arg_str, highlight_role_t::param, arg_colors);
 
     // Now do command substitutions.
     size_t cmdsub_cursor = 0, cmdsub_start = 0, cmdsub_end = 0;
@@ -814,9 +859,9 @@ void highlighter_t::color_argument(tnode_t node) {
         // Highlight the parens. The open paren must exist; the closed paren may not if it was
         // incomplete.
         assert(cmdsub_start < arg_str.size());
-        this->color_array.at(arg_subcmd_start) = highlight_spec_operator;
+        this->color_array.at(arg_subcmd_start) = highlight_role_t::operat;
         if (arg_subcmd_end < this->buff.size())
-            this->color_array.at(arg_subcmd_end) = highlight_spec_operator;
+            this->color_array.at(arg_subcmd_end) = highlight_role_t::operat;
 
         // Compute the cursor's position within the cmdsub. We must be past the open paren (hence >)
         // but can be at the end of the string or closed paren (hence <=).
@@ -886,7 +931,7 @@ void highlighter_t::color_arguments(const std::vector> &arg
                                string_prefixes_string(param, L"-h");
                 if (!is_help && this->io_ok &&
                     !is_potential_cd_path(param, working_directory, vars, PATH_EXPAND_TILDE)) {
-                    this->color_node(arg, highlight_spec_error);
+                    this->color_node(arg, highlight_role_t::error);
                 }
             }
         }
@@ -905,7 +950,7 @@ void highlighter_t::color_redirection(tnode_t redirection_node)
             redirection_type(redirection_node, this->buff, nullptr, &target);
 
         // We may get a missing redirection type if the redirection is invalid.
-        auto hl = redirect_type ? highlight_spec_redirection : highlight_spec_error;
+        auto hl = redirect_type ? highlight_role_t::redirection : highlight_role_t::error;
         this->color_node(redir_prim, hl);
 
         // Check if the argument contains a command substitution. If so, highlight it as a param
@@ -999,7 +1044,7 @@ void highlighter_t::color_redirection(tnode_t redirection_node)
             }
 
             if (redir_target) {
-                auto hl = target_is_valid ? highlight_spec_redirection : highlight_spec_error;
+                auto hl = target_is_valid ? highlight_role_t::redirection : highlight_role_t::error;
                 this->color_node(redir_target, hl);
             }
         }
@@ -1080,7 +1125,7 @@ const highlighter_t::color_array_t &highlighter_t::highlight() {
     if (length == 0) return color_array;
 
     // Start out at zero.
-    std::fill(this->color_array.begin(), this->color_array.end(), 0);
+    std::fill(this->color_array.begin(), this->color_array.end(), highlight_spec_t{});
 
     // Walk the node tree.
     for (const parse_node_t &node : parse_tree) {
@@ -1094,15 +1139,15 @@ const highlighter_t::color_array_t &highlighter_t::highlight() {
             case symbol_case_item:
             case symbol_decorated_statement:
             case symbol_if_statement: {
-                this->color_children(node, parse_token_type_string, highlight_spec_command);
+                this->color_children(node, parse_token_type_string, highlight_role_t::command);
                 break;
             }
             case symbol_switch_statement: {
                 tnode_t switchn(&parse_tree, &node);
                 auto literal_switch = switchn.child<0>();
                 auto switch_arg = switchn.child<1>();
-                this->color_node(literal_switch, highlight_spec_command);
-                this->color_node(switch_arg, highlight_spec_param);
+                this->color_node(literal_switch, highlight_role_t::command);
+                this->color_node(switch_arg, highlight_role_t::param);
                 break;
             }
             case symbol_for_header: {
@@ -1110,8 +1155,8 @@ const highlighter_t::color_array_t &highlighter_t::highlight() {
                 // Color the 'for' and 'in' as commands.
                 auto literal_for = fhead.child<0>();
                 auto literal_in = fhead.child<2>();
-                this->color_node(literal_for, highlight_spec_command);
-                this->color_node(literal_in, highlight_spec_command);
+                this->color_node(literal_for, highlight_role_t::command);
+                this->color_node(literal_in, highlight_role_t::command);
 
                 // Color the variable name as a parameter.
                 this->color_argument(fhead.child<1>());
@@ -1120,22 +1165,22 @@ const highlighter_t::color_array_t &highlighter_t::highlight() {
 
             case parse_token_type_andand:
             case parse_token_type_oror:
-                this->color_node(node, highlight_spec_operator);
+                this->color_node(node, highlight_role_t::operat);
                 break;
 
             case symbol_not_statement:
-                this->color_children(node, parse_token_type_string, highlight_spec_operator);
+                this->color_children(node, parse_token_type_string, highlight_role_t::operat);
                 break;
 
             case symbol_job_decorator:
-                this->color_node(node, highlight_spec_operator);
+                this->color_node(node, highlight_role_t::operat);
                 break;
 
             case parse_token_type_pipe:
             case parse_token_type_background:
             case parse_token_type_end:
             case symbol_optional_background: {
-                this->color_node(node, highlight_spec_statement_terminator);
+                this->color_node(node, highlight_role_t::statement_terminator);
                 break;
             }
             case symbol_plain_statement: {
@@ -1166,7 +1211,7 @@ const highlighter_t::color_array_t &highlighter_t::highlight() {
                     }
                 }
                 if (!is_valid_cmd) {
-                    this->color_node(*cmd_node, highlight_spec_error);
+                    this->color_node(*cmd_node, highlight_role_t::error);
                 } else {
                     this->color_command(cmd_node);
                 }
@@ -1190,16 +1235,16 @@ const highlighter_t::color_array_t &highlighter_t::highlight() {
                 break;
             }
             case symbol_end_command: {
-                this->color_node(node, highlight_spec_command);
+                this->color_node(node, highlight_role_t::command);
                 break;
             }
             case parse_special_type_parse_error:
             case parse_special_type_tokenizer_error: {
-                this->color_node(node, highlight_spec_error);
+                this->color_node(node, highlight_role_t::error);
                 break;
             }
             case parse_special_type_comment: {
-                this->color_node(node, highlight_spec_comment);
+                this->color_node(node, highlight_role_t::comment);
                 break;
             }
             default: { break; }
@@ -1225,10 +1270,10 @@ const highlighter_t::color_array_t &highlighter_t::highlight() {
             node_is_potential_path(buff, node, vars, working_directory)) {
             // It is, underline it.
             for (size_t i = node.source_start; i < node.source_start + node.source_length; i++) {
-                // Don't color highlight_spec_error because it looks dorky. For example,
+                // Don't color highlight_role_t::error because it looks dorky. For example,
                 // trying to cd into a non-directory would show an underline and also red.
-                if (highlight_get_primary(this->color_array.at(i)) != highlight_spec_error) {
-                    this->color_array.at(i) |= highlight_modifier_valid_path;
+                if (this->color_array.at(i).foreground != highlight_role_t::error) {
+                    this->color_array.at(i).valid_path = true;
                 }
             }
         }
@@ -1295,10 +1340,8 @@ static void highlight_universal_internal(const wcstring &buffstr,
                                 pos1 = lst.back();
                                 pos2 = str - buff;
                                 if (pos1 == pos || pos2 == pos) {
-                                    color.at(pos1) |=
-                                        highlight_make_background(highlight_spec_match);
-                                    color.at(pos2) |=
-                                        highlight_make_background(highlight_spec_match);
+                                    color.at(pos1).background = highlight_role_t::match;
+                                    color.at(pos2).background = highlight_role_t::match;
                                     match_found = true;
                                 }
                                 prev_q = *str == L'\"' ? L'\'' : L'\"';
@@ -1318,7 +1361,7 @@ static void highlight_universal_internal(const wcstring &buffstr,
                 str++;
             }
 
-            if (!match_found) color.at(pos) = highlight_make_background(highlight_spec_error);
+            if (!match_found) color.at(pos).background = highlight_role_t::error;
         }
 
         // Highlight matching parenthesis.
@@ -1335,14 +1378,15 @@ static void highlight_universal_internal(const wcstring &buffstr,
                 if (test_char == dec_char) level--;
                 if (level == 0) {
                     long pos2 = i;
-                    color.at(pos) |= highlight_spec_match << 16;
-                    color.at(pos2) |= highlight_spec_match << 16;
+                    color.at(pos).background = highlight_role_t::match;
+                    color.at(pos2).background = highlight_role_t::match;
                     match_found = true;
                     break;
                 }
             }
 
-            if (!match_found) color[pos] = highlight_make_background(highlight_spec_error);
+            if (!match_found)
+                color.at(pos) = highlight_spec_t::make_background(highlight_role_t::error);
         }
     }
 }
@@ -1352,6 +1396,6 @@ void highlight_universal(const wcstring &buff, std::vector &co
     UNUSED(error);
     UNUSED(vars);
     assert(buff.size() == color.size());
-    std::fill(color.begin(), color.end(), 0);
+    std::fill(color.begin(), color.end(), highlight_spec_t{});
     highlight_universal_internal(buff, color, pos);
 }
diff --git a/src/highlight.h b/src/highlight.h
index ed6bbc4d6..a75b76481 100644
--- a/src/highlight.h
+++ b/src/highlight.h
@@ -11,66 +11,64 @@
 #include "common.h"
 #include "env.h"
 
-// Internally, we specify highlight colors using a set of bits. Each highlight_spec is a 32 bit
-// uint. We divide this into low 16 (foreground) and high 16 (background). Each half we further
-// subdivide into low 8 (primary) and high 8 (modifiers). The primary is not a bitmask; specify
-// exactly one. The modifiers are a bitmask; specify any number.
-enum {
-    // The following values are mutually exclusive; specify at most one.
-    highlight_spec_normal = 0,            // normal text
-    highlight_spec_error,                 // error
-    highlight_spec_command,               // command
-    highlight_spec_statement_terminator,  // process separator
-    highlight_spec_param,                 // command parameter (argument)
-    highlight_spec_comment,               // comment
-    highlight_spec_match,                 // matching parenthesis, etc.
-    highlight_spec_search_match,          // search match
-    highlight_spec_operator,              // operator
-    highlight_spec_escape,                // escape sequences
-    highlight_spec_quote,                 // quoted string
-    highlight_spec_redirection,           // redirection
-    highlight_spec_autosuggestion,        // autosuggestion
-    highlight_spec_selection,
+/// Describes the role of a span of text.
+enum class highlight_role_t : uint8_t {
+    normal = 0,            // normal text
+    error,                 // error
+    command,               // command
+    statement_terminator,  // process separator
+    param,                 // command parameter (argument)
+    comment,               // comment
+    match,                 // matching parenthesis, etc.
+    search_match,          // search match
+    operat,                // operator
+    escape,                // escape sequences
+    quote,                 // quoted string
+    redirection,           // redirection
+    autosuggestion,        // autosuggestion
+    selection,
 
     // Pager support.
     // NOTE: pager.cpp relies on these being in this order.
-    highlight_spec_pager_progress,
-    highlight_spec_pager_background,
-    highlight_spec_pager_prefix,
-    highlight_spec_pager_completion,
-    highlight_spec_pager_description,
-    highlight_spec_pager_secondary_background,
-    highlight_spec_pager_secondary_prefix,
-    highlight_spec_pager_secondary_completion,
-    highlight_spec_pager_secondary_description,
-    highlight_spec_pager_selected_background,
-    highlight_spec_pager_selected_prefix,
-    highlight_spec_pager_selected_completion,
-    highlight_spec_pager_selected_description,
-
-    // Used to double check a data structure in highlight.cpp
-    HIGHLIGHT_SPEC_MAX,
-
-    HIGHLIGHT_SPEC_PRIMARY_MASK = 0xFF,
-
-    // The following values are modifiers.
-    highlight_modifier_valid_path = 0x100,
-    highlight_modifier_force_underline = 0x200,
-    /* Very special value */
-    highlight_spec_invalid = 0xFFFF
-
+    pager_progress,
+    pager_background,
+    pager_prefix,
+    pager_completion,
+    pager_description,
+    pager_secondary_background,
+    pager_secondary_prefix,
+    pager_secondary_completion,
+    pager_secondary_description,
+    pager_selected_background,
+    pager_selected_prefix,
+    pager_selected_completion,
+    pager_selected_description,
 };
-typedef uint32_t highlight_spec_t;
 
-inline highlight_spec_t highlight_get_primary(highlight_spec_t val) {
-    return val & HIGHLIGHT_SPEC_PRIMARY_MASK;
-}
+/// Simply value type describing how a character should be highlighted..
+struct highlight_spec_t {
+    highlight_role_t foreground{highlight_role_t::normal};
+    highlight_role_t background{highlight_role_t::normal};
+    bool valid_path{false};
+    bool force_underline{false};
 
-inline highlight_spec_t highlight_make_background(highlight_spec_t val) {
-    assert(val >> 16 ==
-           0);  // should have nothing in upper bits, otherwise this is already a background
-    return val << 16;
-}
+    highlight_spec_t() = default;
+
+    /* implicit */ highlight_spec_t(highlight_role_t fg,
+                                    highlight_role_t bg = highlight_role_t::normal)
+        : foreground(fg), background(bg) {}
+
+    bool operator==(const highlight_spec_t &rhs) const {
+        return foreground == rhs.foreground && background == rhs.background &&
+               valid_path == rhs.valid_path && force_underline == rhs.force_underline;
+    }
+
+    bool operator!=(const highlight_spec_t &rhs) const { return !(*this == rhs); }
+
+    static highlight_spec_t make_background(highlight_role_t bg_role) {
+        return highlight_spec_t{highlight_role_t::normal, bg_role};
+    }
+};
 
 class history_item_t;
 
@@ -104,14 +102,8 @@ void highlight_shell_no_io(const wcstring &buffstr, std::vector &color, size_t pos,
                          wcstring_list_t *error, const environment_t &vars);
 
-/// Translate from HIGHLIGHT_* to FISH_COLOR_* according to environment variables. Defaults to
-/// FISH_COLOR_NORMAL.
-///
-/// Example:
-///
-/// If the environment variable FISH_FISH_COLOR_ERROR is set to 'red', a call to
-/// highlight_get_color( highlight_error) will return FISH_COLOR_RED.
-rgb_color_t highlight_get_color(highlight_spec_t highlight, bool is_background);
+/// \return an RGB color for a given highlight spec.
+rgb_color_t highlight_get_color(const highlight_spec_t &highlight, bool is_background);
 
 /// Given a command 'str' from the history, try to determine whether we ought to suggest it by
 /// specially recognizing the command. Returns true if we validated the command. If so, returns by
diff --git a/src/pager.cpp b/src/pager.cpp
index 548a67223..5066240d2 100644
--- a/src/pager.cpp
+++ b/src/pager.cpp
@@ -7,6 +7,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -137,20 +138,25 @@ line_t pager_t::completion_print_item(const wcstring &prefix, const comp_t *c, s
         assert(comp_width <= width);
     }
 
-    int offset = selected
-        ? (highlight_spec_pager_selected_background - highlight_spec_pager_background)
-        : (secondary
-                ? (highlight_spec_pager_secondary_background - highlight_spec_pager_background)
-                : 0);
-    highlight_spec_t bg_color = highlight_spec_pager_background + offset;
-    highlight_spec_t prefix_fg = highlight_spec_pager_prefix + offset;
-    highlight_spec_t comp_fg = highlight_spec_pager_completion + offset;
-    highlight_spec_t desc_fg = highlight_spec_pager_description + offset;
+    auto modify_role = [=](highlight_role_t role) -> highlight_role_t {
+        using uint_t = typename std::underlying_type::type;
+        uint_t base = static_cast(role);
+        if (selected) {
+            base += static_cast(highlight_role_t::pager_selected_background) -
+                    static_cast(highlight_role_t::pager_background);
+        } else if (secondary) {
+            base += static_cast(highlight_role_t::pager_secondary_background) -
+                    static_cast(highlight_role_t::pager_background);
+        }
+        return static_cast(base);
+    };
+
+    highlight_role_t bg_role = modify_role(highlight_role_t::pager_background);
+    highlight_spec_t bg = {highlight_role_t::normal, bg_role};
+    highlight_spec_t prefix_col = {modify_role(highlight_role_t::pager_prefix), bg_role};
+    highlight_spec_t comp_col = {modify_role(highlight_role_t::pager_completion), bg_role};
+    highlight_spec_t desc_col = {modify_role(highlight_role_t::pager_description), bg_role};
 
-    auto bg = highlight_make_background(bg_color);
-    auto prefix_col = prefix_fg | bg;
-    auto comp_col = comp_fg | bg;
-    auto desc_col = desc_fg | bg;
     // Print the completion part
     size_t comp_remaining = comp_width;
     for (size_t i = 0; i < c->comp.size(); i++) {
@@ -179,7 +185,7 @@ line_t pager_t::completion_print_item(const wcstring &prefix, const comp_t *c, s
         }
 
         assert(desc_remaining >= 2);
-        auto paren_col = highlight_spec_pager_completion | bg;
+        highlight_spec_t paren_col = {highlight_role_t::pager_completion, bg_role};
         desc_remaining -= print_max(L"(", paren_col, 1, false, &line_data);
         desc_remaining -= print_max(c->desc, desc_col, desc_remaining - 1, false, &line_data);
         desc_remaining -= print_max(L")", paren_col, 1, false, &line_data);
@@ -226,7 +232,7 @@ void pager_t::completion_print(size_t cols, const size_t *width_by_column, size_
 
             // If there's more to come, append two spaces.
             if (col + 1 < cols) {
-                line.append(PAGER_SPACER_STRING, 0);
+                line.append(PAGER_SPACER_STRING, highlight_spec_t{});
             }
 
             // Append this to the real line.
@@ -503,8 +509,8 @@ bool pager_t::completion_try_print(size_t cols, const wcstring &prefix, const co
 
     if (!progress_text.empty()) {
         line_t &line = rendering->screen_data.add_line();
-        highlight_spec_t spec = highlight_spec_pager_progress |
-                                highlight_make_background(highlight_spec_pager_progress);
+        highlight_spec_t spec = {highlight_role_t::pager_progress,
+                                 highlight_role_t::pager_progress};
         print_max(progress_text, spec, term_width, true /* has_more */, &line);
     }
 
@@ -521,11 +527,14 @@ bool pager_t::completion_try_print(size_t cols, const wcstring &prefix, const co
     line_t *search_field = &rendering->screen_data.insert_line_at_index(0);
 
     // We limit the width to term_width - 1.
+    highlight_spec_t underline{};
+    underline.force_underline = true;
+
     size_t search_field_remaining = term_width - 1;
-    search_field_remaining -= print_max(SEARCH_FIELD_PROMPT, highlight_spec_normal,
-                                        search_field_remaining, false, search_field);
-    search_field_remaining -= print_max(search_field_text, highlight_modifier_force_underline,
+    search_field_remaining -= print_max(SEARCH_FIELD_PROMPT, highlight_role_t::normal,
                                         search_field_remaining, false, search_field);
+    search_field_remaining -=
+        print_max(search_field_text, underline, search_field_remaining, false, search_field);
     return true;
 }
 
diff --git a/src/reader.cpp b/src/reader.cpp
index 2b62b08b3..38c1524df 100644
--- a/src/reader.cpp
+++ b/src/reader.cpp
@@ -617,10 +617,10 @@ void reader_data_t::repaint() {
     if (len < 1) len = 1;
 
     std::vector colors = this->colors;
-    colors.resize(len, highlight_spec_autosuggestion);
+    colors.resize(len, highlight_role_t::autosuggestion);
 
     if (sel_active) {
-        highlight_spec_t selection_color = highlight_make_background(highlight_spec_selection);
+        highlight_spec_t selection_color = {highlight_role_t::normal, highlight_role_t::selection};
         for (size_t i = sel_start_pos; i < std::min(len, sel_stop_pos); i++) {
             colors[i] = selection_color;
         }
@@ -1433,7 +1433,7 @@ void reader_data_t::flash() {
     struct timespec pollint;
     editable_line_t *el = &command_line;
     for (size_t i = 0; i < el->position; i++) {
-        colors.at(i) = highlight_spec_search_match << 16;
+        colors.at(i) = highlight_spec_t::make_background(highlight_role_t::search_match);
     }
 
     repaint();
@@ -2025,7 +2025,7 @@ void reader_data_t::highlight_search() {
     if (match_pos != wcstring::npos) {
         size_t end = match_pos + needle.size();
         for (size_t i = match_pos; i < end; i++) {
-            colors.at(i) |= (highlight_spec_search_match << 16);
+            colors.at(i).background = highlight_role_t::search_match;
         }
     }
 }
@@ -2059,7 +2059,7 @@ static std::function get_highlight_performer(
             return {};
         }
         s_thread_generation = generation_count;
-        std::vector colors(text.size(), 0);
+        std::vector colors(text.size(), highlight_spec_t{});
         highlight_func(text, colors, match_highlight_pos, NULL /* error */, vars);
         return {std::move(colors), text};
     };
diff --git a/src/screen.cpp b/src/screen.cpp
index edf769352..c3cb517b5 100644
--- a/src/screen.cpp
+++ b/src/screen.cpp
@@ -376,7 +376,8 @@ static void s_check_status(screen_t *s) {
 
 /// Appends a character to the end of the line that the output cursor is on. This function
 /// automatically handles linebreaks and lines longer than the screen width.
-static void s_desired_append_char(screen_t *s, wchar_t b, int c, int indent, size_t prompt_width) {
+static void s_desired_append_char(screen_t *s, wchar_t b, highlight_spec_t c, int indent,
+                                  size_t prompt_width) {
     int line_no = s->desired.cursor.y;
 
     if (b == L'\n') {
@@ -386,7 +387,7 @@ static void s_desired_append_char(screen_t *s, wchar_t b, int c, int indent, siz
         s->desired.cursor.y++;
         s->desired.cursor.x = 0;
         for (size_t i = 0; i < prompt_width + indent * INDENT_STEP; i++) {
-            s_desired_append_char(s, L' ', 0, indent, prompt_width);
+            s_desired_append_char(s, L' ', highlight_spec_t{}, indent, prompt_width);
         }
     } else if (b == L'\r') {
         line_t ¤t = s->desired.line(line_no);
@@ -512,10 +513,7 @@ static void s_move(screen_t *s, int new_x, int new_y) {
 /// Set the pen color for the terminal.
 static void s_set_color(screen_t *s, const environment_t &vars, highlight_spec_t c) {
     UNUSED(s);
-
-    unsigned int uc = (unsigned int)c;
-    s->outp().set_color(highlight_get_color(uc & 0xfff, false),
-                        highlight_get_color((uc >> 16) & 0xffff, true));
+    s->outp().set_color(highlight_get_color(c, false), highlight_get_color(c, true));
 }
 
 /// Convert a wide character to a multibyte string and append it to the buffer.
@@ -723,7 +721,7 @@ static void s_update(screen_t *scr, const wcstring &left_prompt, const wcstring
             clear_remainder = prev_width > current_width;
         }
         if (clear_remainder && clr_eol) {
-            s_set_color(scr, vars, 0xffffffff);
+            s_set_color(scr, vars, highlight_spec_t{});
             s_move(scr, current_width, (int)i);
             s_write_mbs(scr, clr_eol);
         }
@@ -731,7 +729,7 @@ static void s_update(screen_t *scr, const wcstring &left_prompt, const wcstring
         // Output any rprompt if this is the first line.
         if (i == 0 && right_prompt_width > 0) {  //!OCLINT(Use early exit/continue)
             s_move(scr, (int)(screen_width - right_prompt_width), (int)i);
-            s_set_color(scr, vars, 0xffffffff);
+            s_set_color(scr, vars, highlight_spec_t{});
             s_write_str(scr, right_prompt.c_str());
             scr->actual.cursor.x += right_prompt_width;
 
@@ -751,7 +749,7 @@ static void s_update(screen_t *scr, const wcstring &left_prompt, const wcstring
 
     // Clear remaining lines (if any) if we haven't cleared the screen.
     if (!has_cleared_screen && scr->desired.line_count() < lines_with_stuff && clr_eol) {
-        s_set_color(scr, vars, 0xffffffff);
+        s_set_color(scr, vars, highlight_spec_t{});
         for (size_t i = scr->desired.line_count(); i < lines_with_stuff; i++) {
             s_move(scr, 0, (int)i);
             s_write_mbs(scr, clr_eol);
@@ -759,7 +757,7 @@ static void s_update(screen_t *scr, const wcstring &left_prompt, const wcstring
     }
 
     s_move(scr, scr->desired.cursor.x, scr->desired.cursor.y);
-    s_set_color(scr, vars, 0xffffffff);
+    s_set_color(scr, vars, highlight_spec_t{});
 
     // We have now synced our actual screen against our desired screen. Note that this is a big
     // assignment!
@@ -999,13 +997,13 @@ void s_write(screen_t *s, const wcstring &left_prompt, const wcstring &right_pro
 
     // Append spaces for the left prompt.
     for (size_t i = 0; i < layout.left_prompt_space; i++) {
-        s_desired_append_char(s, L' ', 0, 0, layout.left_prompt_space);
+        s_desired_append_char(s, L' ', highlight_spec_t{}, 0, layout.left_prompt_space);
     }
 
     // If overflowing, give the prompt its own line to improve the situation.
     size_t first_line_prompt_space = layout.left_prompt_space;
     if (layout.prompts_get_own_line) {
-        s_desired_append_char(s, L'\n', 0, 0, 0);
+        s_desired_append_char(s, L'\n', highlight_spec_t{}, 0, 0);
         first_line_prompt_space = 0;
     }